diff --git a/content/00-welcome/00-setup-your-website.yaml b/content/00-welcome/00-setup-your-website.yaml index f61188b..da485cd 100644 --- a/content/00-welcome/00-setup-your-website.yaml +++ b/content/00-welcome/00-setup-your-website.yaml @@ -1,7 +1,7 @@ meta: title: 'Setup Your Website' description: 'Typemill provides detailed settings, and you have access to nearly all settings in the author panel. Learn the basics in this short video:' - heroimage: media/live/jack-ward-wbs1qewclne-unsplash.jpeg + heroimage: '' heroimagealt: null owner: trendschau author: 'Sebastian Schürmanns' diff --git a/content/00-welcome/01-manage-access.md b/content/00-welcome/01-manage-access.md index 64c58e1..fd28e7a 100644 --- a/content/00-welcome/01-manage-access.md +++ b/content/00-welcome/01-manage-access.md @@ -2,6 +2,9 @@ Typemill has a build-in system to restrict access to pages or to the whole websites. You can activate both features in the system settings under the section "access rights". If you activate one of the features, then Typemill will use session cookies on all frontend pages. Learn all the details in the following video tutorial: +```pagebreak +``` + ![youtube-video](media/live/youtube-uw-m-4g1kaa.jpeg "click to load video"){#UW_m-4g1kAA .youtube} ## Restrict Access for the Website diff --git a/system/Controllers/ArticleApiController.php b/system/Controllers/ArticleApiController.php index 2f1ea7f..a397d8f 100644 --- a/system/Controllers/ArticleApiController.php +++ b/system/Controllers/ArticleApiController.php @@ -8,6 +8,7 @@ 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; @@ -407,6 +408,78 @@ class ArticleApiController extends ContentController return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404); } } + + public function renameArticle(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri()->withUserInfo(''); + $dir = $this->settings['basePath'] . 'cache'; + $pathToContent = $this->settings['rootPath'] . $this->settings['contentFolder']; + + # minimum permission is that user is allowed to update his own content + if(!$this->c->acl->isAllowed($_SESSION['role'], 'mycontent', 'update')) + { + return $response->withJson(array('data' => false, 'errors' => 'You are not allowed to update content.'), 403); + } + + # validate input + 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); } + + # set information for homepage + $this->setHomepage($args = false); + + # set item + if(!$this->setItem()){ return $response->withJson($this->errors, 404); } + + # validate input part 2 + 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); + } + + # if user has no right to update content from others (eg admin or editor) + if(!$this->c->acl->isAllowed($_SESSION['role'], 'content', 'update')) + { + # 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); + } + } + + # get the folder where file lives in + $pathWithoutFile = str_replace($this->item->originalName, '', $this->item->path); + + # 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) + { + return $response->withJson(['errors' => $error], 500); + } + + # recreates the cache for structure, structure-extended and navigation + $write->getFreshStructure($pathToContent, $this->uri); + + $newUrlRel = str_replace($this->item->slug, $this->params['slug'], $this->item->urlRelWoF); + + $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor'] . $newUrlRel; + + return $response->withJson(array('data' => false, 'errors' => false, 'url' => $url)); + } public function sortArticle(Request $request, Response $response, $args) { diff --git a/system/Controllers/PageController.php b/system/Controllers/PageController.php index 1c508ff..ba3aedc 100644 --- a/system/Controllers/PageController.php +++ b/system/Controllers/PageController.php @@ -46,7 +46,7 @@ class PageController extends Controller # if the cached structure is still valid, use it if($cache->validate('cache', 'lastCache.txt', 600)) { - $structure = $this->getCachedStructure($cache); + $structure = $cache->getCachedStructure(); } else { @@ -57,7 +57,7 @@ class PageController extends Controller if(!isset($structure) OR !$structure) { # if not, get a fresh structure of the content folder - $structure = $this->getFreshStructure($pathToContent, $cache, $uri); + $structure = $cache->getFreshStructure($pathToContent, $uri); # if there is no structure at all, the content folder is probably empty if(!$structure) @@ -356,145 +356,7 @@ class PageController extends Controller ]); } - protected function getCachedStructure($cache) - { - return $cache->getCache('cache', 'structure.txt'); - } - - protected function getFreshStructure($pathToContent, $cache, $uri) - { - /* scan the content of the folder */ - $pagetree = Folder::scanFolder($pathToContent); - /* 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($this->pathToContent, $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 - $cache->updateCache('cache', 'structure.txt', 'lastCache.txt', $structure); - - if($extended && $this->containsHiddenPages($extended)) - { - # generate the navigation (delete empty pages) - $navigation = $this->createNavigationFromStructure($structure); - - # cache navigation - $cache->updateCache('cache', 'navigation.txt', false, $navigation); - } - else - { - # make sure no separate navigation file is set - $cache->deleteFileWithPath('cache' . DIRECTORY_SEPARATOR . 'navigation.txt'); - } - - # load and return the cached structure, because might be manipulated with navigation.... - $structure = $this->getCachedStructure($cache); - - 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'] - protected 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; - } - - # 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; - } - - protected 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; - } - - # not in use, stored the latest version in user settings, but that does not make sense because checkd on the fly with api in admin - protected function updateVersion($baseUrl) - { - /* check the latest public typemill version */ - $version = new VersionCheck(); - $latestVersion = $version->checkVersion($baseUrl); - - if($latestVersion) - { - /* store latest version */ - \Typemill\Settings::updateSettings(array('latestVersion' => $latestVersion)); - } - } - protected function getFirstImage(array $contentBlocks) { foreach($contentBlocks as $block) diff --git a/system/Controllers/SettingsController.php b/system/Controllers/SettingsController.php index ae417b0..5d32191 100644 --- a/system/Controllers/SettingsController.php +++ b/system/Controllers/SettingsController.php @@ -4,6 +4,7 @@ namespace Typemill\Controllers; use \Symfony\Component\Yaml\Yaml; use Typemill\Models\Write; +use Typemill\Models\WriteCache; use Typemill\Models\Fields; use Typemill\Models\Validation; use Typemill\Models\User; @@ -971,38 +972,24 @@ class SettingsController extends Controller public function clearCache($request, $response, $args) { - $settings = $this->c->get('settings'); - $dir = $settings['basePath'] . 'cache'; - $iterator = new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS); - $files = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST); + $settings = $this->c->get('settings'); + $dir = $settings['basePath'] . 'cache'; + $uri = $request->getUri()->withUserInfo(''); + $pathToContent = $settings['rootPath'] . $settings['contentFolder']; - $error = false; + $writeCache = new writeCache(); - foreach($files as $file) - { - if ($file->isDir()) - { - if(!rmdir($file->getRealPath())) - { - $error = 'Could not delete some folders.'; - } - } - elseif($file->getExtension() !== 'css') - { - if(!unlink($file->getRealPath()) ) - { - $error = 'Could not delete some files.'; - } - } - } + $error = $writeCache->deleteCacheFiles($dir); if($error) { return $response->withJson(['errors' => $error], 500); } - return $response->withJson(array('errors' => false)); + # this recreates the cache for structure, structure-extended and navigation + $writeCache->getFreshStructure($pathToContent, $uri); + return $response->withJson(array('errors' => false)); } private function getUserFields($role) diff --git a/system/Models/WriteCache.php b/system/Models/WriteCache.php index 12326e8..8d1de02 100644 --- a/system/Models/WriteCache.php +++ b/system/Models/WriteCache.php @@ -2,6 +2,8 @@ namespace Typemill\Models; +use Typemill\Models\WriteYaml; + class WriteCache extends Write { /** @@ -73,17 +75,159 @@ class WriteCache extends Write return false; } - /** - * @todo Create a function to clear a specific cache file - */ - public function clearCache($name) + public function getCachedStructure() { + return $this->getCache('cache', 'structure.txt'); } /** * @todo Create a function to clear all cache files */ - public function clearAllCacheFiles() + public function deleteCacheFiles($dir) { + $iterator = new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS); + $files = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST); + + $error = false; + + foreach($files as $file) + { + if ($file->isDir()) + { + if(!rmdir($file->getRealPath())) + { + $error = 'Could not delete some folders.'; + } + } + elseif($file->getExtension() !== 'css') + { + if(!unlink($file->getRealPath()) ) + { + $error = 'Could not delete some files.'; + } + } + } + + 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/Routes/Api.php b/system/Routes/Api.php index 030c639..eb8ce8e 100644 --- a/system/Routes/Api.php +++ b/system/Routes/Api.php @@ -20,6 +20,7 @@ $app->post('/api/v1/article/html', ArticleApiController::class . ':getArticleHtm $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'])); diff --git a/system/author/css/style.css b/system/author/css/style.css index 785d4cf..ab080e9 100644 --- a/system/author/css/style.css +++ b/system/author/css/style.css @@ -2,22 +2,22 @@ * TRANSITION * **********************/ -a, a:link, a:visited, a:focus, a:hover, a:active, .link, button, .button, .tab-button, input, .control-group, .sidebar-menu, .sidebar-menu--content, .menu-action, .button-arrow{ - -webkit-transition: color 0.2s ease; - -moz-transition: color 0.2s ease; - -o-transition: color 0.2s ease; - -ms-transition: color 0.2s ease; - transition: color 0.2s ease; - -webkit-transition: background-color 0.2s ease; - -moz-transition: background-color 0.2s ease; - -o-transition: background-color 0.2s ease; - -ms-transition: background-color 0.2s ease; - transition: border-color 0.2s ease; - -webkit-transition: border-color 0.2s ease; - -moz-transition: border-color 0.2s ease; - -o-transition: border-color 0.2s ease; - -ms-transition: border-color 0.2s ease; - transition: border-color 0.2s ease; +a, a:link, a:visited, a:focus, a:hover, a:active, .blox, .link, button, .button, .tab-button, input, .control-group, .sidebar-menu, .sidebar-menu--content, .menu-action, .button-arrow{ + -webkit-transition: color 0.2s ease, + background-color 0.2s ease, + border-color 0.2s ease; + -moz-transition: color 0.2s ease, + background-color 0.2s ease, + border-color 0.2s ease; + -o-transition: color 0.2s ease, + background-color 0.2s ease, + border-color 0.2s ease; + -ms-transition: color 0.2s ease, + background-color 0.2s ease, + border-color 0.2s ease; + transition: color 0.2s ease, + background-color 0.2s ease, + border-color 0.2s ease; } .navi-item a, .navi-item.file a .iconwrapper, @@ -239,7 +239,7 @@ aside.sidebar{ display: block; width: 100%; background: #fff; - margin-bottom: 10px; + margin-bottom: 50px; box-sizing: border-box; } .right{ @@ -449,7 +449,7 @@ li.menu-item{ position: relative; } .navi-item .iconwrapper{ - display: inline-block; + display: none; position: absolute; top: 0px; background: transparent; @@ -459,6 +459,7 @@ li.menu-item{ width: 20px; height: 16px; } + .navi-item .status{ position: absolute; width: 4px; @@ -2700,6 +2701,11 @@ footer a:focus, footer a:hover, footer a:active .mbfix{ margin-bottom: 0px!important; } +.slugbutton{ + right: 20px; + height: 52px; + width: 150px; +} @media only screen and (min-width: 600px) { section{ @@ -2900,7 +2906,10 @@ footer a:focus, footer a:hover, footer a:active } .navi-item .status{ left: -30px; - } + } + .navi-item .iconwrapper{ + display: block; + } .navi-item a .movewrapper, .navi-item a:link .movewrapper, .navi-item a:visited .movewrapper{ diff --git a/system/author/js/vue-meta.js b/system/author/js/vue-meta.js index 97cffba..4ff125b 100644 --- a/system/author/js/vue-meta.js +++ b/system/author/js/vue-meta.js @@ -13,7 +13,19 @@ Vue.filter('translate', function (value) { Vue.component('tab-meta', { props: ['saved', 'errors', 'formdata', 'schema', 'userroles'], + data: function () { + return { + slug: false, + originalSlug: false, + slugerror: false, + disabled: "disabled", + } + }, template: '
' + + '
' + + '' + + '
{{ slugerror }}
' + + '
' + '
' + '
{{field.legend}}' + '
{{ \'Please correct the errors above\'|translate }}
' + '
' + '
', + mounted: function() + { + this.slug = this.$parent.item.slug; + this.originalSlug = this.slug; + }, methods: { selectComponent: function(field) { @@ -49,6 +66,52 @@ Vue.component('tab-meta', { { this.$emit('saveform'); }, + changeSlug: function() + { + if(this.slug == this.originalSlug) + { + this.slugerror = false; + this.disabled = "disabled"; + return; + } + + if(this.slug.match(/^[a-z0-9\-]*$/)) + { + this.slugerror = false; + this.disabled = false; + } + else + { + this.slugerror = 'Only lowercase a-z and 0-9 and "-" is allowed for slugs.'; + this.disabled = "disabled"; + } + }, + storeSlug: function() + { + + if(this.slug.match(/^[a-z0-9\-]*$/) && this.slug != this.originalSlug) + { + var self = this; + + myaxios.post('/api/v1/article/rename',{ + 'url': document.getElementById("path").value, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + 'slug': this.slug, + }) + .then(function (response) + { + window.location.replace(response.data.url); + }) + .catch(function (error) + { + if(error.response.data.errors.message) + { + publishController.errors.message = error.response.data.errors.message; + } + }); + } + } } }) diff --git a/system/author/js/vue-shared.js b/system/author/js/vue-shared.js index 75fcaed..46ac22a 100644 --- a/system/author/js/vue-shared.js +++ b/system/author/js/vue-shared.js @@ -523,7 +523,7 @@ Vue.component('component-image', { '@input="update($event, name)">' + '' + '
' + - '' + + '' + '
' + '' + '

{{ description|translate }}

' + @@ -543,6 +543,7 @@ Vue.component('component-image', { showmedialib: false, load: false, quality: false, + qualitylabel: false, } }, mounted: function(){ @@ -552,10 +553,12 @@ Vue.component('component-image', { if(this.value.indexOf("media/live") > -1 ) { this.quality = 'live'; + this.qualitylabel = 'switch quality to: original'; } else if(this.value.indexOf("media/original") > -1) { this.quality = 'original'; + this.qualitylabel = 'switch quality to: live'; } } }, @@ -579,14 +582,6 @@ Vue.component('component-image', { this.imgpreview = false; this.update(''); }, - getQualityLabel: function() - { - if(this.quality == 'live') - { - return 'switch quality to: original'; - } - return 'switch quality to: resized'; - }, switchQuality: function() { if(this.quality == 'live') @@ -594,12 +589,14 @@ Vue.component('component-image', { var newUrl = this.value.replace("media/live", "media/original"); this.update(newUrl); this.quality = 'original'; + this.qualitylabel = 'switch quality to: live'; } else { var newUrl = this.value.replace("media/original", "media/live"); this.update(newUrl); this.quality = 'live'; + this.qualitylabel = 'switch quality to: original'; } }, openmedialib: function() diff --git a/system/author/settings/system.twig b/system/author/settings/system.twig index 188bd62..1adf078 100644 --- a/system/author/settings/system.twig +++ b/system/author/settings/system.twig @@ -201,8 +201,8 @@
-
{{ __('Delete all cache files') }}
-
+
{{ __('Recreate cached files') }}
+
diff --git a/themes/cyanine/css/style.css b/themes/cyanine/css/style.css index a33292d..3c4e5ce 100644 --- a/themes/cyanine/css/style.css +++ b/themes/cyanine/css/style.css @@ -42,7 +42,7 @@ article pre, article code{ } article pre{ white-space: pre; - padding: 1em; + padding: 0em; display: block; max-width: 100%; overflow-x: auto; @@ -55,7 +55,11 @@ article code{ } pre > code{ font-size: 0.8em; - padding: 0; + padding: 1em; + display: inline-block; +} +pre > code.language-pagebreak{ + display: none; } dl{} dt{}