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: `+ + +`, + 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: `{{ errors[name] }}
+{{ $filters.translate(description) }}
++ + + +`, + 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: `+ + ++ + `, + 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