1
0
mirror of https://github.com/typemill/typemill.git synced 2025-07-30 19:00:32 +02:00

Version 1.4.9: Rewrite slug and recreate cache

This commit is contained in:
trendschau
2021-09-12 20:32:00 +02:00
parent c07f5d54d4
commit eb16fe52a4
12 changed files with 344 additions and 201 deletions

View File

@@ -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'

View File

@@ -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

View File

@@ -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)
{

View File

@@ -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)

View File

@@ -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)

View File

@@ -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;
}
}

View File

@@ -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']));

View File

@@ -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{

View File

@@ -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: '<section><form>' +
'<div><div class="large relative">' +
'<label>Slug / Name in URL</label><input type="text" v-model="slug" @input="changeSlug()"><button @click.prevent="storeSlug()" :disabled="disabled" class="button slugbutton bn br2 bg-tm-green white absolute">change slug</button>' +
'<div v-if="slugerror" class="f6 tm-red mt1">{{ slugerror }}</div>' +
'</div></div>' +
'<div v-for="(field, index) in schema.fields">' +
'<fieldset v-if="field.type == \'fieldset\'" class="fs-formbuilder"><legend>{{field.legend}}</legend>' +
'<component v-for="(subfield, index) in field.fields "' +
@@ -40,6 +52,11 @@ Vue.component('tab-meta', {
'<div v-if="errors" class="metasubmit"><div class="metaErrors">{{ \'Please correct the errors above\'|translate }}</div></div>' +
'<div class="metasubmit"><input type="submit" @click.prevent="saveInput" :value="\'save\'|translate"></input></div>' +
'</form></section>',
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;
}
});
}
}
}
})

View File

@@ -523,7 +523,7 @@ Vue.component('component-image', {
'@input="update($event, name)">' +
'</div>' +
'<div class="dib w-100 mt2">' +
'<button class="w-100 pointer ba br1 b--tm-green bg--tm-gray black pa2 ma0 tc" @click.prevent="switchQuality()">{{ getQualityLabel() }}</button>' +
'<button class="w-100 pointer ba br1 b--tm-green bg--tm-gray black pa2 ma0 tc" @click.prevent="switchQuality()">{{ qualitylabel }}</button>' +
'</div>' +
'</div>' +
'<div v-if="description" class="w-100 dib"><p>{{ description|translate }}</p></div>' +
@@ -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()

View File

@@ -201,8 +201,8 @@
</label>
</div>
<div class="medium">
<div class="label">{{ __('Delete all cache files') }}</div>
<button id="clearcache" class="link bg-tm-green white dim bn br1 ph3 pv2 f6">{{ __('Clear Cache') }}</button><div id="cacheresult" class="dib ph3 pv2"></div>
<div class="label">{{ __('Recreate cached files') }}</div>
<button id="clearcache" class="link bg-tm-green white dim bn br1 ph3 pv2 f6">{{ __('Recreate Cache') }}</button><div id="cacheresult" class="dib ph3 pv2"></div>
</div>
<div class="medium{{ errors.settings.images.live.width ? ' error' : '' }}">
<label for="imagewidth">{{ __('Standard width for images') }}</label>

View File

@@ -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{}