diff --git a/system/typemill/Assets.php b/system/typemill/Assets.php new file mode 100644 index 0000000..ed4b0d1 --- /dev/null +++ b/system/typemill/Assets.php @@ -0,0 +1,304 @@ +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[] = ''; + } + } + + public function addInlineCSS($CSS) + { + $this->inlineCSS[] = ''; + } + + public function addJS($JS) + { + $JSfile = $this->getFileUrl($JS); + + if($JSfile) + { + $this->JS[] = ''; + } + } + + public function addInlineJS($JS) + { + $this->inlineJS[] = ''; + } + + public function activateVue() + { + $vueUrl = ''; + if(!in_array($vueUrl, $this->JS)) + { + $this->JS[] = $vueUrl; + } + } + + public function activateAxios() + { + $axiosUrl = ''; + if(!in_array($axiosUrl, $this->JS)) + { + $this->JS[] = $axiosUrl; + + $axios = ''; + $this->JS[] = $axios; + } + } + + public function activateTachyons() + { + $tachyonsUrl = ''; + 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[] = ''; + } + } + + public function addEditorInlineJS($JS) + { + $this->editorInlineJS[] = ''; + } + + public function addEditorCSS($CSS) + { + $CSSfile = $this->getFileUrl($CSS); + + if($CSSfile) + { + $this->editorCSS[] = ''; + } + } + + 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; + } +} \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerApiAuthorMeta.php b/system/typemill/Controllers/ControllerApiAuthorMeta.php new file mode 100644 index 0000000..ec4dd07 --- /dev/null +++ b/system/typemill/Controllers/ControllerApiAuthorMeta.php @@ -0,0 +1,516 @@ +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 '
';
+		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;
+	}
+}
\ No newline at end of file
diff --git a/system/typemill/Controllers/ControllerWebDownload.php b/system/typemill/Controllers/ControllerWebDownload.php
new file mode 100644
index 0000000..ee2a5a3
--- /dev/null
+++ b/system/typemill/Controllers/ControllerWebDownload.php
@@ -0,0 +1,149 @@
+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
+	}
+}
\ No newline at end of file
diff --git a/system/typemill/Extensions/TwigMarkdownExtension.php b/system/typemill/Extensions/TwigMarkdownExtension.php
new file mode 100644
index 0000000..f025b66
--- /dev/null
+++ b/system/typemill/Extensions/TwigMarkdownExtension.php
@@ -0,0 +1,26 @@
+text($markdown);
+		
+		return $parsedown->markup($markdownArray);
+	}
+}
\ No newline at end of file
diff --git a/system/typemill/Middleware/AssetMiddleware.php b/system/typemill/Middleware/AssetMiddleware.php
new file mode 100644
index 0000000..23f73e3
--- /dev/null
+++ b/system/typemill/Middleware/AssetMiddleware.php
@@ -0,0 +1,41 @@
+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;
+    }
+}
\ No newline at end of file
diff --git a/system/typemill/Middleware/SessionMiddleware.php b/system/typemill/Middleware/SessionMiddleware.php
new file mode 100644
index 0000000..4e2614a
--- /dev/null
+++ b/system/typemill/Middleware/SessionMiddleware.php
@@ -0,0 +1,55 @@
+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;
+    }
+}
\ No newline at end of file
diff --git a/system/typemill/Models/Meta.php b/system/typemill/Models/Meta.php
new file mode 100644
index 0000000..51da75d
--- /dev/null
+++ b/system/typemill/Models/Meta.php
@@ -0,0 +1,404 @@
+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;
+	}
+}
\ No newline at end of file
diff --git a/system/typemill/author/js/vue-forms-local.js b/system/typemill/author/js/vue-forms-local.js
new file mode 100644
index 0000000..cb46e8e
--- /dev/null
+++ b/system/typemill/author/js/vue-forms-local.js
@@ -0,0 +1,66 @@
+const textcomponent = {
+	props: ['id', 'description', 'maxlength', 'hidden', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'css', 'errors'],	
+	template: `
+ + +

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
`, + 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: `
+ + +

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
`, + 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 +}; diff --git a/system/typemill/author/js/vue-meta.js b/system/typemill/author/js/vue-meta.js new file mode 100644 index 0000000..26a2162 --- /dev/null +++ b/system/typemill/author/js/vue-meta.js @@ -0,0 +1,289 @@ +const app = Vue.createApp({ + template: `
+ + + + + + +
`, + 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: `
+
+
+
+ +
+ + +
+
{{ slugerror }}
+
+
+
+
+ {{ fieldDefinition.legend }} + + +
+ + +
+
+
{{ $filters.translate(message) }}
+ +
+
+
`, + 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); + }); + } + } + } +}) \ No newline at end of file