From 5c4a7c9f6a2193dfddae3e8e7f9c51155ff7ad0c Mon Sep 17 00:00:00 2001 From: trendschau Date: Wed, 6 Oct 2021 21:39:16 +0200 Subject: [PATCH] Version 1.4.9-1.5.0: Refactor controller logic --- cache/securitylog.txt | 5 - media/live/multilanguage.png | Bin 0 -> 32724 bytes media/original/multilanguage.png | Bin 0 -> 32373 bytes media/thumbs/multilanguage.png | Bin 0 -> 21528 bytes system/Controllers/Controller.php | 59 --- ...entController.php => ControllerAuthor.php} | 325 ++++--------- ...ler.php => ControllerAuthorArticleApi.php} | 315 +++++++------ ...oller.php => ControllerAuthorBlockApi.php} | 371 +-------------- ...troller.php => ControllerAuthorEditor.php} | 28 +- ...oller.php => ControllerAuthorMediaApi.php} | 11 +- ...roller.php => ControllerAuthorMetaApi.php} | 116 +---- ...troller.php => ControllerFrontendAuth.php} | 2 +- ...roller.php => ControllerFrontendForms.php} | 5 +- ...roller.php => ControllerFrontendSetup.php} | 2 +- ...ller.php => ControllerFrontendWebsite.php} | 139 +++--- ...sController.php => ControllerSettings.php} | 46 +- system/Controllers/ControllerShared.php | 446 ++++++++++++++++++ system/Extensions/ParsedownExtension.php | 6 +- system/Models/Folder.php | 39 +- system/Models/Helpers.php | 1 + system/Models/ProcessAssets.php | 4 +- system/Models/ProcessFile.php | 1 - system/Models/VersionCheck.php | 29 -- system/Models/Write.php | 64 +-- system/Models/WriteCache.php | 120 ----- system/Models/WriteSitemap.php | 45 -- system/Routes/Api.php | 86 ++-- system/Routes/Web.php | 79 ++-- system/Settings.php | 2 + system/author/metatabs.yaml | 28 +- system/author/settings/system.twig | 14 + 31 files changed, 1037 insertions(+), 1351 deletions(-) delete mode 100644 cache/securitylog.txt create mode 100644 media/live/multilanguage.png create mode 100644 media/original/multilanguage.png create mode 100644 media/thumbs/multilanguage.png delete mode 100644 system/Controllers/Controller.php rename system/Controllers/{ContentController.php => ControllerAuthor.php} (51%) rename system/Controllers/{ArticleApiController.php => ControllerAuthorArticleApi.php} (70%) rename system/Controllers/{BlockApiController.php => ControllerAuthorBlockApi.php} (55%) rename system/Controllers/{ContentBackendController.php => ControllerAuthorEditor.php} (81%) rename system/Controllers/{MediaApiController.php => ControllerAuthorMediaApi.php} (95%) rename system/Controllers/{MetaApiController.php => ControllerAuthorMetaApi.php} (80%) rename system/Controllers/{AuthController.php => ControllerFrontendAuth.php} (96%) rename system/Controllers/{FormController.php => ControllerFrontendForms.php} (91%) rename system/Controllers/{SetupController.php => ControllerFrontendSetup.php} (96%) rename system/Controllers/{PageController.php => ControllerFrontendWebsite.php} (76%) rename system/Controllers/{SettingsController.php => ControllerSettings.php} (94%) create mode 100644 system/Controllers/ControllerShared.php delete mode 100644 system/Models/VersionCheck.php delete mode 100644 system/Models/WriteSitemap.php diff --git a/cache/securitylog.txt b/cache/securitylog.txt deleted file mode 100644 index 0123c2b..0000000 --- a/cache/securitylog.txt +++ /dev/null @@ -1,5 +0,0 @@ -127.0.0.1;2021-09-26 12:01:24;wrong captcha http://localhost/typemill/tm/recoverpw -127.0.0.1;2021-09-26 12:06:16;wrong captcha http://localhost/typemill/tm/recoverpw -127.0.0.1;2021-09-27 23:44:57;wrong captcha http://localhost/typemill/tm/recoverpw -127.0.0.1;2021-09-27 23:51:19;wrong captcha http://localhost/typemill/tm/recoverpw -127.0.0.1;2021-09-27 23:51:30;wrong captcha http://localhost/typemill/tm/recoverpw diff --git a/media/live/multilanguage.png b/media/live/multilanguage.png new file mode 100644 index 0000000000000000000000000000000000000000..1f706903553f155bbf4bf7484d5c0a8e8ac03554 GIT binary patch literal 32724 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 literal 0 HcmV?d00001 diff --git a/media/original/multilanguage.png b/media/original/multilanguage.png new file mode 100644 index 0000000000000000000000000000000000000000..482a4488bd1147c8ecad8f4f63d06df326c10788 GIT binary patch literal 32373 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 P00000NkvXXu0mjfNmhke literal 0 HcmV?d00001 diff --git a/system/Controllers/Controller.php b/system/Controllers/Controller.php deleted file mode 100644 index 320f650..0000000 --- a/system/Controllers/Controller.php +++ /dev/null @@ -1,59 +0,0 @@ -c = $c; - $this->settings = $this->c->get('settings'); - - $this->c->dispatcher->dispatch('onTwigLoaded'); - } - - # frontend rendering - protected function render($response, $route, $data) - { - # why commented this out?? - $data = $this->c->dispatcher->dispatch('onPageReady', new OnPageReady($data))->getData(); - - if(isset($_SESSION['old'])) - { - unset($_SESSION['old']); - } - - $response = $response->withoutHeader('Server'); - $response = $response->withAddedHeader('X-Powered-By', 'Typemill'); - - if(!isset($this->settings['headersoff']) or !$this->settings['headersoff']) - { - $response = $response->withAddedHeader('X-Content-Type-Options', 'nosniff'); - $response = $response->withAddedHeader('X-Frame-Options', 'SAMEORIGIN'); - $response = $response->withAddedHeader('X-XSS-Protection', '1;mode=block'); - $response = $response->withAddedHeader('Referrer-Policy', 'no-referrer-when-downgrade'); - if($this->c->request->getUri()->getScheme() == 'https') - { - $response = $response->withAddedHeader('Strict-Transport-Security', 'max-age=63072000'); - } - } - - return $this->c->view->render($response, $route, $data); - } - - protected function render404($response, $data = NULL) - { - return $this->c->view->render($response->withStatus(404), '/404.twig', $data); - } -} \ No newline at end of file diff --git a/system/Controllers/ContentController.php b/system/Controllers/ControllerAuthor.php similarity index 51% rename from system/Controllers/ContentController.php rename to system/Controllers/ControllerAuthor.php index 3a1a183..c68360a 100644 --- a/system/Controllers/ContentController.php +++ b/system/Controllers/ControllerAuthor.php @@ -2,42 +2,22 @@ namespace Typemill\Controllers; -use Slim\Http\Request; -use Slim\Http\Response; -use Psr\Container\ContainerInterface; -use Typemill\Models\Validation; use Typemill\Models\Folder; -use Typemill\Models\Write; -use Typemill\Models\WriteCache; -use Typemill\Models\WriteYaml; use Typemill\Models\WriteMeta; +use Typemill\Models\WriteYaml; +use Typemill\Models\Validation; -abstract class ContentController -{ - # holds the pimple container - protected $c; - +class ControllerAuthor extends ControllerShared +{ # holds the params from request protected $params; - # holds the slim-uri-object + # holds the slim-uri-object from request protected $uri; # holds the errors to output in frontend protected $errors = false; - # holds a write object to write files - protected $write; - - # holds the structure of content folder as a serialized array of objects - protected $structure; - - # holds the name of the structure-file with drafts for author environment - protected $structureDraftName; - - # holds the name of the structure-file without drafts for live site - protected $structureLiveName; - # holds informations about the homepage protected $homepage; @@ -56,72 +36,33 @@ abstract class ContentController # holds the ownership (my content or not my content) protected $mycontent = false; - public function __construct(ContainerInterface $c) - { - $this->c = $c; - $this->settings = $this->c->get('settings'); - $this->structureLiveName = 'structure.txt'; - $this->structureDraftName = 'structure-draft.txt'; - - $this->c->dispatcher->dispatch('onTwigLoaded'); - } - - # admin ui rendering - protected function render($response, $route, $data) - { - if(isset($_SESSION['old'])) - { - unset($_SESSION['old']); - } - - $response = $response->withoutHeader('Server'); - $response = $response->withAddedHeader('X-Powered-By', 'Typemill'); - - if(!isset($this->settings['headersoff']) or !$this->settings['headersoff']) - { - $response = $response->withAddedHeader('X-Content-Type-Options', 'nosniff'); - $response = $response->withAddedHeader('X-Frame-Options', 'SAMEORIGIN'); - $response = $response->withAddedHeader('X-XSS-Protection', '1;mode=block'); - $response = $response->withAddedHeader('Referrer-Policy', 'no-referrer-when-downgrade'); - if($this->c->request->getUri()->getScheme() == 'https') - { - $response = $response->withAddedHeader('Strict-Transport-Security', 'max-age=63072000'); - } - } - - return $this->c->view->render($response, $route, $data); - } - - protected function render404($response, $data = NULL) - { - return $this->c->view->render($response->withStatus(404), '/404.twig', $data); - } - - protected function renderIntern404($response, $data = NULL) - { - return $this->c->view->render($response->withStatus(404), '/intern404.twig', $data); - } - + # author protected function getValidator() { return new Validation(); } + # author protected function validateEditorInput() { - $validate = new Validation(); - $vResult = $validate->editorInput($this->params); + $validate = new Validation(); + $vResult = $validate->editorInput($this->params); if(is_array($vResult)) { - $message = reset($vResult); - $this->errors = ['errors' => $vResult]; - if(isset($message[0])){ $this->errors['errors']['message'] = $message[0]; } + $message = reset($vResult); + $this->errors = ['errors' => $vResult]; + + if(isset($message[0])){ + $this->errors['errors']['message'] = $message[0]; + } + return false; } return true; } + # author protected function validateBlockInput() { $validate = new Validation(); @@ -131,12 +72,18 @@ abstract class ContentController { $message = reset($vResult); $this->errors = ['errors' => $vResult]; - if(isset($message[0])){ $this->errors['errors']['message'] = $message[0]; } + + if(isset($message[0])) + { + $this->errors['errors']['message'] = $message[0]; + } + return false; } return true; } + # author protected function validateNavigationSort() { $validate = new Validation(); @@ -146,12 +93,17 @@ abstract class ContentController { $message = reset($vResult); $this->errors = ['errors' => $vResult]; - if(isset($message[0])){ $this->errors['errors']['message'] = $message[0]; } + + if(isset($message[0])){ + $this->errors['errors']['message'] = $message[0]; + } + return false; } return true; } + # author protected function validateNaviItem() { $validate = new Validation(); @@ -161,12 +113,18 @@ abstract class ContentController { $message = reset($vResult); $this->errors = ['errors' => $vResult]; - if(isset($message[0])){ $this->errors['errors']['message'] = $message[0]; } + + if(isset($message[0])) + { + $this->errors['errors']['message'] = $message[0]; + } + return false; } return true; } + # author protected function validateBaseNaviItem() { $validate = new Validation(); @@ -176,110 +134,18 @@ abstract class ContentController { $message = reset($vResult); $this->errors = ['errors' => $vResult]; - if(isset($message[0])){ $this->errors['errors']['message'] = $message[0]; } + + if(isset($message[0])) + { + $this->errors['errors']['message'] = $message[0]; + } + return false; } return true; } - - protected function setStructure($draft = false, $cache = true) - { - # set initial structure to false - $structure = false; - # name of structure-file for draft or live - $filename = $draft ? $this->structureDraftName : $this->structureLiveName; - - # set variables and objects - $this->write = new writeCache(); - - # check, if cached structure is still valid - if($cache && $this->write->validate('cache', 'lastCache.txt', 600)) - { - # get the cached structure - $structure = $this->write->getCache('cache', $filename); - } - - # if no structure was found or cache is deactivated - if(!$structure) - { - # scan the content of the folder - $structure = Folder::scanFolder($this->settings['rootPath'] . $this->settings['contentFolder'], $draft); - - # if there is content, then get the content details - if(count($structure) > 0) - { - # get the extended structure files with changes like navigation title or hidden pages - $yaml = new writeYaml(); - $extended = $yaml->getYaml('cache', 'structure-extended.yaml'); - - # create an array of object with the whole content of the folder and changes from extended file - $structure = Folder::getFolderContentDetails($structure, $extended, $this->uri->getBaseUrl(), $this->uri->getBasePath()); - } - - # cache navigation - $this->write->updateCache('cache', $filename, 'lastCache.txt', $structure); - } - - $this->structure = $structure; - return true; - } - - protected function renameExtended($item, $newFolder) - { - # get the extended structure files with changes like navigation title or hidden pages - $yaml = new writeYaml(); - $extended = $yaml->getYaml('cache', 'structure-extended.yaml'); - - if(isset($extended[$item->urlRelWoF])) - { - $newUrl = $newFolder->urlRelWoF . '/' . $item->slug; - - $entry = $extended[$item->urlRelWoF]; - - unset($extended[$item->urlRelWoF]); - - $extended[$newUrl] = $entry; - $yaml->updateYaml('cache', 'structure-extended.yaml', $extended); - } - - return true; - } - - protected function deleteFromExtended() - { - # get the extended structure files with changes like navigation title or hidden pages - $yaml = new writeYaml(); - $extended = $yaml->getYaml('cache', 'structure-extended.yaml'); - - if($this->item->elementType == "file" && isset($extended[$this->item->urlRelWoF])) - { - unset($extended[$this->item->urlRelWoF]); - $yaml->updateYaml('cache', 'structure-extended.yaml', $extended); - } - - if($this->item->elementType == "folder") - { - $changed = false; - - # delete all entries with that folder url - foreach($extended as $url => $entries) - { - if( strpos($url, $this->item->urlRelWoF) !== false ) - { - $changed = true; - unset($extended[$url]); - } - } - - if($changed) - { - $yaml->updateYaml('cache', 'structure-extended.yaml', $extended); - } - } - } - - # this is only set by content backend controller + # only backoffice protected function setHomepage($args) { $contentFolder = Folder::scanFolderFlat($this->settings['rootPath'] . $this->settings['contentFolder']); @@ -308,13 +174,14 @@ abstract class ContentController $this->homepage = ['status' => $status, 'active' => $active]; } + # only backoffice protected function setItem() { # home is only set by backend controller, not by api calls $home = isset($this->homepage['active']) ? $this->homepage['active'] : false; # search for the url in the structure - $item = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl(), NULL, $home); + $item = Folder::getItemForUrl($this->structureDraft, $this->params['url'], $this->uri->getBaseUrl(), NULL, $home); if($item) { @@ -327,12 +194,14 @@ abstract class ContentController return false; } + # only backoffice # determine if you want to write to published file (md) or to draft (txt) protected function setItemPath($fileType) { $this->path = $this->item->pathWithoutType . '.' . $fileType; } - + + # only backoffice protected function setPublishStatus() { $this->item->published = false; @@ -360,7 +229,56 @@ abstract class ContentController $this->item->fileType = "txt"; } } + + # only backoffice + protected function setContent() + { + # if the file exists + if($this->item->published OR $this->item->drafted) + { + $content = $this->writeCache->getFile($this->settings['contentFolder'], $this->path); + if($this->item->fileType == 'txt') + { + # decode the json-draft to an array + $content = json_decode($content); + } + } + elseif($this->item->elementType == "folder") + { + $content = ''; + } + else + { + $this->errors = ['errors' => ['message' => 'requested file not found']]; + return false; + } + $this->content = $content; + return true; + } + + # only backoffice + protected function checkContentOwnership() + { + # get page meta + $writeMeta = new writeMeta(); + $pagemeta = $writeMeta->getPageMeta($this->settings, $this->item); + + # check ownership + if(isset($pagemeta['meta']['owner']) && $pagemeta['meta']['owner'] && $pagemeta['meta']['owner'] !== '' ) + { + $allowedusers = array_map('trim', explode(",", $pagemeta['meta']['owner'])); + if(isset($_SESSION['user']) && in_array($_SESSION['user'], $allowedusers)) + { + $this->mycontent = true; + return true; + } + } + + return false; + } + + # only backoffice protected function deleteContentFiles($fileTypes, $folder = false) { $basePath = $this->settings['rootPath'] . $this->settings['contentFolder']; @@ -381,6 +299,7 @@ abstract class ContentController return true; } + # only backoffice protected function deleteContentFolder() { $basePath = $this->settings['rootPath'] . $this->settings['contentFolder']; @@ -418,50 +337,4 @@ abstract class ContentController } return false; } - - protected function setContent() - { - # if the file exists - if($this->item->published OR $this->item->drafted) - { - $content = $this->write->getFile($this->settings['contentFolder'], $this->path); - if($this->item->fileType == 'txt') - { - # decode the json-draft to an array - $content = json_decode($content); - } - } - elseif($this->item->elementType == "folder") - { - $content = ''; - } - else - { - $this->errors = ['errors' => ['message' => 'requested file not found']]; - return false; - } - - $this->content = $content; - return true; - } - - protected function checkContentOwnership() - { - # get page meta - $writeMeta = new writeMeta(); - $pagemeta = $writeMeta->getPageMeta($this->settings, $this->item); - - # check ownership - if(isset($pagemeta['meta']['owner']) && $pagemeta['meta']['owner'] && $pagemeta['meta']['owner'] !== '' ) - { - $allowedusers = array_map('trim', explode(",", $pagemeta['meta']['owner'])); - if(isset($_SESSION['user']) && in_array($_SESSION['user'], $allowedusers)) - { - $this->mycontent = true; - return true; - } - } - - return false; - } } \ No newline at end of file diff --git a/system/Controllers/ArticleApiController.php b/system/Controllers/ControllerAuthorArticleApi.php similarity index 70% rename from system/Controllers/ArticleApiController.php rename to system/Controllers/ControllerAuthorArticleApi.php index a397d8f..9e9c432 100644 --- a/system/Controllers/ArticleApiController.php +++ b/system/Controllers/ControllerAuthorArticleApi.php @@ -5,19 +5,15 @@ namespace Typemill\Controllers; use Slim\Http\Request; use Slim\Http\Response; use Typemill\Models\Folder; -use Typemill\Models\Write; use Typemill\Models\WriteYaml; use Typemill\Models\WriteMeta; -use Typemill\Models\WriteCache; use Typemill\Extensions\ParsedownExtension; use Typemill\Events\OnPagePublished; use Typemill\Events\OnPageUnpublished; use Typemill\Events\OnPageDeleted; use Typemill\Events\OnPageSorted; -use \URLify; - -class ArticleApiController extends ContentController +class ControllerAuthorArticleApi extends ControllerAuthor { public function publishArticle(Request $request, Response $response, $args) { @@ -38,7 +34,7 @@ class ArticleApiController extends ContentController } # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); } + if(!$this->setStructureDraft()){ return $response->withJson($this->errors, 404); } # set information for homepage $this->setHomepage($args = false); @@ -87,16 +83,22 @@ class ArticleApiController extends ContentController $this->setItemPath('md'); # update the file - if($this->write->writeFile($this->settings['contentFolder'], $this->path, $this->content)) + if($this->writeCache->writeFile($this->settings['contentFolder'], $this->path, $this->content)) { # update the file $delete = $this->deleteContentFiles(['txt']); # update the internal structure - $this->setStructure($draft = true, $cache = false); + $this->setFreshStructureDraft(); # update the public structure - $this->setStructure($draft = false, $cache = false); + $this->setFreshStructureLive(); + + # update the navigation + $this->setFreshNavigation(); + + # update the sitemap + $this->updateSitemap(); # complete the page meta if title or description not set $writeMeta = new WriteMeta(); @@ -127,7 +129,7 @@ class ArticleApiController extends ContentController } # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); } + if(!$this->setStructureDraft()){ return $response->withJson($this->errors, 404); } # set information for homepage $this->setHomepage($args = false); @@ -151,7 +153,7 @@ class ArticleApiController extends ContentController # check if draft exists, if not, create one. if(!$this->item->drafted) { - # set path for the file (or folder) + # set path for the live file (or folder) $this->setItemPath('md'); # set content of markdown-file @@ -166,11 +168,11 @@ class ArticleApiController extends ContentController # encode the content into json $contentJson = json_encode($contentArray); - # set path for the file (or folder) + # set path for the draft file (or folder) $this->setItemPath('txt'); - /* update the file */ - if(!$this->write->writeFile($this->settings['contentFolder'], $this->path, $contentJson)) + # update the file + if(!$this->writeCache->writeFile($this->settings['contentFolder'], $this->path, $contentJson)) { return $response->withJson(['errors' => ['message' => 'Could not create a draft of the page. Please check if the folder is writable']], 404); } @@ -189,16 +191,22 @@ class ArticleApiController extends ContentController } } - # update the file + # delete the live file $delete = $this->deleteContentFiles(['md']); if($delete) { # update the internal structure - $this->setStructure($draft = true, $cache = false); + $this->setFreshStructureDraft(); # update the live structure - $this->setStructure($draft = false, $cache = false); + $this->setFreshStructureLive(); + + # update the navigation + $this->setFreshNavigation(); + + # update the sitemap + $this->updateSitemap(); # dispatch event $this->c->dispatcher->dispatch('onPageUnpublished', new OnPageUnpublished($this->item)); @@ -224,7 +232,7 @@ class ArticleApiController extends ContentController } # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); } + if(!$this->setStructureDraft()){ return $response->withJson($this->errors, 404); } # set information for homepage $this->setHomepage($args = false); @@ -242,9 +250,6 @@ class ArticleApiController extends ContentController } } - # remove the unpublished changes - $delete = $this->deleteContentFiles(['txt']); - # set redirect url to edit page $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor']; if(isset($this->item->urlRelWoF) && $this->item->urlRelWoF != '/' ) @@ -258,13 +263,13 @@ class ArticleApiController extends ContentController if($delete) { # update the backend structure - $this->setStructure($draft = true, $cache = false); + $this->setFreshStructureDraft(); - return $response->withJson(['data' => $this->structure, 'errors' => false, 'url' => $url], 200); + return $response->withJson(['data' => $this->structureDraft, 'errors' => false, 'url' => $url], 200); } else { - return $response->withJson(['data' => $this->structure, 'errors' => $this->errors], 404); + return $response->withJson(['data' => $this->structureDraft, 'errors' => $this->errors], 404); } } @@ -284,7 +289,7 @@ class ArticleApiController extends ContentController $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor']; # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); } + if(!$this->setStructureDraft()){ return $response->withJson($this->errors, 404); } # set information for homepage $this->setHomepage($args = false); @@ -317,7 +322,7 @@ class ArticleApiController extends ContentController if(count($this->item->keyPathArray) > 1) { # get the parent item - $parentItem = Folder::getParentItem($this->structure, $this->item->keyPathArray); + $parentItem = Folder::getParentItem($this->structureDraft, $this->item->keyPathArray); if($parentItem) { @@ -327,18 +332,24 @@ class ArticleApiController extends ContentController } # update the live structure - $this->setStructure($draft = false, $cache = false); + $this->setFreshStructureDraft(); # update the backend structure - $this->setStructure($draft = true, $cache = false); + $this->setFreshStructureLive(); # check if page is in extended structure and delete it $this->deleteFromExtended(); + # update the navigation + $this->setFreshNavigation(); + + # update the sitemap + $this->updateSitemap(); + # dispatch event $this->c->dispatcher->dispatch('onPageDeleted', new OnPageDeleted($this->item)); - return $response->withJson(array('data' => $this->structure, 'errors' => false, 'url' => $url), 200); + return $response->withJson(array('data' => $this->structureDraft, 'errors' => false, 'url' => $url), 200); } else { @@ -362,7 +373,7 @@ class ArticleApiController extends ContentController if(!$this->validateEditorInput()){ return $response->withJson($this->errors,422); } # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); } + if(!$this->setStructureDraft()){ return $response->withJson($this->errors, 404); } # set information for homepage $this->setHomepage($args = false); @@ -380,7 +391,7 @@ class ArticleApiController extends ContentController } } - # set path for the file (or folder) + # set draft path for the file (or folder) $this->setItemPath('txt'); # merge title with content for complete markdown document @@ -395,11 +406,11 @@ class ArticleApiController extends ContentController # encode the content into json $contentJson = json_encode($contentArray); - /* update the file */ - if($this->write->writeFile($this->settings['contentFolder'], $this->path, $contentJson)) + # update the file + if($this->writeCache->writeFile($this->settings['contentFolder'], $this->path, $contentJson)) { - # update the internal structure - $this->setStructure($draft = true, $cache = false); + # update the internal structure to show that this page has drafted changes now + $this->setFreshStructureDraft(); return $response->withJson(['success'], 200); } @@ -423,14 +434,14 @@ class ArticleApiController extends ContentController return $response->withJson(array('data' => false, 'errors' => 'You are not allowed to update content.'), 403); } - # validate input + # validate input 1: check if valid characters if(!preg_match("/^[a-z0-9\-]*$/", $this->params['slug'])) { return $response->withJson(['errors' => ['message' => 'the slug contains invalid characters.' ]],422); } # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); } + if(!$this->setStructureDraft()){ return $response->withJson($this->errors, 404); } # set information for homepage $this->setHomepage($args = false); @@ -438,7 +449,7 @@ class ArticleApiController extends ContentController # set item if(!$this->setItem()){ return $response->withJson($this->errors, 404); } - # validate input part 2 + # validate input part 2: check if slug has changed or is empty if($this->params['slug'] == $this->item->slug OR $this->params['slug'] == '') { return $response->withJson(['errors' => ['message' => 'the slug is empty or the same as the old one.' ]],422); @@ -460,19 +471,54 @@ class ArticleApiController extends ContentController # create the new file name with the updated slug $newPathWithoutType = $pathWithoutFile . $this->item->order . '-' . $this->params['slug']; - # rename the file - $write = new WriteCache(); - $write->renamePost($this->item->pathWithoutType, $newPathWithoutType); - - # delete the cache - $error = $write->deleteCacheFiles($dir); - if($error) + # validate input part 3: check if name is taken already + $parentKey = $this->item->keyPathArray; + array_pop($parentKey); + if(!empty($parentKey)) { - return $response->withJson(['errors' => $error], 500); + $parentFolder = Folder::getItemWithKeyPath($this->structureDraft, $parentKey); + + foreach($parentFolder->folderContent as $item) + { + if($item->slug == $this->params['slug']) + { + return $response->withJson(['errors' => ['message' => 'There is already a page with that slug' ]],422); + } + } + } + else + { + foreach($this->structureDraft as $baseItem) + { + if($baseItem->slug == $this->params['slug']) + { + return $response->withJson(['errors' => ['message' => 'There is already a page with that slug' ]],422); + } + } } - # recreates the cache for structure, structure-extended and navigation - $write->getFreshStructure($pathToContent, $this->uri); + # rename the file + if($this->item->elementType == 'file') + { + $this->writeCache->renamePost($this->item->pathWithoutType, $newPathWithoutType); + } + elseif($this->item->elementType == 'folder') + { + $this->writeCache->renameFile('content', $this->item->path, $newPathWithoutType); + } + + # delete the cache + $error = $this->writeCache->deleteCacheFiles($dir); + if($error) + { + return $response->withJson(['errors' => $errors], 500); + } + + # recreates the cache for structure, structure-extended, navigation, sitemap + $this->setFreshStructureDraft(); + $this->setFreshStructureLive(); + $this->setFreshNavigation(); + $this->updateSitemap(); $newUrlRel = str_replace($this->item->slug, $this->params['slug'], $this->item->urlRelWoF); @@ -497,10 +543,10 @@ class ArticleApiController extends ContentController $url = false; # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); } + if(!$this->setStructureDraft()){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); } # validate input - if(!$this->validateNavigationSort()){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Data not valid. Please refresh the page and try again.', 'url' => $url), 422); } + if(!$this->validateNavigationSort()){ return $response->withJson(array('data' => $this->structureDraft, 'errors' => 'Data not valid. Please refresh the page and try again.', 'url' => $url), 422); } # get the ids (key path) for item, old folder and new folder $itemKeyPath = explode('.', $this->params['item_id']); @@ -508,9 +554,9 @@ class ArticleApiController extends ContentController $parentKeyTo = explode('.', $this->params['parent_id_to']); # get the item from structure - $item = Folder::getItemWithKeyPath($this->structure, $itemKeyPath); + $item = Folder::getItemWithKeyPath($this->structureDraft, $itemKeyPath); - if(!$item){ return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not find this page. Please refresh and try again.', 'url' => $url), 404); } + if(!$item){ return $response->withJson(array('data' => $this->structureDraft, 'errors' => 'We could not find this page. Please refresh and try again.', 'url' => $url), 404); } # needed for acl check $this->item = $item; @@ -521,7 +567,7 @@ class ArticleApiController extends ContentController # check ownership. This code should nearly never run, because there is no button/interface to trigger it. if(!$this->checkContentOwnership()) { - return $response->withJson(array('data' => $this->structure, 'errors' => 'You are not allowed to move that content.'), 403); + return $response->withJson(array('data' => $this->structureDraft, 'errors' => 'You are not allowed to move that content.'), 403); } } @@ -531,12 +577,12 @@ class ArticleApiController extends ContentController # create empty and default values so that the logic below still works $newFolder = new \stdClass(); $newFolder->path = ''; - $folderContent = $this->structure; + $folderContent = $this->structureDraft; } else { # get the target folder from structure - $newFolder = Folder::getItemWithKeyPath($this->structure, $parentKeyTo); + $newFolder = Folder::getItemWithKeyPath($this->structureDraft, $parentKeyTo); # get the content of the target folder $folderContent = $newFolder->folderContent; @@ -570,43 +616,42 @@ class ArticleApiController extends ContentController # initialize index $index = 0; - # initialise write object - $write = new Write(); - # iterate through the whole content of the new folder to rename the files $writeError = false; foreach($folderContent as $folderItem) { - if(!$write->moveElement($folderItem, $newFolder->path, $index)) + if(!$this->writeCache->moveElement($folderItem, $newFolder->path, $index)) { $writeError = true; } $index++; } - if($writeError){ return $response->withJson(array('data' => $this->structure, 'errors' => ['message' => 'Something went wrong. Please refresh the page and check, if all folders and files are writable.'], 'url' => $url), 404); } + if($writeError){ return $response->withJson(array('data' => $this->structureDraft, 'errors' => ['message' => 'Something went wrong. Please refresh the page and check, if all folders and files are writable.'], 'url' => $url), 404); } # update the structure for editor - $this->setStructure($draft = true, $cache = false); + $this->setFreshStructureDraft(); # get item for url and set it active again if(isset($this->params['url'])) { - $activeItem = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl()); + $activeItem = Folder::getItemForUrl($this->structureDraft, $this->params['url'], $this->uri->getBaseUrl()); } - - # keep the internal structure for response - $internalStructure = $this->structure; - + # update the structure for website - $this->setStructure($draft = false, $cache = false); + $this->setFreshStructureLive(); + + # update the navigation + $this->setFreshNavigation(); + + # update the sitemap + $this->updateSitemap(); # dispatch event $this->c->dispatcher->dispatch('onPageSorted', new OnPageSorted($this->params)); - return $response->withJson(array('data' => $internalStructure, 'errors' => false, 'url' => $url)); + return $response->withJson(array('data' => $this->structureDraft, 'errors' => false, 'url' => $url)); } - public function createPost(Request $request, Response $response, $args) { # get params from call @@ -623,41 +668,41 @@ class ArticleApiController extends ContentController $url = false; # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); } - + if(!$this->setStructureDraft()){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); } + # validate input - if(!$this->validateNaviItem()){ return $response->withJson(array('data' => $this->structure, 'errors' => ['message' => 'Special Characters not allowed. Length between 1 and 60 chars.'], 'url' => $url), 422); } + if(!$this->validateNaviItem()){ return $response->withJson(array('data' => $this->structureDraft, 'errors' => ['message' => 'Special Characters not allowed. Length between 1 and 60 chars.'], 'url' => $url), 422); } # get the ids (key path) for item, old folder and new folder $folderKeyPath = explode('.', $this->params['folder_id']); # get the item from structure - $folder = Folder::getItemWithKeyPath($this->structure, $folderKeyPath); + $folder = Folder::getItemWithKeyPath($this->structureDraft, $folderKeyPath); - if(!$folder){ return $response->withJson(array('data' => $this->structure, 'errors' => ['message' => 'We could not find this page. Please refresh and try again.'], 'url' => $url), 404); } - + if(!$folder){ return $response->withJson(array('data' => $this->structureDraft, 'errors' => ['message' => 'We could not find this page. Please refresh and try again.'], 'url' => $url), 404); } + $name = $this->params['item_name']; - $slug = URLify::filter(iconv(mb_detect_encoding($this->params['item_name'], mb_detect_order(), true), "UTF-8", $this->params['item_name'])); + $slug = Folder::createSlug($this->params['item_name'], $this->settings); $namePath = date("YmdHi") . '-' . $slug; $folderPath = 'content' . $folder->path; $content = json_encode(['# ' . $name, 'Content']); # initialise write object - $write = new WriteYaml(); + $writeYaml = new WriteYaml(); # check, if name exists - if($write->checkFile($folderPath, $namePath . '.txt') OR $write->checkFile($folderPath, $namePath . '.md')) + if($writeYaml->checkFile($folderPath, $namePath . '.txt') OR $writeYaml->checkFile($folderPath, $namePath . '.md')) { - return $response->withJson(array('data' => $this->structure, 'errors' => 'There is already a page with this name. Please choose another name.', 'url' => $url), 404); + return $response->withJson(array('data' => $this->structureDraft, 'errors' => 'There is already a page with this name. Please choose another name.', 'url' => $url), 404); } - if(!$write->writeFile($folderPath, $namePath . '.txt', $content)) + if(!$writeYaml->writeFile($folderPath, $namePath . '.txt', $content)) { - return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not create the file. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404); + return $response->withJson(array('data' => $this->structureDraft, 'errors' => 'We could not create the file. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404); } # get extended structure - $extended = $write->getYaml('cache', 'structure-extended.yaml'); + $extended = $writeYaml->getYaml('cache', 'structure-extended.yaml'); # create the url for the item $urlWoF = $folder->urlRelWoF . '/' . $slug; @@ -666,17 +711,17 @@ class ArticleApiController extends ContentController $extended[$urlWoF] = ['hide' => false, 'navtitle' => $name]; # store the extended structure - $write->updateYaml('cache', 'structure-extended.yaml', $extended); + $writeYaml->updateYaml('cache', 'structure-extended.yaml', $extended); # update the structure for editor - $this->setStructure($draft = true, $cache = false); + $this->setFreshStructureDraft(); - $folder = Folder::getItemWithKeyPath($this->structure, $folderKeyPath); + $folder = Folder::getItemWithKeyPath($this->structureYaml, $folderKeyPath); # activate this if you want to redirect after creating the page... # $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor'] . $folder->urlRelWoF . '/' . $slug; - return $response->withJson(array('posts' => $folder, $this->structure, 'errors' => false, 'url' => $url)); + return $response->withJson(array('posts' => $folder, $this->structureDraft, 'errors' => false, 'url' => $url)); } public function createArticle(Request $request, Response $response, $args) @@ -695,36 +740,31 @@ class ArticleApiController extends ContentController $url = false; # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); } + if(!$this->setStructureDraft()){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); } # validate input - if(!$this->validateNaviItem()){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Special Characters not allowed. Length between 1 and 60 chars.', 'url' => $url), 422); } + if(!$this->validateNaviItem()){ return $response->withJson(array('data' => $this->structureDraft, 'errors' => 'Special Characters not allowed. Length between 1 and 60 chars.', 'url' => $url), 422); } # get the ids (key path) for item, old folder and new folder $folderKeyPath = explode('.', $this->params['folder_id']); # get the item from structure - $folder = Folder::getItemWithKeyPath($this->structure, $folderKeyPath); + $folder = Folder::getItemWithKeyPath($this->structureDraft, $folderKeyPath); - if(!$folder){ return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not find this page. Please refresh and try again.', 'url' => $url), 404); } + if(!$folder){ return $response->withJson(array('data' => $this->structureDraft, 'errors' => 'We could not find this page. Please refresh and try again.', 'url' => $url), 404); } # Rename all files within the folder to make sure, that namings and orders are correct # get the content of the target folder $folderContent = $folder->folderContent; $name = $this->params['item_name']; - $slug = URLify::filter(iconv(mb_detect_encoding($this->params['item_name'], mb_detect_order(), true), "UTF-8", $this->params['item_name'])); - - # create the name for the new item - # $nameParts = Folder::getStringParts($this->params['item_name']); - # $name = implode("-", $nameParts); - # $slug = $name; + $slug = Folder::createSlug($this->params['item_name'], $this->settings); # initialize index $index = 0; # initialise write object - $write = new WriteYaml(); + $writeYaml = new WriteYaml(); # iterate through the whole content of the new folder $writeError = false; @@ -734,17 +774,17 @@ class ArticleApiController extends ContentController # check, if the same name as new item, then return an error if($folderItem->slug == $slug) { - return $response->withJson(array('data' => $this->structure, 'errors' => 'There is already a page with this name. Please choose another name.', 'url' => $url), 404); + return $response->withJson(array('data' => $this->structureDraft, 'errors' => 'There is already a page with this name. Please choose another name.', 'url' => $url), 404); } - if(!$write->moveElement($folderItem, $folder->path, $index)) + if(!$writeYaml->moveElement($folderItem, $folder->path, $index)) { $writeError = true; } $index++; } - if($writeError){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Something went wrong. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404); } + if($writeError){ return $response->withJson(array('data' => $this->structureDraft, 'errors' => 'Something went wrong. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404); } # add prefix number to the name $namePath = $index > 9 ? $index . '-' . $slug : '0' . $index . '-' . $slug; @@ -755,18 +795,18 @@ class ArticleApiController extends ContentController if($this->params['type'] == 'file') { - if(!$write->writeFile($folderPath, $namePath . '.txt', $content)) + if(!$writeYaml->writeFile($folderPath, $namePath . '.txt', $content)) { - return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not create the file. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404); + return $response->withJson(array('data' => $this->structureDraft, 'errors' => 'We could not create the file. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404); } } elseif($this->params['type'] == 'folder') { - if(!$write->checkPath($folderPath . DIRECTORY_SEPARATOR . $namePath)) + if(!$writeYaml->checkPath($folderPath . DIRECTORY_SEPARATOR . $namePath)) { - return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not create the folder. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404); + return $response->withJson(array('data' => $this->structureDraft, 'errors' => 'We could not create the folder. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404); } - $write->writeFile($folderPath . DIRECTORY_SEPARATOR . $namePath, 'index.txt', $content); + $this->writeCache->writeFile($folderPath . DIRECTORY_SEPARATOR . $namePath, 'index.txt', $content); # always redirect to a folder $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor'] . $folder->urlRelWoF . '/' . $slug; @@ -774,7 +814,7 @@ class ArticleApiController extends ContentController } # get extended structure - $extended = $write->getYaml('cache', 'structure-extended.yaml'); + $extended = $writeYaml->getYaml('cache', 'structure-extended.yaml'); # create the url for the item $urlWoF = $folder->urlRelWoF . '/' . $slug; @@ -783,21 +823,21 @@ class ArticleApiController extends ContentController $extended[$urlWoF] = ['hide' => false, 'navtitle' => $name]; # store the extended structure - $write->updateYaml('cache', 'structure-extended.yaml', $extended); + $writeYaml->updateYaml('cache', 'structure-extended.yaml', $extended); # update the structure for editor - $this->setStructure($draft = true, $cache = false); + $this->setFreshStructureDraft(); # get item for url and set it active again if(isset($this->params['url'])) { - $activeItem = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl()); + $activeItem = Folder::getItemForUrl($this->structureDraft, $this->params['url'], $this->uri->getBaseUrl()); } # activate this if you want to redirect after creating the page... # $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor'] . $folder->urlRelWoF . '/' . $slug; - return $response->withJson(array('data' => $this->structure, 'errors' => false, 'url' => $url)); + return $response->withJson(array('data' => $this->structureDraft, 'errors' => false, 'url' => $url)); } public function createBaseItem(Request $request, Response $response, $args) @@ -816,67 +856,61 @@ class ArticleApiController extends ContentController $url = false; # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); } + if(!$this->setStructureDraft()){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); } # validate input - if(!$this->validateBaseNaviItem()){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Special Characters not allowed. Length between 1 and 20 chars.', 'url' => $url), 422); } - - # create the name for the new item -# $nameParts = Folder::getStringParts($this->params['item_name']); -# $name = implode("-", $nameParts); -# $slug = $name; + if(!$this->validateBaseNaviItem()){ return $response->withJson(array('data' => $this->structureDraft, 'errors' => 'Special Characters not allowed. Length between 1 and 20 chars.', 'url' => $url), 422); } $name = $this->params['item_name']; - $slug = URLify::filter(iconv(mb_detect_encoding($this->params['item_name'], mb_detect_order(), true), "UTF-8", $this->params['item_name'])); + $slug = Folder::createSlug($this->params['item_name'], $this->settings); # initialize index - $index = 0; + $index = 0; # initialise write object - $write = new WriteYaml(); + $writeYaml = new WriteYaml(); # iterate through the whole content of the new folder $writeError = false; - foreach($this->structure as $item) + foreach($this->structureDraft as $item) { # check, if the same name as new item, then return an error if($item->slug == $slug) { - return $response->withJson(array('data' => $this->structure, 'errors' => 'There is already a page with this name. Please choose another name.', 'url' => $url), 422); + return $response->withJson(array('data' => $this->structureDraft, 'errors' => 'There is already a page with this name. Please choose another name.', 'url' => $url), 422); } - if(!$write->moveElement($item, '', $index)) + if(!$writeYaml->moveElement($item, '', $index)) { $writeError = true; } $index++; } - if($writeError){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Something went wrong. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 422); } + if($writeError){ return $response->withJson(array('data' => $this->structureDraft, 'errors' => 'Something went wrong. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 422); } # add prefix number to the name $namePath = $index > 9 ? $index . '-' . $slug : '0' . $index . '-' . $slug; $folderPath = 'content'; # create default content -# $content = json_encode(['# Add Title', 'Add Content']); $content = json_encode(['# ' . $name, 'Content']); if($this->params['type'] == 'file') { - if(!$write->writeFile($folderPath, $namePath . '.txt', $content)) + if(!$writeYaml->writeFile($folderPath, $namePath . '.txt', $content)) { - return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not create the file. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 422); + return $response->withJson(array('data' => $this->structureDraft, 'errors' => 'We could not create the file. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 422); } } elseif($this->params['type'] == 'folder') { - if(!$write->checkPath($folderPath . DIRECTORY_SEPARATOR . $namePath)) + if(!$this->writeCache->checkPath($folderPath . DIRECTORY_SEPARATOR . $namePath)) { - return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not create the folder. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 422); + return $response->withJson(array('data' => $this->structureDraft, 'errors' => 'We could not create the folder. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 422); } - $write->writeFile($folderPath . DIRECTORY_SEPARATOR . $namePath, 'index.txt', $content); + $writeYaml->writeFile($folderPath . DIRECTORY_SEPARATOR . $namePath, 'index.txt', $content); # activate this if you want to redirect after creating the page... $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor'] . '/' . $slug; @@ -884,7 +918,7 @@ class ArticleApiController extends ContentController # get extended structure - $extended = $write->getYaml('cache', 'structure-extended.yaml'); + $extended = $writeYaml->getYaml('cache', 'structure-extended.yaml'); # create the url for the item $urlWoF = '/' . $slug; @@ -893,20 +927,21 @@ class ArticleApiController extends ContentController $extended[$urlWoF] = ['hide' => false, 'navtitle' => $name]; # store the extended structure - $write->updateYaml('cache', 'structure-extended.yaml', $extended); + $writeYaml->updateYaml('cache', 'structure-extended.yaml', $extended); # update the structure for editor - $this->setStructure($draft = true, $cache = false); + $this->setFreshStructureDraft(); # get item for url and set it active again if(isset($this->params['url'])) { - $activeItem = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl()); + $activeItem = Folder::getItemForUrl($this->structureDraft, $this->params['url'], $this->uri->getBaseUrl()); } - return $response->withJson(array('data' => $this->structure, 'errors' => false, 'url' => $url)); + return $response->withJson(array('data' => $this->structureDraft, 'errors' => false, 'url' => $url)); } + # get the backend navigation public function getNavigation(Request $request, Response $response, $args) { # get params from call @@ -914,7 +949,7 @@ class ArticleApiController extends ContentController $this->uri = $request->getUri()->withUserInfo(''); # set structure - if(!$this->setStructure($draft = true, $cache = false)){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); } + if(!$this->setStructureDraft()){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); } # set information for homepage $this->setHomepage($args = false); @@ -922,10 +957,10 @@ class ArticleApiController extends ContentController # get item for url and set it active again if(isset($this->params['url'])) { - $activeItem = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl()); + $activeItem = Folder::getItemForUrl($this->structureDraft, $this->params['url'], $this->uri->getBaseUrl()); } - return $response->withJson(array('data' => $this->structure, 'homepage' => $this->homepage, 'errors' => false)); + return $response->withJson(array('data' => $this->structureDraft, 'homepage' => $this->homepage, 'errors' => false)); } public function getArticleMarkdown(Request $request, Response $response, $args) @@ -941,7 +976,7 @@ class ArticleApiController extends ContentController } # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } + if(!$this->setStructureDraft()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } # set information for homepage $this->setHomepage($args = false); @@ -1006,7 +1041,7 @@ class ArticleApiController extends ContentController } # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } + if(!$this->setStructureDraft()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } # set information for homepage $this->setHomepage($args = false); diff --git a/system/Controllers/BlockApiController.php b/system/Controllers/ControllerAuthorBlockApi.php similarity index 55% rename from system/Controllers/BlockApiController.php rename to system/Controllers/ControllerAuthorBlockApi.php index cd1bfd2..163c10f 100644 --- a/system/Controllers/BlockApiController.php +++ b/system/Controllers/ControllerAuthorBlockApi.php @@ -4,17 +4,10 @@ namespace Typemill\Controllers; use Slim\Http\Request; use Slim\Http\Response; -use Typemill\Models\Folder; -use Typemill\Models\Write; -use Typemill\Models\WriteYaml; -use Typemill\Models\ProcessImage; -use Typemill\Models\ProcessFile; use Typemill\Extensions\ParsedownExtension; -use \URLify; -class BlockApiController extends ContentController +class ControllerAuthorBlockApi extends ControllerAuthor { - public function addBlock(Request $request, Response $response, $args) { /* get params from call */ @@ -31,7 +24,7 @@ class BlockApiController extends ContentController if(!$this->validateBlockInput()){ return $response->withJson($this->errors,422); } # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } + if(!$this->setStructureDraft()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } $this->setHomepage($args = false); @@ -111,10 +104,10 @@ class BlockApiController extends ContentController $this->setItemPath('txt'); /* update the file */ - if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson)) + if($this->writeCache->writeFile($this->settings['contentFolder'], $this->path, $pageJson)) { # update the internal structure - $this->setStructure($draft = true, $cache = false); + $this->setFreshStructureDraft(); $this->content = $pageMarkdown; } else @@ -229,7 +222,7 @@ class BlockApiController extends ContentController if(!$this->validateBlockInput()){ return $response->withJson($this->errors,422); } # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } + if(!$this->setStructureDraft()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } $this->setHomepage($args = false); @@ -312,11 +305,11 @@ class BlockApiController extends ContentController # set path for the file (or folder) $this->setItemPath('txt'); - /* update the file */ - if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson)) + # update the file + if($this->writeCache->writeFile($this->settings['contentFolder'], $this->path, $pageJson)) { # update the internal structure - $this->setStructure($draft = true, $cache = false); + $this->setFreshStructureDraft(); # updated the content variable $this->content = $pageMarkdown; @@ -326,7 +319,7 @@ class BlockApiController extends ContentController return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404); } - /* parse markdown-file to content-array, if title parse title. */ + # parse markdown-file to content-array, if title parse title. if($this->params['block_id'] == 0) { $blockArray = $parsedown->text($blockMarkdownTitle); @@ -380,12 +373,9 @@ class BlockApiController extends ContentController { return $response->withJson(array('data' => false, 'errors' => ['message' => 'You are not allowed to publish content.']), 403); } - - # validate input - # if(!$this->validateBlockInput()){ return $response->withJson($this->errors,422); } # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } + if(!$this->setStructureDraft()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } $this->setHomepage($args = false); @@ -448,10 +438,10 @@ class BlockApiController extends ContentController $this->setItemPath('txt'); /* update the file */ - if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson)) + if($this->writeCache->writeFile($this->settings['contentFolder'], $this->path, $pageJson)) { # update the internal structure - $this->setStructure($draft = true, $cache = false); + $this->setFreshStructureDraft(); # update this content $this->content = $pageMarkdown; @@ -493,7 +483,7 @@ class BlockApiController extends ContentController } # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } + if(!$this->setStructureDraft()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } $this->setHomepage($args = false); @@ -561,10 +551,10 @@ class BlockApiController extends ContentController $this->setItemPath('txt'); /* update the file */ - if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson)) + if($this->writeCache->writeFile($this->settings['contentFolder'], $this->path, $pageJson)) { # update the internal structure - $this->setStructure($draft = true, $cache = false); + $this->setFreshStructureDraft(); } else { @@ -580,335 +570,4 @@ class BlockApiController extends ContentController return $response->withJson(array('markdown' => $pageMarkdown, 'toc' => $toc, 'errors' => $errors)); } - - public function getMediaLibImages(Request $request, Response $response, $args) - { - # get params from call - $this->params = $request->getParams(); - $this->uri = $request->getUri()->withUserInfo(''); - - $imageProcessor = new ProcessImage($this->settings['images']); - if(!$imageProcessor->checkFolders('images')) - { - return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); - } - - $imagelist = $imageProcessor->scanMediaFlat(); - - return $response->withJson(array('images' => $imagelist)); - } - - public function getMediaLibFiles(Request $request, Response $response, $args) - { - # get params from call - $this->params = $request->getParams(); - $this->uri = $request->getUri()->withUserInfo(''); - - $fileProcessor = new ProcessFile(); - if(!$fileProcessor->checkFolders()) - { - return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); - } - - $filelist = $fileProcessor->scanFilesFlat(); - - return $response->withJson(array('files' => $filelist)); - } - - public function getImage(Request $request, Response $response, $args) - { - # get params from call - $this->params = $request->getParams(); - $this->uri = $request->getUri()->withUserInfo(''); - - $this->setStructure($draft = true, $cache = false); - - $imageProcessor = new ProcessImage($this->settings['images']); - if(!$imageProcessor->checkFolders('images')) - { - return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); - } - - $imageDetails = $imageProcessor->getImageDetails($this->params['name'], $this->structure); - - if($imageDetails) - { - return $response->withJson(array('image' => $imageDetails)); - } - - # return $response->withJson(array('image' => false, 'errors' => 'image name invalid or not found')); - return $response->withJson(['errors' => ['message' => 'Image name invalid or not found.']], 404); - } - - public function getFile(Request $request, Response $response, $args) - { - # get params from call - $this->params = $request->getParams(); - $this->uri = $request->getUri()->withUserInfo(''); - - $this->setStructure($draft = true, $cache = false); - - $fileProcessor = new ProcessFile(); - if(!$fileProcessor->checkFolders()) - { - return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); - } - - $fileDetails = $fileProcessor->getFileDetails($this->params['name'], $this->structure); - - if($fileDetails) - { - return $response->withJson(['file' => $fileDetails]); - } - - return $response->withJson(['errors' => ['message' => 'file name invalid or not found']],404); - } - - public function createImage(Request $request, Response $response, $args) - { - # get params from call - $this->params = $request->getParams(); - $this->uri = $request->getUri()->withUserInfo(''); - - # do this shit in the model ... - $imagename = explode('.', $this->params['name']); - array_pop($imagename); - $imagename = implode('-', $imagename); - $name = URLify::filter(iconv(mb_detect_encoding($imagename, mb_detect_order(), true), "UTF-8", $imagename)); - - $imageProcessor = new ProcessImage($this->settings['images']); - if(!$imageProcessor->checkFolders('images')) - { - return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); - } - - if($imageProcessor->createImage($this->params['image'], $name, $this->settings['images'])) - { - return $response->withJson(array('errors' => false)); - } - - return $response->withJson(array('errors' => ['message' => 'could not store image to temporary folder'])); - } - - public function createFile(Request $request, Response $response, $args) - { - # get params from call - $this->params = $request->getParams(); - $this->uri = $request->getUri()->withUserInfo(''); - - $finfo = finfo_open( FILEINFO_MIME_TYPE ); - $mtype = finfo_file( $finfo, $this->params['file'] ); - finfo_close( $finfo ); - - $allowedMimes = $this->getAllowedMtypes(); - if(!in_array($mtype, $allowedMimes)) - { - return $response->withJson(array('errors' => ['message' => 'File-type is not allowed'])); - } - - # sanitize file name - $filename = basename($this->params['name']); - $filename = explode('.', $this->params['name']); - array_pop($filename); - $filename = implode('-', $filename); - $name = URLify::filter(iconv(mb_detect_encoding($filename, mb_detect_order(), true), "UTF-8", $filename)); - - $fileProcessor = new ProcessFile(); - if(!$fileProcessor->checkFolders()) - { - return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); - } - - if($fileProcessor->createFile($this->params['file'], $name)) - { - return $response->withJson(array('errors' => false, 'name' => $name)); - } - - return $response->withJson(array('errors' => ['message' => 'could not store file to temporary folder'])); - } - - public function publishImage(Request $request, Response $response, $args) - { - $params = $request->getParsedBody(); - - $imageProcessor = new ProcessImage($this->settings['images']); - if(!$imageProcessor->checkFolders()) - { - return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); - } - - $imageUrl = $imageProcessor->publishImage(); - if($imageUrl) - { - # replace the image placeholder in markdown with the image url - $params['markdown'] = str_replace('imgplchldr', $imageUrl, $params['markdown']); - - $request = $request->withParsedBody($params); - - if($params['new']) - { - return $this->addBlock($request, $response, $args); - } - return $this->updateBlock($request, $response, $args); - } - - return $response->withJson(array('errors' => ['message' => 'could not store image to media folder'])); - } - - public function deleteImage(Request $request, Response $response, $args) - { - # get params from call - $this->params = $request->getParams(); - $this->uri = $request->getUri()->withUserInfo(''); - - if(!isset($this->params['name'])) - { - return $response->withJson(array('errors' => ['message' => 'image name is missing'])); - } - - $imageProcessor = new ProcessImage($this->settings['images']); - if(!$imageProcessor->checkFolders()) - { - return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); - } - - $errors = $imageProcessor->deleteImage($this->params['name']); - - return $response->withJson(array('errors' => $errors)); - } - - public function deleteFile(Request $request, Response $response, $args) - { - # get params from call - $this->params = $request->getParams(); - $this->uri = $request->getUri()->withUserInfo(''); - - if(!isset($this->params['name'])) - { - return $response->withJson(array('errors' => ['message' => 'file name is missing'])); - } - - $fileProcessor = new ProcessFile(); - - $errors = false; - if($fileProcessor->deleteFile($this->params['name'])) - { - return $response->withJson(array('errors' => false)); - } - - return $response->withJson(array('errors' => ['message' => 'could not delete the file'])); - } - - public function saveVideoImage(Request $request, Response $response, $args) - { - /* get params from call */ - $this->params = $request->getParams(); - $this->uri = $request->getUri()->withUserInfo(''); - $class = false; - - $imageUrl = $this->params['markdown']; - - if(strpos($imageUrl, 'https://www.youtube.com/watch?v=') !== false) - { - $videoID = str_replace('https://www.youtube.com/watch?v=', '', $imageUrl); - $videoID = strpos($videoID, '&') ? substr($videoID, 0, strpos($videoID, '&')) : $videoID; - $class = 'youtube'; - } - if(strpos($imageUrl, 'https://youtu.be/') !== false) - { - $videoID = str_replace('https://youtu.be/', '', $imageUrl); - $videoID = strpos($videoID, '?') ? substr($videoID, 0, strpos($videoID, '?')) : $videoID; - $class = 'youtube'; - } - - if($class == 'youtube') - { - $videoURLmaxres = 'https://i1.ytimg.com/vi/' . $videoID . '/maxresdefault.jpg'; - $videoURL0 = 'https://i1.ytimg.com/vi/' . $videoID . '/0.jpg'; - } - - $ctx = stream_context_create(array( - 'https' => array( - 'timeout' => 1 - ) - ) - ); - - $imageData = @file_get_contents($videoURLmaxres, 0, $ctx); - if($imageData === false) - { - $imageData = @file_get_contents($videoURL0, 0, $ctx); - if($imageData === false) - { - return $response->withJson(['errors' => ['message' => 'We did not find that video or could not get a preview image.']], 500); - } - } - - $imageData64 = 'data:image/jpeg;base64,' . base64_encode($imageData); - $desiredSizes = $this->settings['images']; - $desiredSizes['live'] = ['width' => 560, 'height' => 315]; - $imageProcessor = new ProcessImage($desiredSizes); - - if(!$imageProcessor->checkFolders('images')) - { - return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); - } - - if(!$imageProcessor->createImage($imageData64, 'youtube-' . $videoID, $desiredSizes, $overwrite = true)) - { - return $response->withJson(['errors' => ['message' => 'We could not create the image.']], 500); - } - - $imageUrl = $imageProcessor->publishImage(); - if($imageUrl) - { - - $this->params['markdown'] = '![' . $class . '-video](' . $imageUrl . ' "click to load video"){#' . $videoID. ' .' . $class . '}'; - - $request = $request->withParsedBody($this->params); - - if($this->params['new']) - { - return $this->addBlock($request, $response, $args); - } - return $this->updateBlock($request, $response, $args); - } - - return $response->withJson(array('errors' => ['message' => 'could not store the preview image'])); - } - - private function getAllowedMtypes() - { - return array( - 'application/zip', - 'application/gzip', - 'application/vnd.rar', - 'application/vnd.visio', - 'application/vnd.ms-excel', - 'application/vnd.ms-powerpoint', - 'application/vnd.ms-word.document.macroEnabled.12', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/vnd.apple.keynote', - 'application/vnd.apple.mpegurl', - 'application/vnd.apple.numbers', - 'application/vnd.apple.pages', - 'application/vnd.amazon.mobi8-ebook', - 'application/epub+zip', - 'application/pdf', - 'image/png', - 'image/jpeg', - 'image/jpg', - 'image/gif', - 'image/svg+xml', - 'font/*', - 'audio/mpeg', - 'audio/mp4', - 'audio/ogg', - 'video/mpeg', - 'video/mp4', - 'video/ogg', - ); - } } \ No newline at end of file diff --git a/system/Controllers/ContentBackendController.php b/system/Controllers/ControllerAuthorEditor.php similarity index 81% rename from system/Controllers/ContentBackendController.php rename to system/Controllers/ControllerAuthorEditor.php index 9b67d9d..c410f43 100644 --- a/system/Controllers/ContentBackendController.php +++ b/system/Controllers/ControllerAuthorEditor.php @@ -4,11 +4,10 @@ namespace Typemill\Controllers; use Slim\Http\Request; use Slim\Http\Response; -use Slim\Views\Twig; use Typemill\Models\Folder; use Typemill\Extensions\ParsedownExtension; -class ContentBackendController extends ContentController +class ControllerAuthorEditor extends ControllerAuthor { /** * Show Content for raw editor @@ -25,7 +24,7 @@ class ContentBackendController extends ContentController $this->params = isset($args['params']) ? ['url' => $this->uri->getBasePath() . '/' . $args['params']] : ['url' => $this->uri->getBasePath()]; # set structure - if(!$this->setStructure($draft = true)){ return $this->renderIntern404($response, array( 'navigation' => true, 'content' => $this->errors )); } + if(!$this->setStructureDraft()){ return $this->renderIntern404($response, array( 'navigation' => true, 'content' => $this->errors )); } # set information for homepage $this->setHomepage($args); @@ -37,7 +36,7 @@ class ContentBackendController extends ContentController $this->checkContentOwnership(); # get the breadcrumb (here we need it only to mark the actual item active in navigation) - $breadcrumb = isset($this->item->keyPathArray) ? Folder::getBreadcrumb($this->structure, $this->item->keyPathArray) : false; + $breadcrumb = isset($this->item->keyPathArray) ? Folder::getBreadcrumb($this->structureDraft, $this->item->keyPathArray) : false; # set the status for published and drafted $this->setPublishStatus(); @@ -78,10 +77,10 @@ class ContentBackendController extends ContentController } } - return $this->render($response, 'editor/editor-raw.twig', array( + return $this->renderIntern($response, 'editor/editor-raw.twig', array( 'acl' => $this->c->acl, 'mycontent' => $this->mycontent, - 'navigation' => $this->structure, + 'navigation' => $this->structureDraft, 'homepage' => $this->homepage, 'title' => $title, 'content' => $content, @@ -105,13 +104,13 @@ class ContentBackendController extends ContentController $this->params = isset($args['params']) ? ['url' => $this->uri->getBasePath() . '/' . $args['params']] : ['url' => $this->uri->getBasePath()]; # set structure - if(!$this->setStructure($draft = true)){ return $this->renderIntern404($response, array( 'navigation' => true, 'content' => $this->errors )); } + if(!$this->setStructureDraft()){ return $this->renderIntern404($response, array( 'navigation' => true, 'content' => $this->errors )); } # set information for homepage $this->setHomepage($args); # set item - if(!$this->setItem()){ return $this->renderIntern404($response, array( 'navigation' => $this->structure, 'settings' => $this->settings, 'content' => $this->errors )); } + if(!$this->setItem()){ return $this->renderIntern404($response, array( 'navigation' => $this->structureDraft, 'settings' => $this->settings, 'content' => $this->errors )); } # we have to check ownership here to use it for permission-check in tempates $this->checkContentOwnership(); @@ -168,20 +167,15 @@ class ContentBackendController extends ContentController unset($content[0]); } - return $this->render($response, 'editor/editor-blox.twig', array( + return $this->renderIntern($response, 'editor/editor-blox.twig', array( 'acl' => $this->c->acl, 'mycontent' => $this->mycontent, - 'navigation' => $this->structure, - 'homepage' => $this->homepage, + 'navigation' => $this->structureDraft, + 'homepage' => $this->homepage, 'title' => $title, 'content' => $content, - 'item' => $this->item, + 'item' => $this->item, 'settings' => $this->settings )); } - - public function showEmpty(Request $request, Response $response, $args) - { - return $this->renderIntern404($response, array( 'settings' => $this->settings )); - } } \ No newline at end of file diff --git a/system/Controllers/MediaApiController.php b/system/Controllers/ControllerAuthorMediaApi.php similarity index 95% rename from system/Controllers/MediaApiController.php rename to system/Controllers/ControllerAuthorMediaApi.php index 005eac7..433e4b8 100644 --- a/system/Controllers/MediaApiController.php +++ b/system/Controllers/ControllerAuthorMediaApi.php @@ -7,9 +7,8 @@ use Slim\Http\Response; use Typemill\Models\ProcessImage; use Typemill\Models\ProcessFile; use Typemill\Controllers\BlockApiController; -use \URLify; -class MediaApiController extends ContentController +class ControllerAuthorMediaApi extends ControllerAuthor { public function getMediaLibImages(Request $request, Response $response, $args) { @@ -51,7 +50,7 @@ class MediaApiController extends ContentController $this->params = $request->getParams(); $this->uri = $request->getUri()->withUserInfo(''); - $this->setStructure($draft = true, $cache = false); + $this->setStructureDraft(); $imageProcessor = new ProcessImage($this->settings['images']); if(!$imageProcessor->checkFolders('images')) @@ -59,7 +58,7 @@ class MediaApiController extends ContentController return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); } - $imageDetails = $imageProcessor->getImageDetails($this->params['name'], $this->structure); + $imageDetails = $imageProcessor->getImageDetails($this->params['name'], $this->structureDraft); if($imageDetails) { @@ -75,7 +74,7 @@ class MediaApiController extends ContentController $this->params = $request->getParams(); $this->uri = $request->getUri()->withUserInfo(''); - $this->setStructure($draft = true, $cache = false); + $this->setStructureDraft(); $fileProcessor = new ProcessFile(); if(!$fileProcessor->checkFolders()) @@ -83,7 +82,7 @@ class MediaApiController extends ContentController return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); } - $fileDetails = $fileProcessor->getFileDetails($this->params['name'], $this->structure); + $fileDetails = $fileProcessor->getFileDetails($this->params['name'], $this->structureDraft); if($fileDetails) { diff --git a/system/Controllers/MetaApiController.php b/system/Controllers/ControllerAuthorMetaApi.php similarity index 80% rename from system/Controllers/MetaApiController.php rename to system/Controllers/ControllerAuthorMetaApi.php index 7cb7796..7634014 100644 --- a/system/Controllers/MetaApiController.php +++ b/system/Controllers/ControllerAuthorMetaApi.php @@ -9,7 +9,7 @@ use Typemill\Models\WriteMeta; use Typemill\Models\Folder; use Typemill\Events\OnMetaDefinitionsLoaded; -class MetaApiController extends ContentController +class ControllerAuthorMetaApi extends ControllerAuthor { # get the standard meta-definitions and the meta-definitions from plugins (same for all sites) public function getMetaDefinitions(Request $request, Response $response, $args) @@ -75,7 +75,7 @@ class MetaApiController extends ContentController $this->uri = $request->getUri()->withUserInfo(''); # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); } + if(!$this->setStructureDraft()){ return $response->withJson($this->errors, 404); } # set information for homepage $this->setHomepage($args = false); @@ -148,16 +148,6 @@ class MetaApiController extends ContentController $metadefinitions[$tabname]['fields'][$fieldname]['options'] = $userroles; } } - - /* - # special treatment for customfields - if(isset($fielddefinitions['type']) && ($fielddefinitions['type'] == 'customfields' ) && $metadata[$tabname][$fieldname] ) - { - - $metadata[$tabname][$fieldname] = $this->customfieldsPrepareForEdit($metadata[$tabname][$fieldname]); - - } - */ } } @@ -190,7 +180,7 @@ class MetaApiController extends ContentController } # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); } + if(!$this->setStructureDraft()){ return $response->withJson($this->errors, 404); } # set information for homepage $this->setHomepage($args = false); @@ -256,20 +246,6 @@ class MetaApiController extends ContentController { $errors[$tab][$fieldName] = $result[$fieldName][0]; } - - /* - # special treatment for customfields - if($fieldDefinition && isset($fieldDefinition['type']) && ($fieldDefinition['type'] == 'customfields' ) ) - { - $arrayFeatureOn = false; - if(isset($fieldDefinition['data']) && ($fieldDefinition['data'] == 'array')) - { - $arrayFeatureOn = true; - } - - $metaInput[$fieldName] = $this->customfieldsPrepareForSave($metaInput[$fieldName], $arrayFeatureOn); - } - */ } } @@ -312,7 +288,7 @@ class MetaApiController extends ContentController $writeMeta->renamePost($this->item->pathWithoutType, $newPathWithoutType); # recreate the draft structure - $this->setStructure($draft = true, $cache = false); + $this->setFreshStructureDraft(); # update item $this->setItem(); @@ -372,16 +348,25 @@ class MetaApiController extends ContentController $writeMeta->updateYaml('cache', 'structure-extended.yaml', $extended); # recreate the draft structure - $this->setStructure($draft = true, $cache = false); + $this->setFreshStructureDraft(); + + # recreate the live structure + $this->setFreshStructureLive(); + + # recreate the navigation + $this->setFreshNavigation(); + + # update the sitemap + $this->updateSitemap(); # update item $this->setItem(); # set item in navigation active again - $activeItem = Folder::getItemForUrl($this->structure, $this->item->urlRel, $this->uri->getBaseUrl()); + $activeItem = Folder::getItemForUrl($this->structureDraft, $this->item->urlRel, $this->uri->getBaseUrl()); # send new structure to frontend - $structure = $this->structure; + $structure = $this->structureDraft; } # return with the new metadata @@ -410,75 +395,6 @@ class MetaApiController extends ContentController return $flattab; } - - # can be deleted ?? - private function customfieldsPrepareForEdit($customfields) - { - # to edit fields in vue we have to transform the arrays in yaml into an array of objects like [{key: abc, value: xyz}{...}] - - $customfieldsForEdit = []; - - foreach($customfields as $key => $value) - { - $valuestring = $value; - - # and make sure that arrays are transformed back into strings - if(isset($value) && is_array($value)) - { - $valuestring = '- ' . implode(PHP_EOL . '- ', $value); - } - - $customfieldsForEdit[] = ['key' => $key, 'value' => $valuestring]; - } - - return $customfieldsForEdit; - } - - # can be deleted? - private function customfieldsPrepareForSave($customfields, $arrayFeatureOn) - { - # we have to convert the incoming array of objects from vue [{key: abc, value: xyz}{...}] into key-value arrays for yaml. - - $customfieldsForSave = []; - - foreach($customfields as $valuePair) - { - # doupbe check, not really needed because it is validated already - if(!isset($valuePair['key']) OR ($valuePair['key'] == '')) - { - # do not use data without valid keys - continue; - } - - $key = $valuePair['key']; - $value = ''; - - if(isset($valuePair['value'])) - { - $value = $valuePair['value']; - - # check if value is formatted as a list, then transform it into an array - if($arrayFeatureOn) - { - # normalize line breaks, php-eol does not work here - $cleanvalue = str_replace(array("\r\n", "\r"), "\n", $valuePair['value']); - $cleanvalue = trim($cleanvalue, "\n"); - - $arrayValues = explode("\n- ",$cleanvalue); - - if(count($arrayValues) > 1) - { - $value = array_map(function($item) { return trim($item, '- '); }, $arrayValues); - } - } - } - - $customfieldsForSave[$key] = $value; - } - - return $customfieldsForSave; - } - protected function hasChanged($input, $page, $field) { if(isset($input[$field]) && isset($page[$field]) && $input[$field] == $page[$field]) diff --git a/system/Controllers/AuthController.php b/system/Controllers/ControllerFrontendAuth.php similarity index 96% rename from system/Controllers/AuthController.php rename to system/Controllers/ControllerFrontendAuth.php index 83713b9..00ee77f 100644 --- a/system/Controllers/AuthController.php +++ b/system/Controllers/ControllerFrontendAuth.php @@ -10,7 +10,7 @@ use Typemill\Models\User; use Typemill\Models\WriteYaml; use Typemill\Extensions\ParsedownExtension; -class AuthController extends Controller +class ControllerFrontendAuth extends ControllerShared { # redirect if visit /setup route public function redirect(Request $request, Response $response) diff --git a/system/Controllers/FormController.php b/system/Controllers/ControllerFrontendForms.php similarity index 91% rename from system/Controllers/FormController.php rename to system/Controllers/ControllerFrontendForms.php index a9c4b86..8d03d84 100644 --- a/system/Controllers/FormController.php +++ b/system/Controllers/ControllerFrontendForms.php @@ -5,11 +5,8 @@ namespace Typemill\Controllers; use Typemill\Models\Validation; use Typemill\Models\WriteYaml; -class FormController extends Controller +class ControllerFrontendForms extends ControllerShared { - /************************************* - ** SAVE THEME- AND PLUGIN-SETTINGS ** - *************************************/ public function savePublicForm($request, $response, $args) { diff --git a/system/Controllers/SetupController.php b/system/Controllers/ControllerFrontendSetup.php similarity index 96% rename from system/Controllers/SetupController.php rename to system/Controllers/ControllerFrontendSetup.php index c175c7a..2e13297 100644 --- a/system/Controllers/SetupController.php +++ b/system/Controllers/ControllerFrontendSetup.php @@ -7,7 +7,7 @@ use Typemill\Models\Validation; use Typemill\Models\User; use Typemill\Models\Write; -class SetupController extends Controller +class ControllerFrontendSetup extends ControllerShared { # redirect if visit /setup route diff --git a/system/Controllers/PageController.php b/system/Controllers/ControllerFrontendWebsite.php similarity index 76% rename from system/Controllers/PageController.php rename to system/Controllers/ControllerFrontendWebsite.php index ba3aedc..553de55 100644 --- a/system/Controllers/PageController.php +++ b/system/Controllers/ControllerFrontendWebsite.php @@ -3,14 +3,8 @@ namespace Typemill\Controllers; use Typemill\Models\Folder; -use Typemill\Models\WriteCache; -use Typemill\Models\WriteSitemap; -use Typemill\Models\WriteYaml; use Typemill\Models\WriteMeta; -use \Symfony\Component\Yaml\Yaml; -use Typemill\Models\VersionCheck; -use Typemill\Models\Markdown; -use Typemill\Events\OnCacheUpdated; +use Typemill\Extensions\ParsedownExtension; use Typemill\Events\OnPagetreeLoaded; use Typemill\Events\OnBreadcrumbLoaded; use Typemill\Events\OnItemLoaded; @@ -20,78 +14,59 @@ use Typemill\Events\OnMarkdownLoaded; use Typemill\Events\OnContentArrayLoaded; use Typemill\Events\OnHtmlLoaded; use Typemill\Events\OnRestrictionsLoaded; -use Typemill\Extensions\ParsedownExtension; -class PageController extends Controller +class ControllerFrontendWebsite extends ControllerShared { public function index($request, $response, $args) - { + { + # 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(); - /* Initiate Variables */ - $structure = false; - $contentHTML = false; - $item = false; - $home = false; - $breadcrumb = false; - $currentpage = false; - $pathToContent = $this->settings['rootPath'] . $this->settings['contentFolder']; - $cache = new WriteCache(); - $uri = $request->getUri()->withUserInfo(''); - $base_url = $uri->getBaseUrl(); - - $this->pathToContent = $pathToContent; - - try + # if there is no structure at all, the content folder is probably empty + if(!$this->setStructureLive()) { - # if the cached structure is still valid, use it - if($cache->validate('cache', 'lastCache.txt', 600)) - { - $structure = $cache->getCachedStructure(); - } - else - { - # dispatch message that the cache has been refreshed - $this->c->dispatcher->dispatch('onCacheUpdated', new OnCacheUpdated(false)); - } + return $this->render($response, '/index.twig', array( 'content' => '

No Content

Your content folder is empty.

' )); + } - if(!isset($structure) OR !$structure) - { - # if not, get a fresh structure of the content folder - $structure = $cache->getFreshStructure($pathToContent, $uri); + # 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 there is no structure at all, the content folder is probably empty - if(!$structure) - { - $content = '

No Content

Your content folder is empty.

'; + # 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); - return $this->render($response, '/index.twig', array( 'content' => $content )); - } - elseif(!$cache->validate('cache', 'lastSitemap.txt', 86400)) - { - # update sitemap - $sitemap = new WriteSitemap(); - $sitemap->updateSitemap('cache', 'sitemap.xml', 'lastSitemap.txt', $structure, $uri->getBaseUrl()); - } - } + # update the internal structure + $this->setFreshStructureDraft(); - # dispatch event and let others manipulate the structure - $structure = $this->c->dispatcher->dispatch('onPagetreeLoaded', new OnPagetreeLoaded($structure))->getData(); - } - catch (Exception $e) - { - echo $e->getMessage(); - exit(1); + # update the public structure + $this->setFreshStructureLive(); + + # update the navigation + $this->setFreshNavigation(); + + # update the sitemap + $this->updateSitemap(); } - # get meta-Information - $writeMeta = new WriteMeta(); - $theme = $this->settings['theme']; + # 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 - $customcss = $writeMeta->checkFile('cache', $theme . '-custom.css'); + $theme = $this->settings['theme']; + $customcss = $this->writeCache->checkFile('cache', $theme . '-custom.css'); if($customcss) { - $this->c->assets->addCSS($base_url . '/cache/' . $theme . '-custom.css'); + $this->c->assets->addCSS($this->base_url . '/cache/' . $theme . '-custom.css'); } $logo = false; @@ -106,13 +81,13 @@ class PageController extends Controller $favicon = true; } - # the navigation is a copy of the structure without the hidden pages - $navigation = $cache->getCache('cache', 'navigation.txt'); + # 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 = $structure; + $navigation = $this->structureLive; } # start pagination @@ -149,23 +124,23 @@ class PageController extends Controller if(empty($args)) { $home = true; - $item = Folder::getItemForUrl($navigation, $uri->getBasePath(), $uri->getBaseUrl(), NULL, $home); - $urlRel = $uri->getBasePath(); + $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 = $uri->getBasePath() . '/' . trim($args['params'], "/"); + $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($structure, $urlRel, $uri->getBasePath()); + $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, $uri->getBaseUrl(), NULL, $home); + $item = Folder::getItemForUrl($navigation, $urlRel, $this->uri->getBaseUrl(), NULL, $home); } # if there is still no item, return a 404-page @@ -174,7 +149,7 @@ class PageController extends Controller return $this->render404($response, array( 'navigation' => $navigation, 'settings' => $this->settings, - 'base_url' => $base_url, + 'base_url' => $this->base_url, 'title' => false, 'content' => false, 'item' => false, @@ -187,7 +162,6 @@ class PageController extends Controller } } - if(isset($item->hide)) { # if it is a hidden page @@ -195,11 +169,11 @@ class PageController extends Controller { # get breadcrumb for page and set pages active # use structure here because the hidden item is not part of the navigation - $breadcrumb = Folder::getBreadcrumb($structure, $item->keyPathArray); + $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($structure, $item); + $item = Folder::getPagingForItem($this->structureLive, $item); } else { @@ -217,7 +191,7 @@ class PageController extends Controller $item = $this->c->dispatcher->dispatch('onItemLoaded', new OnItemLoaded($item))->getData(); # set the filepath - $filePath = $pathToContent . $item->path; + $filePath = $this->pathToContent . $item->path; # check if url is a folder and add index.md if($item->elementType == 'folder') @@ -230,6 +204,9 @@ class PageController extends Controller # 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); @@ -243,7 +220,7 @@ class PageController extends Controller $itemUrl = isset($item->urlRel) ? $item->urlRel : false; /* initialize parsedown */ - $parsedown = new ParsedownExtension($base_url, $this->settings); + $parsedown = new ParsedownExtension($this->base_url, $this->settings); /* set safe mode to escape javascript and html in markdown */ $parsedown->setSafeMode(true); @@ -334,21 +311,21 @@ class PageController extends Controller $firstImage = false; if($img_url) { - $firstImage = array('img_url' => $base_url . '/' . $img_url, 'img_alt' => $img_alt); + $firstImage = array('img_url' => $this->base_url . '/' . $img_url, 'img_alt' => $img_alt); } $route = empty($args) && isset($this->settings['themes'][$theme]['cover']) ? '/cover.twig' : '/index.twig'; return $this->render($response, $route, [ 'home' => $home, - 'navigation' => $navigation, + 'navigation' => $navigation, 'title' => $title, 'content' => $contentHTML, 'item' => $item, 'breadcrumb' => $breadcrumb, 'settings' => $this->settings, + 'base_url' => $this->base_url, 'metatabs' => $metatabs, - 'base_url' => $base_url, 'image' => $firstImage, 'logo' => $logo, 'favicon' => $favicon, diff --git a/system/Controllers/SettingsController.php b/system/Controllers/ControllerSettings.php similarity index 94% rename from system/Controllers/SettingsController.php rename to system/Controllers/ControllerSettings.php index f73b4b6..9f6eeda 100644 --- a/system/Controllers/SettingsController.php +++ b/system/Controllers/ControllerSettings.php @@ -14,7 +14,7 @@ use Typemill\Events\OnUserfieldsLoaded; use Typemill\Events\OnSystemnaviLoaded; use Typemill\Events\OnUserDeleted; -class SettingsController extends Controller +class ControllerSettings extends ControllerShared { public function showBlank($request, $response, $args) @@ -22,7 +22,7 @@ class SettingsController extends Controller $user = new User(); $settings = $this->c->get('settings'); $route = $request->getAttribute('route'); - $navigation = $this->getNavigation(); + $navigation = $this->getMainNavigation(); $content = '

Hello

I am the showBlank method from the settings controller

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.

'; @@ -48,7 +48,7 @@ class SettingsController extends Controller $languages = $this->getLanguages(); $locale = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? substr($_SERVER["HTTP_ACCEPT_LANGUAGE"],0,2) : 'en'; $route = $request->getAttribute('route'); - $navigation = $this->getNavigation(); + $navigation = $this->getMainNavigation(); # set navigation active $navigation['System']['active'] = true; @@ -118,6 +118,8 @@ class SettingsController extends Controller 'recoversubject' => $newSettings['recoversubject'], 'recovermessage' => $newSettings['recovermessage'], 'securitylog' => isset($newSettings['securitylog']) ? true : null, + 'oldslug' => isset($newSettings['oldslug']) ? true : null, + 'refreshcache' => isset($newSettings['refreshcache']) ? true : null, ); # https://www.slimframework.com/docs/v3/cookbook/uploading-files.html; @@ -294,7 +296,7 @@ class SettingsController extends Controller /* add the users for navigation */ $route = $request->getAttribute('route'); - $navigation = $this->getNavigation(); + $navigation = $this->getMainNavigation(); # set navigation active $navigation['Themes']['active'] = true; @@ -363,7 +365,7 @@ class SettingsController extends Controller } $route = $request->getAttribute('route'); - $navigation = $this->getNavigation(); + $navigation = $this->getMainNavigation(); # set navigation active $navigation['Plugins']['active'] = true; @@ -591,7 +593,7 @@ class SettingsController extends Controller $userform = $fieldsModel->getFields($userSettings, 'users', 'user', $fieldDefinitions); $route = $request->getAttribute('route'); - $navigation = $this->getNavigation(); + $navigation = $this->getMainNavigation(); # set navigation active $navigation['Account']['active'] = true; @@ -647,7 +649,7 @@ class SettingsController extends Controller $userform = $fieldsModel->getFields($userSettings, 'users', 'user', $fieldDefinitions); $route = $request->getAttribute('route'); - $navigation = $this->getNavigation(); + $navigation = $this->getMainNavigation(); # set navigation active $navigation['Users']['active'] = true; @@ -675,7 +677,7 @@ class SettingsController extends Controller $userdata = array(); $route = $request->getAttribute('route'); $settings = $this->c->get('settings'); - $navigation = $this->getNavigation(); + $navigation = $this->getMainNavigation(); # set navigation active $navigation['Users']['active'] = true; @@ -764,7 +766,7 @@ class SettingsController extends Controller $userroles = $this->c->acl->getRoles(); $route = $request->getAttribute('route'); $settings = $this->c->get('settings'); - $navigation = $this->getNavigation(); + $navigation = $this->getMainNavigation(); # set navigation active $navigation['Users']['active'] = true; @@ -969,22 +971,26 @@ class SettingsController extends Controller public function clearCache($request, $response, $args) { - $settings = $this->c->get('settings'); - $dir = $settings['basePath'] . 'cache'; - $uri = $request->getUri()->withUserInfo(''); - $pathToContent = $settings['rootPath'] . $settings['contentFolder']; - - $writeCache = new writeCache(); - - $error = $writeCache->deleteCacheFiles($dir); + $this->uri = $request->getUri()->withUserInfo(''); + $dir = $this->settings['basePath'] . 'cache'; + $error = $this->writeCache->deleteCacheFiles($dir); if($error) { return $response->withJson(['errors' => $error], 500); } - # this recreates the cache for structure, structure-extended and navigation - $writeCache->getFreshStructure($pathToContent, $uri); + # create a new draft structure + $this->setFreshStructureDraft(); + + # create a new draft structure + $this->setFreshStructureLive(); + + # create a new draft structure + $this->setFreshNavigation(); + + # update the sitemap + $this->updateSitemap(); return $response->withJson(array('errors' => false)); } @@ -1083,7 +1089,7 @@ class SettingsController extends Controller ); } - private function getNavigation() + private function getMainNavigation() { $navigation = [ 'System' => ['routename' => 'settings.show', 'icon' => 'icon-wrench', 'aclresource' => 'system', 'aclprivilege' => 'view'], diff --git a/system/Controllers/ControllerShared.php b/system/Controllers/ControllerShared.php new file mode 100644 index 0000000..3309388 --- /dev/null +++ b/system/Controllers/ControllerShared.php @@ -0,0 +1,446 @@ +c = $c; + $this->settings = $this->c->get('settings'); + + # used everywhere so instantiate it + $this->writeCache = new writeCache(); + + $this->c->dispatcher->dispatch('onTwigLoaded'); + } + + # render page for frontend + protected function render($response, $route, $data) + { + # why commented this out?? + $data = $this->c->dispatcher->dispatch('onPageReady', new OnPageReady($data))->getData(); + + if(isset($_SESSION['old'])) + { + unset($_SESSION['old']); + } + + $response = $response->withoutHeader('Server'); + $response = $response->withAddedHeader('X-Powered-By', 'Typemill'); + + if(!isset($this->settings['headersoff']) or !$this->settings['headersoff']) + { + $response = $response->withAddedHeader('X-Content-Type-Options', 'nosniff'); + $response = $response->withAddedHeader('X-Frame-Options', 'SAMEORIGIN'); + $response = $response->withAddedHeader('X-XSS-Protection', '1;mode=block'); + $response = $response->withAddedHeader('Referrer-Policy', 'no-referrer-when-downgrade'); + if($this->c->request->getUri()->getScheme() == 'https') + { + $response = $response->withAddedHeader('Strict-Transport-Security', 'max-age=63072000'); + } + } + + return $this->c->view->render($response, $route, $data); + } + + # render 404 for frontend + protected function render404($response, $data = NULL) + { + return $this->c->view->render($response->withStatus(404), '/404.twig', $data); + } + + # render page for authors (admin-area) + protected function renderIntern($response, $route, $data) + { + if(isset($_SESSION['old'])) + { + unset($_SESSION['old']); + } + + $response = $response->withoutHeader('Server'); + $response = $response->withAddedHeader('X-Powered-By', 'Typemill'); + + if(!isset($this->settings['headersoff']) or !$this->settings['headersoff']) + { + $response = $response->withAddedHeader('X-Content-Type-Options', 'nosniff'); + $response = $response->withAddedHeader('X-Frame-Options', 'SAMEORIGIN'); + $response = $response->withAddedHeader('X-XSS-Protection', '1;mode=block'); + $response = $response->withAddedHeader('Referrer-Policy', 'no-referrer-when-downgrade'); + if($this->c->request->getUri()->getScheme() == 'https') + { + $response = $response->withAddedHeader('Strict-Transport-Security', 'max-age=63072000'); + } + } + + return $this->c->view->render($response, $route, $data); + } + + # render 404 for authors + protected function renderIntern404($response, $data = NULL) + { + return $this->c->view->render($response->withStatus(404), '/intern404.twig', $data); + } + + # reads the cached structure with published and non-published pages for the author + protected function setStructureDraft() + { + # get the cached structure + $this->structureDraft = $this->writeCache->getCache('cache', $this->structureDraftName); + + # if there is no cached structure + if(!$this->structureDraft) + { + return $this->setFreshStructureDraft(); + } + + return true; + } + + # creates a fresh structure with published and non-published pages for the author + protected function setFreshStructureDraft() + { + # scan the content of the folder + $pagetreeDraft = Folder::scanFolder($this->settings['rootPath'] . $this->settings['contentFolder'], $draft = true ); + + # if there is content, then get the content details + if(count($pagetreeDraft) > 0) + { + # get the extended structure files with changes like navigation title or hidden pages + $yaml = new writeYaml(); + $extended = $this->getExtended(); + + # create an array of object with the whole content of the folder and changes from extended file + $this->structureDraft = Folder::getFolderContentDetails($pagetreeDraft, $extended, $this->settings, $this->uri->getBaseUrl(), $this->uri->getBasePath()); + + # cache structure draft + $this->writeCache->updateCache('cache', $this->structureDraftName, 'lastCache.txt', $this->structureDraft); + + return true; + } + + return false; + } + + # reads the cached structure of published pages + protected function setStructureLive() + { + # get the cached structure + $this->structureLive = $this->writeCache->getCache('cache', $this->structureLiveName); + + # if there is no cached structure + if(!$this->structureLive) + { + return $this->setFreshStructureLive(); + } + + return true; + } + + # creates a fresh structure with published pages + protected function setFreshStructureLive() + { + # scan the content of the folder + $pagetreeLive = Folder::scanFolder($this->settings['rootPath'] . $this->settings['contentFolder'], $draft = false ); + + # if there is content, then get the content details + if(count($pagetreeLive) > 0) + { + # get the extended structure files with changes like navigation title or hidden pages + $yaml = new writeYaml(); + $extended = $this->getExtended(); + + # create an array of object with the whole content of the folder and changes from extended file + $this->structureLive = Folder::getFolderContentDetails($pagetreeLive, $extended, $this->settings, $this->uri->getBaseUrl(), $this->uri->getBasePath()); + + # cache structure live + $this->writeCache->updateCache('cache', $this->structureLiveName, 'lastCache.txt', $this->structureLive); + + return true; + } + + return false; + } + + # reads the live navigation from cache (live structure without hidden pages) + protected function setNavigation() + { + # get the cached structure + $this->navigation = $this->writeCache->getCache('cache', 'navigation.txt'); + + # if there is no cached structure + if(!$this->navigation) + { + return $this->setFreshNavigation(); + } + + return true; + } + + # creates a fresh live navigation (live structure without hidden pages) + protected function setFreshNavigation() + { + + if(!$this->extended) + { + $extended = $this->getExtended(); + } + + if($this->containsHiddenPages($this->extended)) + { + if(!$this->structureLive) + { + $this->setStructureLive(); + } + + $structureLive = $this->structureLive; + $this->navigation = $this->createNavigation($structureLive); + + # cache navigation + $this->writeCache->updateCache('cache', 'navigation.txt', false, $this->navigation); + + return true; + } + + # make sure no old navigation file is left + $this->writeCache->deleteFileWithPath('cache' . DIRECTORY_SEPARATOR . 'navigation.txt'); + + return false; + } + + # create navigation from structure + protected function createNavigation($structureLive) + { + foreach ($structureLive as $key => $element) + { + if($element->hide === true) + { + unset($structureLive[$key]); + } + elseif(isset($element->folderContent)) + { + $structureLive[$key]->folderContent = $this->createNavigation($element->folderContent); + } + } + + return $structureLive; + } + + # controllerFrontendWebsite, but not in use, makes no sense to check on each page load + public function checkSitemap() + { + if(!$this->writeCache->getCache('cache', 'sitemap.xml')) + { + if(!$this->structureLive) + { + $this->setStructureLive(); + } + + $this->updateSitemap(); + } + + return true; + } + + public function updateSitemap() + { + $sitemap = '' . "\n"; + $sitemap .= '' . "\n"; + $sitemap = $this->addUrlSet($sitemap, $this->uri->getBaseUrl()); + $sitemap .= $this->generateUrlSets($this->structureLive); + $sitemap .= ''; + + $this->writeCache->writeFile('cache', 'sitemap.xml', $sitemap); + } + + public function generateUrlSets($structureLive) + { + $urlset = ''; + + foreach($structureLive as $item) + { + if($item->elementType == 'folder') + { + $urlset = $this->addUrlSet($urlset, $item->urlAbs); + $urlset .= $this->generateUrlSets($item->folderContent, $urlset); + } + else + { + $urlset = $this->addUrlSet($urlset, $item->urlAbs); + } + } + return $urlset; + } + + public function addUrlSet($urlset, $url) + { + $urlset .= ' ' . "\n"; + $urlset .= ' ' . $url . '' . "\n"; + $urlset .= ' ' . "\n"; + return $urlset; + } + + protected function getExtended() + { + $yaml = new writeYaml(); + + if(!$this->extended) + { + $this->extended = $yaml->getYaml('cache', 'structure-extended.yaml'); + } + + if(!$this->extended) + { + # scan the content of the folder + $pagetreeDraft = Folder::scanFolder($this->settings['rootPath'] . $this->settings['contentFolder'], $draft = true ); + + # if there is content, then get the content details + if(count($pagetreeDraft) == 0) + { + return false; + } + + # create an array of object with the whole content of the folder and changes from extended file + $structureDraft = Folder::getFolderContentDetails($pagetreeDraft, $extended = false, $this->settings, $this->uri->getBaseUrl(), $this->uri->getBasePath()); + + $this->extended = $this->createExtended($this->settings['rootPath'] . $this->settings['contentFolder'], $yaml, $structureDraft); + + $yaml->updateYaml('cache', 'structure-extended.yaml', $this->extended); + } + + return $this->extended; + } + + # creates a file that holds all hide flags and navigation titles + # reads all meta-files and creates an array with url => ['hide' => bool, 'navtitle' => 'bla'] + public function createExtended($contentPath, $yaml, $structureLive, $extended = NULL) + { + if(!$extended) + { + $extended = []; + } + + foreach ($structureLive as $key => $item) + { + # $filename = ($item->elementType == 'folder') ? DIRECTORY_SEPARATOR . 'index.yaml' : $item->pathWithoutType . '.yaml'; + $filename = $item->pathWithoutType . '.yaml'; + + if(file_exists($contentPath . $filename)) + { + # read file + $meta = $yaml->getYaml('content', $filename); + + $extended[$item->urlRelWoF]['hide'] = isset($meta['meta']['hide']) ? $meta['meta']['hide'] : false; + $extended[$item->urlRelWoF]['navtitle'] = isset($meta['meta']['navtitle']) ? $meta['meta']['navtitle'] : ''; + } + + if ($item->elementType == 'folder') + { + $extended = $this->createExtended($contentPath, $yaml, $item->folderContent, $extended); + } + } + return $extended; + } + + # only backoffice + protected function renameExtended($item, $newFolder) + { + # get the extended structure files with changes like navigation title or hidden pages + $yaml = new writeYaml(); + $extended = $yaml->getYaml('cache', 'structure-extended.yaml'); + + if(isset($extended[$item->urlRelWoF])) + { + $newUrl = $newFolder->urlRelWoF . '/' . $item->slug; + + $entry = $extended[$item->urlRelWoF]; + + unset($extended[$item->urlRelWoF]); + + $extended[$newUrl] = $entry; + $yaml->updateYaml('cache', 'structure-extended.yaml', $extended); + } + + return true; + } + + # only backoffice + protected function deleteFromExtended() + { + # get the extended structure files with changes like navigation title or hidden pages + $yaml = new writeYaml(); + $extended = $yaml->getYaml('cache', 'structure-extended.yaml'); + + if($this->item->elementType == "file" && isset($extended[$this->item->urlRelWoF])) + { + unset($extended[$this->item->urlRelWoF]); + $yaml->updateYaml('cache', 'structure-extended.yaml', $extended); + } + + if($this->item->elementType == "folder") + { + $changed = false; + + # delete all entries with that folder url + foreach($extended as $url => $entries) + { + if( strpos($url, $this->item->urlRelWoF) !== false ) + { + $changed = true; + unset($extended[$url]); + } + } + + if($changed) + { + $yaml->updateYaml('cache', 'structure-extended.yaml', $extended); + } + } + } + + # checks if there is a hidden page, returns true on first find + protected function containsHiddenPages($extended) + { + foreach($extended as $element) + { + if(isset($element['hide']) && $element['hide'] === true) + { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/system/Extensions/ParsedownExtension.php b/system/Extensions/ParsedownExtension.php index 8c6ed6b..50c6f4e 100644 --- a/system/Extensions/ParsedownExtension.php +++ b/system/Extensions/ParsedownExtension.php @@ -2,7 +2,7 @@ namespace Typemill\Extensions; -use \URLify; +use Typemill\Models\Folder; class ParsedownExtension extends \ParsedownExtra { @@ -10,6 +10,8 @@ class ParsedownExtension extends \ParsedownExtra { parent::__construct(); + $this->settings = $settings; + # show anchor next to headline? $this->showAnchor = isset($settings['headlineanchors']) ? $settings['headlineanchors'] : false; @@ -329,7 +331,7 @@ class ParsedownExtension extends \ParsedownExtra } $text = trim($Line['text'], '#'); - $headline = URLify::filter($Line['text']); + $headline = Folder::createSlug($Line['text'], $this->settings); if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') { diff --git a/system/Models/Folder.php b/system/Models/Folder.php index 9c49a81..201668c 100644 --- a/system/Models/Folder.php +++ b/system/Models/Folder.php @@ -69,7 +69,7 @@ class Folder if($fileType == 'md') { - $folderContent[] = $item; + $folderContent[] = $item; } if($draft === true && $fileType == 'txt') @@ -97,7 +97,7 @@ class Folder * returns: array of objects. Each object contains information about an item (file or folder). */ - public static function getFolderContentDetails(array $folderContent, $extended, $baseUrl, $fullSlugWithFolder = NULL, $fullSlugWithoutFolder = NULL, $fullPath = NULL, $keyPath = NULL, $chapter = NULL) + public static function getFolderContentDetails(array $folderContent, $extended, $settings, $baseUrl, $fullSlugWithFolder = NULL, $fullSlugWithoutFolder = NULL, $fullPath = NULL, $keyPath = NULL, $chapter = NULL) { $contentDetails = []; $iteration = 0; @@ -130,14 +130,14 @@ class Folder $item->originalName = $key; $item->elementType = 'folder'; - $item->contains = self::getFolderContentType($name, $fullPath . DIRECTORY_SEPARATOR . $key . DIRECTORY_SEPARATOR . 'index.yaml'); + $item->contains = self::getFolderContentType($name, $fullPath . DIRECTORY_SEPARATOR . $key . DIRECTORY_SEPARATOR . 'index.yaml'); $item->status = $status; $item->fileType = $fileType; $item->order = count($nameParts) > 1 ? array_shift($nameParts) : NULL; $item->name = implode(" ",$nameParts); $item->name = iconv(mb_detect_encoding($item->name, mb_detect_order(), true), "UTF-8", $item->name); $item->slug = implode("-",$nameParts); - $item->slug = URLify::filter(iconv(mb_detect_encoding($item->slug, mb_detect_order(), true), "UTF-8", $item->slug)); + $item->slug = self::createSlug($item->slug, $settings); $item->path = $fullPath . DIRECTORY_SEPARATOR . $key; $item->pathWithoutType = $fullPath . DIRECTORY_SEPARATOR . $key . DIRECTORY_SEPARATOR . 'index'; $item->urlRelWoF = $fullSlugWithoutFolder . '/' . $item->slug; @@ -164,7 +164,7 @@ class Folder rsort($name); } - $item->folderContent = self::getFolderContentDetails($name, $extended, $baseUrl, $item->urlRel, $item->urlRelWoF, $item->path, $item->keyPath, $item->chapter); + $item->folderContent = self::getFolderContentDetails($name, $extended, $settings, $baseUrl, $item->urlRel, $item->urlRelWoF, $item->path, $item->keyPath, $item->chapter); } elseif($name) { @@ -200,7 +200,7 @@ class Folder $item->name = implode(" ",$nameParts); $item->name = iconv(mb_detect_encoding($item->name, mb_detect_order(), true), "UTF-8", $item->name); $item->slug = implode("-",$nameParts); - $item->slug = URLify::filter(iconv(mb_detect_encoding($item->slug, mb_detect_order(), true), "UTF-8", $item->slug)); + $item->slug = self::createSlug($item->slug, $settings); $item->path = $fullPath . DIRECTORY_SEPARATOR . $name; $item->pathWithoutType = $fullPath . DIRECTORY_SEPARATOR . $nameWithoutType; $item->key = $iteration; @@ -637,4 +637,31 @@ class Folder $parts = preg_split('/\./',$fileName); return $parts[0]; } + + public static function createSlug($name, $settings = NULL) + { + $name = iconv(mb_detect_encoding($name, mb_detect_order(), true), "UTF-8", $name); + + # prior version 1.5.0 settings was no language and remove stop words from slug + $language = ""; + $use_remove_list = true; + + # if user has not activated the old slug logig < version 1.5.0 style + if($settings && ( !isset($settings['oldslug']) OR !$settings['oldslug'] ) ) + { + # then use the language attr and do not remove stop words as default behavior + $language = isset($settings['langattr']) ? $settings['langattr'] : ""; + $use_remove_list = false; + } + + return URLify::filter( + $name, + $length = 60, + $language, + $file_name = false, + $use_remove_list, + $lower_case = true, + $treat_underscore_as_space = true + ); + } } \ No newline at end of file diff --git a/system/Models/Helpers.php b/system/Models/Helpers.php index 4cbd24d..f2ff156 100644 --- a/system/Models/Helpers.php +++ b/system/Models/Helpers.php @@ -28,6 +28,7 @@ class Helpers{ return $ip; } + public static function addLogEntry($action) { $line = self::getUserIP(); diff --git a/system/Models/ProcessAssets.php b/system/Models/ProcessAssets.php index 631d7c2..12fc56e 100644 --- a/system/Models/ProcessAssets.php +++ b/system/Models/ProcessAssets.php @@ -1,7 +1,7 @@ extension = strtolower($pathinfo['extension']); - $this->filename = URLify::filter(iconv(mb_detect_encoding($pathinfo['filename'], mb_detect_order(), true), "UTF-8", $pathinfo['filename'])); + $this->filename = Folder::createSlug($pathinfo['filename']); $filename = $this->filename; diff --git a/system/Models/ProcessFile.php b/system/Models/ProcessFile.php index 458416a..115d9d5 100644 --- a/system/Models/ProcessFile.php +++ b/system/Models/ProcessFile.php @@ -3,7 +3,6 @@ namespace Typemill\Models; use Slim\Http\UploadedFile; use Typemill\Models\Helpers; -use \URLify; class ProcessFile extends ProcessAssets { diff --git a/system/Models/VersionCheck.php b/system/Models/VersionCheck.php deleted file mode 100644 index 5c1b43e..0000000 --- a/system/Models/VersionCheck.php +++ /dev/null @@ -1,29 +0,0 @@ -array( - 'method' => "GET", - 'header' => "Referer: $url\r\n" - ) - ); - - $context = stream_context_create($opts); - - if(false === ($version = @file_get_contents('https://typemill.net/api/v1/checkversion', false, $context))) - { - return false; - } - $version = json_decode($version); - die(); - - return $version->system->typemill; - } -} \ No newline at end of file diff --git a/system/Models/Write.php b/system/Models/Write.php index 0f6d771..48c61dc 100644 --- a/system/Models/Write.php +++ b/system/Models/Write.php @@ -127,8 +127,39 @@ class Write } return false; - } + } + public function renamePost($oldPathWithoutType, $newPathWithoutType) + { + $filetypes = array('md', 'txt', 'yaml'); + + $oldPath = $this->basePath . 'content' . $oldPathWithoutType; + $newPath = $this->basePath . 'content' . $newPathWithoutType; + + $result = true; + + foreach($filetypes as $filetype) + { + $oldFilePath = $oldPath . '.' . $filetype; + $newFilePath = $newPath . '.' . $filetype; + + #check if file with filetype exists and rename + if($oldFilePath != $newFilePath && file_exists($oldFilePath)) + { + if(@rename($oldFilePath, $newFilePath)) + { + $result = $result; + } + else + { + $result = false; + } + } + } + + return $result; + } + public function moveElement($item, $folderPath, $index, $date = null) { $filetypes = array('md', 'txt', 'yaml'); @@ -178,35 +209,4 @@ class Write return $result; } - - public function renamePost($oldPathWithoutType, $newPathWithoutType) - { - $filetypes = array('md', 'txt', 'yaml'); - - $oldPath = $this->basePath . 'content' . $oldPathWithoutType; - $newPath = $this->basePath . 'content' . $newPathWithoutType; - - $result = true; - - foreach($filetypes as $filetype) - { - $oldFilePath = $oldPath . '.' . $filetype; - $newFilePath = $newPath . '.' . $filetype; - - #check if file with filetype exists and rename - if($oldFilePath != $newFilePath && file_exists($oldFilePath)) - { - if(@rename($oldFilePath, $newFilePath)) - { - $result = $result; - } - else - { - $result = false; - } - } - } - - return $result; - } } \ No newline at end of file diff --git a/system/Models/WriteCache.php b/system/Models/WriteCache.php index 8d1de02..32e5fbd 100644 --- a/system/Models/WriteCache.php +++ b/system/Models/WriteCache.php @@ -110,124 +110,4 @@ class WriteCache extends Write return $error; } - - public function getFreshStructure($contentPath, $uri) - { - # scan the content of the folder - $pagetree = Folder::scanFolder('content'); - - # if there is no content, render an empty page - if(count($pagetree) == 0) - { - return false; - } - - # get the extended structure files with changes like navigation title or hidden pages - $yaml = new writeYaml(); - $extended = $yaml->getYaml('cache', 'structure-extended.yaml'); - - # create an array of object with the whole content of the folder - $structure = Folder::getFolderContentDetails($pagetree, $extended, $uri->getBaseUrl(), $uri->getBasePath()); - - # now update the extended structure - if(!$extended) - { - $extended = $this->createExtended($contentPath, $yaml, $structure); - - if(!empty($extended)) - { - $yaml->updateYaml('cache', 'structure-extended.yaml', $extended); - - # we have to update the structure with extended again - $structure = Folder::getFolderContentDetails($pagetree, $extended, $uri->getBaseUrl(), $uri->getBasePath()); - } - else - { - $extended = false; - } - } - - # cache structure - $this->updateCache('cache', 'structure.txt', 'lastCache.txt', $structure); - - if($extended && $this->containsHiddenPages($extended)) - { - # generate the navigation (delete empty pages) - $navigation = $this->createNavigationFromStructure($structure); - - # cache navigation - $this->updateCache('cache', 'navigation.txt', false, $navigation); - } - else - { - # make sure no separate navigation file is set - $this->deleteFileWithPath('cache' . DIRECTORY_SEPARATOR . 'navigation.txt'); - } - - # load and return the cached structure, because might be manipulated with navigation.... - $structure = $this->getCachedStructure(); - - return $structure; - } - - # creates a file that holds all hide flags and navigation titles - # reads all meta-files and creates an array with url => ['hide' => bool, 'navtitle' => 'bla'] - public function createExtended($contentPath, $yaml, $structure, $extended = NULL) - { - if(!$extended) - { - $extended = []; - } - - foreach ($structure as $key => $item) - { - # $filename = ($item->elementType == 'folder') ? DIRECTORY_SEPARATOR . 'index.yaml' : $item->pathWithoutType . '.yaml'; - $filename = $item->pathWithoutType . '.yaml'; - - if(file_exists($contentPath . $filename)) - { - # read file - $meta = $yaml->getYaml('content', $filename); - - $extended[$item->urlRelWoF]['hide'] = isset($meta['meta']['hide']) ? $meta['meta']['hide'] : false; - $extended[$item->urlRelWoF]['navtitle'] = isset($meta['meta']['navtitle']) ? $meta['meta']['navtitle'] : ''; - } - - if ($item->elementType == 'folder') - { - $extended = $this->createExtended($contentPath, $yaml, $item->folderContent, $extended); - } - } - return $extended; - } - - public function createNavigationFromStructure($navigation) - { - foreach ($navigation as $key => $element) - { - if($element->hide === true) - { - unset($navigation[$key]); - } - elseif(isset($element->folderContent)) - { - $navigation[$key]->folderContent = $this->createNavigationFromStructure($element->folderContent); - } - } - - return $navigation; - } - - # checks if there is a hidden page, returns true on first find - protected function containsHiddenPages($extended) - { - foreach($extended as $element) - { - if(isset($element['hide']) && $element['hide'] === true) - { - return true; - } - } - return false; - } } \ No newline at end of file diff --git a/system/Models/WriteSitemap.php b/system/Models/WriteSitemap.php deleted file mode 100644 index 409591f..0000000 --- a/system/Models/WriteSitemap.php +++ /dev/null @@ -1,45 +0,0 @@ -' . "\n"; - $sitemap .= '' . "\n"; - $sitemap = $this->addUrlSet($sitemap, $baseUrl); - $sitemap .= $this->generateUrlSets($data); - $sitemap .= ''; - - $this->writeFile($folderName, $sitemapFileName, $sitemap); - $this->writeFile($folderName, $requestFileName, time()); - } - - public function generateUrlSets($data) - { - $urlset = ''; - - foreach($data as $item) - { - if($item->elementType == 'folder') - { - $urlset = $this->addUrlSet($urlset, $item->urlAbs); - $urlset .= $this->generateUrlSets($item->folderContent, $urlset); - } - else - { - $urlset = $this->addUrlSet($urlset, $item->urlAbs); - } - } - return $urlset; - } - - public function addUrlSet($urlset, $url) - { - $urlset .= ' ' . "\n"; - $urlset .= ' ' . $url . '' . "\n"; - $urlset .= ' ' . "\n"; - return $urlset; - } -} \ No newline at end of file diff --git a/system/Routes/Api.php b/system/Routes/Api.php index eb8ce8e..09fec81 100644 --- a/system/Routes/Api.php +++ b/system/Routes/Api.php @@ -1,52 +1,50 @@ get('/api/v1/themes', SettingsController::class . ':getThemeSettings')->setName('api.themes')->add(new RestrictApiAccess($container['router'])); -$app->delete('/api/v1/clearcache', SettingsController::class . ':clearCache')->setName('api.clearcache')->add(new RestrictApiAccess($container['router'])); -$app->get('/api/v1/users/getbynames', SettingsController::class . ':getUsersByNames')->setName('api.usersbynames')->add(new RestrictApiAccess($container['router'])); -$app->get('/api/v1/users/getbyemail', SettingsController::class . ':getUsersByEmail')->setName('api.usersbyemail')->add(new RestrictApiAccess($container['router'])); -$app->get('/api/v1/users/getbyrole', SettingsController::class . ':getUsersByRole')->setName('api.usersbyrole')->add(new RestrictApiAccess($container['router'])); +$app->get('/api/v1/themes', ControllerSettings::class . ':getThemeSettings')->setName('api.themes')->add(new RestrictApiAccess($container['router'])); +$app->delete('/api/v1/clearcache', ControllerSettings::class . ':clearCache')->setName('api.clearcache')->add(new RestrictApiAccess($container['router'])); +$app->get('/api/v1/users/getbynames', ControllerSettings::class . ':getUsersByNames')->setName('api.usersbynames')->add(new RestrictApiAccess($container['router'])); +$app->get('/api/v1/users/getbyemail', ControllerSettings::class . ':getUsersByEmail')->setName('api.usersbyemail')->add(new RestrictApiAccess($container['router'])); +$app->get('/api/v1/users/getbyrole', ControllerSettings::class . ':getUsersByRole')->setName('api.usersbyrole')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/article/markdown', ArticleApiController::class . ':getArticleMarkdown')->setName('api.article.markdown')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/article/html', ArticleApiController::class . ':getArticleHtml')->setName('api.article.html')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/article/publish', ArticleApiController::class . ':publishArticle')->setName('api.article.publish')->add(new RestrictApiAccess($container['router'])); -$app->delete('/api/v1/article/unpublish', ArticleApiController::class . ':unpublishArticle')->setName('api.article.unpublish')->add(new RestrictApiAccess($container['router'])); -$app->delete('/api/v1/article/discard', ArticleApiController::class . ':discardArticleChanges')->setName('api.article.discard')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/article/rename', ArticleApiController::class . ':renameArticle')->setName('api.article.rename')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/article/sort', ArticleApiController::class . ':sortArticle')->setName('api.article.sort')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/article', ArticleApiController::class . ':createArticle')->setName('api.article.create')->add(new RestrictApiAccess($container['router'])); -$app->put('/api/v1/article', ArticleApiController::class . ':updateArticle')->setName('api.article.update')->add(new RestrictApiAccess($container['router'])); -$app->delete('/api/v1/article', ArticleApiController::class . ':deleteArticle')->setName('api.article.delete')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/baseitem', ArticleApiController::class . ':createBaseItem')->setName('api.baseitem.create')->add(new RestrictApiAccess($container['router'])); -$app->get('/api/v1/navigation', ArticleApiController::class . ':getNavigation')->setName('api.navigation.get')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/post', ArticleApiController::class . ':createPost')->setName('api.post.create')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/article/markdown', ControllerAuthorArticleApi::class . ':getArticleMarkdown')->setName('api.article.markdown')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/article/html', ControllerAuthorArticleApi::class . ':getArticleHtml')->setName('api.article.html')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/article/publish', ControllerAuthorArticleApi::class . ':publishArticle')->setName('api.article.publish')->add(new RestrictApiAccess($container['router'])); +$app->delete('/api/v1/article/unpublish', ControllerAuthorArticleApi::class . ':unpublishArticle')->setName('api.article.unpublish')->add(new RestrictApiAccess($container['router'])); +$app->delete('/api/v1/article/discard', ControllerAuthorArticleApi::class . ':discardArticleChanges')->setName('api.article.discard')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/article/rename', ControllerAuthorArticleApi::class . ':renameArticle')->setName('api.article.rename')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/article/sort', ControllerAuthorArticleApi::class . ':sortArticle')->setName('api.article.sort')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/article', ControllerAuthorArticleApi::class . ':createArticle')->setName('api.article.create')->add(new RestrictApiAccess($container['router'])); +$app->put('/api/v1/article', ControllerAuthorArticleApi::class . ':updateArticle')->setName('api.article.update')->add(new RestrictApiAccess($container['router'])); +$app->delete('/api/v1/article', ControllerAuthorArticleApi::class . ':deleteArticle')->setName('api.article.delete')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/baseitem', ControllerAuthorArticleApi::class . ':createBaseItem')->setName('api.baseitem.create')->add(new RestrictApiAccess($container['router'])); +$app->get('/api/v1/navigation', ControllerAuthorArticleApi::class . ':getNavigation')->setName('api.navigation.get')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/post', ControllerAuthorArticleApi::class . ':createPost')->setName('api.post.create')->add(new RestrictApiAccess($container['router'])); -$app->get('/api/v1/metadefinitions', MetaApiController::class . ':getMetaDefinitions')->setName('api.metadefinitions.get')->add(new RestrictApiAccess($container['router'])); -$app->get('/api/v1/article/metaobject', MetaApiController::class . ':getArticleMetaobject')->setName('api.articlemetaobject.get')->add(new RestrictApiAccess($container['router'])); -$app->get('/api/v1/article/metadata', MetaApiController::class . ':getArticleMeta')->setName('api.articlemeta.get')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/article/metadata', MetaApiController::class . ':updateArticleMeta')->setName('api.articlemeta.update')->add(new RestrictApiAccess($container['router'])); +$app->get('/api/v1/metadefinitions', ControllerAuthorMetaApi::class . ':getMetaDefinitions')->setName('api.metadefinitions.get')->add(new RestrictApiAccess($container['router'])); +$app->get('/api/v1/article/metaobject', ControllerAuthorMetaApi::class . ':getArticleMetaobject')->setName('api.articlemetaobject.get')->add(new RestrictApiAccess($container['router'])); +$app->get('/api/v1/article/metadata', ControllerAuthorMetaApi::class . ':getArticleMeta')->setName('api.articlemeta.get')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/article/metadata', ControllerAuthorMetaApi::class . ':updateArticleMeta')->setName('api.articlemeta.update')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/block', BlockApiController::class . ':addBlock')->setName('api.block.add')->add(new RestrictApiAccess($container['router'])); -$app->put('/api/v1/block', BlockApiController::class . ':updateBlock')->setName('api.block.update')->add(new RestrictApiAccess($container['router'])); -$app->delete('/api/v1/block', BlockApiController::class . ':deleteBlock')->setName('api.block.delete')->add(new RestrictApiAccess($container['router'])); -$app->put('/api/v1/moveblock', BlockApiController::class . ':moveBlock')->setName('api.block.move')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/video', BlockApiController::class . ':saveVideoImage')->setName('api.video.save')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/block', ControllerAuthorBlockApi::class . ':addBlock')->setName('api.block.add')->add(new RestrictApiAccess($container['router'])); +$app->put('/api/v1/block', ControllerAuthorBlockApi::class . ':updateBlock')->setName('api.block.update')->add(new RestrictApiAccess($container['router'])); +$app->delete('/api/v1/block', ControllerAuthorBlockApi::class . ':deleteBlock')->setName('api.block.delete')->add(new RestrictApiAccess($container['router'])); +$app->put('/api/v1/moveblock', ControllerAuthorBlockApi::class . ':moveBlock')->setName('api.block.move')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/video', ControllerAuthorBlockApi::class . ':saveVideoImage')->setName('api.video.save')->add(new RestrictApiAccess($container['router'])); -$app->get('/api/v1/medialib/images', MediaApiController::class . ':getMediaLibImages')->setName('api.medialibimg.get')->add(new RestrictApiAccess($container['router'])); -$app->get('/api/v1/medialib/files', MediaApiController::class . ':getMediaLibFiles')->setName('api.medialibfiles.get')->add(new RestrictApiAccess($container['router'])); -$app->get('/api/v1/image', MediaApiController::class . ':getImage')->setName('api.image.get')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/image', MediaApiController::class . ':createImage')->setName('api.image.create')->add(new RestrictApiAccess($container['router'])); -$app->put('/api/v1/image', MediaApiController::class . ':publishImage')->setName('api.image.publish')->add(new RestrictApiAccess($container['router'])); -$app->delete('/api/v1/image', MediaApiController::class . ':deleteImage')->setName('api.image.delete')->add(new RestrictApiAccess($container['router'])); -$app->get('/api/v1/file', MediaApiController::class . ':getFile')->setName('api.file.get')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/file', MediaApiController::class . ':uploadFile')->setName('api.file.upload')->add(new RestrictApiAccess($container['router'])); -$app->put('/api/v1/file', MediaApiController::class . ':publishFile')->setName('api.file.publish')->add(new RestrictApiAccess($container['router'])); -$app->delete('/api/v1/file', MediaApiController::class . ':deleteFile')->setName('api.file.delete')->add(new RestrictApiAccess($container['router'])); \ No newline at end of file +$app->get('/api/v1/medialib/images', ControllerAuthorMediaApi::class . ':getMediaLibImages')->setName('api.medialibimg.get')->add(new RestrictApiAccess($container['router'])); +$app->get('/api/v1/medialib/files', ControllerAuthorMediaApi::class . ':getMediaLibFiles')->setName('api.medialibfiles.get')->add(new RestrictApiAccess($container['router'])); +$app->get('/api/v1/image', ControllerAuthorMediaApi::class . ':getImage')->setName('api.image.get')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/image', ControllerAuthorMediaApi::class . ':createImage')->setName('api.image.create')->add(new RestrictApiAccess($container['router'])); +$app->put('/api/v1/image', ControllerAuthorMediaApi::class . ':publishImage')->setName('api.image.publish')->add(new RestrictApiAccess($container['router'])); +$app->delete('/api/v1/image', ControllerAuthorMediaApi::class . ':deleteImage')->setName('api.image.delete')->add(new RestrictApiAccess($container['router'])); +$app->get('/api/v1/file', ControllerAuthorMediaApi::class . ':getFile')->setName('api.file.get')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/file', ControllerAuthorMediaApi::class . ':uploadFile')->setName('api.file.upload')->add(new RestrictApiAccess($container['router'])); +$app->put('/api/v1/file', ControllerAuthorMediaApi::class . ':publishFile')->setName('api.file.publish')->add(new RestrictApiAccess($container['router'])); +$app->delete('/api/v1/file', ControllerAuthorMediaApi::class . ':deleteFile')->setName('api.file.delete')->add(new RestrictApiAccess($container['router'])); \ No newline at end of file diff --git a/system/Routes/Web.php b/system/Routes/Web.php index 58b826f..6d34221 100644 --- a/system/Routes/Web.php +++ b/system/Routes/Web.php @@ -1,11 +1,10 @@ get('/setup', SetupController::class . ':show')->setName('setup.show'); - $app->post('/setup', SetupController::class . ':create')->setName('setup.create'); + $app->get('/setup', ControllerFrontendSetup::class . ':show')->setName('setup.show'); + $app->post('/setup', ControllerFrontendSetup::class . ':create')->setName('setup.create'); } else { - $app->get('/setup', AuthController::class . ':redirect'); + $app->get('/setup', ControllerFrontendAuth::class . ':redirect'); } if($settings['settings']['welcome']) { - $app->get('/setup/welcome', SetupController::class . ':welcome')->setName('setup.welcome')->add(new RedirectIfUnauthenticated($container['router'], $container['flash'])); + $app->get('/setup/welcome', ControllerFrontendSetup::class . ':welcome')->setName('setup.welcome')->add(new RedirectIfUnauthenticated($container['router'], $container['flash'])); } else { - $app->get('/setup/welcome', AuthController::class . ':redirect')->setName('setup.welcome'); + $app->get('/setup/welcome', ControllerFrontendAuth::class . ':redirect')->setName('setup.welcome'); } -$app->post('/tm/formpost', FormController::class . ':savePublicForm')->setName('form.save'); +$app->post('/tm/formpost', ControllerFrontendForms::class . ':savePublicForm')->setName('form.save'); -$app->get('/tm', AuthController::class . ':redirect'); -$app->get('/tm/login', AuthController::class . ':show')->setName('auth.show')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); -$app->post('/tm/login', AuthController::class . ':login')->setName('auth.login')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); -$app->get('/tm/logout', AuthController::class . ':logout')->setName('auth.logout')->add(new RedirectIfUnauthenticated($container['router'], $container['flash'])); +$app->get('/tm', ControllerFrontendAuth::class . ':redirect'); +$app->get('/tm/login', ControllerFrontendAuth::class . ':show')->setName('auth.show')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); +$app->post('/tm/login', ControllerFrontendAuth::class . ':login')->setName('auth.login')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); +$app->get('/tm/logout', ControllerFrontendAuth::class . ':logout')->setName('auth.logout')->add(new RedirectIfUnauthenticated($container['router'], $container['flash'])); if(isset($settings['settings']['recoverpw']) && $settings['settings']['recoverpw']) { - $app->get('/tm/recoverpw', AuthController::class . ':showrecoverpassword')->setName('auth.recoverpwshow')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); - $app->post('/tm/recoverpw', AuthController::class . ':recoverpassword')->setName('auth.recoverpw')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); - $app->get('/tm/recoverpwnew', AuthController::class . ':showrecoverpasswordnew')->setName('auth.recoverpwshownew')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); - $app->post('/tm/recoverpwnew', AuthController::class . ':createrecoverpasswordnew')->setName('auth.recoverpwnew')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); + $app->get('/tm/recoverpw', ControllerFrontendAuth::class . ':showrecoverpassword')->setName('auth.recoverpwshow')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); + $app->post('/tm/recoverpw', ControllerFrontendAuth::class . ':recoverpassword')->setName('auth.recoverpw')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); + $app->get('/tm/recoverpwnew', ControllerFrontendAuth::class . ':showrecoverpasswordnew')->setName('auth.recoverpwshownew')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); + $app->post('/tm/recoverpwnew', ControllerFrontendAuth::class . ':createrecoverpasswordnew')->setName('auth.recoverpwnew')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); } -$app->get('/tm/settings', SettingsController::class . ':showSettings')->setName('settings.show')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'view')); -$app->post('/tm/settings', SettingsController::class . ':saveSettings')->setName('settings.save')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'update')); +$app->get('/tm/settings', ControllerSettings::class . ':showSettings')->setName('settings.show')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'view')); +$app->post('/tm/settings', ControllerSettings::class . ':saveSettings')->setName('settings.save')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'update')); -$app->get('/tm/themes', SettingsController::class . ':showThemes')->setName('themes.show')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'view')); -$app->post('/tm/themes', SettingsController::class . ':saveThemes')->setName('themes.save')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'update')); +$app->get('/tm/themes', ControllerSettings::class . ':showThemes')->setName('themes.show')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'view')); +$app->post('/tm/themes', ControllerSettings::class . ':saveThemes')->setName('themes.save')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'update')); -$app->get('/tm/plugins', SettingsController::class . ':showPlugins')->setName('plugins.show')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'view')); -$app->post('/tm/plugins', SettingsController::class . ':savePlugins')->setName('plugins.save')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'update')); +$app->get('/tm/plugins', ControllerSettings::class . ':showPlugins')->setName('plugins.show')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'view')); +$app->post('/tm/plugins', ControllerSettings::class . ':savePlugins')->setName('plugins.save')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'update')); -$app->get('/tm/account', SettingsController::class . ':showAccount')->setName('user.account')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'view')); -$app->get('/tm/user/new', SettingsController::class . ':newUser')->setName('user.new')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'create')); -$app->post('/tm/user/create', SettingsController::class . ':createUser')->setName('user.create')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'create')); -$app->post('/tm/user/update', SettingsController::class . ':updateUser')->setName('user.update')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'update')); -$app->post('/tm/user/delete', SettingsController::class . ':deleteUser')->setName('user.delete')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'delete')); -$app->get('/tm/user/{username}', SettingsController::class . ':showUser')->setName('user.show')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'view')); -$app->get('/tm/users', SettingsController::class . ':listUser')->setName('user.list')->add(new accessMiddleware($container['router'], $container['acl'], 'userlist', 'view')); +$app->get('/tm/account', ControllerSettings::class . ':showAccount')->setName('user.account')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'view')); +$app->get('/tm/user/new', ControllerSettings::class . ':newUser')->setName('user.new')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'create')); +$app->post('/tm/user/create', ControllerSettings::class . ':createUser')->setName('user.create')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'create')); +$app->post('/tm/user/update', ControllerSettings::class . ':updateUser')->setName('user.update')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'update')); +$app->post('/tm/user/delete', ControllerSettings::class . ':deleteUser')->setName('user.delete')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'delete')); +$app->get('/tm/user/{username}', ControllerSettings::class . ':showUser')->setName('user.show')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'view')); +$app->get('/tm/users', ControllerSettings::class . ':listUser')->setName('user.list')->add(new accessMiddleware($container['router'], $container['acl'], 'userlist', 'view')); -$app->get('/tm/content/raw[/{params:.*}]', ContentBackendController::class . ':showContent')->setName('content.raw')->add(new accessMiddleware($container['router'], $container['acl'], 'content', 'view')); -$app->get('/tm/content/visual[/{params:.*}]', ContentBackendController::class . ':showBlox')->setName('content.visual')->add(new accessMiddleware($container['router'], $container['acl'], 'content', 'view')); -$app->get('/tm/content[/{params:.*}]', ContentBackendController::class . ':showEmpty')->setName('content.empty')->add(new accessMiddleware($container['router'], $container['acl'], 'content', 'view')); +$app->get('/tm/content/raw[/{params:.*}]', ControllerAuthorEditor::class . ':showContent')->setName('content.raw')->add(new accessMiddleware($container['router'], $container['acl'], 'content', 'view')); +$app->get('/tm/content/visual[/{params:.*}]', ControllerAuthorEditor::class . ':showBlox')->setName('content.visual')->add(new accessMiddleware($container['router'], $container['acl'], 'content', 'view')); +$app->get('/tm/content[/{params:.*}]', ControllerAuthorEditor::class . ':showEmpty')->setName('content.empty')->add(new accessMiddleware($container['router'], $container['acl'], 'content', 'view')); foreach($routes as $pluginRoute) { @@ -85,13 +84,13 @@ foreach($routes as $pluginRoute) if($settings['settings']['setup']) { - $app->get('/[{params:.*}]', SetupController::class . ':redirect'); + $app->get('/[{params:.*}]', ControllerFrontendSetup::class . ':redirect'); } elseif(isset($settings['settings']['access']) && $settings['settings']['access'] != '') { - $app->get('/[{params:.*}]', PageController::class . ':index')->setName('home')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'view')); + $app->get('/[{params:.*}]', ControllerFrontendWebsite::class . ':index')->setName('home')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'view')); } else { - $app->get('/[{params:.*}]', PageController::class . ':index')->setName('home'); + $app->get('/[{params:.*}]', ControllerFrontendWebsite::class . ':index')->setName('home'); } \ No newline at end of file diff --git a/system/Settings.php b/system/Settings.php index 248c433..5e222ad 100644 --- a/system/Settings.php +++ b/system/Settings.php @@ -191,6 +191,8 @@ class Settings 'recovermessage' => true, 'recoverfrom' => true, 'securitylog' => true, + 'oldslug' => true, + 'refreshcache' => true, ]; # cleanup the existing usersettings diff --git a/system/author/metatabs.yaml b/system/author/metatabs.yaml index 72d3750..e96e768 100644 --- a/system/author/metatabs.yaml +++ b/system/author/metatabs.yaml @@ -1,5 +1,19 @@ meta: fields: + fieldsetnavi: + type: fieldset + legend: Navigation + fields: + navtitle: + type: text + label: Navigation Title + class: medium + maxlength: 60 + hide: + type: checkbox + label: Hide + checkboxlabel: Hide page from navigation + class: medium fieldsetcontent: type: fieldset legend: Meta-Content @@ -76,20 +90,6 @@ meta: hidden: true class: hidden pattern: '[0-9][0-9]-[0-9][0-9]-[0-9][0-9]' - fieldsetnavi: - type: fieldset - legend: Navigation - fields: - navtitle: - type: text - label: Navigation Title - class: medium - maxlength: 60 - hide: - type: checkbox - label: Hide - checkboxlabel: Hide page from navigation - class: medium contains: type: radio label: This folder contains diff --git a/system/author/settings/system.twig b/system/author/settings/system.twig index 933a64c..29515ff 100644 --- a/system/author/settings/system.twig +++ b/system/author/settings/system.twig @@ -261,6 +261,13 @@ +
+ + +
+
+ + +