diff --git a/.htaccess b/.htaccess index 39493b6..ef390bb 100644 --- a/.htaccess +++ b/.htaccess @@ -64,7 +64,7 @@ RewriteRule ^(system\/author\/img\/) - [L] RewriteRule ^(system\/author\/js\/) - [L] # redirect all other direct requests to the following physical folders to the index.php so pages with same name work -RewriteRule ^(system|content|data|settings) index.php [QSA,L] +RewriteRule ^(system|content|data|settings|(media\/files\/)) index.php [QSA,L] # disallow browsing other folders generally Options -Indexes diff --git a/content/01-cyanine-theme/03-content-elements.md b/content/01-cyanine-theme/03-content-elements.md index a3e7c38..88d339d 100644 --- a/content/01-cyanine-theme/03-content-elements.md +++ b/content/01-cyanine-theme/03-content-elements.md @@ -2,10 +2,7 @@ Cyanine provides a lot of other settings for your content area. For example: -* Add an edit-button for github, gitlab or other plattforms. -* Show the author. -* Show the publish date. -* Show the chapter numbers in the navigation. +[ebook (EPUB, 496.65 KB)](media/files/ebook.epub){.tm-download file-epub} The Cyanine theme supports all content elements like tables, images, notices or downloads. It also supports anchor-links next to headlines, so you can deep link to certain content sections of your page. You can activate the anchors in the system settings of Typemill. diff --git a/system/Controllers/ControllerAuthorArticleApi.php b/system/Controllers/ControllerAuthorArticleApi.php index d021346..5f061c3 100644 --- a/system/Controllers/ControllerAuthorArticleApi.php +++ b/system/Controllers/ControllerAuthorArticleApi.php @@ -180,7 +180,7 @@ class ControllerAuthorArticleApi extends ControllerAuthor # check if it is a folder and if the folder has published pages. $message = false; - if($this->item->elementType == 'folder') + if($this->item->elementType == 'folder' && isset($this->item->folderContent)) { foreach($this->item->folderContent as $folderContent) { diff --git a/system/Controllers/ControllerAuthorMediaApi.php b/system/Controllers/ControllerAuthorMediaApi.php index 1be1629..4840ce9 100644 --- a/system/Controllers/ControllerAuthorMediaApi.php +++ b/system/Controllers/ControllerAuthorMediaApi.php @@ -6,6 +6,7 @@ use Slim\Http\Request; use Slim\Http\Response; use Typemill\Models\ProcessImage; use Typemill\Models\ProcessFile; +use Typemill\Models\WriteYaml; use Typemill\Controllers\ControllerAuthorBlockApi; class ControllerAuthorMediaApi extends ControllerAuthor @@ -92,6 +93,69 @@ class ControllerAuthorMediaApi extends ControllerAuthor return $response->withJson(['errors' => 'file not found or file name invalid'],404); } + public function getFileRestrictions(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri()->withUserInfo(''); + $restriction = 'all'; + + $userroles = $this->c->acl->getRoles(); + + if(isset($this->params['filename']) && $this->params['filename'] != '') + { + $writeYaml = new WriteYaml(); + $restrictions = $writeYaml->getYaml('media' . DIRECTORY_SEPARATOR . 'files', 'filerestrictions.yaml'); + if(isset($restrictions[$this->params['filename']])) + { + $restriction = $restrictions[$this->params['filename']]; + } + } + + return $response->withJson(['userroles' => $userroles, 'restriction' => $restriction]); + } + + public function updateFileRestrictions(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri()->withUserInfo(''); + $filename = isset($this->params['filename']) ? $this->params['filename'] : false; + $role = isset($this->params['role']) ? $this->params['role'] : false; + + if(!$filename OR !$role) + { + return $response->withJson(['errors' => ['message' => 'Filename or userrole is missing.']], 422); + } + + $userroles = $this->c->acl->getRoles(); + + if($role != 'all' AND !in_array($role, $userroles)) + { + return $response->withJson(['errors' => ['message' => 'Userrole is unknown.']], 422); + } + + $writeYaml = new WriteYaml(); + $restrictions = $writeYaml->getYaml('media' . DIRECTORY_SEPARATOR . 'files', 'filerestrictions.yaml'); + if(!$restrictions) + { + $restrictions = []; + } + + if($role == 'all') + { + unset($restrictions[$filename]); + } + else + { + $restrictions[$filename] = $role; + } + + $writeYaml->updateYaml('media' . DIRECTORY_SEPARATOR . 'files', 'filerestrictions.yaml', $restrictions); + + return $response->withJson(['restrictions' => $restrictions]); + } + public function createImage(Request $request, Response $response, $args) { # get params from call @@ -382,14 +446,14 @@ class ControllerAuthorMediaApi extends ControllerAuthor return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); } - $tmpImage = $imageProcessor->createImage($imageData64, $desiredSizes); + $tmpImage = $imageProcessor->createImage($imageData64, $videoID, $desiredSizes); if(!$tmpImage) { return $response->withJson(array('errors' => 'could not create temporary image')); } - $imageUrl = $imageProcessor->publishImage($desiredSizes, $videoID); + $imageUrl = $imageProcessor->publishImage(); if($imageUrl) { $this->params['markdown'] = '{#' . $videoID. ' .' . $class . '}'; diff --git a/system/Controllers/ControllerAuthorMetaApi.php b/system/Controllers/ControllerAuthorMetaApi.php index 7634014..ab36e3f 100644 --- a/system/Controllers/ControllerAuthorMetaApi.php +++ b/system/Controllers/ControllerAuthorMetaApi.php @@ -313,6 +313,7 @@ class ControllerAuthorMetaApi extends ControllerAuthor # normalize the meta-input $metaInput['navtitle'] = (isset($metaInput['navtitle']) && $metaInput['navtitle'] !== null )? $metaInput['navtitle'] : ''; $metaInput['hide'] = (isset($metaInput['hide']) && $metaInput['hide'] !== null) ? $metaInput['hide'] : false; + $metaInput['noindex'] = (isset($metaInput['noindex']) && $metaInput['noindex'] !== null) ? $metaInput['noindex'] : false; # input values are empty but entry in structure exists if(!$metaInput['hide'] && $metaInput['navtitle'] == "" && isset($extended[$this->item->urlRelWoF])) @@ -327,10 +328,12 @@ class ControllerAuthorMetaApi extends ControllerAuthor ($this->hasChanged($metaInput, $metaPage['meta'], 'navtitle')) OR ($this->hasChanged($metaInput, $metaPage['meta'], 'hide')) + OR + ($this->hasChanged($metaInput, $metaPage['meta'], 'noindex')) ) { # add new file data. Also makes sure that the value is set. - $extended[$this->item->urlRelWoF] = ['hide' => $metaInput['hide'], 'navtitle' => $metaInput['navtitle']]; + $extended[$this->item->urlRelWoF] = ['hide' => $metaInput['hide'], 'navtitle' => $metaInput['navtitle'], 'noindex' => $metaInput['noindex']]; $structure = true; } diff --git a/system/Controllers/ControllerDownload.php b/system/Controllers/ControllerDownload.php new file mode 100644 index 0000000..6d151f3 --- /dev/null +++ b/system/Controllers/ControllerDownload.php @@ -0,0 +1,144 @@ +c->get('settings')['rootPath']; + $mediapath = 'media/files/'; + $filepath = $root . $mediapath; + + if(!$filename) + { + die('the requested file does not exist.'); + } + + # validate + $allowedFiletypes = []; + if(!$this->validate($filepath, $filename, $allowedFiletypes)) + { + die('the requested file is not allowed.'); + } + + $writeYaml = new WriteYaml(); + $restrictions = $writeYaml->getYaml('media' . DIRECTORY_SEPARATOR . 'files', 'filerestrictions.yaml'); + + if($restrictions && isset($restrictions[$mediapath . $filename])) + { + $allowedrole = $restrictions[$mediapath . $filename]; + + if(!isset($_SESSION['role'])) + { + die("You have to be an authenticated $allowedrole to download this file."); + } + elseif( + $_SESSION['role'] != 'administrator' + AND $_SESSION['role'] != $allowedrole + AND !$this->c->acl->inheritsRole($_SESSION['role'], $allowedrole) + ) + { + die("You have to be a $allowedrole to download this file."); + } + } + + $file = $filepath . $filename; + + # for now we only allow one download + $this->sendDownload($file); + exit; + } + + /** + * Validate if the file exists and if + * there is a permission (download dir) to download this file + * + * You should ALWAYS call this method if you don't want + * somebody to download files not intended to be for the public. + * + * @param string $file GET parameter + * @param array $allowedFiletypes (defined in the head of this file) + * @return bool true if validation was successfull + */ + private function validate($path, $file, $allowedFiletypes) + { + $filepath = $path . $file; + + # check if file exists + if (!isset($filepath) || empty($filepath) || !file_exists($filepath)) + { + return false; + } + + # check allowed filetypes + if(!empty($allowedFiletypes)) + { + $fileAllowed = false; + foreach ($allowedFiletypes as $filetype) + { + if (strpos($file, $filetype) === (strlen($file) - strlen($filetype))) + { + $fileAllowed = true; //ends with $filetype + } + } + + if (!$fileAllowed) return false; + } + + # check download directory + if (strpos($file, '..') !== false) + { + return false; + } + return true; + } + + /** + * Download function. + * Sets the HTTP header and supplies the given file + * as a download to the browser. + * + * @param string $file path to file + */ + private function sendDownload($file) + { + # Parse information + $pathinfo = pathinfo($file); + $extension = strtolower($pathinfo['extension']); + $mimetype = null; + + # Get mimetype for extension + # This list can be extended as you need it. + # A good start to find mimetypes is the apache mime.types list + # http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types + switch ($extension) { + case 'zip': $mimetype = "application/zip"; break; + default: $mimetype = "application/force-download"; + } + + # Required for some browsers like Safari and IE + if (ini_get('zlib.output_compression')) + { + ini_set('zlib.output_compression', 'Off'); + } + + header('Pragma: public'); + header('Content-Encoding: none'); + header('Expires: 0'); + header('Accept-Ranges: bytes'); # Allow support for download resume + header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($file)) . ' GMT'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Cache-Control: private', false); # required for some browsers + header('Content-Type: application/zip'); + header('Content-Disposition: attachment; filename="'.basename($file).'";'); # Make the browser display the Save As dialog + header('Content-Transfer-Encoding: binary'); + header('Content-Length: '.filesize($file)); + ob_end_flush(); + readfile($file); # This is necessary in order to get it to actually download the file, otherwise it will be 0Kb + } +} \ No newline at end of file diff --git a/system/Controllers/ControllerShared.php b/system/Controllers/ControllerShared.php index 65df936..5b5ca46 100644 --- a/system/Controllers/ControllerShared.php +++ b/system/Controllers/ControllerShared.php @@ -177,7 +177,7 @@ abstract class ControllerShared $pagetreeLive = Folder::scanFolder($this->settings['rootPath'] . $this->settings['contentFolder'], $draft = false ); # if there is content, then get the content details - if(count($pagetreeLive) > 0) + if($pagetreeLive && count($pagetreeLive) > 0) { # get the extended structure files with changes like navigation title or hidden pages $yaml = new writeYaml(); @@ -295,28 +295,37 @@ abstract class ControllerShared $opts = array( 'http'=>array( 'method'=>"GET", + 'ignore_errors' => true, 'timeout' => 5 ) ); - $context = stream_context_create($opts); + $context = stream_context_create($opts); - $resultBing = file_get_contents($pingBingUrl, false, $context); - $resultGoogle = file_get_contents($pingGoogleUrl, false, $context); + $responseBing = file_get_contents($pingBingUrl, false, $context); + $responseGoogle = file_get_contents($pingGoogleUrl, false, $context); } } public function generateUrlSets($structureLive) - { + { $urlset = ''; - + foreach($structureLive as $item) { - if($item->elementType == 'folder') + if($item->elementType == 'folder' && isset($item->noindex) && $item->noindex === true) + { + $urlset .= $this->generateUrlSets($item->folderContent, $urlset); + } + elseif($item->elementType == 'folder') { $urlset = $this->addUrlSet($urlset, $item->urlAbs); - $urlset .= $this->generateUrlSets($item->folderContent, $urlset); + $urlset .= $this->generateUrlSets($item->folderContent, $urlset); + } + elseif(isset($item->noindex) && $item->noindex === true ) + { + continue; } else { diff --git a/system/Models/Folder.php b/system/Models/Folder.php index 201668c..1d2261f 100644 --- a/system/Models/Folder.php +++ b/system/Models/Folder.php @@ -154,8 +154,9 @@ class Folder # check if there are extended information if($extended && isset($extended[$item->urlRelWoF])) { - $item->name = ($extended[$item->urlRelWoF]['navtitle'] != '') ? $extended[$item->urlRelWoF]['navtitle'] : $item->name; - $item->hide = ($extended[$item->urlRelWoF]['hide'] === true) ? true : false; + $item->name = ($extended[$item->urlRelWoF]['navtitle'] != '') ? $extended[$item->urlRelWoF]['navtitle'] : $item->name; + $item->hide = ($extended[$item->urlRelWoF]['hide'] === true) ? true : false; + $item->noindex = (isset($extended[$item->urlRelWoF]['noindex']) && $extended[$item->urlRelWoF]['noindex'] === true) ? true : false; } # sort posts in descending order @@ -217,8 +218,9 @@ class Folder # check if there are extended information if($extended && isset($extended[$item->urlRelWoF])) { - $item->name = ($extended[$item->urlRelWoF]['navtitle'] != '') ? $extended[$item->urlRelWoF]['navtitle'] : $item->name; - $item->hide = ($extended[$item->urlRelWoF]['hide'] === true) ? true : false; + $item->name = ($extended[$item->urlRelWoF]['navtitle'] != '') ? $extended[$item->urlRelWoF]['navtitle'] : $item->name; + $item->hide = ($extended[$item->urlRelWoF]['hide'] === true) ? true : false; + $item->noindex = (isset($extended[$item->urlRelWoF]['noindex']) && $extended[$item->urlRelWoF]['noindex'] === true) ? true : false; } } diff --git a/system/Routes/Api.php b/system/Routes/Api.php index 09fec81..7630251 100644 --- a/system/Routes/Api.php +++ b/system/Routes/Api.php @@ -36,7 +36,7 @@ $app->post('/api/v1/block', ControllerAuthorBlockApi::class . ':addBlock')->setN $app->put('/api/v1/block', ControllerAuthorBlockApi::class . ':updateBlock')->setName('api.block.update')->add(new RestrictApiAccess($container['router'])); $app->delete('/api/v1/block', ControllerAuthorBlockApi::class . ':deleteBlock')->setName('api.block.delete')->add(new RestrictApiAccess($container['router'])); $app->put('/api/v1/moveblock', ControllerAuthorBlockApi::class . ':moveBlock')->setName('api.block.move')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/video', ControllerAuthorBlockApi::class . ':saveVideoImage')->setName('api.video.save')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/video', ControllerAuthorMediaApi::class . ':saveVideoImage')->setName('api.video.save')->add(new RestrictApiAccess($container['router'])); $app->get('/api/v1/medialib/images', ControllerAuthorMediaApi::class . ':getMediaLibImages')->setName('api.medialibimg.get')->add(new RestrictApiAccess($container['router'])); $app->get('/api/v1/medialib/files', ControllerAuthorMediaApi::class . ':getMediaLibFiles')->setName('api.medialibfiles.get')->add(new RestrictApiAccess($container['router'])); @@ -44,6 +44,8 @@ $app->get('/api/v1/image', ControllerAuthorMediaApi::class . ':getImage')->setNa $app->post('/api/v1/image', ControllerAuthorMediaApi::class . ':createImage')->setName('api.image.create')->add(new RestrictApiAccess($container['router'])); $app->put('/api/v1/image', ControllerAuthorMediaApi::class . ':publishImage')->setName('api.image.publish')->add(new RestrictApiAccess($container['router'])); $app->delete('/api/v1/image', ControllerAuthorMediaApi::class . ':deleteImage')->setName('api.image.delete')->add(new RestrictApiAccess($container['router'])); +$app->get('/api/v1/filerestrictions', ControllerAuthorMediaApi::class . ':getFileRestrictions')->setName('api.file.getrestrictions')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/filerestrictions', ControllerAuthorMediaApi::class . ':updateFileRestrictions')->setName('api.file.updaterestrictions')->add(new RestrictApiAccess($container['router'])); $app->get('/api/v1/file', ControllerAuthorMediaApi::class . ':getFile')->setName('api.file.get')->add(new RestrictApiAccess($container['router'])); $app->post('/api/v1/file', ControllerAuthorMediaApi::class . ':uploadFile')->setName('api.file.upload')->add(new RestrictApiAccess($container['router'])); $app->put('/api/v1/file', ControllerAuthorMediaApi::class . ':publishFile')->setName('api.file.publish')->add(new RestrictApiAccess($container['router'])); diff --git a/system/Routes/Web.php b/system/Routes/Web.php index 6d34221..2a95a2e 100644 --- a/system/Routes/Web.php +++ b/system/Routes/Web.php @@ -1,6 +1,7 @@ get('/tm/content/raw[/{params:.*}]', ControllerAuthorEditor::class . ':sho $app->get('/tm/content/visual[/{params:.*}]', ControllerAuthorEditor::class . ':showBlox')->setName('content.visual')->add(new accessMiddleware($container['router'], $container['acl'], 'content', 'view')); $app->get('/tm/content[/{params:.*}]', ControllerAuthorEditor::class . ':showEmpty')->setName('content.empty')->add(new accessMiddleware($container['router'], $container['acl'], 'content', 'view')); +$app->get('/media/files[/{params:.*}]', ControllerDownload::class . ':download')->setName('download.file'); + foreach($routes as $pluginRoute) { $method = $pluginRoute['httpMethod']; diff --git a/system/author/js/vue-blox.js b/system/author/js/vue-blox.js index 9620996..96570cb 100644 --- a/system/author/js/vue-blox.js +++ b/system/author/js/vue-blox.js @@ -17,7 +17,7 @@ const contentComponent = Vue.component('content-block', { '