1
0
mirror of https://github.com/typemill/typemill.git synced 2025-08-01 20:00:37 +02:00

Version 1.5.1: File restrictions, download controller, noindex, fix youtube, cyanine third teaser

This commit is contained in:
trendschau
2021-11-01 21:11:59 +01:00
parent 2e12e184d3
commit 7d662daf86
16 changed files with 422 additions and 122 deletions

View File

@@ -64,7 +64,7 @@ RewriteRule ^(system\/author\/img\/) - [L]
RewriteRule ^(system\/author\/js\/) - [L]
# redirect all other direct requests to the following physical folders to the index.php so pages with same name work
RewriteRule ^(system|content|data|settings) index.php [QSA,L]
RewriteRule ^(system|content|data|settings|(media\/files\/)) index.php [QSA,L]
# disallow browsing other folders generally
Options -Indexes

View File

@@ -2,10 +2,7 @@
Cyanine provides a lot of other settings for your content area. For example:
* Add an edit-button for github, gitlab or other plattforms.
* Show the author.
* Show the publish date.
* Show the chapter numbers in the navigation.
[ebook (EPUB, 496.65 KB)](media/files/ebook.epub){.tm-download file-epub}
The Cyanine theme supports all content elements like tables, images, notices or downloads. It also supports anchor-links next to headlines, so you can deep link to certain content sections of your page. You can activate the anchors in the system settings of Typemill.

View File

@@ -180,7 +180,7 @@ class ControllerAuthorArticleApi extends ControllerAuthor
# check if it is a folder and if the folder has published pages.
$message = false;
if($this->item->elementType == 'folder')
if($this->item->elementType == 'folder' && isset($this->item->folderContent))
{
foreach($this->item->folderContent as $folderContent)
{

View File

@@ -6,6 +6,7 @@ use Slim\Http\Request;
use Slim\Http\Response;
use Typemill\Models\ProcessImage;
use Typemill\Models\ProcessFile;
use Typemill\Models\WriteYaml;
use Typemill\Controllers\ControllerAuthorBlockApi;
class ControllerAuthorMediaApi extends ControllerAuthor
@@ -92,6 +93,69 @@ class ControllerAuthorMediaApi extends ControllerAuthor
return $response->withJson(['errors' => 'file not found or file name invalid'],404);
}
public function getFileRestrictions(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParams();
$this->uri = $request->getUri()->withUserInfo('');
$restriction = 'all';
$userroles = $this->c->acl->getRoles();
if(isset($this->params['filename']) && $this->params['filename'] != '')
{
$writeYaml = new WriteYaml();
$restrictions = $writeYaml->getYaml('media' . DIRECTORY_SEPARATOR . 'files', 'filerestrictions.yaml');
if(isset($restrictions[$this->params['filename']]))
{
$restriction = $restrictions[$this->params['filename']];
}
}
return $response->withJson(['userroles' => $userroles, 'restriction' => $restriction]);
}
public function updateFileRestrictions(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParams();
$this->uri = $request->getUri()->withUserInfo('');
$filename = isset($this->params['filename']) ? $this->params['filename'] : false;
$role = isset($this->params['role']) ? $this->params['role'] : false;
if(!$filename OR !$role)
{
return $response->withJson(['errors' => ['message' => 'Filename or userrole is missing.']], 422);
}
$userroles = $this->c->acl->getRoles();
if($role != 'all' AND !in_array($role, $userroles))
{
return $response->withJson(['errors' => ['message' => 'Userrole is unknown.']], 422);
}
$writeYaml = new WriteYaml();
$restrictions = $writeYaml->getYaml('media' . DIRECTORY_SEPARATOR . 'files', 'filerestrictions.yaml');
if(!$restrictions)
{
$restrictions = [];
}
if($role == 'all')
{
unset($restrictions[$filename]);
}
else
{
$restrictions[$filename] = $role;
}
$writeYaml->updateYaml('media' . DIRECTORY_SEPARATOR . 'files', 'filerestrictions.yaml', $restrictions);
return $response->withJson(['restrictions' => $restrictions]);
}
public function createImage(Request $request, Response $response, $args)
{
# get params from call
@@ -382,14 +446,14 @@ class ControllerAuthorMediaApi extends ControllerAuthor
return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500);
}
$tmpImage = $imageProcessor->createImage($imageData64, $desiredSizes);
$tmpImage = $imageProcessor->createImage($imageData64, $videoID, $desiredSizes);
if(!$tmpImage)
{
return $response->withJson(array('errors' => 'could not create temporary image'));
}
$imageUrl = $imageProcessor->publishImage($desiredSizes, $videoID);
$imageUrl = $imageProcessor->publishImage();
if($imageUrl)
{
$this->params['markdown'] = '![' . $class . '-video](' . $imageUrl . ' "click to load video"){#' . $videoID. ' .' . $class . '}';

View File

@@ -313,6 +313,7 @@ class ControllerAuthorMetaApi extends ControllerAuthor
# normalize the meta-input
$metaInput['navtitle'] = (isset($metaInput['navtitle']) && $metaInput['navtitle'] !== null )? $metaInput['navtitle'] : '';
$metaInput['hide'] = (isset($metaInput['hide']) && $metaInput['hide'] !== null) ? $metaInput['hide'] : false;
$metaInput['noindex'] = (isset($metaInput['noindex']) && $metaInput['noindex'] !== null) ? $metaInput['noindex'] : false;
# input values are empty but entry in structure exists
if(!$metaInput['hide'] && $metaInput['navtitle'] == "" && isset($extended[$this->item->urlRelWoF]))
@@ -327,10 +328,12 @@ class ControllerAuthorMetaApi extends ControllerAuthor
($this->hasChanged($metaInput, $metaPage['meta'], 'navtitle'))
OR
($this->hasChanged($metaInput, $metaPage['meta'], 'hide'))
OR
($this->hasChanged($metaInput, $metaPage['meta'], 'noindex'))
)
{
# add new file data. Also makes sure that the value is set.
$extended[$this->item->urlRelWoF] = ['hide' => $metaInput['hide'], 'navtitle' => $metaInput['navtitle']];
$extended[$this->item->urlRelWoF] = ['hide' => $metaInput['hide'], 'navtitle' => $metaInput['navtitle'], 'noindex' => $metaInput['noindex']];
$structure = true;
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Typemill\Controllers;
use Typemill\Models\WriteYaml;
class ControllerDownload extends ControllerShared
{
public function download($request, $response, $args)
{
$filename = isset($args['params']) ? $args['params'] : false;
$root = $this->c->get('settings')['rootPath'];
$mediapath = 'media/files/';
$filepath = $root . $mediapath;
if(!$filename)
{
die('the requested file does not exist.');
}
# validate
$allowedFiletypes = [];
if(!$this->validate($filepath, $filename, $allowedFiletypes))
{
die('the requested file is not allowed.');
}
$writeYaml = new WriteYaml();
$restrictions = $writeYaml->getYaml('media' . DIRECTORY_SEPARATOR . 'files', 'filerestrictions.yaml');
if($restrictions && isset($restrictions[$mediapath . $filename]))
{
$allowedrole = $restrictions[$mediapath . $filename];
if(!isset($_SESSION['role']))
{
die("You have to be an authenticated $allowedrole to download this file.");
}
elseif(
$_SESSION['role'] != 'administrator'
AND $_SESSION['role'] != $allowedrole
AND !$this->c->acl->inheritsRole($_SESSION['role'], $allowedrole)
)
{
die("You have to be a $allowedrole to download this file.");
}
}
$file = $filepath . $filename;
# for now we only allow one download
$this->sendDownload($file);
exit;
}
/**
* Validate if the file exists and if
* there is a permission (download dir) to download this file
*
* You should ALWAYS call this method if you don't want
* somebody to download files not intended to be for the public.
*
* @param string $file GET parameter
* @param array $allowedFiletypes (defined in the head of this file)
* @return bool true if validation was successfull
*/
private function validate($path, $file, $allowedFiletypes)
{
$filepath = $path . $file;
# check if file exists
if (!isset($filepath) || empty($filepath) || !file_exists($filepath))
{
return false;
}
# check allowed filetypes
if(!empty($allowedFiletypes))
{
$fileAllowed = false;
foreach ($allowedFiletypes as $filetype)
{
if (strpos($file, $filetype) === (strlen($file) - strlen($filetype)))
{
$fileAllowed = true; //ends with $filetype
}
}
if (!$fileAllowed) return false;
}
# check download directory
if (strpos($file, '..') !== false)
{
return false;
}
return true;
}
/**
* Download function.
* Sets the HTTP header and supplies the given file
* as a download to the browser.
*
* @param string $file path to file
*/
private function sendDownload($file)
{
# Parse information
$pathinfo = pathinfo($file);
$extension = strtolower($pathinfo['extension']);
$mimetype = null;
# Get mimetype for extension
# This list can be extended as you need it.
# A good start to find mimetypes is the apache mime.types list
# http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
switch ($extension) {
case 'zip': $mimetype = "application/zip"; break;
default: $mimetype = "application/force-download";
}
# Required for some browsers like Safari and IE
if (ini_get('zlib.output_compression'))
{
ini_set('zlib.output_compression', 'Off');
}
header('Pragma: public');
header('Content-Encoding: none');
header('Expires: 0');
header('Accept-Ranges: bytes'); # Allow support for download resume
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($file)) . ' GMT');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Cache-Control: private', false); # required for some browsers
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="'.basename($file).'";'); # Make the browser display the Save As dialog
header('Content-Transfer-Encoding: binary');
header('Content-Length: '.filesize($file));
ob_end_flush();
readfile($file); # This is necessary in order to get it to actually download the file, otherwise it will be 0Kb
}
}

View File

@@ -177,7 +177,7 @@ abstract class ControllerShared
$pagetreeLive = Folder::scanFolder($this->settings['rootPath'] . $this->settings['contentFolder'], $draft = false );
# if there is content, then get the content details
if(count($pagetreeLive) > 0)
if($pagetreeLive && count($pagetreeLive) > 0)
{
# get the extended structure files with changes like navigation title or hidden pages
$yaml = new writeYaml();
@@ -295,28 +295,37 @@ abstract class ControllerShared
$opts = array(
'http'=>array(
'method'=>"GET",
'ignore_errors' => true,
'timeout' => 5
)
);
$context = stream_context_create($opts);
$context = stream_context_create($opts);
$resultBing = file_get_contents($pingBingUrl, false, $context);
$resultGoogle = file_get_contents($pingGoogleUrl, false, $context);
$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')
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);
$urlset .= $this->generateUrlSets($item->folderContent, $urlset);
}
elseif(isset($item->noindex) && $item->noindex === true )
{
continue;
}
else
{

View File

@@ -154,8 +154,9 @@ class Folder
# check if there are extended information
if($extended && isset($extended[$item->urlRelWoF]))
{
$item->name = ($extended[$item->urlRelWoF]['navtitle'] != '') ? $extended[$item->urlRelWoF]['navtitle'] : $item->name;
$item->hide = ($extended[$item->urlRelWoF]['hide'] === true) ? true : false;
$item->name = ($extended[$item->urlRelWoF]['navtitle'] != '') ? $extended[$item->urlRelWoF]['navtitle'] : $item->name;
$item->hide = ($extended[$item->urlRelWoF]['hide'] === true) ? true : false;
$item->noindex = (isset($extended[$item->urlRelWoF]['noindex']) && $extended[$item->urlRelWoF]['noindex'] === true) ? true : false;
}
# sort posts in descending order
@@ -217,8 +218,9 @@ class Folder
# check if there are extended information
if($extended && isset($extended[$item->urlRelWoF]))
{
$item->name = ($extended[$item->urlRelWoF]['navtitle'] != '') ? $extended[$item->urlRelWoF]['navtitle'] : $item->name;
$item->hide = ($extended[$item->urlRelWoF]['hide'] === true) ? true : false;
$item->name = ($extended[$item->urlRelWoF]['navtitle'] != '') ? $extended[$item->urlRelWoF]['navtitle'] : $item->name;
$item->hide = ($extended[$item->urlRelWoF]['hide'] === true) ? true : false;
$item->noindex = (isset($extended[$item->urlRelWoF]['noindex']) && $extended[$item->urlRelWoF]['noindex'] === true) ? true : false;
}
}

View File

@@ -36,7 +36,7 @@ $app->post('/api/v1/block', ControllerAuthorBlockApi::class . ':addBlock')->setN
$app->put('/api/v1/block', ControllerAuthorBlockApi::class . ':updateBlock')->setName('api.block.update')->add(new RestrictApiAccess($container['router']));
$app->delete('/api/v1/block', ControllerAuthorBlockApi::class . ':deleteBlock')->setName('api.block.delete')->add(new RestrictApiAccess($container['router']));
$app->put('/api/v1/moveblock', ControllerAuthorBlockApi::class . ':moveBlock')->setName('api.block.move')->add(new RestrictApiAccess($container['router']));
$app->post('/api/v1/video', ControllerAuthorBlockApi::class . ':saveVideoImage')->setName('api.video.save')->add(new RestrictApiAccess($container['router']));
$app->post('/api/v1/video', ControllerAuthorMediaApi::class . ':saveVideoImage')->setName('api.video.save')->add(new RestrictApiAccess($container['router']));
$app->get('/api/v1/medialib/images', ControllerAuthorMediaApi::class . ':getMediaLibImages')->setName('api.medialibimg.get')->add(new RestrictApiAccess($container['router']));
$app->get('/api/v1/medialib/files', ControllerAuthorMediaApi::class . ':getMediaLibFiles')->setName('api.medialibfiles.get')->add(new RestrictApiAccess($container['router']));
@@ -44,6 +44,8 @@ $app->get('/api/v1/image', ControllerAuthorMediaApi::class . ':getImage')->setNa
$app->post('/api/v1/image', ControllerAuthorMediaApi::class . ':createImage')->setName('api.image.create')->add(new RestrictApiAccess($container['router']));
$app->put('/api/v1/image', ControllerAuthorMediaApi::class . ':publishImage')->setName('api.image.publish')->add(new RestrictApiAccess($container['router']));
$app->delete('/api/v1/image', ControllerAuthorMediaApi::class . ':deleteImage')->setName('api.image.delete')->add(new RestrictApiAccess($container['router']));
$app->get('/api/v1/filerestrictions', ControllerAuthorMediaApi::class . ':getFileRestrictions')->setName('api.file.getrestrictions')->add(new RestrictApiAccess($container['router']));
$app->post('/api/v1/filerestrictions', ControllerAuthorMediaApi::class . ':updateFileRestrictions')->setName('api.file.updaterestrictions')->add(new RestrictApiAccess($container['router']));
$app->get('/api/v1/file', ControllerAuthorMediaApi::class . ':getFile')->setName('api.file.get')->add(new RestrictApiAccess($container['router']));
$app->post('/api/v1/file', ControllerAuthorMediaApi::class . ':uploadFile')->setName('api.file.upload')->add(new RestrictApiAccess($container['router']));
$app->put('/api/v1/file', ControllerAuthorMediaApi::class . ':publishFile')->setName('api.file.publish')->add(new RestrictApiAccess($container['router']));

View File

@@ -1,6 +1,7 @@
<?php
use Typemill\Controllers\ControllerAuthorEditor;
use Typemill\Controllers\ControllerSettings;
use Typemill\Controllers\ControllerDownload;
use Typemill\Controllers\ControllerFrontendWebsite;
use Typemill\Controllers\ControllerFrontendForms;
use Typemill\Controllers\ControllerFrontendAuth;
@@ -64,6 +65,8 @@ $app->get('/tm/content/raw[/{params:.*}]', ControllerAuthorEditor::class . ':sho
$app->get('/tm/content/visual[/{params:.*}]', ControllerAuthorEditor::class . ':showBlox')->setName('content.visual')->add(new accessMiddleware($container['router'], $container['acl'], 'content', 'view'));
$app->get('/tm/content[/{params:.*}]', ControllerAuthorEditor::class . ':showEmpty')->setName('content.empty')->add(new accessMiddleware($container['router'], $container['acl'], 'content', 'view'));
$app->get('/media/files[/{params:.*}]', ControllerDownload::class . ':download')->setName('download.file');
foreach($routes as $pluginRoute)
{
$method = $pluginRoute['httpMethod'];

View File

@@ -17,7 +17,7 @@ const contentComponent = Vue.component('content-block', {
'<div v-if="newblock" class="newblock-info">Choose a content-type <button class="newblock-close" @click.prevent="closeNewBlock($event)">close</button></div>' +
'<div class="blox-wrapper" :class="{ editactive: edit }">' +
'<div class="sideaction" slot="header" v-if="body">' +
'<button class="add" :disabled="disabled" :title="\'add content-block\'|translate" @mousedown.prevent="disableSort()" @click.prevent="addNewBlock($event)"><svg class="icon icon-plus"><use xlink:href="#icon-plus"></use></svg></button>' +
'<button class="add" :disabled="disabled" :title="\'add content-block\'|translate" @mousedown.prevent="disableSort()" @click.prevent="addNewBlock($event)"><svg class="icon icon-plus"><use xlink:href="#icon-plus"></use></svg></button>' +
'<button class="delete" :disabled="disabled" :title="\'delete content-block\'|translate" @mousedown.prevent="disableSort()" @click.prevent="deleteBlock($event)"><svg class="icon icon-close"><use xlink:href="#icon-close"></use></svg></button>' +
'</div>' +
'<div class="background-helper" @keyup.enter="submitBlock" @click="getData">' +
@@ -1769,7 +1769,12 @@ const fileComponent = Vue.component('file-component', {
'<label for="filetitle">{{ \'Title\'|translate }}: </label>' +
'<input name="filetitle" type="text" placeholder="Add a title for the download-link" v-model="filetitle" @input="createmarkdown" max="64" />' +
'<input title="fileid" type="hidden" placeholder="id" v-model="fileid" @input="createmarkdown" max="140" />' +
'</div></div>',
'<label for="filerestriction">{{ \'Access for\'|translate }}: </label>' +
'<select name="filerestriction" v-model="selectedrole" @change="updaterestriction">' +
'<option disabled value="">{{ \'Please select\'|translate }}</option>' +
'<option v-for="role in userroles">{{ role }}</option>' +
'</select>' +
'</div></div>',
data: function(){
return {
maxsize: 20, // megabyte
@@ -1779,7 +1784,9 @@ const fileComponent = Vue.component('file-component', {
filetitle: '',
fileextension: '',
fileurl: '',
fileid: ''
fileid: '',
userroles: ['all'],
selectedrole: '',
}
},
mounted: function(){
@@ -1814,6 +1821,27 @@ const fileComponent = Vue.component('file-component', {
this.fileurl = fileurl[0].slice(1,-1);
}
}
self = this;
myaxios.get('/api/v1/filerestrictions',{
params: {
'url': document.getElementById("path").value,
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'filename': this.fileurl,
}
})
.then(function (response) {
self.userroles = self.userroles.concat(response.data.userroles);
self.selectedrole = response.data.restriction;
})
.catch(function (error)
{
if(error.response)
{
}
});
},
methods: {
openmedialib: function()
@@ -1831,6 +1859,27 @@ const fileComponent = Vue.component('file-component', {
{
this.$emit('updatedMarkdown', event.target.value);
},
updaterestriction: function()
{
myaxios.post('/api/v1/filerestrictions',{
'url': document.getElementById("path").value,
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'filename': this.fileurl,
'role': this.selectedrole,
})
.then(function (response) {
})
.catch(function (error)
{
if(error.response)
{
}
});
console.info(this.selectedrole);
},
createmarkdown: function()
{
var errors = false;
@@ -1939,9 +1988,8 @@ for(var i = 0; i < formatConfig.length; i++)
}
let editor = new Vue({
delimiters: ['${', '}'],
delimiters: ['${', '}'],
el: '#blox',
/* components: componentList, */
data: {
errors: [],
root: document.getElementById("main").dataset.url,
@@ -1960,7 +2008,7 @@ let editor = new Vue({
bloxOverlay: false,
sortdisabled: false,
showEditor: 'show',
formats: activeFormats
formats: activeFormats,
},
mounted: function(){
@@ -1969,110 +2017,109 @@ let editor = new Vue({
var self = this;
myaxios.post('/api/v1/article/html',{
'url': document.getElementById("path").value,
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
})
.then(function (response) {
var contenthtml = response.data.data;
self.title = contenthtml[0];
self.html = contenthtml;
var initialcontent = document.getElementById("initial-content");
'url': document.getElementById("path").value,
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
})
.then(function (response){
var contenthtml = response.data.data;
self.title = contenthtml[0];
self.html = contenthtml;
var initialcontent = document.getElementById("initial-content");
initialcontent.parentNode.removeChild(initialcontent);
initialcontent.parentNode.removeChild(initialcontent);
})
.catch(function (error)
{
if(error.response)
{
self.errors.title = error.response.errors;
if(error.response)
{
self.errors.title = error.response.errors;
}
});
myaxios.post('/api/v1/article/markdown',{
'url': document.getElementById("path").value,
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
})
'url': document.getElementById("path").value,
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
})
.then(function (response) {
self.markdown = response.data.data;
/* activate math plugin */
if (typeof renderMathInElement === "function") {
self.$nextTick(function () {
renderMathInElement(document.getElementById("blox"));
});
}
/* check for youtube videos */
if (typeof typemillUtilities !== "undefined") {
setTimeout(function(){
self.markdown = response.data.data;
/* activate math plugin */
if (typeof renderMathInElement === "function") {
self.$nextTick(function () {
typemillUtilities.start();
renderMathInElement(document.getElementById("blox"));
});
}, 200);
}
}
/* check for youtube videos */
if (typeof typemillUtilities !== "undefined") {
setTimeout(function(){
self.$nextTick(function () {
typemillUtilities.start();
});
}, 200);
}
})
.catch(function (error)
{
if(error.response)
{
self.errors.title = error.response.errors;
if(error.response)
{
self.errors.title = error.response.errors;
}
});
},
methods: {
onStart: function()
{
},
moveBlock: function(evt)
{
publishController.errors.message = false;
methods: {
onStart: function()
{
var self = this;
},
moveBlock: function(evt)
{
publishController.errors.message = false;
myaxios.put('/api/v1/moveblock',{
'url': document.getElementById("path").value,
'old_index': evt.oldIndex,
'new_index': evt.newIndex,
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
})
.then(function (response) {
var self = this;
myaxios.put('/api/v1/moveblock',{
'url': document.getElementById("path").value,
'old_index': evt.oldIndex,
'new_index': evt.newIndex,
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
})
.then(function (response) {
self.freeze = false;
self.freeze = false;
self.markdown = response.data.markdown;
self.blockMarkdown = '';
self.blockType = '';
self.markdown = response.data.markdown;
self.blockMarkdown = '';
self.blockType = '';
if(response.data.toc)
{
self.html.splice(response.data.toc.id, 1, response.data.toc);
}
if(response.data.toc)
{
self.html.splice(response.data.toc.id, 1, response.data.toc);
}
publishController.publishDisabled = false;
publishController.publishResult = "";
publishController.publishDisabled = false;
publishController.publishResult = "";
/* update the navigation and mark navigation item as modified */
navi.getNavi();
/* update the navigation and mark navigation item as modified */
navi.getNavi();
/* update the math if plugin is there */
self.checkMath(params.new_index+1);
})
.catch(function (error)
/* update the math if plugin is there */
self.checkMath(params.new_index+1);
})
.catch(function (error)
{
if(error)
{
publishController.publishDisabled = false;
}
if(error.response)
{
if(error)
publishController.publishDisabled = false;
if(error.response)
{
publishController.errors.message = error.response.data.errors.message;
}
});
publishController.errors.message = error.response.data.errors.message;
}
});
},
setData: function(event, blocktype, body)
{

View File

@@ -119,7 +119,7 @@ Vue.component('tab-meta', {
})
let meta = new Vue({
delimiters: ['${', '}'],
delimiters: ['${', '}'],
el: '#metanav',
data: function () {
return {

View File

@@ -36,10 +36,10 @@ meta:
label: Hide
checkboxlabel: Hide page from navigation
class: medium
nofollow:
noindex:
type: checkbox
label: Nofollow
checkboxlabel: Add no-follow and exclude from sitemap
label: Noindex
checkboxlabel: Add noindex tag and exclude from sitemap
class: medium
fieldsetauthor:
type: fieldset

View File

@@ -1,5 +1,5 @@
name: Cyanine Theme
version: 1.1.8
version: 1.1.9
description: Cyanine is a modern and flexible multi-purpose theme and the standard theme for typemill.
author: Trendschau
homepage: https://trendschau.net
@@ -138,6 +138,20 @@ forms:
type: text
label: Teaser 2 Label
fieldsize: half
teaser3title:
type: text
label: Teaser 3 Title
teaser3text:
type: text
label: Teaser 3 Text
teaser3link:
type: text
label: Teaser 3 Link
fieldsize: half
teaser3label:
type: text
label: Teaser 3 Label
fieldsize: half
landingpageContrast:
type: fieldset

View File

@@ -1,16 +1,27 @@
<section class="landingpageteaser w-100 dib tc bl br bb">
<div class="mw7 pv6 ph3 center flex-m flex-l justify-between">
<div class="mw5 center ml0-l mr0-l mv4 dib">
<h2 class="f2">{{ settings.themes.cyanine.teaser1title }}</h2>
<p class="f5 f4-ns fw3 lh-copy">{{ settings.themes.cyanine.teaser1text }}</p>
<a href="{{ settings.themes.cyanine.teaser1link }}" class="button link dim w-100 dib pa3 ma1 ba">{{ settings.themes.cyanine.teaser1label }}</a>
</div>
<div class="mw5 center ml0-l mr0-l mv4 dib">
<h2 class="f2">{{ settings.themes.cyanine.teaser2title }}</h2>
<p class="f5 f4-ns fw3 lh-copy">{{ settings.themes.cyanine.teaser2text }}</p>
<a href="{{ settings.themes.cyanine.teaser2link }}" class="button link dim w-100 dib pa3 ma1 ba">{{ settings.themes.cyanine.teaser2label }}</a>
</div>
<div class="mw8 pv6 ph3 center flex-m flex-l justify-around">
{% if settings.themes.cyanine.teaser1title %}
<div class="mw5 center ml0-l mr0-l mv4 dib">
<h2 class="f2">{{ settings.themes.cyanine.teaser1title }}</h2>
<p class="f5 f4-ns fw3 lh-copy">{{ settings.themes.cyanine.teaser1text }}</p>
<a href="{{ settings.themes.cyanine.teaser1link }}" class="button link dim w-100 dib pa3 ma1 ba">{{ settings.themes.cyanine.teaser1label }}</a>
</div>
{% endif %}
{% if settings.themes.cyanine.teaser2title %}
<div class="mw5 center ml0-l mr0-l mv4 dib">
<h2 class="f2">{{ settings.themes.cyanine.teaser2title }}</h2>
<p class="f5 f4-ns fw3 lh-copy">{{ settings.themes.cyanine.teaser2text }}</p>
<a href="{{ settings.themes.cyanine.teaser2link }}" class="button link dim w-100 dib pa3 ma1 ba">{{ settings.themes.cyanine.teaser2label }}</a>
</div>
{% endif %}
{% if settings.themes.cyanine.teaser3title %}
<div class="mw5 center ml0-l mr0-l mv4 dib">
<h2 class="f2">{{ settings.themes.cyanine.teaser3title }}</h2>
<p class="f5 f4-ns fw3 lh-copy">{{ settings.themes.cyanine.teaser3text }}</p>
<a href="{{ settings.themes.cyanine.teaser3link }}" class="button link dim w-100 dib pa3 ma1 ba">{{ settings.themes.cyanine.teaser3label }}</a>
</div>
{% endif %}
</div>
</section>

View File

@@ -9,6 +9,10 @@
<meta name="author" content="{{ metatabs.meta.author }}" />
<meta name="generator" content="TYPEMILL" />
<meta name="msapplication-TileColor" content="#F9F8F6" />
{% if metatabs.meta.noindex %}
<meta name="robots" content="noindex">
{% endif %}
{% if favicon %}
<meta name="msapplication-TileColor" content="#F9F8F6" />