1
0
mirror of https://github.com/typemill/typemill.git synced 2025-08-06 06:07:31 +02:00

old tm-dev stuff

This commit is contained in:
trendschau
2023-06-27 14:59:41 +02:00
parent fe34fd0ad4
commit 68d44c5663
9 changed files with 1850 additions and 0 deletions

304
system/typemill/Assets.php Normal file
View File

@@ -0,0 +1,304 @@
<?php
namespace Typemill;
use Typemill\Models\ProcessImage;
# this class is available to the container and to all plugins
class Assets
{
public $baseUrl;
public function __construct($baseUrl)
{
$this->baseUrl = $baseUrl;
$this->JS = array();
$this->CSS = array();
$this->inlineJS = array();
$this->inlineCSS = array();
$this->editorJS = array();
$this->editorCSS = array();
$this->editorInlineJS = array();
$this->svgSymbols = array();
$this->meta = array();
$this->imageUrl = false;
$this->imageFolder = 'original';
}
public function setUri($uri)
{
$this->uri = $uri;
}
public function setBaseUrl($baseUrl)
{
$this->baseUrl = $baseUrl;
}
public function image($url)
{
$this->imageUrl = $url;
return $this;
}
public function resize($width,$height)
{
$pathinfo = pathinfo($this->imageUrl);
$extension = strtolower($pathinfo['extension']);
$imageName = $pathinfo['filename'];
$desiredSizes = ['custom' => []];
$resize = '-';
if(is_int($width) && $width < 10000)
{
$resize .= $width;
$desiredSizes['custom']['width'] = $width;
}
$resize .= 'x';
if(is_int($height) && $height < 10000)
{
$resize .= $height;
$desiredSizes['custom']['height'] = $height;
}
$processImage = new ProcessImage($desiredSizes);
$processImage->checkFolders('images');
$imageNameResized = $imageName . $resize;
$imagePathResized = $processImage->customFolder . $imageNameResized . '.' . $extension;
$imageUrlResized = 'media/custom/' . $imageNameResized . '.' . $extension;
if(!file_exists( $imagePathResized ))
{
# if custom version does not exist, use original version for resizing
$imageFolder = ($this->imageFolder == 'original') ? $processImage->originalFolder : $processImage->customFolder;
$imagePath = $imageFolder . $pathinfo['basename'];
$resizedImage = $processImage->generateSizesFromImageFile($imageUrlResized, $imagePath);
$savedImage = $processImage->saveImage($processImage->customFolder, $resizedImage['custom'], $imageNameResized, $extension);
if(!$savedImage)
{
# return old image url without resize
return $this;
}
}
# set folder to custom, so that the next method uses the correct (resized) version
$this->imageFolder = 'custom';
$this->imageUrl = $imageUrlResized;
return $this;
}
public function grayscale()
{
$pathinfo = pathinfo($this->imageUrl);
$extension = strtolower($pathinfo['extension']);
$imageName = $pathinfo['filename'];
$processImage = new ProcessImage([]);
$processImage->checkFolders('images');
$imageNameGrayscale = $imageName . '-grayscale';
$imagePathGrayscale = $processImage->customFolder . $imageNameGrayscale . '.' . $extension;
$imageUrlGrayscale = 'media/custom/' . $imageNameGrayscale . '.' . $extension;
if(!file_exists( $imagePathGrayscale ))
{
# if custom-version does not exist, use live-version for grayscale-manipulation.
$imageFolder = ($this->imageFolder == 'original') ? $processImage->liveFolder : $processImage->customFolder;
$imagePath = $imageFolder . $pathinfo['basename'];
$grayscaleImage = $processImage->grayscale($imagePath, $extension);
$savedImage = $processImage->saveImage($processImage->customFolder, $grayscaleImage, $imageNameGrayscale, $extension);
if(!$savedImage)
{
# return old image url without resize
return $this;
}
}
# set folder to custom, so that the next method uses the correct (resized) version
$this->imageFolder = 'custom';
$this->imageUrl = $imageUrlGrayscale;
return $this;
}
public function src()
{
# when we finish it, we shoud reset all settings
$imagePath = $this->baseUrl . '/' . $this->imageUrl;
$this->imageUrl = false;
$this->imageFolder = 'original';
return $imagePath;
}
public function addCSS($CSS)
{
$CSSfile = $this->getFileUrl($CSS);
if($CSSfile)
{
$this->CSS[] = '<link rel="stylesheet" href="' . $CSSfile . '" />';
}
}
public function addInlineCSS($CSS)
{
$this->inlineCSS[] = '<style>' . $CSS . '</style>';
}
public function addJS($JS)
{
$JSfile = $this->getFileUrl($JS);
if($JSfile)
{
$this->JS[] = '<script src="' . $JSfile . '"></script>';
}
}
public function addInlineJS($JS)
{
$this->inlineJS[] = '<script>' . $JS . '</script>';
}
public function activateVue()
{
$vueUrl = '<script src="' . $this->baseUrl . '/system/author/js/vue.min.js"></script>';
if(!in_array($vueUrl, $this->JS))
{
$this->JS[] = $vueUrl;
}
}
public function activateAxios()
{
$axiosUrl = '<script src="' . $this->baseUrl . '/system/author/js/axios.min.js"></script>';
if(!in_array($axiosUrl, $this->JS))
{
$this->JS[] = $axiosUrl;
$axios = '<script>const myaxios = axios.create({ baseURL: \'' . $this->baseUrl . '\' });</script>';
$this->JS[] = $axios;
}
}
public function activateTachyons()
{
$tachyonsUrl = '<link rel="stylesheet" href="' . $this->baseUrl . '/system/author/css/tachyons.min.css" />';
if(!in_array($tachyonsUrl, $this->CSS))
{
$this->CSS[] = $tachyonsUrl;
}
}
public function addSvgSymbol($symbol)
{
$this->svgSymbols[] = $symbol;
}
# add JS to enhance the blox-editor in author area
public function addEditorJS($JS)
{
$JSfile = $this->getFileUrl($JS);
if($JSfile)
{
$this->editorJS[] = '<script src="' . $JSfile . '"></script>';
}
}
public function addEditorInlineJS($JS)
{
$this->editorInlineJS[] = '<script>' . $JS . '</script>';
}
public function addEditorCSS($CSS)
{
$CSSfile = $this->getFileUrl($CSS);
if($CSSfile)
{
$this->editorCSS[] = '<link rel="stylesheet" href="' . $CSSfile . '" />';
}
}
public function addMeta($key,$meta)
{
$this->meta[$key] = $meta;
}
public function renderEditorJS()
{
return implode("\n", $this->editorJS) . implode("\n", $this->editorInlineJS);
}
public function renderEditorCSS()
{
return implode("\n", $this->editorCSS);
}
public function renderCSS()
{
return implode("\n", $this->CSS) . implode("\n", $this->inlineCSS);
}
public function renderJS()
{
return implode("\n", $this->JS) . implode("\n", $this->inlineJS);
}
public function renderSvg()
{
return implode('', $this->svgSymbols);
}
public function renderMeta()
{
$metaLines = '';
foreach($this->meta as $meta)
{
$metaLines .= "\n";
$metaLines .= $meta;
}
return $metaLines;
}
/**
* Checks, if a string is a valid internal or external ressource like js-file or css-file
* @params $path string
* @return string or false
*/
public function getFileUrl($path)
{
# check system path of file without parameter for fingerprinting
$internalFile = __DIR__ . '/../../plugins' . strtok($path, "?");
if(file_exists($internalFile))
{
return $this->baseUrl . '/plugins' . $path;
}
return $path;
if(fopen($path, "r"))
{
return $path;
}
return false;
}
}

View File

@@ -0,0 +1,516 @@
<?php
namespace Typemill\Controllers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Routing\RouteContext;
use Typemill\Models\Validation;
use Typemill\Models\Content;
use Typemill\Models\Navigation;
use Typemill\Models\Meta;
use Typemill\Static\Slug;
class ControllerApiAuthorMeta extends Controller
{
public function getMeta(Request $request, Response $response, $args)
{
$validRights = $this->validateRights($request->getAttribute('c_userrole'), 'content', 'update');
if(!$validRights)
{
$response->getBody()->write(json_encode([
'message' => 'You do not have enough rights.',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(422);
}
$url = $request->getQueryParams()['url'] ?? false;
$navigation = new Navigation();
$urlinfo = $this->c->get('urlinfo');
$item = $this->getItem($navigation, $url, $urlinfo);
if(!$item)
{
$response->getBody()->write(json_encode([
'message' => 'page not found',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(404);
}
$meta = new Meta();
$metadata = $meta->getMetaData($item);
if(!$metadata)
{
die('no page meta');
# $pagemeta = $writeMeta->getPageMetaBlank($this->content, $this->settings, $this->item);
}
# if item is a folder
if($item->elementType == "folder" && isset($item->contains))
{
$metadata['meta']['contains'] = isset($pagemeta['meta']['contains']) ? $pagemeta['meta']['contains'] : $item->contains;
# get global metadefinitions
$metadefinitions = $meta->getMetaDefinitions($this->settings, $folder = true);
}
else
{
# get global metadefinitions
$metadefinitions = $meta->getMetaDefinitions($this->settings, $folder = false);
}
# cleanup metadata to the current metadefinitions (e.g. strip out deactivated plugins)
$metacleared = [];
# store the metadata-scheme for frontend, so frontend does not use obsolete data
$metascheme = [];
foreach($metadefinitions as $tabname => $tabfields )
{
# add userroles and other datasets
$metadefinitions[$tabname]['fields'] = $this->addDatasets($tabfields['fields']);
$tabfields = $this->flattenTabFields($tabfields['fields'],[]);
$metacleared[$tabname] = [];
foreach($tabfields as $fieldname => $fielddefinitions)
{
$metascheme[$tabname][$fieldname] = true;
$metacleared[$tabname][$fieldname] = isset($metadata[$tabname][$fieldname]) ? $metadata[$tabname][$fieldname] : null;
}
}
# store the metascheme in cache for frontend
# $writeMeta->updateYaml('cache', 'metatabs.yaml', $metascheme);
$response->getBody()->write(json_encode([
'metadata' => $metacleared,
'metadefinitions' => $metadefinitions,
]));
return $response->withHeader('Content-Type', 'application/json');
}
public function updateMetaData(Request $request, Response $response, $args)
{
$validRights = $this->validateRights($request->getAttribute('c_userrole'), 'content', 'update');
if(!$validRights)
{
$response->getBody()->write(json_encode([
'message' => 'You do not have enough rights.',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(422);
}
$params = $request->getParsedBody();
$validate = new Validation();
$validInput = $validate->metaInput($params);
if($validInput !== true)
{
$errors = $validate->returnFirstValidationErrors($validInput);
$response->getBody()->write(json_encode([
'message' => reset($errors),
'errors' => $errors
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
$navigation = new Navigation();
$urlinfo = $this->c->get('urlinfo');
$item = $this->getItem($navigation, $params['url'], $urlinfo);
if(!$item)
{
$response->getBody()->write(json_encode([
'message' => 'page not found',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(404);
}
$meta = new Meta();
# if item is a folder
if($item->elementType == "folder" && isset($item->contains))
{
$metadata['meta']['contains'] = isset($pagemeta['meta']['contains']) ? $pagemeta['meta']['contains'] : $item->contains;
# get global metadefinitions
$metadefinitions = $meta->getMetaDefinitions($this->settings, $folder = true);
}
else
{
# get global metadefinitions
$metadefinitions = $meta->getMetaDefinitions($this->settings, $folder = false);
}
$tabdefinitions = $metadefinitions[$params['tab']] ?? false;
if(!$tabdefinitions)
{
$response->getBody()->write(json_encode([
'message' => 'Tab not found',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(404);
}
$tabdefinitions['fields'] = $this->addDatasets($tabdefinitions['fields']);
$tabdefinitions = $this->flattenTabFields($tabdefinitions['fields'], []);
# create validation object
$errors = false;
# take the user input data and iterate over all fields and values
foreach($params['data'] as $fieldname => $fieldvalue)
{
# get the corresponding field definition from original plugin settings
$fielddefinition = $tabdefinitions[$fieldname] ?? false;
if(!$fielddefinition)
{
$errors[$tab][$fieldname] = 'This field is not defined';
}
else
{
# validate user input for this field
$result = $validate->field($fieldname, $fieldvalue, $fielddefinition);
if($result !== true)
{
$errors[$params['tab']][$fieldname] = $result[$fieldname][0];
}
}
}
# return validation errors
if($errors)
{
$response->getBody()->write(json_encode([
'message' => 'Please correct the errors.',
'errors' => $errors
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(422);
}
$pageMeta = $meta->getMetaData($item);
# extended
$navigation = new Navigation();
$extended = $navigation->getExtendedNavigation($urlinfo, $this->settings['langattr']);
if($params['tab'] == 'meta')
{
# if manual date has been modified
if( $this->hasChanged($params['data'], $pageMeta['meta'], 'manualdate'))
{
# update the time
$params['data']['time'] = date('H-i-s', time());
# if it is a post, then rename the post
if($item->elementType == "file" && strlen($item->order) == 12)
{
# create file-prefix with date
$metadate = $params['data']['manualdate'];
if($metadate == '')
{
$metadate = $pageMeta['meta']['created'];
}
$datetime = $metadate . '-' . $params['data']['time'];
$datetime = implode(explode('-', $datetime));
$datetime = substr($datetime,0,12);
# create the new filename
$pathWithoutFile = str_replace($item->originalName, "", $item->path);
$newPathWithoutType = $pathWithoutFile . $datetime . '-' . $item->slug;
$meta->renamePost($item->pathWithoutType, $newPathWithoutType);
$navigation->clearNavigation();
}
}
# if folder has changed and contains pages instead of posts or posts instead of pages
if($item->elementType == "folder" && isset($params['data']['contains']) && isset($pageMeta['meta']['contains']) && $this->hasChanged($params['data'], $pageMeta['meta'], 'contains'))
{
if($meta->folderContainsFolders($item))
{
$response->getBody()->write(json_encode([
'message' => 'The folder contains another folder so we cannot transform it. Please make sure there are only files in this folder.',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(422);
}
if($params['data']['contains'] == "posts" && !$meta->transformPagesToPosts($item))
{
$response->getBody()->write(json_encode([
'message' => 'One or more files could not be transformed.',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(422);
}
if($params['data']['contains'] == "pages" && !$meta->transformPostsToPages($item))
{
$response->getBody()->write(json_encode([
'message' => 'One or more files could not be transformed.',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(422);
}
$navigation->clearNavigation();
}
# normalize the meta-input
$params['data']['navtitle'] = (isset($params['data']['navtitle']) && $params['data']['navtitle'] !== null )? $params['data']['navtitle'] : '';
$params['data']['hide'] = (isset($params['data']['hide']) && $params['data']['hide'] !== null) ? $params['data']['hide'] : false;
$params['data']['noindex'] = (isset($params['data']['noindex']) && $params['data']['noindex'] !== null) ? $params['data']['noindex'] : false;
# input values are empty but entry in structure exists
if(
!$params['data']['hide']
&& $params['data']['navtitle'] == ""
&& isset($extended[$item->urlRelWoF])
)
{
$navigation->clearNavigation();
}
elseif(
# check if navtitle or hide-value has been changed
($this->hasChanged($params['data'], $pageMeta['meta'], 'navtitle'))
OR
($this->hasChanged($params['data'], $pageMeta['meta'], 'hide'))
OR
($this->hasChanged($params['data'], $pageMeta['meta'], 'noindex'))
)
{
$navigation->clearNavigation();
}
}
# add the new/edited metadata
$pageMeta[$params['tab']] = $params['data'];
# store the metadata
$store = $meta->updateMeta($pageMeta, $item);
if($store === true)
{
$draftNavigation = $navigation->getDraftNavigation($urlinfo, $this->settings['langattr']);
$draftNavigation = $navigation->setActiveNaviItems($draftNavigation, $item->keyPathArray);
$item = $navigation->getItemWithKeyPath($draftNavigation, $item->keyPathArray);
$response->getBody()->write(json_encode([
'navigation' => $draftNavigation,
'item' => $item
]));
return $response->withHeader('Content-Type', 'application/json');
}
$response->getBody()->write(json_encode([
'message' => $store,
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(422);
}
/*
# get the standard meta-definitions and the meta-definitions from plugins (same for all sites)
public function aggregateMetaDefinitions($folder = null)
{
$metatabs = $this->meta->getMetaDefinitions();
# the fields for user or role based access
if(!isset($this->settings['pageaccess']) || $this->settings['pageaccess'] === NULL )
{
unset($metatabs['meta']['fields']['fieldsetrights']);
}
# add radio buttons to choose posts or pages for folder.
if(!$folder)
{
unset($metatabs['meta']['fields']['contains']);
}
echo '<pre>';
print_r($metatabs);
die();
# loop through all plugins
if(!empty($this->settings['plugins']))
{
foreach($this->settings['plugins'] as $name => $plugin)
{
if($plugin['active'])
{
$pluginSettings = \Typemill\Settings::getObjectSettings('plugins', $name);
if($pluginSettings && isset($pluginSettings['metatabs']))
{
$metatabs = array_merge_recursive($metatabs, $pluginSettings['metatabs']);
}
}
}
}
# add the meta from theme settings here
$themeSettings = \Typemill\Settings::getObjectSettings('themes', $this->settings['theme']);
if($themeSettings && isset($themeSettings['metatabs']))
{
$metatabs = array_merge_recursive($metatabs, $themeSettings['metatabs']);
}
# dispatch meta
# $metatabs = $this->c->dispatcher->dispatch('onMetaDefinitionsLoaded', new OnMetaDefinitionsLoaded($metatabs))->getData();
return $metatabs;
}
*/
public function publishArticle(Request $request, Response $response, $args)
{
$validRights = $this->validateRights($request->getAttribute('c_userrole'), 'content', 'update');
if(!$validRights)
{
$response->getBody()->write(json_encode([
'message' => 'You do not have enough rights.',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(422);
}
$params = $request->getParsedBody();
$validate = new Validation();
$validInput = $validate->articlePublish($params);
if($validInput !== true)
{
$errors = $validate->returnFirstValidationErrors($validInput);
$response->getBody()->write(json_encode([
'message' => reset($errors),
'errors' => $errors
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
$navigation = new Navigation();
$urlinfo = $this->c->get('urlinfo');
$item = $this->getItem($navigation, $params['url'], $urlinfo);
if(!$item)
{
$response->getBody()->write(json_encode([
'message' => 'page not found',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(404);
}
# publish content
$content = new Content($urlinfo['baseurl']);
$draftMarkdown = $content->getDraftMarkdown($item);
$content->publishMarkdown($item, $draftMarkdown);
# refresh navigation and item
$navigation->clearNavigation();
$draftNavigation = $navigation->getDraftNavigation($urlinfo, $this->settings['langattr']);
$draftNavigation = $navigation->setActiveNaviItems($draftNavigation, $item->keyPathArray);
$item = $navigation->getItemWithKeyPath($draftNavigation, $item->keyPathArray);
$response->getBody()->write(json_encode([
'navigation' => $draftNavigation,
'item' => $item
]));
return $response->withHeader('Content-Type', 'application/json');
}
# get the standard meta-definitions and the meta-definitions from plugins (same for all sites)
public function getMetaDefinitions(Request $request, Response $response, $args)
{
$validRights = $this->validateRights($request->getAttribute('c_userrole'), 'content', 'update');
if(!$validRights)
{
$response->getBody()->write(json_encode([
'message' => 'You do not have enough rights.',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(422);
}
$metatabs = $this->aggregateMetaDefinitions();
$response->getBody()->write(json_encode([
'definitions' => $metatabs
]));
return $response->withHeader('Content-Type', 'application/json');
}
# we have to flatten field definitions for tabs if there are fieldsets in it
public function flattenTabFields($tabfields, $flattab, $fieldset = null)
{
foreach($tabfields as $name => $field)
{
if($field['type'] == 'fieldset')
{
$flattab = $this->flattenTabFields($field['fields'], $flattab, $name);
}
else
{
# add the name of the fieldset so we know to which fieldset it belongs for references
if($fieldset)
{
$field['fieldset'] = $fieldset;
}
$flattab[$name] = $field;
}
}
return $flattab;
}
protected function hasChanged($input, $page, $field)
{
if(isset($input[$field]) && isset($page[$field]) && $input[$field] == $page[$field])
{
return false;
}
if(!isset($input[$field]) && !isset($input[$field]))
{
return false;
}
return true;
}
}

View File

@@ -0,0 +1,149 @@
<?php
namespace Typemill\Controllers;
use Typemill\Models\StorageWrapper;
class ControllerWebDownload extends Controller
{
public function download($request, $response, $args)
{
$filename = isset($args['params']) ? $args['params'] : false;
if(!$filename)
{
die('the requested file does not exist.');
}
$storage = new StorageWrapper('\Typemill\Models\Storage');
$restrictions = $storage->getYaml('fileFolder', '', 'filerestrictions.yaml');
$filepath = $storage->getFolderPath('fileFolder');
$filefolder = 'media/files/';
# validate
$allowedFiletypes = [];
if(!$this->validate($filepath, $filename, $allowedFiletypes))
{
die('the requested filetype is not allowed.');
}
if($restrictions && isset($restrictions[$filefolder . $filename]))
{
$userrole = $request->getAttribute('c_userrole');
$allowedrole = $restrictions[$filefolder . $filename];
if(!$userrole)
{
$this->c->get('flash')->addMessage('error', "You have to be an authenticated $allowedrole to download this file.");
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302);
}
elseif(
$userrole != 'administrator'
AND $userrole != $allowedrole
AND !$this->c->get('acl')->inheritsRole($userrole, $allowedrole)
)
{
$this->c->get('flash')->addMessage('error', "You have to be a $allowedrole to download this file.");
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302);
}
}
$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, $filename, $allowedFiletypes)
{
$filepath = $path . $filename;
# 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($filename, $filetype) === (strlen($filename) - strlen($filetype)))
{
$fileAllowed = true; //ends with $filetype
}
}
if (!$fileAllowed) return false;
}
# check download directory
if (strpos($filename, '..') !== 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('Accept-Ranges: bytes'); # Allow support for download resume
header('Expires: 0');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($file)) . ' GMT');
header_remove("Last-Modified");
header('Cache-Control: max-age=0, no-cache, no-store, must-revalidate');
header('Cache-Control: private', false); # required for some browsers
header('Content-Type: ' . $mimetype);
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

@@ -0,0 +1,26 @@
<?php
namespace Typemill\Extensions;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use Typemill\Extensions\ParsedownExtension;
class TwigMarkdownExtension extends AbstractExtension
{
public function getFunctions()
{
return [
new TwigFunction('markdown', array($this, 'renderMarkdown' ))
];
}
public function renderMarkdown($markdown)
{
$parsedown = new ParsedownExtension();
$markdownArray = $parsedown->text($markdown);
return $parsedown->markup($markdownArray);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Typemill\Middleware;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
class AssetMiddleware implements MiddlewareInterface
{
protected $assets;
protected $view;
public function __construct($assets, $view)
{
$this->assets = $assets;
$this->view = $view;
}
public function process(Request $request, RequestHandler $handler) :response
{
# get url from request
# update the asset object in the container (for plugins) with the new url
# $this->container->assets->setBaseUrl($uri->getBaseUrl());
# add the asset object to twig-frontend for themes
$this->view->getEnvironment()->addGlobal('assets', $this->assets);
# use {{ base_url() }} in twig templates
# $this->container['view']['base_url'] = $uri->getBaseUrl();
# $this->container['view']['current_url'] = $uri->getPath();
$response = $handler->handle($request);
return $response;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Typemill\Middleware;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
use Typemill\Static\Session;
use Typemill\Models\User;
class SessionMiddleware implements MiddlewareInterface
{
protected $segments;
protected $route;
public function __construct($segments, $route)
{
$this->segments = $segments;
$this->route = $route;
}
public function process(Request $request, RequestHandler $handler) :response
{
# start session for routes
Session::startSessionForSegments($this->segments, $this->route);
$authenticated = (
(isset($_SESSION['username'])) &&
(isset($_SESSION['login']))
)
? true : false;
if($authenticated)
{
# add userdata to the request for later use
$user = new User();
if($user->setUser($_SESSION['username']))
{
$userdata = $user->getUserData();
$request = $request->withAttribute('c_username', $userdata['username']);
$request = $request->withAttribute('c_userrole', $userdata['userrole']);
}
}
$response = $handler->handle($request);
return $response;
}
}

View File

@@ -0,0 +1,404 @@
<?php
namespace Typemill\Models;
use Typemill\Models\StorageWrapper;
use Typemill\Models\Content;
class Meta
{
private $storage;
public function __construct($baseurl = NULL)
{
$this->storage = new StorageWrapper('\Typemill\Models\Storage');
}
# used by contentApiController (backend) and pageController (frontend) and TwigMetaExtension (list pages)
public function getMetaData($item)
{
$metadata = $this->storage->getYaml('contentFolder', '', $item->pathWithoutType . '.yaml');
return $metadata;
# compare with meta that are in use right now (e.g. changed theme, disabled plugin)
$metascheme = $this->getYaml('cache', 'metatabs.yaml');
if($metascheme)
{
$meta = $this->whitelistMeta($meta,$metascheme);
}
return $meta;
}
public function getMetaDefinitions($settings, $folder)
{
$metadefinitions = $this->storage->getYaml('systemSettings', '', 'metatabs.yaml');
# loop through all plugins
if(!empty($settings['plugins']))
{
foreach($settings['plugins'] as $name => $plugin)
{
if($plugin['active'])
{
$pluginSettings = \Typemill\Static\Settings::getObjectSettings('plugins', $name);
if($pluginSettings && isset($pluginSettings['metatabs']))
{
$metadefinitions = array_merge_recursive($metadefinitions, $pluginSettings['metatabs']);
}
}
}
}
# add the meta from theme settings here
$themeSettings = \Typemill\Static\Settings::getObjectSettings('themes', $settings['theme']);
if($themeSettings && isset($themeSettings['metatabs']))
{
$metadefinitions = array_merge_recursive($metadefinitions, $themeSettings['metatabs']);
}
# conditional fieldset for user or role based access
if(!isset($settings['pageaccess']) || $settings['pageaccess'] === NULL )
{
unset($metadefinitions['meta']['fields']['fieldsetrights']);
}
# conditional fieldset for folders
if(!$folder)
{
unset($metadefinitions['meta']['fields']['fieldsetfolder']);
}
# dispatch meta
# $metatabs = $this->c->dispatcher->dispatch('onMetaDefinitionsLoaded', new OnMetaDefinitionsLoaded($metatabs))->getData();
return $metadefinitions;
}
public function updateMeta($meta, $item)
{
$filename = $item->pathWithoutType . '.yaml';
if($this->storage->updateYaml('contentFolder', '', $filename, $meta))
{
return true;
}
return $this->storage->getError();
}
public function addMetaDefaults($meta, $item, $authorFromSettings, $currentuser = false)
{
$modified = false;
if(!isset($meta['meta']['owner']))
{
$meta['meta']['owner'] = $currentuser ? $currentuser : false;
$modified = true;
}
if(!isset($meta['meta']['author']))
{
$meta['meta']['owner'] = $currentuser ? $currentuser : $authorFromSettings;
$modified = true;
}
if(!isset($meta['meta']['created']))
{
$meta['meta']['created'] = date("Y-m-d");
$modified = true;
}
if(!isset($meta['meta']['time']))
{
$meta['meta']['time'] = date("H-i-s");
$modified = true;
}
if(!isset($meta['meta']['navtitle']))
{
$meta['meta']['navtitle'] = $item->name;
$modified = true;
}
if($modified)
{
$this->updateMeta($meta, $item);
}
$filePath = $item->path;
if($item->elementType == 'folder')
{
$filePath = $item->path . DIRECTORY_SEPARATOR . 'index.md';
}
$meta['meta']['modified'] = $this->storage->getFileTime('contentFolder', '', $filePath);
return $meta;
}
public function addMetaTitleDescription(array $meta, $item, array $markdown)
{
$title = (isset($meta['meta']['title']) && $meta['meta']['title'] != '') ? $meta['meta']['title'] : false;
$description = (isset($meta['meta']['description']) && $meta['meta']['description'] != '') ? $meta['meta']['description'] : false;
if(!$title OR !$description)
{
$content = new Content();
if(!$title)
{
$meta['meta']['title'] = $content->getTitle($markdown);
}
if(!$description)
{
$meta['meta']['description'] = $content->getDescription($markdown);
}
$this->updateMeta($meta, $item);
}
return $meta;
}
public function getNavtitle($url)
{
# get the extended structure where the navigation title is stored
$extended = $this->getYaml('cache', 'structure-extended.yaml');
if(isset($extended[$url]['navtitle']))
{
return $extended[$url]['navtitle'];
}
return '';
}
# used by articleApiController and pageController to add title and description if an article is published
public function completePageMeta($content, $settings, $item)
{
$meta = $this->getPageMeta($settings, $item);
if(!$meta)
{
return $this->getPageMetaDefaults($content, $settings, $item);
}
$title = (isset($meta['meta']['title']) AND $meta['meta']['title'] !== '') ? true : false;
$description = (isset($meta['meta']['description']) AND $meta['meta']['description'] !== '') ? true : false;
if($title && $description)
{
return $meta;
}
# initialize parsedown extension
$parsedown = new ParsedownExtension();
# if content is not an array, then transform it
if(!is_array($content))
{
# turn markdown into an array of markdown-blocks
$content = $parsedown->markdownToArrayBlocks($content);
}
# delete markdown from title
if(!$title && isset($content[0]))
{
$meta['meta']['title'] = trim($content[0], "# ");
}
if(!$description && isset($content[1]))
{
$meta['meta']['description'] = $this->generateDescription($content, $parsedown, $item);
}
$this->updateYaml($settings['contentFolder'], $item->pathWithoutType . '.yaml', $meta);
return $meta;
}
private function whitelistMeta($meta, $metascheme)
{
# we have only 2 dimensions, so no recursive needed
foreach($meta as $tab => $values)
{
if(!isset($metascheme[$tab]))
{
unset($meta[$tab]);
}
foreach($values as $key => $value)
{
if(!isset($metascheme[$tab][$key]))
{
unset($meta[$tab][$key]);
}
}
}
return $meta;
}
public function generateDescription($content, $parsedown, $item)
{
$description = isset($content[1]) ? $content[1] : '';
# create description or abstract from content
if($description !== '')
{
$firstLineArray = $parsedown->text($description);
$description = strip_tags($parsedown->markup($firstLineArray, $item->urlAbs));
# if description is very short
if(strlen($description) < 100 && isset($content[2]))
{
$secondLineArray = $parsedown->text($content[2]);
$description .= ' ' . strip_tags($parsedown->markup($secondLineArray, $item->urlAbs));
}
# if description is too long
if(strlen($description) > 300)
{
$description = substr($description, 0, 300);
$lastSpace = strrpos($description, ' ');
$description = substr($description, 0, $lastSpace);
}
}
return $description;
}
public function transformPagesToPosts($folder)
{
$filetypes = array('md', 'txt', 'yaml');
$result = true;
foreach($folder->folderContent as $page)
{
# create old filename without filetype
$oldFile = $this->basePath . 'content' . $page->pathWithoutType;
# set default date
$date = date('Y-m-d', time());
$time = date('H-i', time());
$meta = $this->getYaml('content', $page->pathWithoutType . '.yaml');
if($meta)
{
# get dates from meta
if(isset($meta['meta']['manualdate'])){ $date = $meta['meta']['manualdate']; }
elseif(isset($meta['meta']['created'])){ $date = $meta['meta']['created']; }
elseif(isset($meta['meta']['modified'])){ $date = $meta['meta']['modified']; }
# set time
if(isset($meta['meta']['time']))
{
$time = $meta['meta']['time'];
}
}
$datetime = $date . '-' . $time;
$datetime = implode(explode('-', $datetime));
$datetime = substr($datetime,0,12);
# create new file-name without filetype
$newFile = $this->basePath . 'content' . $folder->path . DIRECTORY_SEPARATOR . $datetime . '-' . $page->slug;
foreach($filetypes as $filetype)
{
$oldFilePath = $oldFile . '.' . $filetype;
$newFilePath = $newFile . '.' . $filetype;
#check if file with filetype exists and rename
if($oldFilePath != $newFilePath && file_exists($oldFilePath))
{
if(@rename($oldFilePath, $newFilePath))
{
$result = $result;
}
else
{
$result = false;
}
}
}
}
return $result;
}
public function transformPostsToPages($folder)
{
$filetypes = array('md', 'txt', 'yaml');
$index = 0;
$result = true;
foreach($folder->folderContent as $page)
{
# create old filename without filetype
$oldFile = $this->basePath . 'content' . $page->pathWithoutType;
$order = $index;
if($index < 10)
{
$order = '0' . $index;
}
# create new file-name without filetype
$newFile = $this->basePath . 'content' . $folder->path . DIRECTORY_SEPARATOR . $order . '-' . $page->slug;
foreach($filetypes as $filetype)
{
$oldFilePath = $oldFile . '.' . $filetype;
$newFilePath = $newFile . '.' . $filetype;
#check if file with filetype exists and rename
if($oldFilePath != $newFilePath && file_exists($oldFilePath))
{
if(@rename($oldFilePath, $newFilePath))
{
$result = $result;
}
else
{
$result = false;
}
}
}
$index++;
}
return $result;
}
public function folderContainsFolders($folder)
{
foreach($folder->folderContent as $page)
{
if($page->elementType == 'folder')
{
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,66 @@
const textcomponent = {
props: ['id', 'description', 'maxlength', 'hidden', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'css', 'errors'],
template: `<div :class="css ? css : 'w-full'" class="mt-5 mb-5">
<label :for="name" class="block mb-1 font-medium">{{ $filters.translate(label) }}</label>
<input type="text" class="h-12 w-full border px-2 py-3" :class="errors[name] ? ' border-red-500 bg-red-100' : ' border-stone-300 bg-stone-200'"
:id="id"
:maxlength="maxlength"
:readonly="readonly"
:hidden="hidden"
:required="required"
:disabled="disabled"
:name="name"
:placeholder="placeholder"
:value="value"
@input="update($event, name)">
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
</div>`,
methods: {
update: function($event, name)
{
eventBus.$emit('forminput', {'name': name, 'value': $event.target.value});
},
},
};
const textareacomponent = {
props: ['id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'css', 'value', 'errors'],
template: `<div :class="css ? css : 'w-full'" class="mt-5 mb-5">
<label :for="name" class="block mb-1 font-medium">{{ $filters.translate(label) }}</label>
<textarea rows="8" class="w-full border border-stone-300 bg-stone-200 px-2 py-3"
:id="id"
:class="css"
:readonly="readonly"
:required="required"
:disabled="disabled"
:name="name"
:placeholder="placeholder"
:value="value"
@input="update($event, name)"></textarea>
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
</div>`,
methods: {
update: function($event, name)
{
eventBus.$emit('forminput', {'name': name, 'value': $event.target.value});
},
formatValue: function(value)
{
/*
if(value !== null && typeof value === 'object')
{
this.textareaclass = 'codearea';
return JSON.stringify(value, undefined, 4);
}
return value;
*/
},
},
};
const formcomponents = {
'component-text' : textcomponent,
'component-textarea' : textareacomponent
};

View File

@@ -0,0 +1,289 @@
const app = Vue.createApp({
template: `<div>
<button
v-for="tab in tabs"
v-on:click="currentTab = tab"
:key="tab"
class="px-4 py-2 border-b-2 border-stone-200 hover:border-stone-700 hover:bg-stone-50 transition duration-100"
:class="(tab == currentTab) ? 'bg-stone-50 border-stone-700' : ''"
>
{{ $filters.translate(tab) }}
</button>
<component
:class="css"
:is="currentTabComponent"
:saved="saved"
:errors="formErrors[currentTab]"
:message="message"
:messageClass="messageClass"
:formDefinitions="formDefinitions[currentTab]"
:formData="formData[currentTab]"
:item="item"
v-on:saveform="saveForm">
</component>
</div>`,
data: function () {
return {
item: data.item,
currentTab: 'Content',
tabs: ['Content'],
formDefinitions: [],
formData: [],
formErrors: {},
formErrorsReset: {},
message: false,
messageClass: false,
css: "px-16 py-16 bg-stone-50 shadow-md mb-16",
saved: false,
}
},
computed: {
currentTabComponent: function ()
{
if(this.currentTab == 'Content')
{
eventBus.$emit("showEditor");
}
else
{
eventBus.$emit("hideEditor");
return 'tab-' + this.currentTab.toLowerCase()
}
}
},
mounted: function(){
var self = this;
tmaxios.get('/api/v1/meta',{
params: {
'url': data.urlinfo.route,
}
})
.then(function (response){
var formdefinitions = response.data.metadefinitions;
for (var key in formdefinitions)
{
if (formdefinitions.hasOwnProperty(key))
{
self.tabs.push(key);
self.formErrors[key] = false;
}
}
self.formErrorsReset = self.formErrors;
self.formDefinitions = formdefinitions;
self.formData = response.data.metadata;
/*
self.userroles = response.data.userroles;
self.item = response.data.item;
if(self.item.elementType == "folder" && self.item.contains == "posts")
{
posts.posts = self.item.folderContent;
posts.folderid = self.item.keyPath;
}
else
{
posts.posts = false;
}
*/
})
.catch(function (error)
{
if(error.response)
{
}
});
eventBus.$on('forminput', formdata => {
this.formData[this.currentTab][formdata.name] = formdata.value;
});
/*
update values that are objects
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
eventBus.$on('forminputobject', formdata => {
this.formData[this.currentTab][formdata.name] = Object.assign({}, this.formData[this.currentTab][formdata.name], formdata.value);
});
*/
},
methods: {
saveForm: function()
{
this.saved = false;
self = this;
tmaxios.post('/api/v1/metadata',{
'url': data.urlinfo.route,
'tab': self.currentTab,
'data': self.formData[self.currentTab]
})
.then(function (response){
self.saved = true;
self.message = 'saved successfully';
self.messageClass = 'bg-teal-500';
self.formErrors = self.formErrorsReset;
if(response.data.navigation)
{
eventBus.$emit('navigation', response.data.navigation);
}
if(response.data.item)
{
eventBus.$emit('item', response.data.item);
}
})
.catch(function (error)
{
if(error.response)
{
self.formErrors = error.response.data.errors;
self.message = 'please correct the errors above';
self.messageClass = 'bg-rose-500';
}
});
},
}
});
app.component('tab-meta', {
props: ['item', 'formData', 'formDefinitions', 'saved', 'errors', 'message', 'messageClass'],
data: function () {
return {
slug: false,
originalSlug: false,
slugerror: false,
disabled: true,
}
},
template: `<section>
<form>
<div v-if="slug !== false">
<div class="w-full relative">
<label class="block mb-1 font-medium">{{ $filters.translate('Slug') }}</label>
<div class="flex">
<input
class="h-12 w-3/4 border px-2 py-3 border-stone-300 bg-stone-200"
type="text"
v-model="slug"
pattern="[a-z0-9\- ]"
@input="changeSlug()"
/>
<button
class="w-1/4 px-2 py-3 ml-2 text-stone-50 bg-stone-700 hover:bg-stone-900 hover:text-white transition duration-100 cursor-pointer disabled:cursor-not-allowed disabled:bg-stone-200 disabled:text-stone-800"
@click.prevent="storeSlug()"
:disabled="disabled"
>
{{ $filters.translate('change slug') }}
</button>
</div>
<div v-if="slugerror" class="f6 tm-red mt1">{{ slugerror }}</div>
</div>
</div>
<div v-for="(fieldDefinition, fieldname) in formDefinitions.fields">
<fieldset class="flex flex-wrap justify-between border-2 border-stone-200 p-4 my-8" v-if="fieldDefinition.type == 'fieldset'">
<legend class="text-lg font-medium">{{ fieldDefinition.legend }}</legend>
<component v-for="(subfieldDefinition, subfieldname) in fieldDefinition.fields"
:key="subfieldname"
:is="selectComponent(subfieldDefinition.type)"
:errors="errors"
:name="subfieldname"
:userroles="userroles"
:value="formData[subfieldname]"
v-bind="subfieldDefinition">
</component>
</fieldset>
<component v-else
:key="fieldname"
:is="selectComponent(fieldDefinition.type)"
:errors="errors"
:name="fieldname"
:userroles="userroles"
:value="formData[fieldname]"
v-bind="fieldDefinition">
</component>
</div>
<div class="my-5">
<div :class="messageClass" class="block w-full h-8 px-3 py-1 my-1 text-white transition duration-100">{{ $filters.translate(message) }}</div>
<input type="submit" @click.prevent="saveInput()" :value="$filters.translate('save')" class="w-full p-3 my-1 bg-stone-700 hover:bg-stone-900 text-white cursor-pointer transition duration-100">
</div>
</form>
</section>`,
mounted: function()
{
if(this.item.slug != '')
{
this.slug = this.item.slug;
this.originalSlug = this.item.slug;
}
},
methods: {
selectComponent: function(type)
{
return 'component-' + type;
},
saveInput: function()
{
this.$emit('saveform');
},
changeSlug: function()
{
if(this.slug == this.originalSlug)
{
this.slugerror = false;
this.disabled = true;
return;
}
if(this.slug == '')
{
this.slugerror = 'empty slugs are not allowed';
this.disabled = true;
return;
}
this.slug = this.slug.replace(/ /g, '-');
this.slug = this.slug.toLowerCase();
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 = true;
}
},
storeSlug: function()
{
if(this.slug.match(/^[a-z0-9\-]*$/) && this.slug != this.originalSlug)
{
var self = this;
tmaxios.post('/api/v1/article/rename',{
'url': data.urlinfo.route,
'slug': this.slug,
'oldslug': this.originalSlug,
})
.then(function (response)
{
window.location.replace(response.data.url);
})
.catch(function (error)
{
eventBus.$emit('publishermessage', error.response.data.message);
});
}
}
}
})