1
0
mirror of https://github.com/typemill/typemill.git synced 2025-08-06 22:26: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

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