diff --git a/composer.lock b/composer.lock
index 71d8f89..b7e9a5a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -1742,16 +1742,16 @@
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.25.0",
+ "version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "30885182c981ab175d4d034db0f6f469898070ab"
+ "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab",
- "reference": "30885182c981ab175d4d034db0f6f469898070ab",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
+ "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
"shasum": ""
},
"require": {
@@ -1766,7 +1766,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.23-dev"
+ "dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -1804,7 +1804,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0"
},
"funding": [
{
@@ -1820,20 +1820,20 @@
"type": "tidelift"
}
],
- "time": "2021-10-20T20:35:02+00:00"
+ "time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.25.0",
+ "version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825"
+ "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825",
- "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
+ "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"shasum": ""
},
"require": {
@@ -1848,7 +1848,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.23-dev"
+ "dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -1887,7 +1887,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0"
},
"funding": [
{
@@ -1903,20 +1903,20 @@
"type": "tidelift"
}
],
- "time": "2021-11-30T18:21:41+00:00"
+ "time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.25.0",
+ "version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
- "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c"
+ "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c",
- "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
+ "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"shasum": ""
},
"require": {
@@ -1925,7 +1925,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.23-dev"
+ "dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -1970,7 +1970,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0"
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0"
},
"funding": [
{
@@ -1986,20 +1986,20 @@
"type": "tidelift"
}
],
- "time": "2022-03-04T08:16:47+00:00"
+ "time": "2022-05-10T07:21:04+00:00"
},
{
"name": "symfony/polyfill-php81",
- "version": "v1.25.0",
+ "version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
- "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f"
+ "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f",
- "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f",
+ "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/13f6d1271c663dc5ae9fb843a8f16521db7687a1",
+ "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1",
"shasum": ""
},
"require": {
@@ -2008,7 +2008,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.23-dev"
+ "dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -2049,7 +2049,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php81/tree/v1.25.0"
+ "source": "https://github.com/symfony/polyfill-php81/tree/v1.26.0"
},
"funding": [
{
@@ -2065,7 +2065,7 @@
"type": "tidelift"
}
],
- "time": "2021-09-13T13:58:11+00:00"
+ "time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/yaml",
diff --git a/public/index.php b/public/index.php
index 3e92aaf..6b5a90a 100644
--- a/public/index.php
+++ b/public/index.php
@@ -1,3 +1,5 @@
-1)
- {
- this.collapse.splice(index, 1);
- }
- else
- {
- this.collapse.push(name);
- }
- localStorage.setItem("collapse", this.collapse.toString());
+ naviBus.$emit('toggleNavi', name);
},
isCollapsed: function(name)
{
@@ -222,9 +207,46 @@ let navi = new Vue({
folderName: '',
showForm: false,
newItem: '',
+ collapse: [],
}
},
+ mounted: function(){
+ var collapse = localStorage.getItem('collapse');
+ if(collapse !== null)
+ {
+ var collapseArray = collapse.split(',');
+ var collapseLength = collapseArray.length;
+ var cleanCollapseArray = [];
+ for(var i = 0; i < collapseLength; i++)
+ {
+ if(typeof collapseArray[i] === 'string' && collapseArray[i] != '')
+ {
+ cleanCollapseArray.push(collapseArray[i]);
+ }
+ }
+ this.collapse = collapse.split(',');
+ }
+ naviBus.$on('toggleNavi', this.toggleNavigation);
+ },
methods:{
+ clearToggle: function()
+ {
+ this.collapse = [];
+ localStorage.removeItem('collapse');
+ },
+ toggleNavigation: function(name)
+ {
+ var index = this.collapse.indexOf(name);
+ if (index > -1)
+ {
+ this.collapse.splice(index, 1);
+ }
+ else
+ {
+ this.collapse.push(name);
+ }
+ localStorage.setItem("collapse", this.collapse.toString());
+ },
checkMove: function(evt){
/* this.$refs.draggit[0].checkMove(evt); */
if(evt.dragged.classList.contains('folder') && evt.from.parentNode.id != evt.to.parentNode.id)
diff --git a/system/author/partials/editorNavi.twig b/system/author/partials/editorNavi.twig
index eeebaf8..a7d4a0c 100644
--- a/system/author/partials/editorNavi.twig
+++ b/system/author/partials/editorNavi.twig
@@ -4,6 +4,7 @@
@@ -15,7 +16,7 @@
:move="checkMove"
group="file"
animation="150"
- :disabled="freeze">
+ :disabled="freeze">
@@ -58,7 +60,7 @@
{% verbatim %}
-
-
+
{{ name }}
diff --git a/system/typemill/Controllers/Controller.php b/system/typemill/Controllers/Controller.php
new file mode 100644
index 0000000..567a477
--- /dev/null
+++ b/system/typemill/Controllers/Controller.php
@@ -0,0 +1,499 @@
+c = $container;
+
+ $this->routeParser = $container->get('routeParser');
+
+ $this->settings = $container->get('settings');
+ }
+
+
+/*
+ # holds the pimple container
+ protected $c;
+
+ # holds the settings
+ protected $settings;
+
+ # holds the write cache object
+ protected $writeCache;
+
+ # holds the structure of content folder as a serialized array of objects
+ protected $structureDraft = false;
+
+ # holds the structure of content folder as a serialized array of objects
+ protected $structureLive = false;
+
+ # holds the name of the structure-file with drafts for author environment
+ protected $structureDraftName = 'structure-draft.txt';
+
+ # holds the name of the structure-file without drafts for live site
+ protected $structureLiveName = 'structure.txt';
+
+ # holds the frontend navigation without hidden pages
+ protected $navigation = false;
+
+ # holds the list of pages with navigation titles and hidden pages. It extends the structures and navigations
+ protected $extended = false;
+
+ public function __construct(ContainerInterface $c)
+ {
+ $this->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($pagetreeLive && 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->c->dispatcher->dispatch('onPagetreeLoaded', new OnPagetreeLoaded($this->structureLive))->getData();
+ $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($ping = false)
+ {
+ $sitemap = '' . "\n";
+ $sitemap .= '' . "\n";
+ $sitemap = $this->addUrlSet($sitemap, $this->uri->getBaseUrl());
+ $sitemap .= $this->generateUrlSets($this->structureLive);
+ $sitemap .= '';
+
+ $this->writeCache->writeFile('cache', 'sitemap.xml', $sitemap);
+
+ if($ping && isset($this->settings['pingsitemap']) && $this->settings['pingsitemap'])
+ {
+ $sitemapUrl = $this->uri->getBaseUrl() . '/cache/sitemap.xml';
+
+ $pingGoogleUrl = 'http://www.google.com/ping?sitemap=' . urlencode($sitemapUrl);
+ $pingBingUrl = 'http://www.bing.com/ping?sitemap=' . urlencode($sitemapUrl);
+
+ $opts = array(
+ 'http'=>array(
+ 'method'=>"GET",
+ 'ignore_errors' => true,
+ 'timeout' => 5
+ )
+ );
+
+ $context = stream_context_create($opts);
+
+ $responseBing = file_get_contents($pingBingUrl, false, $context);
+ $responseGoogle = file_get_contents($pingGoogleUrl, false, $context);
+ }
+
+ }
+
+ public function generateUrlSets($structureLive)
+ {
+ $urlset = '';
+
+ foreach($structureLive as $item)
+ {
+ if($item->elementType == 'folder' && isset($item->noindex) && $item->noindex === true)
+ {
+ $urlset .= $this->generateUrlSets($item->folderContent, $urlset);
+ }
+ elseif($item->elementType == 'folder')
+ {
+ $urlset = $this->addUrlSet($urlset, $item->urlAbs);
+ $urlset .= $this->generateUrlSets($item->folderContent, $urlset);
+ }
+ elseif(isset($item->noindex) && $item->noindex === true )
+ {
+ continue;
+ }
+ 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/typemill/Controllers/ControllerWeb.php b/system/typemill/Controllers/ControllerWeb.php
new file mode 100644
index 0000000..0bd9827
--- /dev/null
+++ b/system/typemill/Controllers/ControllerWeb.php
@@ -0,0 +1,54 @@
+add twig';
+
+ $settings = $this->settings;
+
+ $csrf = isset($_SESSION) ? $this->c->get('csrf') : false;
+
+ $this->c->set('view', function() use ($settings, $csrf)
+ {
+ $twig = Twig::create(
+ [
+ # path to templates
+ $settings['rootPath'] . $settings['authorFolder'],
+ $settings['rootPath'] . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . $settings['theme'],
+ ],
+ [
+ # settings
+ 'cache' => ( isset($settings['twigcache']) && $settings['twigcache'] ) ? $settings['rootPath'] . '/cache/twig' : false,
+ 'debug' => isset($settings['displayErrorDetails'])
+ ]
+ );
+
+ # placeholder for flash and errors, will be filled later with middleware
+ $twig->getEnvironment()->addGlobal('errors', NULL);
+ $twig->getEnvironment()->addGlobal('flash', NULL);
+
+ # add extensions
+ $twig->addExtension(new \Twig\Extension\DebugExtension());
+ # $twig->addExtension(new \Nquire\Extensions\TwigUserExtension());
+ if($csrf)
+ {
+ $twig->addExtension(new \Typemill\Extensions\TwigCsrfExtension($csrf));
+ }
+
+ return $twig;
+ });
+
+ $this->c->get('dispatcher')->dispatch(new OnTwigLoaded(false), 'onTwigLoaded');
+
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Controllers/ControllerWebFrontend.php b/system/typemill/Controllers/ControllerWebFrontend.php
new file mode 100644
index 0000000..b22a46f
--- /dev/null
+++ b/system/typemill/Controllers/ControllerWebFrontend.php
@@ -0,0 +1,503 @@
+c->get('view')->render($response, 'home.twig', [
+ 'title' => 'Typemill Version 2',
+ 'description' => 'Typemill Version 2 wird noch besser als Version 1.'
+ ]);
+
+ # Initiate Variables
+ $contentHTML = false;
+ $item = false;
+ $home = false;
+ $breadcrumb = false;
+ $currentpage = false;
+ $this->pathToContent = $this->settings['rootPath'] . $this->settings['contentFolder'];
+ $this->uri = $request->getUri()->withUserInfo('');
+ $this->base_url = $this->uri->getBaseUrl();
+
+ # if there is no structure at all, the content folder is probably empty
+ if(!$this->setStructureLive())
+ {
+ return $this->render($response, '/index.twig', array( 'content' => 'No Content
Your content folder is empty.
' ));
+ }
+
+ # we can create an initial sitemap here, but makes no sense for every pagecall. Sitemap will be created on first author interaction (publish/delete/channge page).
+ # $this->checkSitemap();
+
+ # if the admin activated to refresh the cache automatically each 10 minutes (e.g. use without admin area)
+ if(isset($this->settings['refreshcache']) && $this->settings['refreshcache'] && !$this->writeCache->validate('cache', 'lastCache.txt', 600))
+ {
+ # delete the cache
+ $dir = $this->settings['basePath'] . 'cache';
+ $this->writeCache->deleteCacheFiles($dir);
+
+ # update the internal structure
+ $this->setFreshStructureDraft();
+
+ # update the public structure
+ $this->setFreshStructureLive();
+
+ # update the navigation
+ $this->setFreshNavigation();
+
+ # update the sitemap
+ $this->updateSitemap();
+ }
+
+ # dispatch event and let others manipulate the structure
+ $this->structureLive = $this->c->dispatcher->dispatch('onPagetreeLoaded', new OnPagetreeLoaded($this->structureLive))->getData();
+
+ # check if there is a custom theme css
+ $theme = $this->settings['theme'];
+ $customcss = $this->writeCache->checkFile('cache', $theme . '-custom.css');
+ if($customcss)
+ {
+ $this->c->assets->addCSS($this->base_url . '/cache/' . $theme . '-custom.css');
+ }
+
+ $logo = false;
+ if(isset($this->settings['logo']) && $this->settings['logo'] != '')
+ {
+ # check if logo exists
+ if(file_exists($this->settings['rootPath'] . 'media/live/' . $this->settings['logo']))
+ {
+ $logo = 'media/live/' . $this->settings['logo'];
+ }
+ elseif(file_exists($this->settings['rootPath'] . 'media/files/' . $this->settings['logo']))
+ {
+ $logo = 'media/files/' . $this->settings['logo'];
+ }
+ }
+
+ $favicon = false;
+ if(isset($this->settings['favicon']) && $this->settings['favicon'] != '')
+ {
+ $favicon = true;
+ $this->c->assets->addMeta('tilecolor','');
+ $this->c->assets->addMeta('tileimage','');
+ $this->c->assets->addMeta('icon16','');
+ $this->c->assets->addMeta('icon32','');
+ $this->c->assets->addMeta('icon72','');
+ $this->c->assets->addMeta('icon114','');
+ $this->c->assets->addMeta('icon144','');
+ $this->c->assets->addMeta('icon180','');
+ }
+
+ # the navigation is a copy of the structure without the hidden pages
+ # hint: if the navigation has been deleted from the cache, then we do not recreate it here to save performace. Instead you have to recreate cache in admin or change a page (publish/unpublish/delete/move)
+ $navigation = $this->writeCache->getCache('cache', 'navigation.txt');
+ if(!$navigation)
+ {
+ # use the structure if there is no cached navigation
+ $navigation = $this->structureLive;
+ }
+
+ # start pagination
+ if(isset($args['params']))
+ {
+ $argSegments = explode("/", $args['params']);
+
+ # check if the last url segment is a number
+ $pageNumber = array_pop($argSegments);
+ if(is_numeric($pageNumber) && $pageNumber < 10000)
+ {
+ # then check if the segment before the page is a "p" that indicates a paginator
+ $pageIndicator = array_pop($argSegments);
+ if($pageIndicator == "p")
+ {
+ # use page number as current page variable
+ $currentpage = $pageNumber;
+
+ # set empty args for startpage
+ $args = [];
+
+ # if there are still params
+ if(!empty($argSegments))
+ {
+ # add them to the args again
+ $args['params'] = implode("/", $argSegments);
+ }
+ }
+ }
+ }
+
+ # if the user is on startpage
+ $home = false;
+ if(empty($args))
+ {
+ $home = true;
+ $item = Folder::getItemForUrl($navigation, $this->uri->getBasePath(), $this->uri->getBaseUrl(), NULL, $home);
+ $urlRel = $this->uri->getBasePath();
+ }
+ else
+ {
+ # get the request url, trim args so physical folders have no trailing slash
+ $urlRel = $this->uri->getBasePath() . '/' . trim($args['params'], "/");
+
+ # find the url in the content-item-tree and return the item-object for the file
+ # important to use the structure here so it is found, even if the item is hidden.
+ $item = Folder::getItemForUrl($this->structureLive, $urlRel, $this->uri->getBasePath());
+
+ # if the item is a folder and if that folder is not hidden
+ if($item && $item->elementType == 'folder' && isset($item->hide) && !$item->hide)
+ {
+ # use the navigation instead of the structure so that hidden elements are erased
+ $item = Folder::getItemForUrl($navigation, $urlRel, $this->uri->getBaseUrl(), NULL, $home);
+ }
+
+ # if there is still no item, return a 404-page
+ if(!$item)
+ {
+ return $this->render404($response, array(
+ 'navigation' => $navigation,
+ 'settings' => $this->settings,
+ 'base_url' => $this->base_url,
+ 'title' => false,
+ 'content' => false,
+ 'item' => false,
+ 'breadcrumb' => false,
+ 'metatabs' => false,
+ 'image' => false,
+ 'logo' => $logo,
+ 'favicon' => $favicon
+ ));
+ }
+ }
+
+ if(isset($item->hide))
+ {
+ # if it is a hidden page
+ if($item->hide)
+ {
+ # get breadcrumb for page and set pages active
+ # use structure here because the hidden item is not part of the navigation
+ $breadcrumb = Folder::getBreadcrumb($this->structureLive, $item->keyPathArray);
+ $breadcrumb = $this->c->dispatcher->dispatch('onBreadcrumbLoaded', new OnBreadcrumbLoaded($breadcrumb))->getData();
+
+ # add the paging to the item
+ $item = Folder::getPagingForItem($this->structureLive, $item);
+ }
+ else
+ {
+ # get breadcrumb for page and set pages active
+ # use navigation, because it is used for frontend
+ $breadcrumb = Folder::getBreadcrumb($navigation, $item->keyPathArray);
+ $breadcrumb = $this->c->dispatcher->dispatch('onBreadcrumbLoaded', new OnBreadcrumbLoaded($breadcrumb))->getData();
+
+ # add the paging to the item
+ $item = Folder::getPagingForItem($navigation, $item);
+ }
+ }
+
+ # dispatch the item
+ $item = $this->c->dispatcher->dispatch('onItemLoaded', new OnItemLoaded($item))->getData();
+
+ # set the filepath
+ $filePath = $this->pathToContent . $item->path;
+
+ # check if url is a folder and add index.md
+ if($item->elementType == 'folder')
+ {
+ $filePath = $filePath . DIRECTORY_SEPARATOR . 'index.md';
+ }
+
+ # read the content of the file
+ $contentMD = file_exists($filePath) ? file_get_contents($filePath) : false;
+
+ # dispatch the original content without plugin-manipulations for case anyone wants to use it
+ $this->c->dispatcher->dispatch('onOriginalLoaded', new OnOriginalLoaded($contentMD));
+
+ # initiate object for metadata
+ $writeMeta = new WriteMeta();
+
+ # makes sure that you always have the full meta with title, description and all the rest.
+ $metatabs = $writeMeta->completePageMeta($contentMD, $this->settings, $item);
+
+ # write meta
+ if(isset($metatabs['meta']['noindex']) && $metatabs['meta']['noindex'])
+ {
+ $this->c->assets->addMeta('noindex','');
+ }
+
+ $this->c->assets->addMeta('og_site_name','');
+ $this->c->assets->addMeta('og_title','');
+ $this->c->assets->addMeta('og_description','');
+ $this->c->assets->addMeta('og_type','');
+ $this->c->assets->addMeta('og_url','');
+
+ # dispatch meta
+ $metatabs = $this->c->dispatcher->dispatch('onMetaLoaded', new OnMetaLoaded($metatabs))->getData();
+
+ # dispatch content
+ $contentMD = $this->c->dispatcher->dispatch('onMarkdownLoaded', new OnMarkdownLoaded($contentMD))->getData();
+
+ $itemUrl = isset($item->urlRel) ? $item->urlRel : false;
+
+ /* initialize parsedown */
+ $parsedown = new ParsedownExtension($this->base_url, $this->settings, $this->c->dispatcher);
+
+ /* set safe mode to escape javascript and html in markdown */
+ $parsedown->setSafeMode(true);
+
+ # check access restriction here
+ $restricted = $this->checkRestrictions($metatabs['meta']);
+ if($restricted)
+ {
+ # convert markdown into array of markdown block-elements
+ $markdownBlocks = $parsedown->markdownToArrayBlocks($contentMD);
+
+ # infos that plugins need to add restriction content
+ $restrictions = [
+ 'restricted' => $restricted,
+ 'defaultContent' => true,
+ 'markdownBlocks' => $markdownBlocks,
+ ];
+
+ # dispatch the data
+ $restrictions = $this->c->dispatcher->dispatch('onRestrictionsLoaded', new OnRestrictionsLoaded( $restrictions ))->getData();
+
+ # use the returned markdown
+ $markdownBlocks = $restrictions['markdownBlocks'];
+
+ # if no plugin has disabled the default behavior
+ if($restrictions['defaultContent'])
+ {
+ # cut the restricted content
+ $shortenedPage = $this->cutRestrictedContent($markdownBlocks);
+
+ # check if there is customized content
+ $restrictionnotice = $this->prepareRestrictionNotice();
+
+ # add notice to shortened content
+ $shortenedPage[] = $restrictionnotice;
+
+ # Use the shortened page
+ $markdownBlocks = $shortenedPage;
+ }
+
+ # finally transform the markdown blocks back to pure markdown text
+ $contentMD = $parsedown->arrayBlocksToMarkdown($markdownBlocks);
+ }
+
+ /* parse markdown-file to content-array */
+ $contentArray = $parsedown->text($contentMD);
+ $contentArray = $this->c->dispatcher->dispatch('onContentArrayLoaded', new OnContentArrayLoaded($contentArray))->getData();
+
+ /* parse markdown-content-array to content-string */
+ $contentHTML = $parsedown->markup($contentArray);
+ $contentHTML = $this->c->dispatcher->dispatch('onHtmlLoaded', new OnHtmlLoaded($contentHTML))->getData();
+
+ /* extract the h1 headline*/
+ $contentParts = explode("", $contentHTML, 2);
+ $title = isset($contentParts[0]) ? strip_tags($contentParts[0]) : $this->settings['title'];
+
+ $contentHTML = isset($contentParts[1]) ? $contentParts[1] : $contentHTML;
+
+ # get the first image from content array */
+ $img_url = isset($metatabs['meta']['heroimage']) ? $metatabs['meta']['heroimage'] : false;
+ $img_alt = isset($metatabs['meta']['heroimagealt']) ? $metatabs['meta']['heroimagealt'] : false;
+
+ # get url and alt-tag for first image, if exists */
+ if(!$img_url OR $img_url == '')
+ {
+ # extract first image from content
+ $firstImageMD = $this->getFirstImage($contentArray);
+
+ if($firstImageMD)
+ {
+ preg_match('#\((.*?)\)#', $firstImageMD, $img_url_result);
+ $img_url = isset($img_url_result[1]) ? $img_url_result[1] : false;
+
+ if($img_url)
+ {
+ preg_match('#\[(.*?)\]#', $firstImageMD, $img_alt_result);
+ $img_alt = isset($img_alt_result[1]) ? $img_alt_result[1] : false;
+ }
+ }
+ elseif($logo)
+ {
+ $img_url = $logo;
+ $pathinfo = pathinfo($this->settings['logo']);
+ $img_alt = $pathinfo['filename'];
+ }
+ }
+
+ $firstImage = false;
+ if($img_url)
+ {
+ $firstImage = array('img_url' => $this->base_url . '/' . $img_url, 'img_alt' => $img_alt);
+
+ $this->c->assets->addMeta('og_image','');
+ $this->c->assets->addMeta('twitter_image_alt','');
+ $this->c->assets->addMeta('twitter_card','');
+ }
+
+ $route = empty($args) && isset($this->settings['themes'][$theme]['cover']) ? '/cover.twig' : '/index.twig';
+
+ return $this->render($response, $route, [
+ 'home' => $home,
+ 'navigation' => $navigation,
+ 'title' => $title,
+ 'content' => $contentHTML,
+ 'item' => $item,
+ 'breadcrumb' => $breadcrumb,
+ 'settings' => $this->settings,
+ 'base_url' => $this->base_url,
+ 'metatabs' => $metatabs,
+ 'image' => $firstImage,
+ 'logo' => $logo,
+ 'favicon' => $favicon,
+ 'currentpage' => $currentpage
+ ]);
+ }
+
+
+ protected function getFirstImage(array $contentBlocks)
+ {
+ foreach($contentBlocks as $block)
+ {
+ /* is it a paragraph? */
+ if(isset($block['name']) && $block['name'] == 'p')
+ {
+ if(isset($block['handler']['argument']) && substr($block['handler']['argument'], 0, 2) == '![' )
+ {
+ return $block['handler']['argument'];
+ }
+ }
+ }
+
+ return false;
+ }
+
+ # checks if a page has a restriction in meta and if the current user is blocked by that restriction
+ protected function checkRestrictions($meta)
+ {
+ # check if content restrictions are active
+ if(isset($this->settings['pageaccess']) && $this->settings['pageaccess'])
+ {
+
+ # check if page is restricted to certain user
+ if(isset($meta['alloweduser']) && $meta['alloweduser'] && $meta['alloweduser'] !== '' )
+ {
+ $alloweduser = array_map('trim', explode(",", $meta['alloweduser']));
+ if(isset($_SESSION['user']) && in_array($_SESSION['user'], $alloweduser))
+ {
+ # user has access to the page, so there are no restrictions
+ return false;
+ }
+
+ # otherwise return array with type of restriction and allowed username
+ return [ 'alloweduser' => $meta['alloweduser'] ];
+ }
+
+ # check if page is restricted to certain userrole
+ if(isset($meta['allowedrole']) && $meta['allowedrole'] && $meta['allowedrole'] !== '' )
+ {
+ # var_dump($this->c->acl->inheritsRole('editor', 'member'));
+ # die();
+ if(
+ isset($_SESSION['role'])
+ AND (
+ $_SESSION['role'] == 'administrator'
+ OR $_SESSION['role'] == $meta['allowedrole']
+ OR $this->c->acl->inheritsRole($_SESSION['role'], $meta['allowedrole'])
+ )
+ )
+ {
+ # role has access to page, so there are no restrictions
+ return false;
+ }
+
+ return [ 'allowedrole' => $meta['allowedrole'] ];
+ }
+
+ }
+
+ return false;
+
+ }
+
+ protected function cutRestrictedContent($markdown)
+ {
+ #initially add only the title of the page.
+ $restrictedMarkdown = [$markdown[0]];
+ unset($markdown[0]);
+
+ if(isset($this->settings['hrdelimiter']) && $this->settings['hrdelimiter'] !== NULL )
+ {
+ foreach ($markdown as $block)
+ {
+ $firstCharacters = substr($block, 0, 3);
+ if($firstCharacters == '---' OR $firstCharacters == '***')
+ {
+ return $restrictedMarkdown;
+ }
+ $restrictedMarkdown[] = $block;
+ }
+
+ # no delimiter found, so use the title only
+ $restrictedMarkdown = [$restrictedMarkdown[0]];
+ }
+
+ return $restrictedMarkdown;
+ }
+
+ protected function prepareRestrictionNotice()
+ {
+ if( isset($this->settings['restrictionnotice']) && $this->settings['restrictionnotice'] != '' )
+ {
+ $restrictionNotice = $this->settings['restrictionnotice'];
+ }
+ else
+ {
+ $restrictionNotice = 'You are not allowed to access this content.';
+ }
+
+ if( isset($this->settings['wraprestrictionnotice']) && $this->settings['wraprestrictionnotice'] )
+ {
+ # standardize line breaks
+ $text = str_replace(array("\r\n", "\r"), "\n", $restrictionNotice);
+
+ # remove surrounding line breaks
+ $text = trim($text, "\n");
+
+ # split text into lines
+ $lines = explode("\n", $text);
+
+ $restrictionNotice = '';
+
+ foreach($lines as $key => $line)
+ {
+ $restrictionNotice .= "!!!! " . $line . "\n";
+ }
+ }
+
+ return $restrictionNotice;
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Controllers/ControllerWebLogin.php b/system/typemill/Controllers/ControllerWebLogin.php
new file mode 100644
index 0000000..2bc802d
--- /dev/null
+++ b/system/typemill/Controllers/ControllerWebLogin.php
@@ -0,0 +1,108 @@
+withRedirect($this->c->router->pathFor('content.raw'));
+ }
+ else
+ {
+ return $response->withRedirect($this->c->router->pathFor('auth.show'));
+ }
+ }
+
+ /**
+ * show login form
+ *
+ * @param obj $request the slim request object.
+ * @param obj $response the slim response object.
+ * @param array $args with arguments past to the slim router
+ * @return obj $response and string route.
+ */
+
+ public function show(Request $request, Response $response, $args)
+ {
+ return $this->c->get('view')->render($response, 'login.twig', [
+ #'captcha' => $this->checkIfAddCaptcha(),
+ #'url' => $this->urlCollection,
+ ]);
+
+# $settings = $this->c->get('settings');
+
+# return $this->render($response, '/auth/login.twig', ['settings' => $settings]);
+ }
+
+ /**
+ * signin an existing user
+ *
+ * @param obj $request the slim request object with form data in the post params.
+ * @param obj $response the slim response object.
+ * @return obj $response with redirect to route.
+ */
+
+ public function login(Request $request, Response $response)
+ {
+ if( ( null !== $request->getattribute('csrf_result') ) OR ( $request->getattribute('csrf_result') === false ) )
+ {
+ $this->c->flash->addMessage('error', 'The form has a timeout, please try again.');
+ return $response->withRedirect($this->c->router->pathFor('auth.show'));
+ }
+
+ /* authentication */
+ $params = $request->getParams();
+ $validation = new Validation();
+ $settings = $this->c->get('settings');
+
+ if($validation->signin($params))
+ {
+ $user = new User();
+ $userdata = $user->getUser($params['username']);
+
+ if($userdata && password_verify($params['password'], $userdata['password']))
+ {
+ # check if user has confirmed the account
+ if(isset($userdata['optintoken']) && $userdata['optintoken'])
+ {
+ $this->c->flash->addMessage('error', 'Your registration is not confirmed yet. Please check your e-mails and use the confirmation link.');
+ return $response->withRedirect($this->c->router->pathFor('auth.show'));
+ }
+
+ $user->login($userdata['username']);
+
+ # if user is allowed to view content-area
+ if($this->c->acl->hasRole($userdata['userrole']) && $this->c->acl->isAllowed($userdata['userrole'], 'content', 'view'))
+ {
+ $settings = $this->c->get('settings');
+ $editor = (isset($settings['editor']) && $settings['editor'] == 'visual') ? 'visual' : 'raw';
+
+ return $response->withRedirect($this->c->router->pathFor('content.' . $editor));
+ }
+ return $response->withRedirect($this->c->router->pathFor('user.account'));
+ }
+ }
+
+ if(isset($this->settings['securitylog']) && $this->settings['securitylog'])
+ {
+ \Typemill\Models\Helpers::addLogEntry('wrong login');
+ }
+
+ $this->c->flash->addMessage('error', 'Ups, wrong password or username, please try again.');
+ return $response->withRedirect($this->c->router->pathFor('auth.show'));
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Events/BaseEvent.php b/system/typemill/Events/BaseEvent.php
new file mode 100644
index 0000000..9333e1f
--- /dev/null
+++ b/system/typemill/Events/BaseEvent.php
@@ -0,0 +1,27 @@
+data = $data;
+ }
+
+ public function getData()
+ {
+ return $this->data;
+ }
+
+ public function setData($data)
+ {
+ $this->data = $data;
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Events/OnBreadcrumbLoaded.php b/system/typemill/Events/OnBreadcrumbLoaded.php
new file mode 100644
index 0000000..56c1fa9
--- /dev/null
+++ b/system/typemill/Events/OnBreadcrumbLoaded.php
@@ -0,0 +1,14 @@
+data = $data;
+ }
+
+ public function getMarkdown()
+ {
+ return $this->data;
+ }
+
+ public function getHTML($urlrel)
+ {
+ $parsedown = new ParsedownExtension();
+ $contentArray = $parsedown->text($this->data);
+ $contentHTML = $parsedown->markup($contentArray, $urlrel);
+
+ return $contentHTML;
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Events/OnPageDeleted.php b/system/typemill/Events/OnPageDeleted.php
new file mode 100644
index 0000000..0240baa
--- /dev/null
+++ b/system/typemill/Events/OnPageDeleted.php
@@ -0,0 +1,14 @@
+csrf = $csrf;
+ }
+
+ public function getFunctions()
+ {
+ return [
+ new \Twig\TwigFunction('csrf', [$this, 'csrf'])
+ ];
+ }
+
+ public function csrf()
+ {
+ $csrf = 'TokenNameValue: '. $this->csrf->getTokenName() .'
+ ';
+
+ return $csrf;
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Middleware/CreateSession.php b/system/typemill/Middleware/CreateSession.php
new file mode 100644
index 0000000..0c3b678
--- /dev/null
+++ b/system/typemill/Middleware/CreateSession.php
@@ -0,0 +1,56 @@
+sessionSegments = $session_segments;
+
+ $this->routepath = $routepath;
+ }
+
+ public function process(Request $request, RequestHandler $handler) :response
+ {
+ foreach($this->sessionSegments as $segment)
+ {
+ if(substr( $this->routepath, 0, strlen($segment) ) === ltrim($segment, '/'))
+ {
+ echo '
Create Session';
+
+ // configure session
+ ini_set('session.cookie_httponly', 1 );
+ ini_set('session.use_strict_mode', 1);
+ ini_set('session.cookie_samesite', 'lax');
+ /*
+ if($uri->getScheme() == 'https')
+ {
+ ini_set('session.cookie_secure', 1);
+ session_name('__Secure-typemill-session');
+ }
+ else
+ {
+ session_name('typemill-session');
+ }
+ */
+ // start session
+ session_start();
+
+ $request = $request->withAttribute('session', $_SESSION);
+ }
+ }
+
+ return $handler->handle($request);
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Middleware/CsrfProtection.php b/system/typemill/Middleware/CsrfProtection.php
new file mode 100644
index 0000000..4a45875
--- /dev/null
+++ b/system/typemill/Middleware/CsrfProtection.php
@@ -0,0 +1,41 @@
+container = $container;
+ $this->responseFactory = $responseFactory;
+ }
+
+ public function process(Request $request, RequestHandler $handler) :response
+ {
+ if(is_array($request->getAttribute('session')))
+ {
+ echo '
csrf protection';
+
+ $responseFactory = $this->responseFactory;
+
+ # Register Middleware On Container
+ $this->container->set('csrf', function () use ($responseFactory)
+ {
+ return new Guard($responseFactory);
+ });
+ }
+
+ return $handler->handle($request);
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Middleware/CsrfProtectionToMiddleware.php b/system/typemill/Middleware/CsrfProtectionToMiddleware.php
new file mode 100644
index 0000000..bfe7259
--- /dev/null
+++ b/system/typemill/Middleware/CsrfProtectionToMiddleware.php
@@ -0,0 +1,30 @@
+container = $container;
+ }
+
+ public function __invoke(Request $request, RequestHandler $handler)
+ {
+ if(is_array($request->getAttribute('session')))
+ {
+ echo '
csrf protection to middleware';
+
+ return $this->container->get('csrf');
+ }
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Middleware/FlashMessages.php b/system/typemill/Middleware/FlashMessages.php
new file mode 100644
index 0000000..8775f69
--- /dev/null
+++ b/system/typemill/Middleware/FlashMessages.php
@@ -0,0 +1,34 @@
+container = $container;
+ }
+
+ public function process(Request $request, RequestHandler $handler) :response
+ {
+ if(is_array($request->getAttribute('session')))
+ {
+ echo '
flash messages';
+
+ $this->container->set('flash', function(){
+ return new Messages();
+ });
+ }
+
+ return $handler->handle($request);
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Middleware/JsonBodyParser.php b/system/typemill/Middleware/JsonBodyParser.php
new file mode 100644
index 0000000..ff7f18e
--- /dev/null
+++ b/system/typemill/Middleware/JsonBodyParser.php
@@ -0,0 +1,29 @@
+ JSON Body parser';
+
+ $contentType = $request->getHeaderLine('Content-Type');
+
+ if (strstr($contentType, 'application/json'))
+ {
+ $contents = json_decode(file_get_contents('php://input'), true);
+ if (json_last_error() === JSON_ERROR_NONE)
+ {
+ $request = $request->withParsedBody($contents);
+ }
+ }
+
+ return $handler->handle($request);
+ }
+}
diff --git a/system/typemill/Middleware/RedirectIfAuthenticated.php b/system/typemill/Middleware/RedirectIfAuthenticated.php
new file mode 100644
index 0000000..dcf7f49
--- /dev/null
+++ b/system/typemill/Middleware/RedirectIfAuthenticated.php
@@ -0,0 +1,34 @@
+router = $router;
+ $this->settings = $settings;
+ }
+
+ public function process(Request $request, RequestHandler $handler) :response
+ {
+ $response = $handler->handle($request);
+
+ $editor = (isset($this->settings['editor']) && $this->settings['editor'] == 'visual') ? 'visual' : 'raw';
+
+ if(isset($_SESSION['login']))
+ {
+ return $response->withHeader('Location', $this->router->pathFor('content.' . $editor))->withStatus(302);
+# $response = $response->withRedirect($this->router->pathFor('content.' . $editor));
+ }
+
+ return $response;
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Plugin.php b/system/typemill/Plugin.php
new file mode 100644
index 0000000..7a4c224
--- /dev/null
+++ b/system/typemill/Plugin.php
@@ -0,0 +1,333 @@
+container = $container;
+/*
+ $this->path = trim($this->container['request']->getUri()->getPath(),"/");
+
+ if(substr($this->path, 0, 3) === "tm/")
+ {
+ $this->adminpath = true;
+ }
+*/
+ }
+
+ protected function isXhr()
+ {
+ if($this->container['request']->isXhr())
+ {
+ return true;
+ }
+ return false;
+ }
+
+ protected function getParams()
+ {
+ return $this->container['request']->getParams();
+ }
+
+ protected function returnJson($data)
+ {
+ return $this->container['response']
+ ->withHeader("Content-Type", "application/json")
+ ->withStatus(200)
+ ->write(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
+ }
+
+ protected function returnJsonError($data)
+ {
+ return $this->container['response']
+ ->withHeader("Content-Type", "application/json")
+ ->withStatus(400)
+ ->write(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
+ }
+
+ protected function getSettings()
+ {
+ return $this->container->get('settings');
+ }
+
+ protected function getPluginSettings($plugin)
+ {
+ return $this->container->get('settings')['plugins'][$plugin];
+ }
+
+ protected function getRoute()
+ {
+ return $this->container['request']->getUri()->withUserInfo('');
+ }
+
+ protected function getPath()
+ {
+ return $this->container['request']->getUri()->getPath();
+ }
+
+ protected function getDispatcher()
+ {
+ return $this->container['dispatcher'];
+ }
+
+ protected function getTwig()
+ {
+ return $this->container['view'];
+ }
+
+ protected function addTwigGlobal($name, $class)
+ {
+ $this->container->view->getEnvironment()->addGlobal($name, $class);
+ }
+
+ protected function addTwigFilter($name, $filter)
+ {
+ $filter = new \Twig_SimpleFilter($name, $filter);
+ $this->container->view->getEnvironment()->addFilter($filter);
+ }
+
+ protected function addTwigFunction($name, $function)
+ {
+ $function = new \Twig_SimpleFunction($name, $function);
+ $this->container->view->getEnvironment()->addFunction($function);
+ }
+
+ protected function addJS($JS)
+ {
+ $this->container->assets->addJS($JS);
+ }
+
+ protected function addEditorJS($JS)
+ {
+ $this->container->assets->addEditorJS($JS);
+ }
+
+ protected function addInlineJS($JS)
+ {
+ $this->container->assets->addInlineJS($JS);
+ }
+
+ protected function addSvgSymbol($symbol)
+ {
+ $this->container->assets->addSvgSymbol($symbol);
+ }
+
+ protected function addEditorInlineJS($JS)
+ {
+ $this->container->assets->addEditorInlineJS($JS);
+ }
+
+ protected function addCSS($CSS)
+ {
+ $this->container->assets->addCSS($CSS);
+ }
+
+ protected function addInlineCSS($CSS)
+ {
+ $this->container->assets->addInlineCSS($CSS);
+ }
+
+ protected function addEditorCSS($CSS)
+ {
+ $this->container->assets->addEditorCSS($CSS);
+ }
+
+ protected function getMeta()
+ {
+ return $this->container->assets->meta;
+ }
+
+ public function addMeta($key,$meta)
+ {
+ $this->container->assets->addMeta($key, $meta);
+ }
+
+ protected function activateAxios()
+ {
+ $this->container->assets->activateAxios();
+ }
+
+ protected function activateVue()
+ {
+ $this->container->assets->activateVue();
+ }
+
+ protected function activateTachyons()
+ {
+ $this->container->assets->activateTachyons();
+ }
+
+ protected function markdownToHtml($markdown)
+ {
+ $parsedown = new ParsedownExtension();
+
+ $contentArray = $parsedown->text($markdown);
+ $html = $parsedown->markup($contentArray);
+
+ return $html;
+ }
+
+ protected function getFormData($pluginName)
+ {
+ $flash = $this->container->flash->getMessages();
+
+ if(isset($flash['formdata']))
+ {
+ $yaml = new Models\WriteYaml();
+ $formdata = $yaml->getYaml('settings', 'formdata.yaml');
+ $yaml->updateYaml('settings', 'formdata.yaml', '');
+
+ if($flash['formdata'][0] == $pluginName && isset($formdata[$pluginName]))
+ {
+ return $formdata[$pluginName];
+ }
+ }
+ elseif(isset($flash['publicform']) && $flash['publicform'][0] == 'bot')
+ {
+ return 'bot';
+ }
+ return false;
+ }
+
+ protected function generateForm($pluginName, $routename)
+ {
+ $fieldsModel = new Fields();
+
+ $settings = $this->getSettings();
+ $form = false;
+
+ $pluginDefinitions = \Typemill\Settings::getObjectSettings('plugins', $pluginName);
+ if(isset($settings['plugins'][$pluginName]['publicformdefinitions']) && $settings['plugins'][$pluginName]['publicformdefinitions'] != '')
+ {
+ $arrayFromYaml = \Symfony\Component\Yaml\Yaml::parse($settings['plugins'][$pluginName]['publicformdefinitions']);
+ $pluginDefinitions['public']['fields'] = $arrayFromYaml;
+ }
+
+ $buttonlabel = isset($settings['plugins'][$pluginName]['button_label']) ? $settings['plugins'][$pluginName]['button_label'] : false;
+ $captchaoptions = isset($settings['plugins'][$pluginName]['captchaoptions']) ? $settings['plugins'][$pluginName]['captchaoptions'] : false;
+ $recaptcha = isset($settings['plugins'][$pluginName]['recaptcha']) ? $settings['plugins'][$pluginName]['recaptcha_webkey'] : false;
+
+ if($captchaoptions == 'disabled')
+ {
+ # in case a captcha has failed on another page like login, the captcha-session must be deleted, otherwise it will not pass the security middleware
+ unset($_SESSION['captcha']);
+ }
+
+ $fieldsModel = new Fields();
+
+ if(isset($pluginDefinitions['public']['fields']))
+ {
+ # get all the fields and prefill them with the dafault-data, the user-data or old input data
+ $fields = $fieldsModel->getFields($settings, 'plugins', $pluginName, $pluginDefinitions, 'public');
+
+ # get Twig Instance
+ $twig = $this->getTwig();
+
+ # render each field and add it to the form
+ $form = $twig->fetch('/partials/form.twig', [
+ 'routename' => $routename,
+ 'fields' => $fields,
+ 'itemName' => $pluginName,
+ 'object' => 'plugins',
+ 'buttonlabel' => $buttonlabel,
+ 'captchaoptions' => $captchaoptions,
+ 'recaptcha_webkey' => $recaptcha,
+ ]);
+ }
+
+ return $form;
+ }
+
+ protected function validateParams($params)
+ {
+ $pluginName = key($params);
+
+ if(isset($params[$pluginName]))
+ {
+ $userInput = $params[$pluginName];
+ $settings = $this->getSettings();
+
+ # get settings and start validation
+ $originalSettings = \Typemill\Settings::getObjectSettings('plugins', $pluginName);
+ if(isset($settings['plugins'][$pluginName]['publicformdefinitions']) && $settings['plugins'][$pluginName]['publicformdefinitions'] != '')
+ {
+ $arrayFromYaml = \Symfony\Component\Yaml\Yaml::parse($settings['plugins'][$pluginName]['publicformdefinitions']);
+ $originalSettings['public']['fields'] = $arrayFromYaml;
+ }
+ elseif(isset($originalSettings['settings']['publicformdefinitions']))
+ {
+ $arrayFromYaml = \Symfony\Component\Yaml\Yaml::parse($originalSettings['settings']['publicformdefinitions']);
+ $originalSettings['public']['fields'] = $arrayFromYaml;
+ }
+
+ $validate = new Validation();
+
+ if(isset($originalSettings['public']['fields']))
+ {
+ # flaten the multi-dimensional array with fieldsets to a one-dimensional array
+ $originalFields = array();
+ foreach($originalSettings['public']['fields'] as $fieldName => $fieldValue)
+ {
+ if(isset($fieldValue['fields']))
+ {
+ foreach($fieldValue['fields'] as $subFieldName => $subFieldValue)
+ {
+ $originalFields[$subFieldName] = $subFieldValue;
+ }
+ }
+ else
+ {
+ $originalFields[$fieldName] = $fieldValue;
+ }
+ }
+
+ # take the user input data and iterate over all fields and values
+ foreach($userInput as $fieldName => $fieldValue)
+ {
+ # get the corresponding field definition from original plugin settings
+ $fieldDefinition = isset($originalFields[$fieldName]) ? $originalFields[$fieldName] : false;
+
+ if($fieldDefinition)
+ {
+ # validate user input for this field
+ $validate->objectField($fieldName, $fieldValue, $pluginName, $fieldDefinition);
+ }
+ if(!$fieldDefinition && $fieldName != 'active')
+ {
+ $_SESSION['errors'][$pluginName][$fieldName] = array('This field is not defined!');
+ }
+ }
+
+ if(isset($_SESSION['errors']))
+ {
+ $this->container->flash->addMessage('error', 'Please correct the errors');
+ return false;
+ }
+
+ return $params[$pluginName];
+ }
+ }
+
+ $this->container->flash->addMessage('error', 'The data from the form was invalid (missing or not defined)');
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Static/Helpers.php b/system/typemill/Static/Helpers.php
new file mode 100644
index 0000000..957fc2d
--- /dev/null
+++ b/system/typemill/Static/Helpers.php
@@ -0,0 +1,27 @@
+';
+ $table .= 'Breakpoint | Time | Duration |
';
+ foreach($timer as $breakpoint => $time)
+ {
+ $duration = $time - $lastTime;
+
+ $table .= '';
+ $table .= '' . $breakpoint . ' | ';
+ $table .= '' . $time . ' | ';
+ $table .= '' . $duration . ' | ';
+ $table .= '
';
+
+ $lastTime = $time;
+ }
+ $table .= '