diff --git a/.gitignore b/.gitignore index 377c36b..86b8388 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,20 @@ -cache/lastCache.txt -cache/lastSitemap.txt -cache/metatabs.yaml -cache/navigation.txt -cache/sitemap.xml -cache/structure-draft.txt -cache/structure-extended.yaml -cache/structure.txt -content/index.yaml -content/00-Welcome/index.yaml -content/00-Welcome/00-Setup.yaml -content/00-Welcome/01-Write-Content.yaml -content/00-Welcome/02-Get-Help.yaml -content/00-Welcome/03-Markdown-Test.yaml -settings/settings.yaml -settings/users -system/vendor -plugins/demo -zips +cache/lastCache.txt +cache/lastSitemap.txt +cache/metatabs.yaml +cache/navigation.txt +cache/sitemap.xml +cache/structure-draft.txt +cache/structure-extended.yaml +cache/structure.txt +content/index.yaml +content/00-welcome/index.yaml +content/00-welcome/00-setup.yaml +content/00-welcome/01-write-content.yaml +content/00-welcome/02-get-help.yaml +content/00-welcome/03-markdown-test.yaml +settings/settings.yaml +settings/users +system/vendor +plugins/demo +zips build.php \ No newline at end of file diff --git a/.htaccess b/.htaccess index 586f7af..92e92fa 100644 --- a/.htaccess +++ b/.htaccess @@ -1,41 +1,41 @@ -RewriteEngine On - -# If your homepage is http://yourdomain.com/yoursite -# Set the RewriteBase to: -# RewriteBase /yoursite - -# In some environements, an empty RewriteBase is required: -# RewriteBase / - -# Protect your system files from prying eyes -RewriteRule ^(system\/author\/) - [L] -RewriteRule ^(system) - [F,L] -RewriteRule ^(content) - [F,L] -RewriteRule ^(settings) - [F,L] -RewriteRule ^(.*)?\.yml$ - [F,L] -Rewriterule ^(.*)?\.yaml$ - [F,L] -RewriteRule ^(.*)?\.txt$ - [F,L] -RewriteRule ^(.*)?\.example$ - [F,L] -RewriteRule ^(.*/)?\.git+ - [F,L] - -# Use this to redirect HTTP to HTTPS on apache servers -# RewriteCond %{HTTPS} off -# RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] - -# Use this to redirect www to non-wwww on apache servers -# RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC] -# RewriteRule ^(.*)$ http://%1/$1 [R=301,L] - -# Use this to redirect slash/ to no slash urls on apache servers -# RewriteCond %{REQUEST_FILENAME} !-d -# RewriteRule ^(.*)/$ /$1 [R=301,L] - -# Removes index.php -RewriteCond %{THE_REQUEST} ^GET.*index\.php [NC] -RewriteRule (.*?)index\.php/*(.*) /$1$2 [R=301,NE,L] - -# Directs all web requests through the site index file -RewriteCond %{REQUEST_URI} !^/index\.php -RewriteCond %{REQUEST_FILENAME} !-f -RewriteCond %{REQUEST_FILENAME} !-d +RewriteEngine On + +# If your homepage is http://yourdomain.com/yoursite +# Set the RewriteBase to: +# RewriteBase /yoursite + +# In some environements, an empty RewriteBase is required: +# RewriteBase / + +# Protect your system files from prying eyes +RewriteRule ^(system\/author\/) - [L] +RewriteRule ^(system) - [F,L] +RewriteRule ^(content) - [F,L] +RewriteRule ^(settings) - [F,L] +RewriteRule ^(.*)?\.yml$ - [F,L] +Rewriterule ^(.*)?\.yaml$ - [F,L] +RewriteRule ^(.*)?\.txt$ - [F,L] +RewriteRule ^(.*)?\.example$ - [F,L] +RewriteRule ^(.*/)?\.git+ - [F,L] + +# Use this to redirect HTTP to HTTPS on apache servers +# RewriteCond %{HTTPS} off +# RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] + +# Use this to redirect www to non-wwww on apache servers +# RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC] +# RewriteRule ^(.*)$ http://%1/$1 [R=301,L] + +# Use this to redirect slash/ to no slash urls on apache servers +# RewriteCond %{REQUEST_FILENAME} !-d +# RewriteRule ^(.*)/$ /$1 [R=301,L] + +# Removes index.php +RewriteCond %{THE_REQUEST} ^GET.*index\.php [NC] +RewriteRule (.*?)index\.php/*(.*) /$1$2 [R=301,NE,L] + +# Directs all web requests through the site index file +RewriteCond %{REQUEST_URI} !^/index\.php +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^ index.php [QSA,L] \ No newline at end of file diff --git a/media/markdown.png b/media/markdown.png deleted file mode 100644 index 9470f08..0000000 Binary files a/media/markdown.png and /dev/null differ diff --git a/settings/languages/en.yaml b/settings/languages/en.yaml index 7001473..be63f0f 100644 --- a/settings/languages/en.yaml +++ b/settings/languages/en.yaml @@ -58,8 +58,10 @@ QUOTES: Quote TABLE_OF_CONTENTS: Table of Contents TABLE: Table TEXT_FILE: text-file +UPLOAD: upload VIDEO: Video +# others ACCOUNT: Account ACTIVE: Active ACTUAL_PASSWORD: Actual Password @@ -70,7 +72,9 @@ ADD_ITEM: add item ALL_USERS: All users AUTHOR: Author BACK_TO_STARTPAGE: back to startpage +BROWSE: BROWSE BY: by +CHOOSE_FILE: Choose file CODE: code CONTENT: Content COPYRIGHT: Copyright diff --git a/settings/languages/vuejs-en.yaml b/settings/languages/vuejs-en.yaml index fd6176b..4ca8d44 100644 --- a/settings/languages/vuejs-en.yaml +++ b/settings/languages/vuejs-en.yaml @@ -22,7 +22,7 @@ en: delete content-block: delete content-block delete row: delete row description: description - drag a picture or click to select: drag a picture or click to select + drag a picture or click to select: upload an image Head: Head Headline: Headline Horizontal Line: Horizontal Line diff --git a/system/Controllers/ContentApiController.php b/system/Controllers/ArticleApiController.php similarity index 58% rename from system/Controllers/ContentApiController.php rename to system/Controllers/ArticleApiController.php index 8f9b6e6..5ed5e19 100644 --- a/system/Controllers/ContentApiController.php +++ b/system/Controllers/ArticleApiController.php @@ -7,7 +7,6 @@ use Slim\Http\Response; use Typemill\Models\Folder; use Typemill\Models\Write; use Typemill\Models\WriteYaml; -use Typemill\Models\ProcessImage; use Typemill\Extensions\ParsedownExtension; use Typemill\Events\OnPagePublished; use Typemill\Events\OnPageUnpublished; @@ -16,7 +15,7 @@ use Typemill\Events\OnPageSorted; use \URLify; -class ContentApiController extends ContentController +class ArticleApiController extends ContentController { public function publishArticle(Request $request, Response $response, $args) { @@ -847,619 +846,4 @@ class ContentApiController extends ContentController return $response->withJson(array('data' => $content, 'errors' => false)); } - - public function addBlock(Request $request, Response $response, $args) - { - /* get params from call */ - $this->params = $request->getParams(); - $this->uri = $request->getUri(); - - /* validate input */ - if(!$this->validateBlockInput()){ return $response->withJson($this->errors,422); } - - # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } - - /* set item */ - if(!$this->setItem()){ return $response->withJson($this->errors, 404); } - - # set the status for published and drafted - $this->setPublishStatus(); - - # set path - $this->setItemPath($this->item->fileType); - - # read content from file - if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } - - # make it more clear which content we have - $pageMarkdown = $this->content; - - $blockMarkdown = $this->params['markdown']; - - # standardize line breaks - $blockMarkdown = str_replace(array("\r\n", "\r"), "\n", $blockMarkdown); - - # remove surrounding line breaks - $blockMarkdown = trim($blockMarkdown, "\n"); - - if($pageMarkdown == '') - { - $pageMarkdown = []; - } - - # initialize parsedown extension - $parsedown = new ParsedownExtension(); - - # if content is not an array, then transform it - if(!is_array($pageMarkdown)) - { - # turn markdown into an array of markdown-blocks - $pageMarkdown = $parsedown->markdownToArrayBlocks($pageMarkdown); - } - - # if it is a new content-block - if($this->params['block_id'] == 99999) - { - # set the id of the markdown-block (it will be one more than the actual array, so count is perfect) - $id = count($pageMarkdown); - - # add the new markdown block to the page content - $pageMarkdown[] = $blockMarkdown; - } - elseif(($this->params['block_id'] == 0) OR !isset($pageMarkdown[$this->params['block_id']])) - { - # if the block does not exists, return an error - return $response->withJson(array('data' => false, 'errors' => 'The ID of the content-block is wrong.'), 404); - } - else - { - # insert new markdown block - array_splice( $pageMarkdown, $this->params['block_id'], 0, $blockMarkdown ); - $id = $this->params['block_id']; - } - - # encode the content into json - $pageJson = json_encode($pageMarkdown); - - # set path for the file (or folder) - $this->setItemPath('txt'); - - /* update the file */ - if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson)) - { - # update the internal structure - $this->setStructure($draft = true, $cache = false); - $this->content = $pageMarkdown; - } - else - { - return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404); - } - - /* set safe mode to escape javascript and html in markdown */ - $parsedown->setSafeMode(true); - - /* parse markdown-file to content-array */ - $blockArray = $parsedown->text($blockMarkdown); - - # we assume that toc is not relevant - $toc = false; - - # needed for ToC links - $relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel; - - if($blockMarkdown == '[TOC]') - { - # if block is table of content itself, then generate the table of content - $tableofcontent = $this->generateToc(); - - # and only use the html-markup - $blockHTML = $tableofcontent['html']; - } - else - { - # parse markdown-content-array to content-string - $blockHTML = $parsedown->markup($blockArray, $relurl); - - # if it is a headline - if($blockMarkdown[0] == '#') - { - # then the TOC holds either false (if no toc used in the page) or it holds an object with the id and toc-markup - $toc = $this->generateToc(); - } - } - - return $response->withJson(array('content' => [ 'id' => $id, 'html' => $blockHTML ] , 'markdown' => $blockMarkdown, 'id' => $id, 'toc' => $toc, 'errors' => false)); - } - - protected function generateToc() - { - # we assume that page has no table of content - $toc = false; - - # make sure $this->content is updated - $content = $this->content; - - if($content == '') - { - $content = []; - } - - # 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); - } - - # needed for ToC links - $relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel; - - # loop through mardkown-array and create html-blocks - foreach($content as $key => $block) - { - # parse markdown-file to content-array - $contentArray = $parsedown->text($block); - - if($block == '[TOC]') - { - # toc is true and holds the key of the table of content now - $toc = $key; - } - - # parse markdown-content-array to content-string - $content[$key] = ['id' => $key, 'html' => $parsedown->markup($contentArray, $relurl)]; - } - - # if page has a table of content - if($toc) - { - # generate the toc markup - $tocMarkup = $parsedown->buildTOC($parsedown->headlines); - - # toc holds the id of the table of content and the html-markup now - $toc = ['id' => $toc, 'html' => $tocMarkup]; - } - - return $toc; - } - - public function updateBlock(Request $request, Response $response, $args) - { - /* get params from call */ - $this->params = $request->getParams(); - $this->uri = $request->getUri(); - - /* validate input */ - if(!$this->validateBlockInput()){ return $response->withJson($this->errors,422); } - - # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } - - /* set item */ - if(!$this->setItem()){ return $response->withJson($this->errors, 404); } - - # set the status for published and drafted - $this->setPublishStatus(); - - # set path - $this->setItemPath($this->item->fileType); - - # read content from file - if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } - - # make it more clear which content we have - $pageMarkdown = $this->content; - - $blockMarkdown = $this->params['markdown']; - - # standardize line breaks - $blockMarkdown = str_replace(array("\r\n", "\r"), "\n", $blockMarkdown); - - # remove surrounding line breaks - $blockMarkdown = trim($blockMarkdown, "\n"); - - if($pageMarkdown == '') - { - $pageMarkdown = []; - } - - # initialize parsedown extension - $parsedown = new ParsedownExtension(); - $parsedown->setVisualMode(); - - # if content is not an array, then transform it - if(!is_array($pageMarkdown)) - { - # turn markdown into an array of markdown-blocks - $pageMarkdown = $parsedown->markdownToArrayBlocks($pageMarkdown); - } - - if(!isset($pageMarkdown[$this->params['block_id']])) - { - # if the block does not exists, return an error - return $response->withJson(array('data' => false, 'errors' => 'The ID of the content-block is wrong.'), 404); - } - elseif($this->params['block_id'] == 0) - { - # if it is the title, then delete the "# " if it exists - $blockMarkdown = trim($blockMarkdown, "# "); - - # store the markdown-headline in a separate variable - $blockMarkdownTitle = '# ' . $blockMarkdown; - - # add the markdown-headline to the page-markdown - $pageMarkdown[0] = $blockMarkdownTitle; - $id = 0; - } - else - { - # update the markdown block in the page content - $pageMarkdown[$this->params['block_id']] = $blockMarkdown; - $id = $this->params['block_id']; - } - - # encode the content into json - $pageJson = json_encode($pageMarkdown); - - # set path for the file (or folder) - $this->setItemPath('txt'); - - /* update the file */ - if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson)) - { - # update the internal structure - $this->setStructure($draft = true, $cache = false); - - # updated the content variable - $this->content = $pageMarkdown; - } - else - { - return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404); - } - - - /* parse markdown-file to content-array, if title parse title. */ - if($this->params['block_id'] == 0) - { - $blockArray = $parsedown->text($blockMarkdownTitle); - } - else - { - /* set safe mode to escape javascript and html in markdown */ - $parsedown->setSafeMode(true); - - $blockArray = $parsedown->text($blockMarkdown); - } - - # we assume that toc is not relevant - $toc = false; - - # needed for ToC links - $relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel; - - if($blockMarkdown == '[TOC]') - { - # if block is table of content itself, then generate the table of content - $tableofcontent = $this->generateToc(); - - # and only use the html-markup - $blockHTML = $tableofcontent['html']; - } - else - { - # parse markdown-content-array to content-string - $blockHTML = $parsedown->markup($blockArray, $relurl); - - # if it is a headline - if($blockMarkdown[0] == '#') - { - # then the TOC holds either false (if no toc used in the page) or it holds an object with the id and toc-markup - $toc = $this->generateToc(); - } - } - - return $response->withJson(array('content' => [ 'id' => $id, 'html' => $blockHTML ] , 'markdown' => $blockMarkdown, 'id' => $id, 'toc' => $toc, 'errors' => false)); - } - - public function moveBlock(Request $request, Response $response, $args) - { - # get params from call - $this->params = $request->getParams(); - $this->uri = $request->getUri(); - - # validate input - # if(!$this->validateBlockInput()){ return $response->withJson($this->errors,422); } - - # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } - - # set item - if(!$this->setItem()){ return $response->withJson($this->errors, 404); } - - # set the status for published and drafted - $this->setPublishStatus(); - - # set path - $this->setItemPath($this->item->fileType); - - # read content from file - if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } - - # make it more clear which content we have - $pageMarkdown = $this->content; - - if($pageMarkdown == '') - { - $pageMarkdown = []; - } - - # initialize parsedown extension - $parsedown = new ParsedownExtension(); - - # if content is not an array, then transform it - if(!is_array($pageMarkdown)) - { - # turn markdown into an array of markdown-blocks - $pageMarkdown = $parsedown->markdownToArrayBlocks($pageMarkdown); - } - - $oldIndex = ($this->params['old_index'] + 1); - $newIndex = ($this->params['new_index'] + 1); - - if(!isset($pageMarkdown[$oldIndex])) - { - # if the block does not exists, return an error - return $response->withJson(array('data' => false, 'errors' => 'The ID of the content-block is wrong.'), 404); - } - - $extract = array_splice($pageMarkdown, $oldIndex, 1); - array_splice($pageMarkdown, $newIndex, 0, $extract); - - # encode the content into json - $pageJson = json_encode($pageMarkdown); - - # set path for the file (or folder) - $this->setItemPath('txt'); - - /* update the file */ - if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson)) - { - # update the internal structure - $this->setStructure($draft = true, $cache = false); - - # update this content - $this->content = $pageMarkdown; - } - else - { - return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404); - } - - # we assume that toc is not relevant - $toc = false; - - # needed for ToC links - $relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel; - - # if the moved item is a headline - if($extract[0][0] == '#') - { - $toc = $this->generateToc(); - } - - # if it is the title, then delete the "# " if it exists - $pageMarkdown[0] = trim($pageMarkdown[0], "# "); - - return $response->withJson(array('markdown' => $pageMarkdown, 'toc' => $toc, 'errors' => false)); - } - - public function deleteBlock(Request $request, Response $response, $args) - { - /* get params from call */ - $this->params = $request->getParams(); - $this->uri = $request->getUri(); - $errors = false; - - # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } - - # set item - if(!$this->setItem()){ return $response->withJson($this->errors, 404); } - - # set the status for published and drafted - $this->setPublishStatus(); - - # set path - $this->setItemPath($this->item->fileType); - - # read content from file - if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } - - # get content - $this->content; - - if($this->content == '') - { - $this->content = []; - } - - # initialize parsedown extension - $parsedown = new ParsedownExtension(); - - # if content is not an array, then transform it - if(!is_array($this->content)) - { - # turn markdown into an array of markdown-blocks - $this->content = $parsedown->markdownToArrayBlocks($this->content); - } - - # check if id exists - if(!isset($this->content[$this->params['block_id']])){ return $response->withJson(array('data' => false, 'errors' => 'The ID of the content-block is wrong.'), 404); } - - # check if block is image - $contentBlock = $this->content[$this->params['block_id']]; - $contentBlockStart = substr($contentBlock, 0, 2); - if($contentBlockStart == '[!' OR $contentBlockStart == '![') - { - # extract image path - preg_match("/\((.*?)\)/",$contentBlock,$matches); - if(isset($matches[1])) - { - $imageBaseName = explode('-', $matches[1]); - $imageBaseName = str_replace('media/live/', '', $imageBaseName[0]); - $processImage = new ProcessImage(); - if(!$processImage->deleteImage($imageBaseName)) - { - $errors = 'Could not delete some of the images, please check manually'; - } - } - } - - # delete the block - unset($this->content[$this->params['block_id']]); - $this->content = array_values($this->content); - - $pageMarkdown = $this->content; - - # delete markdown from title - if(isset($pageMarkdown[0])) - { - $pageMarkdown[0] = trim($pageMarkdown[0], "# "); - } - - # encode the content into json - $pageJson = json_encode($this->content); - - # set path for the file (or folder) - $this->setItemPath('txt'); - - /* update the file */ - if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson)) - { - # update the internal structure - $this->setStructure($draft = true, $cache = false); - } - else - { - return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404); - } - - $toc = false; - - if($contentBlock[0] == '#') - { - $toc = $this->generateToc(); - } - - return $response->withJson(array('markdown' => $pageMarkdown, 'toc' => $toc, 'errors' => $errors)); - } - - public function createImage(Request $request, Response $response, $args) - { - /* get params from call */ - $this->params = $request->getParams(); - $this->uri = $request->getUri(); - - $imageProcessor = new ProcessImage(); - - if($imageProcessor->createImage($this->params['image'], $this->settings['images'])) - { - return $response->withJson(array('errors' => false)); - } - - return $response->withJson(array('errors' => 'could not store image to temporary folder')); - } - - public function publishImage(Request $request, Response $response, $args) - { - $params = $request->getParsedBody(); - - $imageProcessor = new ProcessImage(); - - $imageUrl = $imageProcessor->publishImage($this->settings['images'], $name = false); - if($imageUrl) - { - $params['markdown'] = str_replace('imgplchldr', $imageUrl, $params['markdown']); - - $request = $request->withParsedBody($params); - - return $this->addBlock($request, $response, $args); - } - - return $response->withJson(array('errors' => 'could not store image to media folder')); - } - - public function saveVideoImage(Request $request, Response $response, $args) - { - /* get params from call */ - $this->params = $request->getParams(); - $this->uri = $request->getUri(); - $class = false; - - $imageUrl = $this->params['markdown']; - - if(strpos($imageUrl, 'https://www.youtube.com/watch?v=') !== false) - { - $videoID = str_replace('https://www.youtube.com/watch?v=', '', $imageUrl); - $videoID = strpos($videoID, '&') ? substr($videoID, 0, strpos($videoID, '&')) : $videoID; - $class = 'youtube'; - } - if(strpos($imageUrl, 'https://youtu.be/') !== false) - { - $videoID = str_replace('https://youtu.be/', '', $imageUrl); - $videoID = strpos($videoID, '?') ? substr($videoID, 0, strpos($videoID, '?')) : $videoID; - $class = 'youtube'; - } - - if($class == 'youtube') - { - $videoURLmaxres = 'https://i1.ytimg.com/vi/' . $videoID . '/maxresdefault.jpg'; - $videoURL0 = 'https://i1.ytimg.com/vi/' . $videoID . '/0.jpg'; - } - - $ctx = stream_context_create(array( - 'https' => array( - 'timeout' => 1 - ) - ) - ); - - $imageData = @file_get_contents($videoURLmaxres, 0, $ctx); - if($imageData === false) - { - $imageData = @file_get_contents($videoURL0, 0, $ctx); - if($imageData === false) - { - return $response->withJson(array('errors' => 'could not get the video image')); - } - } - - $imageData64 = 'data:image/jpeg;base64,' . base64_encode($imageData); - $desiredSizes = ['live' => ['width' => 560, 'height' => 315]]; - $imageProcessor = new ProcessImage(); - $tmpImage = $imageProcessor->createImage($imageData64, $desiredSizes); - - if(!$tmpImage) - { - return $response->withJson(array('errors' => 'could not create temporary image')); - } - - $imageUrl = $imageProcessor->publishImage($desiredSizes, $videoID); - if($imageUrl) - { - $this->params['markdown'] = '![' . $class . '-video](' . $imageUrl . ' "click to load video"){#' . $videoID. ' .' . $class . '}'; - - $request = $request->withParsedBody($this->params); - - return $this->addBlock($request, $response, $args); - } - - return $response->withJson(array('errors' => 'could not store the preview image')); - } } \ No newline at end of file diff --git a/system/Controllers/BlockApiController.php b/system/Controllers/BlockApiController.php new file mode 100644 index 0000000..9c77ff4 --- /dev/null +++ b/system/Controllers/BlockApiController.php @@ -0,0 +1,841 @@ +params = $request->getParams(); + $this->uri = $request->getUri(); + + /* validate input */ + if(!$this->validateBlockInput()){ return $response->withJson($this->errors,422); } + + # set structure + if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } + + /* set item */ + if(!$this->setItem()){ return $response->withJson($this->errors, 404); } + + # set the status for published and drafted + $this->setPublishStatus(); + + # set path + $this->setItemPath($this->item->fileType); + + # read content from file + if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } + + # make it more clear which content we have + $pageMarkdown = $this->content; + + $blockMarkdown = $this->params['markdown']; + + # standardize line breaks + $blockMarkdown = str_replace(array("\r\n", "\r"), "\n", $blockMarkdown); + + # remove surrounding line breaks + $blockMarkdown = trim($blockMarkdown, "\n"); + + if($pageMarkdown == '') + { + $pageMarkdown = []; + } + + # initialize parsedown extension + $parsedown = new ParsedownExtension(); + + # if content is not an array, then transform it + if(!is_array($pageMarkdown)) + { + # turn markdown into an array of markdown-blocks + $pageMarkdown = $parsedown->markdownToArrayBlocks($pageMarkdown); + } + + # if it is a new content-block + if($this->params['block_id'] == 99999) + { + # set the id of the markdown-block (it will be one more than the actual array, so count is perfect) + $id = count($pageMarkdown); + + # add the new markdown block to the page content + $pageMarkdown[] = $blockMarkdown; + } + elseif(($this->params['block_id'] == 0) OR !isset($pageMarkdown[$this->params['block_id']])) + { + # if the block does not exists, return an error + return $response->withJson(array('data' => false, 'errors' => 'The ID of the content-block is wrong.'), 404); + } + else + { + # insert new markdown block + array_splice( $pageMarkdown, $this->params['block_id'], 0, $blockMarkdown ); + $id = $this->params['block_id']; + } + + # encode the content into json + $pageJson = json_encode($pageMarkdown); + + # set path for the file (or folder) + $this->setItemPath('txt'); + + /* update the file */ + if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson)) + { + # update the internal structure + $this->setStructure($draft = true, $cache = false); + $this->content = $pageMarkdown; + } + else + { + return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404); + } + + /* set safe mode to escape javascript and html in markdown */ + $parsedown->setSafeMode(true); + + /* parse markdown-file to content-array */ + $blockArray = $parsedown->text($blockMarkdown); + + # we assume that toc is not relevant + $toc = false; + + # needed for ToC links + $relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel; + + if($blockMarkdown == '[TOC]') + { + # if block is table of content itself, then generate the table of content + $tableofcontent = $this->generateToc(); + + # and only use the html-markup + $blockHTML = $tableofcontent['html']; + } + else + { + # parse markdown-content-array to content-string + $blockHTML = $parsedown->markup($blockArray, $relurl); + + # if it is a headline + if($blockMarkdown[0] == '#') + { + # then the TOC holds either false (if no toc used in the page) or it holds an object with the id and toc-markup + $toc = $this->generateToc(); + } + } + + return $response->withJson(array('content' => [ 'id' => $id, 'html' => $blockHTML ] , 'markdown' => $blockMarkdown, 'id' => $id, 'toc' => $toc, 'errors' => false)); + } + + protected function generateToc() + { + # we assume that page has no table of content + $toc = false; + + # make sure $this->content is updated + $content = $this->content; + + if($content == '') + { + $content = []; + } + + # 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); + } + + # needed for ToC links + $relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel; + + # loop through mardkown-array and create html-blocks + foreach($content as $key => $block) + { + # parse markdown-file to content-array + $contentArray = $parsedown->text($block); + + if($block == '[TOC]') + { + # toc is true and holds the key of the table of content now + $toc = $key; + } + + # parse markdown-content-array to content-string + $content[$key] = ['id' => $key, 'html' => $parsedown->markup($contentArray, $relurl)]; + } + + # if page has a table of content + if($toc) + { + # generate the toc markup + $tocMarkup = $parsedown->buildTOC($parsedown->headlines); + + # toc holds the id of the table of content and the html-markup now + $toc = ['id' => $toc, 'html' => $tocMarkup]; + } + + return $toc; + } + + public function updateBlock(Request $request, Response $response, $args) + { + /* get params from call */ + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + /* validate input */ + if(!$this->validateBlockInput()){ return $response->withJson($this->errors,422); } + + # set structure + if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } + + /* set item */ + if(!$this->setItem()){ return $response->withJson($this->errors, 404); } + + # set the status for published and drafted + $this->setPublishStatus(); + + # set path + $this->setItemPath($this->item->fileType); + + # read content from file + if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } + + # make it more clear which content we have + $pageMarkdown = $this->content; + + $blockMarkdown = $this->params['markdown']; + + # standardize line breaks + $blockMarkdown = str_replace(array("\r\n", "\r"), "\n", $blockMarkdown); + + # remove surrounding line breaks + $blockMarkdown = trim($blockMarkdown, "\n"); + + if($pageMarkdown == '') + { + $pageMarkdown = []; + } + + # initialize parsedown extension + $parsedown = new ParsedownExtension(); + $parsedown->setVisualMode(); + + # if content is not an array, then transform it + if(!is_array($pageMarkdown)) + { + # turn markdown into an array of markdown-blocks + $pageMarkdown = $parsedown->markdownToArrayBlocks($pageMarkdown); + } + + if(!isset($pageMarkdown[$this->params['block_id']])) + { + # if the block does not exists, return an error + return $response->withJson(array('data' => false, 'errors' => 'The ID of the content-block is wrong.'), 404); + } + elseif($this->params['block_id'] == 0) + { + # if it is the title, then delete the "# " if it exists + $blockMarkdown = trim($blockMarkdown, "# "); + + # store the markdown-headline in a separate variable + $blockMarkdownTitle = '# ' . $blockMarkdown; + + # add the markdown-headline to the page-markdown + $pageMarkdown[0] = $blockMarkdownTitle; + $id = 0; + } + else + { + # update the markdown block in the page content + $pageMarkdown[$this->params['block_id']] = $blockMarkdown; + $id = $this->params['block_id']; + } + + # encode the content into json + $pageJson = json_encode($pageMarkdown); + + # set path for the file (or folder) + $this->setItemPath('txt'); + + /* update the file */ + if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson)) + { + # update the internal structure + $this->setStructure($draft = true, $cache = false); + + # updated the content variable + $this->content = $pageMarkdown; + } + else + { + return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404); + } + + + /* parse markdown-file to content-array, if title parse title. */ + if($this->params['block_id'] == 0) + { + $blockArray = $parsedown->text($blockMarkdownTitle); + } + else + { + /* set safe mode to escape javascript and html in markdown */ + $parsedown->setSafeMode(true); + + $blockArray = $parsedown->text($blockMarkdown); + } + + # we assume that toc is not relevant + $toc = false; + + # needed for ToC links + $relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel; + + if($blockMarkdown == '[TOC]') + { + # if block is table of content itself, then generate the table of content + $tableofcontent = $this->generateToc(); + + # and only use the html-markup + $blockHTML = $tableofcontent['html']; + } + else + { + # parse markdown-content-array to content-string + $blockHTML = $parsedown->markup($blockArray, $relurl); + + # if it is a headline + if($blockMarkdown[0] == '#') + { + # then the TOC holds either false (if no toc used in the page) or it holds an object with the id and toc-markup + $toc = $this->generateToc(); + } + } + + return $response->withJson(array('content' => [ 'id' => $id, 'html' => $blockHTML ] , 'markdown' => $blockMarkdown, 'id' => $id, 'toc' => $toc, 'errors' => false)); + } + + public function moveBlock(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + # validate input + # if(!$this->validateBlockInput()){ return $response->withJson($this->errors,422); } + + # set structure + if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } + + # set item + if(!$this->setItem()){ return $response->withJson($this->errors, 404); } + + # set the status for published and drafted + $this->setPublishStatus(); + + # set path + $this->setItemPath($this->item->fileType); + + # read content from file + if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } + + # make it more clear which content we have + $pageMarkdown = $this->content; + + if($pageMarkdown == '') + { + $pageMarkdown = []; + } + + # initialize parsedown extension + $parsedown = new ParsedownExtension(); + + # if content is not an array, then transform it + if(!is_array($pageMarkdown)) + { + # turn markdown into an array of markdown-blocks + $pageMarkdown = $parsedown->markdownToArrayBlocks($pageMarkdown); + } + + $oldIndex = ($this->params['old_index'] + 1); + $newIndex = ($this->params['new_index'] + 1); + + if(!isset($pageMarkdown[$oldIndex])) + { + # if the block does not exists, return an error + return $response->withJson(array('data' => false, 'errors' => 'The ID of the content-block is wrong.'), 404); + } + + $extract = array_splice($pageMarkdown, $oldIndex, 1); + array_splice($pageMarkdown, $newIndex, 0, $extract); + + # encode the content into json + $pageJson = json_encode($pageMarkdown); + + # set path for the file (or folder) + $this->setItemPath('txt'); + + /* update the file */ + if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson)) + { + # update the internal structure + $this->setStructure($draft = true, $cache = false); + + # update this content + $this->content = $pageMarkdown; + } + else + { + return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404); + } + + # we assume that toc is not relevant + $toc = false; + + # needed for ToC links + $relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel; + + # if the moved item is a headline + if($extract[0][0] == '#') + { + $toc = $this->generateToc(); + } + + # if it is the title, then delete the "# " if it exists + $pageMarkdown[0] = trim($pageMarkdown[0], "# "); + + return $response->withJson(array('markdown' => $pageMarkdown, 'toc' => $toc, 'errors' => false)); + } + + public function deleteBlock(Request $request, Response $response, $args) + { + /* get params from call */ + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + $errors = false; + + # set structure + if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } + + # set item + if(!$this->setItem()){ return $response->withJson($this->errors, 404); } + + # set the status for published and drafted + $this->setPublishStatus(); + + # set path + $this->setItemPath($this->item->fileType); + + # read content from file + if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); } + + # get content + $this->content; + + if($this->content == '') + { + $this->content = []; + } + + # initialize parsedown extension + $parsedown = new ParsedownExtension(); + + # if content is not an array, then transform it + if(!is_array($this->content)) + { + # turn markdown into an array of markdown-blocks + $this->content = $parsedown->markdownToArrayBlocks($this->content); + } + + # check if id exists + if(!isset($this->content[$this->params['block_id']])){ return $response->withJson(array('data' => false, 'errors' => 'The ID of the content-block is wrong.'), 404); } + + $contentBlock = $this->content[$this->params['block_id']]; + + # delete the block + unset($this->content[$this->params['block_id']]); + $this->content = array_values($this->content); + + $pageMarkdown = $this->content; + + # delete markdown from title + if(isset($pageMarkdown[0])) + { + $pageMarkdown[0] = trim($pageMarkdown[0], "# "); + } + + # encode the content into json + $pageJson = json_encode($this->content); + + # set path for the file (or folder) + $this->setItemPath('txt'); + + /* update the file */ + if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson)) + { + # update the internal structure + $this->setStructure($draft = true, $cache = false); + } + else + { + return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404); + } + + $toc = false; + + if($contentBlock[0] == '#') + { + $toc = $this->generateToc(); + } + + return $response->withJson(array('markdown' => $pageMarkdown, 'toc' => $toc, 'errors' => $errors)); + } + + public function getMediaLibImages(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders('images')) + { + return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); + } + + $imagelist = $imageProcessor->scanMediaFlat(); + + return $response->withJson(array('images' => $imagelist)); + } + + public function getMediaLibFiles(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + $fileProcessor = new ProcessFile(); + if(!$fileProcessor->checkFolders()) + { + return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); + } + + $filelist = $fileProcessor->scanFilesFlat(); + + return $response->withJson(array('files' => $filelist)); + } + + public function getImage(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + $this->setStructure($draft = true, $cache = false); + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders('images')) + { + return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); + } + + $imageDetails = $imageProcessor->getImageDetails($this->params['name'], $this->structure); + + if($imageDetails) + { + return $response->withJson(array('image' => $imageDetails)); + } + + # return $response->withJson(array('image' => false, 'errors' => 'image name invalid or not found')); + return $response->withJson(['errors' => ['message' => 'Image name invalid or not found.']], 404); + } + + public function getFile(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + $this->setStructure($draft = true, $cache = false); + + $fileProcessor = new ProcessFile(); + if(!$fileProcessor->checkFolders()) + { + return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); + } + + $fileDetails = $fileProcessor->getFileDetails($this->params['name'], $this->structure); + + if($fileDetails) + { + return $response->withJson(['file' => $fileDetails]); + } + + return $response->withJson(['errors' => ['message' => 'file name invalid or not found']],404); + } + + public function createImage(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + # do this shit in the model ... + $imagename = explode('.', $this->params['name']); + array_pop($imagename); + $imagename = implode('-', $imagename); + $name = URLify::filter(iconv(mb_detect_encoding($imagename, mb_detect_order(), true), "UTF-8", $imagename)); + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders('images')) + { + return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); + } + + if($imageProcessor->createImage($this->params['image'], $name, $this->settings['images'])) + { + return $response->withJson(array('errors' => false)); + } + + return $response->withJson(array('errors' => 'could not store image to temporary folder')); + } + + public function createFile(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + $finfo = finfo_open( FILEINFO_MIME_TYPE ); + $mtype = finfo_file( $finfo, $this->params['file'] ); + finfo_close( $finfo ); + + $allowedMimes = $this->getAllowedMtypes(); + if(!in_array($mtype, $allowedMimes)) + { + return $response->withJson(array('errors' => 'File-type is not allowed')); + } + + # sanitize file name + $filename = basename($this->params['name']); + $filename = explode('.', $this->params['name']); + array_pop($filename); + $filename = implode('-', $filename); + $name = URLify::filter(iconv(mb_detect_encoding($filename, mb_detect_order(), true), "UTF-8", $filename)); + + $fileProcessor = new ProcessFile(); + if(!$fileProcessor->checkFolders()) + { + return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); + } + + if($fileProcessor->createFile($this->params['file'], $name)) + { + return $response->withJson(array('errors' => false, 'name' => $name)); + } + + return $response->withJson(array('errors' => 'could not store file to temporary folder')); + } + + public function publishImage(Request $request, Response $response, $args) + { + $params = $request->getParsedBody(); + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders()) + { + return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); + } + + $imageUrl = $imageProcessor->publishImage(); + if($imageUrl) + { + # replace the image placeholder in markdown with the image url + $params['markdown'] = str_replace('imgplchldr', $imageUrl, $params['markdown']); + + $request = $request->withParsedBody($params); + + if($params['new']) + { + return $this->addBlock($request, $response, $args); + } + return $this->updateBlock($request, $response, $args); + } + + return $response->withJson(array('errors' => 'could not store image to media folder')); + } + + public function deleteImage(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + if(!isset($this->params['name'])) + { + return $response->withJson(array('errors' => 'image name is missing')); + } + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders()) + { + return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); + } + + $errors = $imageProcessor->deleteImage($this->params['name']); + + return $response->withJson(array('errors' => $errors)); + } + + public function deleteFile(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + if(!isset($this->params['name'])) + { + return $response->withJson(array('errors' => 'file name is missing')); + } + + $fileProcessor = new ProcessFile(); + + $errors = false; + if($fileProcessor->deleteFile($this->params['name'])) + { + return $response->withJson(array('errors' => false)); + } + + return $response->withJson(array('errors' => 'could not delete the file')); + } + + public function saveVideoImage(Request $request, Response $response, $args) + { + /* get params from call */ + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + $class = false; + + $imageUrl = $this->params['markdown']; + + if(strpos($imageUrl, 'https://www.youtube.com/watch?v=') !== false) + { + $videoID = str_replace('https://www.youtube.com/watch?v=', '', $imageUrl); + $videoID = strpos($videoID, '&') ? substr($videoID, 0, strpos($videoID, '&')) : $videoID; + $class = 'youtube'; + } + if(strpos($imageUrl, 'https://youtu.be/') !== false) + { + $videoID = str_replace('https://youtu.be/', '', $imageUrl); + $videoID = strpos($videoID, '?') ? substr($videoID, 0, strpos($videoID, '?')) : $videoID; + $class = 'youtube'; + } + + if($class == 'youtube') + { + $videoURLmaxres = 'https://i1.ytimg.com/vi/' . $videoID . '/maxresdefault.jpg'; + $videoURL0 = 'https://i1.ytimg.com/vi/' . $videoID . '/0.jpg'; + } + + $ctx = stream_context_create(array( + 'https' => array( + 'timeout' => 1 + ) + ) + ); + + $imageData = @file_get_contents($videoURLmaxres, 0, $ctx); + if($imageData === false) + { + $imageData = @file_get_contents($videoURL0, 0, $ctx); + if($imageData === false) + { + return $response->withJson(array('errors' => 'could not get the video image')); + } + } + + $imageData64 = 'data:image/jpeg;base64,' . base64_encode($imageData); + $desiredSizes = ['live' => ['width' => 560, 'height' => 315]]; + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders()) + { + return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); + } + + $tmpImage = $imageProcessor->createImage($imageData64, $desiredSizes); + + if(!$tmpImage) + { + return $response->withJson(array('errors' => 'could not create temporary image')); + } + + $imageUrl = $imageProcessor->publishImage($desiredSizes, $videoID); + if($imageUrl) + { + $this->params['markdown'] = '![' . $class . '-video](' . $imageUrl . ' "click to load video"){#' . $videoID. ' .' . $class . '}'; + + $request = $request->withParsedBody($this->params); + + if($this->params['new']) + { + return $this->addBlock($request, $response, $args); + } + return $this->updateBlock($request, $response, $args); + } + + return $response->withJson(array('errors' => 'could not store the preview image')); + } + + private function getAllowedMtypes() + { + return array( + 'application/zip', + 'application/gzip', + 'application/vnd.rar', + 'application/vnd.visio', + 'application/vnd.ms-excel', + 'application/vnd.ms-powerpoint', + 'application/vnd.ms-word.document.macroEnabled.12', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.apple.keynote', + 'application/vnd.apple.mpegurl', + 'application/vnd.apple.numbers', + 'application/vnd.apple.pages', + 'application/vnd.amazon.mobi8-ebook', + 'application/epub+zip', + 'application/pdf', + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/svg+xml', + 'font/*', + 'audio/mpeg', + 'audio/mp4', + 'audio/ogg', + 'video/mpeg', + 'video/mp4', + 'video/ogg', + ); + } +} \ No newline at end of file diff --git a/system/Controllers/MediaApiController.php b/system/Controllers/MediaApiController.php new file mode 100644 index 0000000..a721197 --- /dev/null +++ b/system/Controllers/MediaApiController.php @@ -0,0 +1,356 @@ +params = $request->getParams(); + $this->uri = $request->getUri(); + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders('images')) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + $imagelist = $imageProcessor->scanMediaFlat(); + + return $response->withJson(['images' => $imagelist]); + } + + public function getMediaLibFiles(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + $fileProcessor = new ProcessFile(); + if(!$fileProcessor->checkFolders()) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + $filelist = $fileProcessor->scanFilesFlat(); + + return $response->withJson(['files' => $filelist]); + } + + public function getImage(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + $this->setStructure($draft = true, $cache = false); + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders('images')) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + $imageDetails = $imageProcessor->getImageDetails($this->params['name'], $this->structure); + + if($imageDetails) + { + return $response->withJson(['image' => $imageDetails]); + } + + return $response->withJson(['errors' => 'Image not found or image name not valid.'], 404); + } + + public function getFile(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + $this->setStructure($draft = true, $cache = false); + + $fileProcessor = new ProcessFile(); + if(!$fileProcessor->checkFolders()) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + $fileDetails = $fileProcessor->getFileDetails($this->params['name'], $this->structure); + + if($fileDetails) + { + return $response->withJson(['file' => $fileDetails]); + } + + return $response->withJson(['errors' => 'file not found or file name invalid'],404); + } + + public function createImage(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + $imageProcessor = new ProcessImage($this->settings['images']); + + if(!$imageProcessor->checkFolders('images')) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + if($imageProcessor->createImage($this->params['image'], $this->params['name'], $this->settings['images'])) + { + return $response->withJson(['name' => 'media/live/' . $imageProcessor->getFullName(),'errors' => false]); + } + + return $response->withJson(['errors' => 'could not store image to temporary folder']); + } + + public function uploadFile(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + # make sure only allowed filetypes are uploaded + $finfo = finfo_open( FILEINFO_MIME_TYPE ); + $mtype = finfo_file( $finfo, $this->params['file'] ); + finfo_close( $finfo ); + $allowedMimes = $this->getAllowedMtypes(); + if(!in_array($mtype, $allowedMimes)) + { + return $response->withJson(array('errors' => 'File-type is not allowed')); + } + + $fileProcessor = new ProcessFile(); + + if(!$fileProcessor->checkFolders()) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + $fileinfo = $fileProcessor->storeFile($this->params['file'], $this->params['name']); + if($fileinfo) + { + return $response->withJson(['errors' => false, 'info' => $fileinfo]); + } + + return $response->withJson(['errors' => 'could not store file to temporary folder'],500); + } + + public function publishImage(Request $request, Response $response, $args) + { + $params = $request->getParsedBody(); + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders()) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + if($imageProcessor->publishImage()) + { + $request = $request->withParsedBody($params); + + $block = new BlockApiController($this->c); + if($params['new']) + { + return $block->addBlock($request, $response, $args); + } + return $block->updateBlock($request, $response, $args); + } + + return $response->withJson(['errors' => 'could not store image to media folder'],500); + } + + public function publishFile(Request $request, Response $response, $args) + { + $params = $request->getParsedBody(); + + $fileProcessor = new ProcessFile(); + if(!$fileProcessor->checkFolders()) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + if($fileProcessor->publishFile()) + { + $request = $request->withParsedBody($params); + + $block = new BlockApiController($this->c); + if($params['new']) + { + return $block->addBlock($request, $response, $args); + } + return $block->updateBlock($request, $response, $args); + } + + return $response->withJson(['errors' => 'could not store file to media folder'],500); + } + + public function deleteImage(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + if(!isset($this->params['name'])) + { + return $response->withJson(['errors' => 'image name is missing'],500); + } + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders()) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + if($imageProcessor->deleteImage($this->params['name'])) + { + return $response->withJson(['errors' => false]); + } + + return $response->withJson(['errors' => 'Oops, looks like we could not delete all sizes of that image.'], 500); + } + + public function deleteFile(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + if(!isset($this->params['name'])) + { + return $response->withJson(['errors' => 'file name is missing'],500); + } + + $fileProcessor = new ProcessFile(); + + if($fileProcessor->deleteFile($this->params['name'])) + { + return $response->withJson(['errors' => false]); + } + + return $response->withJson(['errors' => 'could not delete the file'],500); + } + + public function saveVideoImage(Request $request, Response $response, $args) + { + /* get params from call */ + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + $class = false; + + $imageUrl = $this->params['markdown']; + + if(strpos($imageUrl, 'https://www.youtube.com/watch?v=') !== false) + { + $videoID = str_replace('https://www.youtube.com/watch?v=', '', $imageUrl); + $videoID = strpos($videoID, '&') ? substr($videoID, 0, strpos($videoID, '&')) : $videoID; + $class = 'youtube'; + } + if(strpos($imageUrl, 'https://youtu.be/') !== false) + { + $videoID = str_replace('https://youtu.be/', '', $imageUrl); + $videoID = strpos($videoID, '?') ? substr($videoID, 0, strpos($videoID, '?')) : $videoID; + $class = 'youtube'; + } + + if($class == 'youtube') + { + $videoURLmaxres = 'https://i1.ytimg.com/vi/' . $videoID . '/maxresdefault.jpg'; + $videoURL0 = 'https://i1.ytimg.com/vi/' . $videoID . '/0.jpg'; + } + + $ctx = stream_context_create(array( + 'https' => array( + 'timeout' => 1 + ) + ) + ); + + $imageData = @file_get_contents($videoURLmaxres, 0, $ctx); + if($imageData === false) + { + $imageData = @file_get_contents($videoURL0, 0, $ctx); + if($imageData === false) + { + return $response->withJson(array('errors' => 'could not get the video image')); + } + } + + $imageData64 = 'data:image/jpeg;base64,' . base64_encode($imageData); + $desiredSizes = ['live' => ['width' => 560, 'height' => 315]]; + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders()) + { + return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); + } + + $tmpImage = $imageProcessor->createImage($imageData64, $desiredSizes); + + if(!$tmpImage) + { + return $response->withJson(array('errors' => 'could not create temporary image')); + } + + $imageUrl = $imageProcessor->publishImage($desiredSizes, $videoID); + if($imageUrl) + { + $this->params['markdown'] = '![' . $class . '-video](' . $imageUrl . ' "click to load video"){#' . $videoID. ' .' . $class . '}'; + + $request = $request->withParsedBody($this->params); + + $block = new BlockApiController($this->c); + if($params['new']) + { + return $block->addBlock($request, $response, $args); + } + return $block->updateBlock($request, $response, $args); + } + + return $response->withJson(array('errors' => 'could not store the preview image')); + } + + private function getAllowedMtypes() + { + return array( + 'application/zip', + 'application/gzip', + 'application/vnd.rar', + 'application/vnd.visio', + 'application/vnd.ms-excel', + 'application/vnd.ms-powerpoint', + 'application/vnd.ms-word.document.macroEnabled.12', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.apple.keynote', + 'application/vnd.apple.mpegurl', + 'application/vnd.apple.numbers', + 'application/vnd.apple.pages', + 'application/vnd.amazon.mobi8-ebook', + 'application/epub+zip', + 'application/pdf', + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/svg+xml', + 'font/*', + 'audio/mpeg', + 'audio/mp4', + 'audio/ogg', + 'video/mpeg', + 'video/mp4', + 'video/ogg', + ); + } +} \ No newline at end of file diff --git a/system/Controllers/MetaApiController.php b/system/Controllers/MetaApiController.php index 9e80c5e..ab997a1 100644 --- a/system/Controllers/MetaApiController.php +++ b/system/Controllers/MetaApiController.php @@ -200,9 +200,8 @@ class MetaApiController extends ContentController if($tab == 'meta') { - # if manual date has been modified - if(isset($metaInput['manualdate']) && !isset($metaPage['meta']['manualdate']) OR ($metaInput['manualdate'] != $metaPage['meta']['manualdate'])) + if( $this->hasChanged($metaInput, $metaPage['meta'], 'manualdate')) { # update the time $metaInput['time'] = date('H-i-s', time()); @@ -230,7 +229,7 @@ class MetaApiController extends ContentController } # if folder has changed and contains pages instead of posts or posts instead of pages - if($this->item->elementType == "folder" && ($metaPage['meta']['contains'] !== $metaInput['contains'])) + if($this->item->elementType == "folder" && isset($metaInput['contains']) && $this->hasChanged($metaInput, $metaPage['meta'], 'contains')) { $structure = true; @@ -258,9 +257,9 @@ class MetaApiController extends ContentController } elseif( # check if navtitle or hide-value has been changed - ($metaPage['meta']['navtitle'] != $metaInput['navtitle']) + ($this->hasChanged($metaInput, $metaPage['meta'], 'navtitle')) OR - ($metaPage['meta']['hide'] != $metaInput['hide']) + ($this->hasChanged($metaInput, $metaPage['meta'], 'hide')) ) { # add new file data. Also makes sure that the value is set. @@ -297,6 +296,19 @@ class MetaApiController extends ContentController # return with the new metadata return $response->withJson(array('metadata' => $metaInput, 'structure' => $structure, 'item' => $this->item, 'errors' => false)); } + + 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; + } } # check models -> writeYaml for getPageMeta and getPageMetaDefaults. \ No newline at end of file diff --git a/system/Controllers/PageController.php b/system/Controllers/PageController.php index 929ca2e..b721941 100644 --- a/system/Controllers/PageController.php +++ b/system/Controllers/PageController.php @@ -108,13 +108,16 @@ class PageController extends Controller /* get breadcrumb for page */ $breadcrumb = Folder::getBreadcrumb($structure, $item->keyPathArray); $breadcrumb = $this->c->dispatcher->dispatch('onBreadcrumbLoaded', new OnBreadcrumbLoaded($breadcrumb))->getData(); + + # set pages active for navigation again + Folder::getBreadcrumb($navigation, $item->keyPathArray); /* add the paging to the item */ $item = Folder::getPagingForItem($structure, $item); } # dispatch the item - $item = $this->c->dispatcher->dispatch('onItemLoaded', new OnItemLoaded($item))->getData(); + $item = $this->c->dispatcher->dispatch('onItemLoaded', new OnItemLoaded($item))->getData(); # set the filepath $filePath = $pathToContent . $item->path; @@ -211,6 +214,18 @@ class PageController extends Controller $this->c->assets->addCSS($base_url . '/cache/' . $theme . '-custom.css'); } + $logo = false; + if(isset($settings['logo']) && $settings['logo'] != '') + { + $logo = 'media/files/' . $settings['logo']; + } + + $favicon = false; + if(isset($settings['favicon']) && $settings['favicon'] != '') + { + $favicon = true; + } + return $this->render($response, $route, [ 'home' => $home, 'navigation' => $navigation, @@ -221,7 +236,10 @@ class PageController extends Controller 'settings' => $settings, 'metatabs' => $metatabs, 'base_url' => $base_url, - 'image' => $firstImage ]); + 'image' => $firstImage, + 'logo' => $logo, + 'favicon' => $favicon + ]); } protected function getCachedStructure($cache) diff --git a/system/Controllers/SettingsController.php b/system/Controllers/SettingsController.php index ebe6d3f..bad290f 100644 --- a/system/Controllers/SettingsController.php +++ b/system/Controllers/SettingsController.php @@ -7,6 +7,8 @@ use Typemill\Models\Write; use Typemill\Models\Fields; use Typemill\Models\Validation; use Typemill\Models\User; +use Typemill\Models\ProcessFile; +use Typemill\Models\ProcessImage; class SettingsController extends Controller { @@ -15,7 +17,7 @@ class SettingsController extends Controller *********************/ public function showSettings($request, $response, $args) - { + { $user = new User(); $settings = $this->c->get('settings'); $defaultSettings = \Typemill\Settings::getDefaultSettings(); @@ -46,8 +48,10 @@ class SettingsController extends Controller $settings = \Typemill\Settings::getUserSettings(); $defaultSettings = \Typemill\Settings::getDefaultSettings(); $params = $request->getParams(); + $files = $request->getUploadedFiles(); $newSettings = isset($params['settings']) ? $params['settings'] : false; $validate = new Validation(); + $processFiles = new ProcessFile(); if($newSettings) { @@ -62,6 +66,8 @@ class SettingsController extends Controller 'formats' => $newSettings['formats'], ); + # https://www.slimframework.com/docs/v3/cookbook/uploading-files.html; + $copyright = $this->getCopyright(); $validate->settings($newSettings, $copyright, $defaultSettings['formats'], 'settings'); @@ -77,8 +83,80 @@ class SettingsController extends Controller $this->c->flash->addMessage('error', 'Please correct the errors'); return $response->withRedirect($this->c->router->pathFor('settings.show')); } - - /* store updated settings */ + + if(!$processFiles->checkFolders()) + { + $this->c->flash->addMessage('error', 'Please make sure that your media folder exists and is writable.'); + return $response->withRedirect($this->c->router->pathFor('settings.show')); + } + + # handle single input with single file upload + $logo = $files['settings']['logo']; + if($logo->getError() === UPLOAD_ERR_OK) + { + $allowed = ['jpg', 'jpeg', 'png', 'svg']; + $extension = pathinfo($logo->getClientFilename(), PATHINFO_EXTENSION); + if(!in_array(strtolower($extension), $allowed)) + { + $_SESSION['errors']['settings']['logo'] = array('Only jpg, jpeg, png and svg allowed'); + $this->c->flash->addMessage('error', 'Please correct the errors'); + return $response->withRedirect($this->c->router->pathFor('settings.show')); + } + + $processFiles->deleteFileWithName('logo'); + $newSettings['logo'] = $processFiles->moveUploadedFile($logo, $overwrite = true, $name = 'logo'); + } + elseif(isset($params['settings']['deletelogo']) && $params['settings']['deletelogo'] == 'delete') + { + $processFiles->deleteFileWithName('logo'); + $newSettings['logo'] = ''; + } + else + { + $newSettings['logo'] = isset($settings['logo']) ? $settings['logo'] : ''; + } + + # handle single input with single file upload + $favicon = $files['settings']['favicon']; + if ($favicon->getError() === UPLOAD_ERR_OK) + { + $extension = pathinfo($favicon->getClientFilename(), PATHINFO_EXTENSION); + if(strtolower($extension) != 'png') + { + $_SESSION['errors']['settings']['favicon'] = array('Only .png-files allowed'); + $this->c->flash->addMessage('error', 'Please correct the errors'); + return $response->withRedirect($this->c->router->pathFor('settings.show')); + } + + $processImage = new ProcessImage([ + '16' => ['width' => 16, 'height' => 16], + '32' => ['width' => 32, 'height' => 32], + '72' => ['width' => 72, 'height' => 72], + '114' => ['width' => 114, 'height' => 114], + '144' => ['width' => 144, 'height' => 144], + '180' => ['width' => 180, 'height' => 180], + ]); + $favicons = $processImage->generateSizesFromImageFile('favicon.png', $favicon->file); + + foreach($favicons as $key => $favicon) + { + imagepng( $favicon, $processFiles->fileFolder . 'favicon-' . $key . '.png' ); + # $processFiles->moveUploadedFile($favicon, $overwrite = true, $name = 'favicon-' . $key); + } + + $newSettings['favicon'] = 'favicon'; + } + elseif(isset($params['settings']['deletefav']) && $params['settings']['deletefav'] == 'delete') + { + $processFiles->deleteFileWithName('favicon'); + $newSettings['favicon'] = ''; + } + else + { + $newSettings['favicon'] = isset($settings['favicon']) ? $settings['favicon'] : ''; + } + + # store updated settings \Typemill\Settings::updateSettings(array_merge($settings, $newSettings)); $this->c->flash->addMessage('info', 'Settings are stored'); diff --git a/system/Controllers/SetupController.php b/system/Controllers/SetupController.php index 5a1acca..e149ed1 100644 --- a/system/Controllers/SetupController.php +++ b/system/Controllers/SetupController.php @@ -9,6 +9,13 @@ use Typemill\Models\Write; class SetupController extends Controller { + + # redirect if visit /setup route + public function redirect($request, $response) + { + return $response->withRedirect($this->c->router->pathFor('setup.show')); + } + public function show($request, $response, $args) { /* make some checks befor you install */ diff --git a/system/Extensions/TwigPagelistExtension.php b/system/Extensions/TwigPagelistExtension.php new file mode 100644 index 0000000..e85e1b6 --- /dev/null +++ b/system/Extensions/TwigPagelistExtension.php @@ -0,0 +1,22 @@ + $item) + { + # set item active, needed to move item in navigation + if($item->urlRelWoF === $url) + { + $item->active = true; + $result = $item; + } + elseif($item->elementType === "folder") + { + $result = self::getItemForUrlFrontend($item->folderContent, $url, $result); + } + } + + return $result; + } + public static function getPagingForItem($content, $item) { $keyPos = count($item->keyPathArray)-1; @@ -475,6 +493,7 @@ class Folder while($i < count($searchArray)) { + if(!isset($content[$searchArray[$i]])){ return false; } $item = $content[$searchArray[$i]]; if($i == count($searchArray)-1) diff --git a/system/Models/Helpers.php b/system/Models/Helpers.php index ed35cde..1a189b8 100644 --- a/system/Models/Helpers.php +++ b/system/Models/Helpers.php @@ -24,4 +24,39 @@ class Helpers{ $table .= ''; echo $table; } + + public static function array_sort($array, $on, $order=SORT_ASC) + { + $new_array = array(); + $sortable_array = array(); + + if (count($array) > 0) { + foreach ($array as $k => $v) { + if (is_array($v)) { + foreach ($v as $k2 => $v2) { + if ($k2 == $on) { + $sortable_array[$k] = $v2; + } + } + } else { + $sortable_array[$k] = $v; + } + } + + switch ($order) { + case SORT_ASC: + asort($sortable_array); + break; + case SORT_DESC: + arsort($sortable_array); + break; + } + + foreach ($sortable_array as $k => $v) { + $new_array[] = $array[$k]; + } + } + + return $new_array; + } } \ No newline at end of file diff --git a/system/Models/ProcessAssets.php b/system/Models/ProcessAssets.php new file mode 100644 index 0000000..aaf66c4 --- /dev/null +++ b/system/Models/ProcessAssets.php @@ -0,0 +1,231 @@ +baseFolder = getcwd() . DIRECTORY_SEPARATOR; + + $this->mediaFolder = $this->baseFolder . 'media' . DIRECTORY_SEPARATOR; + + $this->tmpFolder = $this->mediaFolder . 'tmp' . DIRECTORY_SEPARATOR; + + $this->originalFolder = $this->mediaFolder . 'original' . DIRECTORY_SEPARATOR; + + $this->liveFolder = $this->mediaFolder . 'live' . DIRECTORY_SEPARATOR; + + $this->thumbFolder = $this->mediaFolder . 'thumbs' . DIRECTORY_SEPARATOR; + + $this->fileFolder = $this->mediaFolder . 'files' . DIRECTORY_SEPARATOR; + + $this->desiredSizes = $desiredSizes; + } + + public function checkFolders($forassets = null) + { + + $folders = [$this->mediaFolder, $this->tmpFolder, $this->fileFolder]; + + if($forassets == 'images') + { + $folders = [$this->mediaFolder, $this->tmpFolder, $this->originalFolder, $this->liveFolder, $this->thumbFolder]; + } + + foreach($folders as $folder) + { + if(!file_exists($folder)) + { + if(!mkdir($folder, 0774, true)) + { + return false; + } + if($folder == $this->thumbFolder) + { + # cleanup old systems + $this->cleanupLiveFolder(); + + # generate thumbnails from live folder + $this->generateThumbs(); + } + } + elseif(!is_writeable($folder)) + { + return false; + } + } + return true; + } + + public function setFileName($originalname, $type, $overwrite = null) + { + $pathinfo = pathinfo($originalname); + + $this->extension = strtolower($pathinfo['extension']); + $this->filename = URLify::filter(iconv(mb_detect_encoding($pathinfo['filename'], mb_detect_order(), true), "UTF-8", $pathinfo['filename'])); + + $filename = $this->filename; + + # check if file name is + if(!$overwrite) + { + $suffix = 1; + + $destination = $this->liveFolder; + if($type == 'file') + { + $destination = $this->fileFolder; + } + + while(file_exists($destination . $filename . '.' . $this->extension)) + { + $filename = $this->filename . '-' . $suffix; + $suffix++; + } + } + + $this->filename = $filename; + + return true; + } + + public function getName() + { + return $this->filename; + } + + public function getExtension() + { + return $this->extension; + } + + public function getFullName() + { + return $this->filename . '.' . $this->extension; + } + + public function clearTempFolder() + { + $files = scandir($this->tmpFolder); + $result = true; + + foreach($files as $file) + { + if (!in_array($file, array(".",".."))) + { + $filelink = $this->tmpFolder . $file; + if(!unlink($filelink)) + { + $success = false; + } + } + } + + return $result; + } + + public function cleanupLiveFolder() + { + # delete all old thumbs mlibrary in live folder + foreach(glob($this->liveFolder . '*mlibrary*') as $filename) + { + unlink($filename); + } + + return true; + } + + public function findPagesWithUrl($structure, $url, $result) + { + foreach ($structure as $key => $item) + { + if($item->elementType == 'folder') + { + $result = $this->findPagesWithUrl($item->folderContent, $url, $result); + } + else + { + $live = getcwd() . DIRECTORY_SEPARATOR . 'content' . $item->pathWithoutType . '.md'; + $draft = getcwd() . DIRECTORY_SEPARATOR . 'content' . $item->pathWithoutType . '.txt'; + + # check live first + if(file_exists($live)) + { + $content = file_get_contents($live); + + if (stripos($content, $url) !== false) + { + $result[] = $item->urlRelWoF; + } + # if not in live, check in draft + elseif(file_exists($draft)) + { + $content = file_get_contents($draft); + + if (stripos($content, $url) !== false) + { + $result[] = $item->urlRelWoF; + } + } + } + } + } + return $result; + } + + public function formatSizeUnits($bytes) + { + if ($bytes >= 1073741824) + { + $bytes = number_format($bytes / 1073741824, 2) . ' GB'; + } + elseif ($bytes >= 1048576) + { + $bytes = number_format($bytes / 1048576, 2) . ' MB'; + } + elseif ($bytes >= 1024) + { + $bytes = number_format($bytes / 1024, 2) . ' KB'; + } + elseif ($bytes > 1) + { + $bytes = $bytes . ' bytes'; + } + elseif ($bytes == 1) + { + $bytes = $bytes . ' byte'; + } + else + { + $bytes = '0 bytes'; + } + + return $bytes; + } +} \ No newline at end of file diff --git a/system/Models/ProcessFile.php b/system/Models/ProcessFile.php new file mode 100644 index 0000000..6912956 --- /dev/null +++ b/system/Models/ProcessFile.php @@ -0,0 +1,165 @@ +setFileName($uploadedFile->getClientFilename(), 'file'); + + if($name) + { + $this->setFileName($name . '.' . $this->extension, 'file', $overwrite); + } + + $uploadedFile->moveTo($this->fileFolder . $this->getFullName()); + + return $this->getFullName(); + } + + public function storeFile($file, $name) + { + $this->setFileName($name, 'file'); + + $this->clearTempFolder(); + + $file = $this->decodeFile($file); + + $path = $this->tmpFolder . $this->getFullName(); + + if(file_put_contents($path, $file)) + { + $size = filesize($path); + $size = $this->formatSizeUnits($size); + + $title = str_replace('-', ' ', $this->filename); + $title = $title . ' (' . strtoupper($this->extension) . ', ' . $size .')'; + + return ['title' => $title, 'name' => $this->filename, 'extension' => $this->extension, 'size' => $size, 'url' => 'media/files/' . $this->getFullName()]; + } + + return false; + } + + public function publishFile() + { + $files = scandir($this->tmpFolder); + $success = true; + + foreach($files as $file) + { + if (!in_array($file, array(".",".."))) + { + $success = rename($this->tmpFolder . $file, $this->fileFolder . $file); + } + } + + return $success; + } + + public function decodeFile(string $file) + { + $fileParts = explode(";base64,", $file); + $fileType = explode("/", $fileParts[0]); + $fileData = base64_decode($fileParts[1]); + + if ($fileData !== false) + { + return array("file" => $fileData, "type" => $fileType[1]); + } + + return false; + } + + + public function deleteFile($name) + { + # validate name + $name = basename($name); + + if(file_exists($this->fileFolder . $name) && unlink($this->fileFolder . $name)) + { + return true; + } + + return false; + } + + + public function deleteFileWithName($name) + { + # e.g. delete $name = 'logo'; + + $name = basename($name); + + if($name != '' && !in_array($name, array(".",".."))) + { + foreach(glob($this->fileFolder . $name . '.*') as $file) + { + unlink($file); + } + } + } + + + /* + * scans content of a folder (without recursion) + * vars: folder path as string + * returns: one-dimensional array with names of folders and files + */ + public function scanFilesFlat() + { + $files = scandir($this->fileFolder); + $filelist = array(); + + foreach ($files as $key => $name) + { + if (!in_array($name, array(".","..")) && file_exists($this->fileFolder . $name)) + { + $filelist[] = [ + 'name' => $name, + 'timestamp' => filemtime($this->fileFolder . $name), + 'info' => pathinfo($this->fileFolder . $name), + 'url' => 'media/files/' . $name, + ]; + } + } + + $filelist = Helpers::array_sort($filelist, 'timestamp', SORT_DESC); + + return $filelist; + } + + + public function getFileDetails($name, $structure) + { + $name = basename($name); + + if (!in_array($name, array(".","..")) && file_exists($this->fileFolder . $name)) + { + $filedetails = [ + 'name' => $name, + 'timestamp' => filemtime($this->fileFolder . $name), + 'bytes' => filesize($this->fileFolder . $name), + 'info' => pathinfo($this->fileFolder . $name), + 'url' => 'media/files/' . $name, + 'pages' => $this->findPagesWithUrl($structure, $name, $result = []) + ]; + + return $filedetails; + } + + return false; + } +} \ No newline at end of file diff --git a/system/Models/ProcessImage.php b/system/Models/ProcessImage.php index 2922633..82a21bd 100644 --- a/system/Models/ProcessImage.php +++ b/system/Models/ProcessImage.php @@ -1,9 +1,11 @@ clearTempFolder(); - + + # set the name of the image + $this->setFileName($name, 'image'); + # decode the image from base64-string $imageDecoded = $this->decodeImage($image); $imageData = $imageDecoded["image"]; @@ -28,25 +33,24 @@ class ProcessImage # resize the images $resizedImages = $this->imageResize($image, $imageSize, $desiredSizes, $imageType); - - $basePath = getcwd() . DIRECTORY_SEPARATOR . 'media'; - $tmpFolder = $basePath . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR; - $this->saveOriginal($tmpFolder, $imageData, 'original', $imageType); - - if($imageType == "gif" && $this->detectAnimatedGif($imageData)) - { - $this->saveOriginal($tmpFolder, $imageData, 'live', $imageType); + # store the original name as txt-file + $tmpname = fopen($this->tmpFolder . $this->getName() . '.' . $imageType . ".txt", "w"); + + $this->saveOriginal($this->tmpFolder, $imageData, $name = 'original', $imageType); - return true; - } - # temporary store resized images foreach($resizedImages as $key => $resizedImage) { - $this->saveImage($tmpFolder, $resizedImage, $key, $imageType); + $this->saveImage($this->tmpFolder, $resizedImage, $key, $imageType); } - + + # if the image is an animated gif, then overwrite the resized version for live use with the original version + if($imageType == "gif" && $this->detectAnimatedGif($imageData)) + { + $this->saveOriginal($this->tmpFolder, $imageData, $name = 'live', $imageType); + } + return true; } @@ -60,42 +64,54 @@ class ProcessImage return false; } - public function publishImage(array $desiredSizes, $name = false) + public function publishImage() { - /* get images from tmp folder */ - $basePath = getcwd() . DIRECTORY_SEPARATOR . 'media'; - $tmpFolder = $basePath . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR; - $originalFolder = $basePath . DIRECTORY_SEPARATOR . 'original' . DIRECTORY_SEPARATOR; - $liveFolder = $basePath . DIRECTORY_SEPARATOR . 'live' . DIRECTORY_SEPARATOR; + # name is stored in temporary folder as name of the .txt-file + foreach(glob($this->tmpFolder . '*.txt') as $imagename) + { + $tmpname = str_replace('.txt', '', basename($imagename)); - if(!file_exists($originalFolder)){ mkdir($originalFolder, 0774, true); } - if(!file_exists($liveFolder)){ mkdir($liveFolder, 0774, true); } + # set extension and sanitize name + $this->setFileName($tmpname, 'image'); - $name = $name ? $name : uniqid(); - - $files = scandir($tmpFolder); + unlink($imagename); + } + + $name = uniqid(); + + if($this->filename && $this->extension) + { + $name = $this->filename; + } + + $files = scandir($this->tmpFolder); $success = true; foreach($files as $file) { if (!in_array($file, array(".",".."))) - { + { $tmpfilename = explode(".", $file); if($tmpfilename[0] == 'original') { - $success = rename($tmpFolder . $file, $originalFolder . $name . '-' . $file); + $success = rename($this->tmpFolder . $file, $this->originalFolder . $name . '.' . $tmpfilename[1]); } - else + if($tmpfilename[0] == 'live') { - $success = rename($tmpFolder . $file, $liveFolder . $name . '-' . $file); + $success = rename($this->tmpFolder . $file, $this->liveFolder . $name . '.' . $tmpfilename[1]); + } + if($tmpfilename[0] == 'thumbs') + { + $success = rename($this->tmpFolder . $file, $this->thumbFolder . $name . '.' . $tmpfilename[1]); } } } if($success) { - return 'media/live/' . $name . '-live.' . $tmpfilename[1]; + return true; + return 'media/live/' . $name . '.' . $tmpfilename[1]; } return false; @@ -126,6 +142,7 @@ class ProcessImage { foreach($desiredSizes as $key => $desiredSize) { + # if desired size is bigger than the actual image, then drop the desired sizes and use the actual image size instead if($desiredSize['width'] > $imageSize['width']) { $desiredSizes[$key] = $imageSize; @@ -141,129 +158,61 @@ class ProcessImage return $desiredSizes; } - public function imageResize($imageData, array $imageSize, array $desiredSizes, $imageType) + public function imageResize($imageData, array $source, array $desiredSizes, $imageType) { + $copiedImages = array(); - $source_aspect_ratio = $imageSize['width'] / $imageSize['height']; - - foreach($desiredSizes as $key => $desiredSize) + + foreach($desiredSizes as $key => $desired) { - $desired_aspect_ratio = $desiredSize['width'] / $desiredSize['height']; + // resize + $ratio = max($desired['width']/$source['width'], $desired['height']/$source['height']); + $h = $desired['height'] / $ratio; + $x = ($source['width'] - $desired['width'] / $ratio) / 2; + $y = ($source['height'] - $desired['height'] / $ratio) / 2; + $w = $desired['width'] / $ratio; - if ( $source_aspect_ratio > $desired_aspect_ratio ) - { - # when source image is wider - $temp_height = $desiredSize['height']; - $temp_width = ( int ) ($desiredSize['height'] * $source_aspect_ratio); - $temp_width = round($temp_width, 0); - } - else - { - # when source image is similar or taller - $temp_width = $desiredSize['width']; - $temp_height = ( int ) ($desiredSize['width'] / $source_aspect_ratio); - $temp_height = round($temp_height, 0); - } + $new = imagecreatetruecolor($desired['width'], $desired['height']); - # Create a temporary GD image with desired size - $temp_gdim = imagecreatetruecolor( $temp_width, $temp_height ); + // preserve transparency + if($imageType == "gif" or $imageType == "png") + { + imagecolortransparent($new, imagecolorallocatealpha($new, 0, 0, 0, 127)); + imagealphablending($new, false); + imagesavealpha($new, true); + } - if ($imageType == "gif") - { - $transparent_index = imagecolortransparent($imageData); - imagepalettecopy($imageData, $temp_gdim); - imagefill($temp_gdim, 0, 0, $transparent_index); - imagecolortransparent($temp_gdim, $transparent_index); - imagetruecolortopalette($temp_gdim, true, 256); - } - elseif($imageType == "png") - { - imagealphablending($temp_gdim, false); - imagesavealpha($temp_gdim, true); - $transparent = imagecolorallocatealpha($temp_gdim, 255, 255, 255, 127); - imagefilledrectangle($temp_gdim, 0, 0, $temp_width, $temp_height, $transparent); - } - - # resize image - imagecopyresampled( - $temp_gdim, - $imageData, - 0, 0, - 0, 0, - $temp_width, $temp_height, - $imageSize['width'], $imageSize['height'] - ); + imagecopyresampled($new, $imageData, 0, 0, $x, $y, $desired['width'], $desired['height'], $w, $h); - $copiedImages[$key] = $temp_gdim; - - /* - - # Copy cropped region from temporary image into the desired GD image - $x0 = ( $temp_width - $desiredSize['width'] ) / 2; - $y0 = ( $temp_height - $desiredSize['height'] ) / 2; - - $desired_gdim = imagecreatetruecolor( $desiredSize['width'], $desiredSize['height'] ); - - if ($imageType == "gif") - { - imagepalettecopy($temp_gdim, $desired_gdim); - imagefill($desired_gdim, 0, 0, $transparent_index); - imagecolortransparent($desired_gdim, $transparent_index); - imagetruecolortopalette($desired_gdim, true, 256); - } - elseif($imageType == "png") - { - imagealphablending($desired_gdim, false); - imagesavealpha($desired_gdim,true); - $transparent = imagecolorallocatealpha($desired_gdim, 255, 255, 255, 127); - imagefilledrectangle($desired_gdim, 0, 0, $desired_size['with'], $desired_size['height'], $transparent); - } - - imagecopyresampled( - $desired_gdim, - $temp_gdim, - 0, 0, - 0, 0, - $x0, $y0, - $desiredSize['width'], $desiredSize['height'] - ); - $copiedImages[$key] = $desired_gdim; - - */ + $copiedImages[$key] = $new; } + return $copiedImages; } + # save original in temporary folder public function saveOriginal($folder, $image, $name, $type) - { - if(!file_exists($folder)) - { - mkdir($folder, 0774, true); - } - + { $path = $folder . $name . '.' . $type; file_put_contents($path, $image); } + + # save resized images in temporary folder public function saveImage($folder, $image, $name, $type) - { - if(!file_exists($folder)) - { - mkdir($folder, 0774, true); - } - + { if($type == "png") { - $result = imagepng( $image, $folder . '/' . $name . '.png' ); + $result = imagepng( $image, $folder . $name . '.png' ); } elseif($type == "gif") { - $result = imagegif( $image, $folder . '/' . $name . '.gif' ); + $result = imagegif( $image, $folder . $name . '.gif' ); } else { - $result = imagejpeg( $image, $folder . '/' . $name . '.jpeg' ); + $result = imagejpeg( $image, $folder . $name . '.jpeg' ); $type = 'jpeg'; } @@ -277,60 +226,156 @@ class ProcessImage return false; } - public function clearTempFolder() - { - $folder = getcwd() . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR; - - if(!file_exists($folder)) - { - mkdir($folder, 0774, true); - return true; - } - - $files = scandir($folder); - $result = true; - - foreach($files as $file) - { - if (!in_array($file, array(".",".."))) - { - $filelink = $folder . $file; - if(!unlink($filelink)) - { - $success = false; - } - } - } - - return $result; - } - public function deleteImage($name) { - $baseFolder = getcwd() . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR; - $original = $baseFolder . 'original' . DIRECTORY_SEPARATOR . $name . '*'; - $live = $baseFolder . 'live' . DIRECTORY_SEPARATOR . $name . '*'; - $success = true; - - foreach(glob($original) as $image) + + # validate name + $name = basename($name); + + $result = true; + + if(!file_exists($this->originalFolder . $name) OR !unlink($this->originalFolder . $name)) + { + $result = false; + } + + if(!file_exists($this->liveFolder . $name) OR !unlink($this->liveFolder . $name)) + { + $result = false; + } + + if(!file_exists($this->thumbFolder . $name) OR !unlink($this->thumbFolder . $name)) + { + $result = false; + } + + # you should not use glob but exact name with ending + /* + foreach(glob($this->originalFolder . $name) as $image) { if(!unlink($image)) { $success = false; } } + */ - foreach(glob($live) as $image) - { - if(!unlink($image)) - { - $success = false; - } - } - - return $success; + # array_map('unlink', glob("some/dir/*.txt")); + + return $result; + } + + /* + * scans content of a folder (without recursion) + * vars: folder path as string + * returns: one-dimensional array with names of folders and files + */ + public function scanMediaFlat() + { + $thumbs = array_diff(scandir($this->thumbFolder), array('..', '.')); + $imagelist = array(); + + foreach ($thumbs as $key => $name) + { + if (file_exists($this->liveFolder . $name)) + { + $imagelist[] = [ + 'name' => $name, + 'timestamp' => filemtime($this->liveFolder . $name), + 'src_thumb' => 'media/thumbs/' . $name, + 'src_live' => 'media/live/' . $name, + ]; + } + } + + $imagelist = Helpers::array_sort($imagelist, 'timestamp', SORT_DESC); + + return $imagelist; } -} -?> \ No newline at end of file + public function getImageDetails($name, $structure) + { + $name = basename($name); + + if (!in_array($name, array(".","..")) && file_exists($this->liveFolder . $name)) + { + $imageinfo = getimagesize($this->liveFolder . $name); + + $imagedetails = [ + 'name' => $name, + 'timestamp' => filemtime($this->liveFolder . $name), + 'bytes' => filesize($this->liveFolder . $name), + 'width' => $imageinfo[0], + 'height' => $imageinfo[1], + 'type' => $imageinfo['mime'], + 'src_thumb' => 'media/thumbs/' . $name, + 'src_live' => 'media/live/' . $name, + 'pages' => $this->findPagesWithUrl($structure, $name, $result = []) + ]; + + return $imagedetails; + } + + return false; + } + + public function generateThumbs() + { + # generate images from live folder to 'tmthumbs' + $liveImages = scandir($this->liveFolder); + + foreach ($liveImages as $key => $name) + { + if (!in_array($name, array(".",".."))) + { + $this->generateThumbFromImageFile($name); + } + } + } + + public function generateThumbFromImageFile($filename) + { + $this->setFileName($filename, 'image', $overwrite = true); + + if($this->extension == 'jpeg') $this->extension = 'jpg'; + + switch($this->extension) + { + case 'gif': $image = imagecreatefromgif($this->liveFolder . $filename); break; + case 'jpg': $image = imagecreatefromjpeg($this->liveFolder . $filename); break; + case 'png': $image = imagecreatefrompng($this->liveFolder . $filename); break; + default: return 'image type not supported'; + } + + $originalSize = $this->getImageSize($image); + + $thumbSize = $this->desiredSizes['thumbs']; + + $thumb = $this->imageResize($image, $originalSize, ['thumbs' => $thumbSize ], $this->extension); + + $this->saveImage($this->thumbFolder, $thumb['thumbs'], $this->filename, $this->extension); + } + + public function generateSizesFromImageFile($filename, $image) + { + $this->setFileName($filename, 'image'); + + if($this->extension == 'jpeg') $this->extension = 'jpg'; + + switch($this->extension) + { + case 'gif': $image = imagecreatefromgif($image); break; + case 'jpg': $image = imagecreatefromjpeg($image); break; + case 'png': $image = imagecreatefrompng($image); break; + default: return 'image type not supported'; + } + + $originalSize = $this->getImageSize($image); + + $resizedImages = $this->imageResize($image, $originalSize, $this->desiredSizes, $this->extension); + + return $resizedImages; + } + +} \ No newline at end of file diff --git a/system/Routes/Api.php b/system/Routes/Api.php index a81adbd..f1095a6 100644 --- a/system/Routes/Api.php +++ b/system/Routes/Api.php @@ -3,34 +3,45 @@ use Typemill\Controllers\SettingsController; use Typemill\Controllers\ContentController; use Typemill\Controllers\ContentApiController; +use Typemill\Controllers\ArticleApiController; +use Typemill\Controllers\BlockApiController; +use Typemill\Controllers\MediaApiController; use Typemill\Controllers\MetaApiController; use Typemill\Middleware\RestrictApiAccess; $app->get('/api/v1/themes', SettingsController::class . ':getThemeSettings')->setName('api.themes')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/article/markdown', ContentApiController::class . ':getArticleMarkdown')->setName('api.article.markdown')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/article/html', ContentApiController::class . ':getArticleHtml')->setName('api.article.html')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/article/publish', ContentApiController::class . ':publishArticle')->setName('api.article.publish')->add(new RestrictApiAccess($container['router'])); -$app->delete('/api/v1/article/unpublish', ContentApiController::class . ':unpublishArticle')->setName('api.article.unpublish')->add(new RestrictApiAccess($container['router'])); -$app->delete('/api/v1/article/discard', ContentApiController::class . ':discardArticleChanges')->setName('api.article.discard')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/post', ContentApiController::class . ':createPost')->setName('api.post.create')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/article', ContentApiController::class . ':createArticle')->setName('api.article.create')->add(new RestrictApiAccess($container['router'])); -$app->put('/api/v1/article', ContentApiController::class . ':updateArticle')->setName('api.article.update')->add(new RestrictApiAccess($container['router'])); -$app->delete('/api/v1/article', ContentApiController::class . ':deleteArticle')->setName('api.article.delete')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/article/sort', ContentApiController::class . ':sortArticle')->setName('api.article.sort')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/article/markdown', ArticleApiController::class . ':getArticleMarkdown')->setName('api.article.markdown')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/article/html', ArticleApiController::class . ':getArticleHtml')->setName('api.article.html')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/article/publish', ArticleApiController::class . ':publishArticle')->setName('api.article.publish')->add(new RestrictApiAccess($container['router'])); +$app->delete('/api/v1/article/unpublish', ArticleApiController::class . ':unpublishArticle')->setName('api.article.unpublish')->add(new RestrictApiAccess($container['router'])); +$app->delete('/api/v1/article/discard', ArticleApiController::class . ':discardArticleChanges')->setName('api.article.discard')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/article/sort', ArticleApiController::class . ':sortArticle')->setName('api.article.sort')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/article', ArticleApiController::class . ':createArticle')->setName('api.article.create')->add(new RestrictApiAccess($container['router'])); +$app->put('/api/v1/article', ArticleApiController::class . ':updateArticle')->setName('api.article.update')->add(new RestrictApiAccess($container['router'])); +$app->delete('/api/v1/article', ArticleApiController::class . ':deleteArticle')->setName('api.article.delete')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/baseitem', ArticleApiController::class . ':createBaseItem')->setName('api.baseitem.create')->add(new RestrictApiAccess($container['router'])); +$app->get('/api/v1/navigation', ArticleApiController::class . ':getNavigation')->setName('api.navigation.get')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/post', ArticleApiController::class . ':createPost')->setName('api.post.create')->add(new RestrictApiAccess($container['router'])); + +$app->get('/api/v1/metadefinitions', MetaApiController::class . ':getMetaDefinitions')->setName('api.metadefinitions.get')->add(new RestrictApiAccess($container['router'])); $app->get('/api/v1/article/metaobject', MetaApiController::class . ':getArticleMetaobject')->setName('api.articlemetaobject.get')->add(new RestrictApiAccess($container['router'])); $app->get('/api/v1/article/metadata', MetaApiController::class . ':getArticleMeta')->setName('api.articlemeta.get')->add(new RestrictApiAccess($container['router'])); $app->post('/api/v1/article/metadata', MetaApiController::class . ':updateArticleMeta')->setName('api.articlemeta.update')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/baseitem', ContentApiController::class . ':createBaseItem')->setName('api.baseitem.create')->add(new RestrictApiAccess($container['router'])); -$app->get('/api/v1/navigation', ContentApiController::class . ':getNavigation')->setName('api.navigation.get')->add(new RestrictApiAccess($container['router'])); -$app->get('/api/v1/metadefinitions', MetaApiController::class . ':getMetaDefinitions')->setName('api.metadefinitions.get')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/block', ContentApiController::class . ':addBlock')->setName('api.block.add')->add(new RestrictApiAccess($container['router'])); -$app->put('/api/v1/block', ContentApiController::class . ':updateBlock')->setName('api.block.update')->add(new RestrictApiAccess($container['router'])); -$app->delete('/api/v1/block', ContentApiController::class . ':deleteBlock')->setName('api.block.delete')->add(new RestrictApiAccess($container['router'])); -$app->put('/api/v1/moveblock', ContentApiController::class . ':moveBlock')->setName('api.block.move')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/block', BlockApiController::class . ':addBlock')->setName('api.block.add')->add(new RestrictApiAccess($container['router'])); +$app->put('/api/v1/block', BlockApiController::class . ':updateBlock')->setName('api.block.update')->add(new RestrictApiAccess($container['router'])); +$app->delete('/api/v1/block', BlockApiController::class . ':deleteBlock')->setName('api.block.delete')->add(new RestrictApiAccess($container['router'])); +$app->put('/api/v1/moveblock', BlockApiController::class . ':moveBlock')->setName('api.block.move')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/video', BlockApiController::class . ':saveVideoImage')->setName('api.video.save')->add(new RestrictApiAccess($container['router'])); -$app->post('/api/v1/image', ContentApiController::class . ':createImage')->setName('api.image.create')->add(new RestrictApiAccess($container['router'])); -$app->put('/api/v1/image', ContentApiController::class . ':publishImage')->setName('api.image.publish')->add(new RestrictApiAccess($container['router'])); - -$app->post('/api/v1/video', ContentApiController::class . ':saveVideoImage')->setName('api.video.save')->add(new RestrictApiAccess($container['router'])); \ No newline at end of file +$app->get('/api/v1/medialib/images', MediaApiController::class . ':getMediaLibImages')->setName('api.medialibimg.get')->add(new RestrictApiAccess($container['router'])); +$app->get('/api/v1/medialib/files', MediaApiController::class . ':getMediaLibFiles')->setName('api.medialibfiles.get')->add(new RestrictApiAccess($container['router'])); +$app->get('/api/v1/image', MediaApiController::class . ':getImage')->setName('api.image.get')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/image', MediaApiController::class . ':createImage')->setName('api.image.create')->add(new RestrictApiAccess($container['router'])); +$app->put('/api/v1/image', MediaApiController::class . ':publishImage')->setName('api.image.publish')->add(new RestrictApiAccess($container['router'])); +$app->delete('/api/v1/image', MediaApiController::class . ':deleteImage')->setName('api.image.delete')->add(new RestrictApiAccess($container['router'])); +$app->get('/api/v1/file', MediaApiController::class . ':getFile')->setName('api.file.get')->add(new RestrictApiAccess($container['router'])); +$app->post('/api/v1/file', MediaApiController::class . ':uploadFile')->setName('api.file.upload')->add(new RestrictApiAccess($container['router'])); +$app->put('/api/v1/file', MediaApiController::class . ':publishFile')->setName('api.file.publish')->add(new RestrictApiAccess($container['router'])); +$app->delete('/api/v1/file', MediaApiController::class . ':deleteFile')->setName('api.file.delete')->add(new RestrictApiAccess($container['router'])); \ No newline at end of file diff --git a/system/Routes/Web.php b/system/Routes/Web.php index 133e5dd..e3d227b 100644 --- a/system/Routes/Web.php +++ b/system/Routes/Web.php @@ -69,4 +69,11 @@ foreach($routes as $pluginRoute) } } -$app->get('/[{params:.*}]', PageController::class . ':index')->setName('home'); \ No newline at end of file +if($settings['settings']['setup']) +{ + $app->get('/[{params:.*}]', SetupController::class . ':redirect'); +} +else +{ + $app->get('/[{params:.*}]', PageController::class . ':index')->setName('home'); +} \ No newline at end of file diff --git a/system/Settings.php b/system/Settings.php index b2931fb..6e469ae 100644 --- a/system/Settings.php +++ b/system/Settings.php @@ -16,6 +16,25 @@ class Settings $settings = array_merge($defaultSettings, $userSettings); } + # if there is no theme set + if(!isset($settings['theme'])) + { + # scan theme folder and get the first theme + $themefolder = $settings['rootPath'] . $settings['themeFolder'] . DIRECTORY_SEPARATOR; + $themes = array_diff(scandir($themefolder), array('..', '.')); + $firsttheme = reset($themes); + + # if there is a theme with valid theme settings-file + if($firsttheme && self::getObjectSettings('themes', $firsttheme)) + { + $settings['theme'] = $firsttheme; + } + else + { + die('There is no theme in the theme-folder. Please add a theme from https://themes.typemill.net'); + } + } + # i18n # load the strings of the set language $language = $settings['language']; @@ -30,7 +49,7 @@ class Settings { $themeSettings = self::getObjectSettings('themes', $settings['theme']); $settings['themes'][$settings['theme']] = isset($themeSettings['settings']) ? $themeSettings['settings'] : false; - } + } return array('settings' => $settings); } @@ -48,7 +67,6 @@ class Settings 'language' => 'en', 'startpage' => true, 'rootPath' => $rootPath, - 'theme' => 'typemill', 'themeFolder' => 'themes', 'themeBasePath' => $rootPath, 'themePath' => '', @@ -56,14 +74,14 @@ class Settings 'userPath' => $rootPath . 'settings' . DIRECTORY_SEPARATOR . 'users', 'authorPath' => __DIR__ . DIRECTORY_SEPARATOR . 'author' . DIRECTORY_SEPARATOR, 'editor' => 'visual', - 'formats' => ['markdown', 'headline', 'ulist', 'olist', 'table', 'quote', 'image', 'video', 'toc', 'hr', 'definition', 'code'], + 'formats' => ['markdown', 'headline', 'ulist', 'olist', 'table', 'quote', 'image', 'video', 'file', 'toc', 'hr', 'definition', 'code'], 'contentFolder' => 'content', 'cache' => true, 'cachePath' => $rootPath . 'cache', 'version' => '1.3.3', 'setup' => true, 'welcome' => true, - 'images' => ['live' => ['width' => 820], 'mlibrary' => ['width' => 50, 'height' => 50]], + 'images' => ['live' => ['width' => 820], 'thumbs' => ['width' => 250, 'height' => 150]], ]; } @@ -137,23 +155,25 @@ class Settings if($userSettings) { - # whitelist settings that can be stored in usersettings (values are not relevant here, only keys) + # whitelist settings that can be stored in usersettings (values are not relevant here, only keys) $allowedUserSettings = ['displayErrorDetails' => true, - 'title' => false, - 'copyright' => false, - 'language' => false, - 'startpage' => false, - 'author' => false, - 'year' => false, - 'theme' => false, - 'editor' => false, - 'formats' => false, - 'setup' => false, - 'welcome' => false, - 'images' => false, - 'plugins' => false, - 'themes' => false, - 'latestVersion' => false + 'title' => true, + 'copyright' => true, + 'language' => true, + 'startpage' => true, + 'author' => true, + 'year' => true, + 'theme' => true, + 'editor' => true, + 'formats' => true, + 'setup' => true, + 'welcome' => true, + 'images' => true, + 'plugins' => true, + 'themes' => true, + 'latestVersion' => true, + 'logo' => true, + 'favicon' => true, ]; # cleanup the existing usersettings diff --git a/system/author/css/style.css b/system/author/css/style.css index 29d5df1..d951ff6 100644 --- a/system/author/css/style.css +++ b/system/author/css/style.css @@ -2,7 +2,7 @@ * TRANSITION * **********************/ -a, a:link, a:visited, a:focus, a:hover, a:active, button, .button, .tab-button, input, .control-group, .sidebar-menu, .sidebar-menu--content, .menu-action, .button-arrow{ +a, a:link, a:visited, a:focus, a:hover, a:active, .link, button, .button, .tab-button, input, .control-group, .sidebar-menu, .sidebar-menu--content, .menu-action, .button-arrow{ -webkit-transition: color 0.2s ease; -moz-transition: color 0.2s ease; -o-transition: color 0.2s ease; @@ -30,6 +30,130 @@ a, a:link, a:visited, a:focus, a:hover, a:active, button, .button, .tab-button, -ms-transition: all 0.1s ease; transition: all 0.1s ease; } +/******************** +* TACHYONS * +********************/ + +.tm-red{ + color:#e0474c; +} +.bg-tm-red{ + background:#e0474c; +} +.b--tm-red{ + border-color: #e0474c; +} +.hover-tm-red:hover{ + color:#e0474c; +} +.hover-bg-tm-red:hover{ + background:#e0474c; +} +.hover-b--tm-red:hover{ + border-color:#e0474c; +} +.tm-green{ + color:#66b0a3; +} +.bg-tm-green{ + background:#66b0a3; +} +.b--tm-green{ + border-color:#66b0a3; +} +.hover-tm-green:hover{ + color:#66b0a3; +} +.hover-bg-tm-green:hover{ + background:#66b0a3; +} +.hover-b--tm-green:hover{ + border-color:#66b0a3; +} +.w-100{ + width:100%!important; +} +.w-15{ + width: 15%; +} +.w-29{ + width: 29%; +} +.w-95{ + width:95%; +} +.w6{ + width: 22rem; +} +.h0{ + height: 0; +} +.h6{ + height: 22rem; +} +.mw6{ + max-width:22rem; +} +.max-h6{ + max-height: 22rem; +} +.shadow-tm{ + box-shadow: 0 0 2px 1px rgba(0,0,0,.1); +} +.bg-chess{ + background-image: linear-gradient(45deg, #808080 25%, transparent 25%), linear-gradient(-45deg, #808080 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #808080 75%), linear-gradient(-45deg, transparent 75%, #808080 75%); + background-size: 20px 20px; + background-position: 0 0, 0 10px, 10px -10px, -10px 0px; +} +input.upload{ + position: absolute; + top: 0; + right: 0; + margin: 0; + padding: 0; + font-size: 20px; + cursor: pointer; + opacity: 0; +} +.top-3{ + top: 3rem; +} +.list-enter-active, .list-leave-active { + transition: all .3s; +} +.list-enter, .list-leave-to /* .list-leave-active below version 2.1.8 */ { + opacity: 0; +} + +/**************************** +* download-commponent * +****************************/ + +a.tm-download +{ + line-height: 35px; + margin-left: 40px; +} +a.tm-download::before{ + content: '\2193'; + position: absolute; + margin-left: -40px; + width: 30px; + height: 30px; + line-height: 30px; + font-family: "Comic Sans MS",cursive,sans-serif; + border: 2px solid #e0474c; + border-radius: 50%; + text-align: center; + text-decoration: underline; +} +a.tm-download:hover::before{ + text-decoration:underline; + color: #fff; + background: #e0474c; +} + + /******************** * COMMONS * ********************/ @@ -453,8 +577,7 @@ footer{ bottom: 0; left: 0; right: 0; - background: #FFF; - background: rgba(255,255,255,0.9); + background: rgba(0,0,0,0.4); } .modal{ display: none; @@ -1526,14 +1649,14 @@ label .help, .label .help{ } .blox-buttons button.edit{ background: #70c1b3; - color: #eee; + color: #fff; } .blox-buttons button.edit:hover{ background: #4D978A; } .blox-buttons button.cancel:hover{ background: #e0474c; - color: #eee; + color: #fff; } .blox-buttons button.edit:disabled, .blox-buttons button.cancel:disabled{ background: #eee; @@ -1750,6 +1873,9 @@ button.post-button:hover{ width: 0.7142857142857142em; margin-right: 10px; } +.icon-upload { + width: 0.9285714285714285em; +} .format-bar .hidden{ display: none; } @@ -1764,7 +1890,7 @@ button.post-button:hover{ .format-bar .editactive{ position: relative; margin-left: -20px; - margin-right: 20px; + margin-right: -20px; } .format-bar.blox{ width: auto; @@ -2121,6 +2247,25 @@ button.delDL i:hover{ .blox blockquote p{ margin-left: 50px; } +.imageupload{ + width: 50%; + position: relative; + display: inline-block; + border-right: 1px dotted grey; + box-sizing:border-box; +} +.imageselect{ + width: 50%; + position: relative; + display: inline-block; + box-sizing:border-box; + border:0px; + padding: 0 0 0 0; + margin: 0 0 0 0; + min-height: 70px; + background: #f9f8f6; + font-family: Helvetica, Calibri, Arial, sans-serif; +} .dropbox{ min-height: 70px; background: #f9f8f6; @@ -2212,6 +2357,22 @@ button.delDL i:hover{ border-color: transparent transparent transparent rgba(255, 255, 255, 0.75); content: ' '; } +.medialib{ + margin: auto; + width: 100%; + height: 80%; + overflow: auto; + background: #f9f8f6; + max-width: 1200px; +} +.imagecard{ + margin: 10px; + box-shadow:0 2px 5px rgba(22,23,26,.05); + display: inline-block; + vertical-align: top; + background: #fff; +} + sup{} cite{} abbr{} diff --git a/system/author/js/author.js b/system/author/js/author.js index 0c85f9c..372e401 100644 --- a/system/author/js/author.js +++ b/system/author/js/author.js @@ -125,7 +125,7 @@ /********************************** ** START VERSION CHECK ** **********************************/ - + if(document.getElementById("system")) { getVersions('system', document.getElementsByClassName("fc-system-version")); @@ -243,7 +243,6 @@ } return segmentsA.length - segmentsB.length; } - /************************************* ** CARDS: ACTIVATE/OPEN CLOSE ** @@ -269,6 +268,38 @@ } } + /************************************* + ** Input Type File ** + *************************************/ + + var fileinputs = document.querySelectorAll( ".fileinput" ); + + for (i = 0; i < fileinputs.length; ++i) + { + (function () { + + thisfileinput = fileinputs[i]; + + var deletefilebutton = thisfileinput.getElementsByClassName("deletefilebutton")[0]; + var deletefileinput = thisfileinput.getElementsByClassName("deletefileinput")[0]; + var visiblefilename = thisfileinput.getElementsByClassName("visiblefilename")[0]; + var hiddenfile = thisfileinput.getElementsByClassName("hiddenfile")[0]; + + hiddenfile.onchange = function() + { + visiblefilename.value = this.files[0].name; + } + + deletefilebutton.onclick = function(event) + { + event.preventDefault(); + deletefileinput.value = 'delete'; + visiblefilename.value = ''; + } + + }()); + } + /************************************* ** COLOR PICKER ** *************************************/ diff --git a/system/author/js/vue-blox-config.js b/system/author/js/vue-blox-config.js index a64aac5..170b501 100644 --- a/system/author/js/vue-blox-config.js +++ b/system/author/js/vue-blox-config.js @@ -42,6 +42,13 @@ let determiner = { } return false; }, + file: function(block,lines,firstChar,secondChar,thirdChar){ + if( (firstChar == '[' && lines[0].indexOf('{.tm-download') != -1) ) + { + return "file-component"; + } + return false; + }, code: function(block,lines,firstChar,secondChar,thirdChar){ if( firstChar == '`' && secondChar == '`' && thirdChar == '`') { @@ -67,6 +74,7 @@ let bloxFormats = { quote: { label: '', title: 'Quote', component: 'quote-component' }, image: { label: '', title: 'Image', component: 'image-component' }, video: { label: '', title: 'Video', component: 'video-component' }, + file: { label: '', title: 'File', component: 'file-component' }, toc: { label: '', title: 'Table of Contents', component: 'toc-component' }, hr: { label: '', title: 'Horizontal Line', component: 'hr-component' }, definition: { label: '', title: 'Definition List', component: 'definition-component' }, diff --git a/system/author/js/vue-blox.js b/system/author/js/vue-blox.js index 9a5b6fc..a64347b 100644 --- a/system/author/js/vue-blox.js +++ b/system/author/js/vue-blox.js @@ -5,9 +5,9 @@ const contentComponent = Vue.component('content-block', { template: '
' + '
Choose a content-type
' + '
' + - '
' + - '' + - '' + + '
' + + '' + + '' + '
' + '
' + '
' + @@ -40,6 +40,10 @@ const contentComponent = Vue.component('content-block', { eventBus.$on('closeComponents', this.closeComponents); }, methods: { + disableSort: function(event) + { + this.$root.$data.sortdisabled = true; + }, addNewBlock: function(event) { /* we have to get from dom because block-data might not be set when user clicked on add button before opened the component */ @@ -76,9 +80,27 @@ const contentComponent = Vue.component('content-block', { updateMarkdown: function($event) { this.compmarkdown = $event; - this.$nextTick(function () { - this.$refs.preview.style.minHeight = this.$refs.component.offsetHeight + 'px'; - }); + this.setComponentSize(); + }, + setComponentSize: function() + { + if(this.componentType == 'image-component') + { + myself = this; + setTimeout(function(){ + myself.$nextTick(function () + { + myself.$refs.preview.style.minHeight = myself.$refs.component.offsetHeight + 'px'; + }); + }, 200); + } + else + { + this.$nextTick(function () + { + this.$refs.preview.style.minHeight = this.$refs.component.offsetHeight + 'px'; + }); + } }, switchToEditMode: function() { @@ -91,22 +113,7 @@ const contentComponent = Vue.component('content-block', { this.edit = true; /* show the edit-mode */ this.compmarkdown = self.$root.$data.blockMarkdown; /* get markdown data */ this.componentType = self.$root.$data.blockType; /* get block-type of element */ - if(this.componentType == 'image-component') - { - setTimeout(function(){ - self.$nextTick(function () - { - self.$refs.preview.style.minHeight = self.$refs.component.offsetHeight + 'px'; - }); - }, 200); - } - else - { - this.$nextTick(function () - { - this.$refs.preview.style.minHeight = self.$refs.component.offsetHeight + 'px'; - }); - } + this.setComponentSize(); }, closeComponents: function() { @@ -216,7 +223,8 @@ const contentComponent = Vue.component('content-block', { { var compmarkdown = this.compmarkdown.split('\n\n').join('\n'); } -*/ var compmarkdown = this.compmarkdown; +*/ + var compmarkdown = this.compmarkdown; var params = { 'url': document.getElementById("path").value, @@ -228,13 +236,33 @@ const contentComponent = Vue.component('content-block', { if(this.componentType == 'image-component' && self.$root.$data.file) { - var url = self.$root.$data.root + '/api/v1/image'; + var url = self.$root.$data.root + '/api/v1/image'; var method = 'PUT'; + params.new = false; + if(self.$root.$data.newblock || self.$root.$data.blockId == 99999) + { + params.new = true; + } } else if(this.componentType == 'video-component') { - var url = self.$root.$data.root + '/api/v1/video'; - var method = 'POST'; + var url = self.$root.$data.root + '/api/v1/video'; + var method = 'POST'; + params.new = false; + if(self.$root.$data.newblock || self.$root.$data.blockId == 99999) + { + params.new = true; + } + } + else if(this.componentType == 'file-component') + { + var url = self.$root.$data.root + '/api/v1/file'; + var method = 'PUT'; + params.new = false; + if(self.$root.$data.newblock || self.$root.$data.blockId == 99999) + { + params.new = true; + } } else if(self.$root.$data.newblock || self.$root.$data.blockId == 99999) { @@ -246,7 +274,7 @@ const contentComponent = Vue.component('content-block', { var url = self.$root.$data.root + '/api/v1/block'; var method = 'PUT'; } - + sendJson(function(response, httpStatus) { if(httpStatus == 400) @@ -1180,12 +1208,21 @@ const videoComponent = Vue.component('video-component', { }, }) + const imageComponent = Vue.component('image-component', { props: ['compmarkdown', 'disabled'], template: '
' + '' + - ' ' + - '

{{ $t(\'drag a picture or click to select\') }}

' + + '
' + + ' ' + + '

{{ $t(\'drag a picture or click to select\') }}

' + + '
' + + '' + + '' + + '' + + '' + '
' + '' + '
' + @@ -1201,6 +1238,7 @@ const imageComponent = Vue.component('image-component', { return { maxsize: 5, // megabyte imgpreview: false, + showmedialib: false, load: false, imgmeta: false, imgalt: '', @@ -1209,7 +1247,7 @@ const imageComponent = Vue.component('image-component', { imglink: '', imgclass: 'center', imgid: '', - imgfile: 'imgplchldr', + imgfile: '', } }, mounted: function(){ @@ -1280,6 +1318,10 @@ const imageComponent = Vue.component('image-component', { } }, methods: { + openmedialib: function() + { + this.showmedialib = true; + }, isChecked: function(classname) { if(this.imgclass == classname) @@ -1372,7 +1414,7 @@ const imageComponent = Vue.component('image-component', { errors = 'Maximum size of image caption is 140 characters'; } } - + if(errors) { this.$parent.freezePage(); @@ -1402,17 +1444,17 @@ const imageComponent = Vue.component('image-component', { } else { - self = this; - this.$parent.freezePage(); - this.$root.$data.file = true; - this.load = true; + self = this; + self.$parent.freezePage(); + self.$root.$data.file = true; + self.load = true; + let reader = new FileReader(); reader.readAsDataURL(imageFile); reader.onload = function(e) { + self.imgpreview = e.target.result; - self.$emit('updatedMarkdown', '![](imgplchldr)'); - /* load image to server */ var url = self.$root.$data.root + '/api/v1/image'; @@ -1420,22 +1462,23 @@ const imageComponent = Vue.component('image-component', { var params = { 'url': document.getElementById("path").value, 'image': e.target.result, + 'name': imageFile.name, 'csrf_name': document.getElementById("csrf_name").value, 'csrf_value': document.getElementById("csrf_value").value, }; - + var method = 'POST'; - + sendJson(function(response, httpStatus) - { + { if(httpStatus == 400) { - self.$parent.activatePage(); + self.activatePage(); } if(response) { - self.$parent.activatePage(); self.load = false; + self.$parent.activatePage(); var result = JSON.parse(response); @@ -1446,6 +1489,200 @@ const imageComponent = Vue.component('image-component', { else { self.imgmeta = true; + self.imgfile = result.name; + self.$emit('updatedMarkdown', '![]('+ result.name +')'); + } + } + }, method, url, params); + } + } + } + } + } +}) + +const fileComponent = Vue.component('file-component', { + props: ['compmarkdown', 'disabled'], + template: '
' + + '' + + '
' + + ' ' + + '

{{ $t(\'upload file\') }}

' + + '
' + + '' + + '' + + '' + + '' + + '
' + + '
' + + '
' + + '' + + '' + + '' + + '
', + data: function(){ + return { + maxsize: 5, // megabyte + showmedialib: false, + load: false, + filemeta: false, + filetitle: '', + fileextension: '', + fileurl: '', + fileid: '' + } + }, + mounted: function(){ + + this.$refs.markdown.focus(); + + if(this.compmarkdown) + { + this.filemeta = true; + + var filemarkdown = this.compmarkdown; + + var filetitle = filemarkdown.match(/\[.*?\]/); + if(filetitle) + { + filemarkdown = filemarkdown.replace(filetitle[0],''); + this.filetitle = filetitle[0].slice(1,-1); + } + + var fileattr = filemarkdown.match(/\{.*?\}/); + var fileextension = filemarkdown.match(/file-(.*)?\}/); + if(fileattr && fileextension) + { + filemarkdown = filemarkdown.replace(fileattr[0],''); + this.fileextension = fileextension[1].trim(); + } + + var fileurl = filemarkdown.match(/\(.*?\)/g); + if(fileurl) + { + filemarkdown = filemarkdown.replace(fileurl[0],''); + this.fileurl = fileurl[0].slice(1,-1); + } + } + }, + methods: { + openmedialib: function() + { + this.showmedialib = true; + }, + isChecked: function(classname) + { + if(this.fileclass == classname) + { + return ' checked'; + } + }, + updatemarkdown: function(event) + { + this.$emit('updatedMarkdown', event.target.value); + }, + createmarkdown: function() + { + var errors = false; + + if(this.filetitle.length < 101) + { + filemarkdown = '[' + this.filetitle + ']'; + } + else + { + errors = 'Maximum size of file-text is 100 characters'; + filemarkdown = '[]'; + } + if(this.fileurl != '') + { + if(this.fileurl.length < 101) + { + filemarkdown = '[' + this.filetitle + '](' + this.fileurl + ')'; + } + else + { + errors = 'Maximum size of file link is 100 characters'; + } + } + if(this.fileextension != '') + { + filemarkdown = filemarkdown + '{.tm-download file-' + this.fileextension + '}'; + } + if(errors) + { + this.$parent.freezePage(); + publishController.errors.message = errors; + } + else + { + publishController.errors.message = false; + this.$parent.activatePage(); + this.$emit('updatedMarkdown', filemarkdown); + } + }, + onFileChange: function( e ) + { + if(e.target.files.length > 0) + { + let uploadedFile = e.target.files[0]; + let size = uploadedFile.size / 1024 / 1024; + + if (size > this.maxsize) + { + publishController.errors.message = "The maximal size of a file is " + this.maxsize + " MB"; + } + else + { + self = this; + + self.$parent.freezePage(); + self.$root.$data.file = true; + self.load = true; + + let reader = new FileReader(); + reader.readAsDataURL(uploadedFile); + reader.onload = function(e) { + + /* load file to server */ + var url = self.$root.$data.root + '/api/v1/file'; + + var params = { + 'url': document.getElementById("path").value, + 'file': e.target.result, + 'name': uploadedFile.name, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + }; + + var method = 'POST'; + + sendJson(function(response, httpStatus) + { + if(httpStatus == 400) + { + self.activatePage(); + } + if(response) + { + self.load = false; + self.$parent.activatePage(); + + var result = JSON.parse(response); + + if(result.errors) + { + publishController.errors.message = result.errors; + } + else + { + self.filemeta = true; + self.filetitle = result.info.title; + self.fileextension = result.info.extension; + self.fileurl = result.info.url; + self.createmarkdown(); } } }, method, url, params); @@ -1456,6 +1693,493 @@ const imageComponent = Vue.component('image-component', { } }) +const medialib = Vue.component('medialib', { + props: ['parentcomponent'], + template: '
' + + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
{{errors}}
' + + '' + + '
' + + '' + + ' click to select' + + '' + + '
' + + '
{{ image.name }}
' + + '' + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + '
Name
{{ imagedetaildata.name}}
' + + '
URL
{{ imagedetaildata.src_live}}
' + + '
' + + '
' + + '
Size
{{ getSize(imagedetaildata.bytes) }}
' + + '
' + + '
' + + '
Dimensions
{{ imagedetaildata.width }}x{{ imagedetaildata.height }} px
' + + '
' + + '
' + + '
Type
{{ imagedetaildata.type }}
' + + '
' + + '
' + + '
Date
{{ getDate(imagedetaildata.timestamp) }}
' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '' + + '
' + + '

Image used in:

' + + '' + + '
No pages found.
'+ + '
' + + '
' + + '' + + '
' + + '' + + '
' + + '' + + '
' + + '
' + + '
{{ file.name }}
' + + '' + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
{{ filedetaildata.info.extension }}
' + + '
' + + '
' + + '
' + + '
Name
{{ filedetaildata.name}}
' + + '
URL
{{ filedetaildata.url}}
' + + '
' + + '
' + + '
Size
{{ getSize(filedetaildata.bytes) }}
' + + '
' + + '
' + + '
Type
{{ filedetaildata.info.extension }}
' + + '
' + + '
' + + '
Date
{{ getDate(filedetaildata.timestamp) }}
' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '' + + '
' + + '

File used in:

' + + '' + + '
No pages found.
'+ + '
' + + '
' + + '
' + + '
' + + '
', + data: function(){ + return { + imagedata: false, + showimages: true, + imagedetaildata: false, + showimagedetails: false, + filedata: false, + showfiles: false, + filedetaildata: false, + showfiledetails: false, + detailindex: false, + load: false, + baseurl: false, + search: '', + errors: false, + } + }, + mounted: function(){ + + if(this.parentcomponent == 'files') + { + this.showFiles(); + } + + this.errors = false; + var self = this; + + myaxios.get('/api/v1/medialib/images',{ + params: { + 'url': document.getElementById("path").value, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + } + }) + .then(function (response) + { + self.imagedata = response.data.images; + }) + .catch(function (error) + { + if(error.response) + { + self.errors = error.response.data.errors; + } + }); + }, + computed: { + filteredImages() { + + var searchimages = this.search; + var filteredImages = {}; + var images = this.imagedata; + Object.keys(images).forEach(function(key) { + var searchindex = key + ' ' + images[key].name; + if(searchindex.toLowerCase().indexOf(searchimages.toLowerCase()) !== -1) + { + filteredImages[key] = images[key]; + } + }); + return filteredImages; + }, + filteredFiles() { + + var searchfiles = this.search; + var filteredFiles = {}; + var files = this.filedata; + Object.keys(files).forEach(function(key) { + var searchindex = key + ' ' + files[key].name; + if(searchindex.toLowerCase().indexOf(searchfiles.toLowerCase()) !== -1) + { + filteredFiles[key] = files[key]; + } + }); + return filteredFiles; + } + }, + methods: { + isImagesActive: function() + { + if(this.showimages) + { + return 'bg-tm-green white'; + } + return 'bg-light-gray black'; + }, + isFilesActive: function() + { + if(this.showfiles) + { + return 'bg-tm-green white'; + } + return 'bg-light-gray black'; + }, + closemedialib: function() + { + this.$parent.showmedialib = false; + }, + getBackgroundImage: function(image) + { + return 'background-image: url(' + image.src_thumb + ');width:250px'; + }, + showImages: function() + { + this.errors = false; + this.showimages = true; + this.showfiles = false; + this.showimagedetails = false; + this.showfiledetails = false; + this.imagedetaildata = false; + this.detailindex = false; + }, + showFiles: function() + { + this.showimages = false; + this.showfiles = true; + this.showimagedetails = false; + this.showfiledetails = false; + this.imagedetaildata = false; + this.filedetaildata = false; + this.detailindex = false; + + if(!this.files) + { + this.errors = false; + var filesself = this; + + myaxios.get('/api/v1/medialib/files',{ + params: { + 'url': document.getElementById("path").value, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + } + }) + .then(function (response) + { + filesself.filedata = response.data.files; + }) + .catch(function (error) + { + if(error.response) + { + filesself.errors = error.response.data.errors; + } + }); + } + }, + showImageDetails: function(image,index) + { + this.errors = false; + this.showimages = false; + this.showfiles = false; + this.showimagedetails = true; + this.detailindex = index; + this.baseurl = myaxios.defaults.baseURL + '/tm/content/visual'; + + var imageself = this; + + myaxios.get('/api/v1/image',{ + params: { + 'url': document.getElementById("path").value, + 'name': image.name, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + } + }) + .then(function (response) + { + imageself.imagedetaildata = response.data.image; + }) + .catch(function (error) + { + if(error.response) + { + imageself.errors = error.response.data.errors; + } + }); + }, + showFileDetails: function(file,index) + { + this.errors = false; + this.showimages = false; + this.showfiles = false; + this.showimagedetails = false; + this.showfiledetails = true; + this.detailindex = index; + + this.baseurl = myaxios.defaults.baseURL + '/tm/content/visual'; + + var fileself = this; + + myaxios.get('/api/v1/file',{ + params: { + 'url': document.getElementById("path").value, + 'name': file.name, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + } + }) + .then(function (response) + { + fileself.filedetaildata = response.data.file; + }) + .catch(function (error) + { + if(error.response) + { + fileself.errors = error.response.data.errors; + } + }); + }, + selectImage: function(image) + { + this.showImages(); + + if(this.parentcomponent == 'images') + { + var imgmarkdown = {target: {value: '![alt]('+ image.src_live +')' }}; + + this.$parent.imgfile = image.src_live; + this.$parent.imgpreview = image.src_live; + this.$parent.imgmeta = true; + + this.$parent.showmedialib = false; + + this.$parent.updatemarkdown(imgmarkdown); + } + if(this.parentcomponent == 'files') + { + var filemarkdown = {target: {value: '[' + image.name + '](' + image.src_live +'){.tm-download}' }}; + + this.$parent.filemeta = true; + this.$parent.filetitle = image.name; + + this.$parent.showmedialib = false; + + this.$parent.updatemarkdown(filemarkdown); + } + }, + selectFile: function(file) + { + /* if image component is open */ + if(this.parentcomponent == 'images') + { + var imgextensions = ['png','jpg', 'jpeg', 'gif', 'svg']; + if(imgextensions.indexOf(file.info.extension) == -1) + { + this.errors = "you cannot insert a file into an image component"; + return; + } + var imgmarkdown = {target: {value: '![alt]('+ file.url +')' }}; + + this.$parent.imgfile = file.url; + this.$parent.imgpreview = file.url; + this.$parent.imgmeta = true; + + this.$parent.showmedialib = false; + + this.$parent.updatemarkdown(imgmarkdown); + } + if(this.parentcomponent == 'files') + { + var filemarkdown = {target: {value: '['+ file.name +']('+ file.url +'){.tm-download file-' + file.info.extension + '}' }}; + + this.$parent.showmedialib = false; + + this.$parent.filemeta = true; + this.$parent.filetitle = file.info.filename + ' (' + file.info.extension.toUpperCase() + ')'; + + this.$parent.updatemarkdown(filemarkdown); + } + this.showFiles(); + }, + removeImage: function(index) + { + this.imagedata.splice(index,1); + }, + removeFile: function(index) + { + this.filedata.splice(index,1); + }, + deleteImage: function(image, index) + { + imageself = this; + + myaxios.delete('/api/v1/image',{ + data: { + 'url': document.getElementById("path").value, + 'name': image.name, + 'index': index, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + } + }) + .then(function (response) + { + imageself.showImages(); + imageself.removeImage(index); + }) + .catch(function (error) + { + if(error.response) + { + imageself.errors = error.response.data.errors; + } + }); + }, + deleteFile: function(file, index) + { + fileself = this; + + myaxios.delete('/api/v1/file',{ + data: { + 'url': document.getElementById("path").value, + 'name': file.name, + 'index': index, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + } + }) + .then(function (response) + { + fileself.showFiles(); + fileself.removeFile(index); + }) + .catch(function (error) + { + if(error.response) + { + fileself.errors = error.response.data.errors; + } + }); + }, + getDate(timestamp) + { + date = new Date(timestamp * 1000); + + datevalues = { + 'year': date.getFullYear(), + 'month': date.getMonth()+1, + 'day': date.getDate(), + 'hour': date.getHours(), + 'minute': date.getMinutes(), + 'second': date.getSeconds(), + }; + return datevalues.year + '-' + datevalues.month + '-' + datevalues.day; + }, + getSize(bytes) + { + var i = Math.floor(Math.log(bytes) / Math.log(1024)), + sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i]; + }, + isChecked: function(classname) + { + if(this.imgclass == classname) + { + return ' checked'; + } + }, + }, +}) + + let activeFormats = []; for(var i = 0; i < formatConfig.length; i++) @@ -1576,11 +2300,13 @@ let editor = new Vue({ }, method, url, params); }, methods: { - onStart: function(evt) + onStart: function() { + }, moveBlock: function(evt) { + var params = { 'url': document.getElementById("path").value, 'old_index': evt.oldIndex, diff --git a/system/author/layouts/layout.twig b/system/author/layouts/layout.twig index 79f6800..67650ad 100644 --- a/system/author/layouts/layout.twig +++ b/system/author/layouts/layout.twig @@ -17,6 +17,7 @@ + diff --git a/system/author/layouts/layoutBlox.twig b/system/author/layouts/layoutBlox.twig index 2d9b7e8..ab88393 100644 --- a/system/author/layouts/layoutBlox.twig +++ b/system/author/layouts/layoutBlox.twig @@ -17,6 +17,7 @@ + @@ -68,14 +69,20 @@ {{ __('FOLDER') }} - - + + {{ __('UPLOAD') }} + + {{ __('IMAGE') }} + + {{ __('PAPERCLIP') }} + + {{ __('VIDEO') }} @@ -142,12 +149,31 @@ {{ __('CROSS') }} + + {{ __('TRASH') }} + + + + {{ __('INFO') }} + + + + - eye-blocked + {{ __('EYE_BLOCKED') }} - + + + {{ __('SEARCH') }} + + + + {{ __('CANCEL') }} + + + {{ assets.renderSvg() }} diff --git a/system/author/settings/system.twig b/system/author/settings/system.twig index da810db..424a954 100644 --- a/system/author/settings/system.twig +++ b/system/author/settings/system.twig @@ -10,7 +10,7 @@
-
+
@@ -63,6 +63,34 @@
+
+ +
+ + + +
+ {{ __('BROWSE') }} + +
+
+ {% if errors.settings.favicon %} + {{ errors.settings.favicon | first }} + {% endif %}

@@ -102,5 +130,4 @@
- {% endblock %} diff --git a/system/system.php b/system/system.php index c01da87..da86c2d 100644 --- a/system/system.php +++ b/system/system.php @@ -199,8 +199,9 @@ $container['view'] = function ($container) $view->addExtension(new Slim\Views\TwigExtension($container['router'], $basePath)); $view->addExtension(new Twig_Extension_Debug()); $view->addExtension(new Typemill\Extensions\TwigUserExtension()); - $view->addExtension(new Typemill\Extensions\TwigMarkdownExtension()); + $view->addExtension(new Typemill\Extensions\TwigMarkdownExtension()); $view->addExtension(new Typemill\Extensions\TwigMetaExtension()); + $view->addExtension(new Typemill\Extensions\TwigPagelistExtension()); // i18n $view->addExtension(new Typemill\Extensions\TwigLanguageExtension( $container->get('settings')['labels'] )); diff --git a/themes/typemill/cover.twig b/themes/typemill/cover.twig index c5a3cd0..3e6c3eb 100644 --- a/themes/typemill/cover.twig +++ b/themes/typemill/cover.twig @@ -2,7 +2,15 @@ {% block content %} -

{{ title }}

+ {% if logo and settings.themes.typemill.coverlogo %} + + + + {% else %} + +

{{ title }}

+ + {% endif %}
diff --git a/themes/typemill/css/style.css b/themes/typemill/css/style.css index 13ce32f..cb01334 100644 --- a/themes/typemill/css/style.css +++ b/themes/typemill/css/style.css @@ -578,6 +578,41 @@ cite{} abbr{} hr{} +img.logo{ + width: 100%; +} +img.coverlogo{ + margin-top: 4em; +} +/**************************** +* download-commponent * +****************************/ + +a.tm-download +{ + line-height: 35px; + margin-left: 40px; +} +a.tm-download::before{ + content: '\2193'; + position: absolute; + margin-left: -40px; + width: 30px; + height: 30px; + line-height: 30px; + font-family: "Comic Sans MS",cursive,sans-serif; + border: 2px solid #e0474c; + border-radius: 50%; + text-align: center; + text-decoration: underline; +} +a.tm-download:hover::before{ + text-decoration:underline; + color: #fff; + background: #e0474c; +} + + /************************ * TABLE OF CONTENTS * ************************/ diff --git a/themes/typemill/img/apple-touch-icon-144x144.png b/themes/typemill/img/apple-touch-icon-144x144.png deleted file mode 100644 index cb836ce..0000000 Binary files a/themes/typemill/img/apple-touch-icon-144x144.png and /dev/null differ diff --git a/themes/typemill/img/apple-touch-icon-152x152.png b/themes/typemill/img/apple-touch-icon-152x152.png deleted file mode 100644 index fd6cef2..0000000 Binary files a/themes/typemill/img/apple-touch-icon-152x152.png and /dev/null differ diff --git a/themes/typemill/img/favicon-16x16.png b/themes/typemill/img/favicon-16x16.png deleted file mode 100644 index 85ce61a..0000000 Binary files a/themes/typemill/img/favicon-16x16.png and /dev/null differ diff --git a/themes/typemill/img/favicon-32x32.png b/themes/typemill/img/favicon-32x32.png deleted file mode 100644 index 23ca6ce..0000000 Binary files a/themes/typemill/img/favicon-32x32.png and /dev/null differ diff --git a/themes/typemill/img/favicon.ico b/themes/typemill/img/favicon.ico deleted file mode 100644 index 22a5bc9..0000000 Binary files a/themes/typemill/img/favicon.ico and /dev/null differ diff --git a/themes/typemill/img/mstile-144x144.png b/themes/typemill/img/mstile-144x144.png deleted file mode 100644 index cb836ce..0000000 Binary files a/themes/typemill/img/mstile-144x144.png and /dev/null differ diff --git a/themes/typemill/partials/layout.twig b/themes/typemill/partials/layout.twig index 30dfb3d..3478689 100644 --- a/themes/typemill/partials/layout.twig +++ b/themes/typemill/partials/layout.twig @@ -10,14 +10,15 @@ - - - - - - - + {% if favicon %} + + + + + + {% endif %} + @@ -41,7 +42,13 @@
-

{{ settings.title }}

+

+ {% if logo %} + + {% else %} + {{ settings.title }} + {% endif %} +

diff --git a/themes/typemill/partials/layoutCover.twig b/themes/typemill/partials/layoutCover.twig index 0756b05..e0b8883 100644 --- a/themes/typemill/partials/layoutCover.twig +++ b/themes/typemill/partials/layoutCover.twig @@ -10,13 +10,14 @@ - - - - - - + {% if favicon %} + + + + + + {% endif %} diff --git a/themes/typemill/typemill.yaml b/themes/typemill/typemill.yaml index daeb2ae..829b7bf 100644 --- a/themes/typemill/typemill.yaml +++ b/themes/typemill/typemill.yaml @@ -1,5 +1,5 @@ name: Typemill Theme -version: 1.2.2 +version: 1.2.3 description: The standard theme for Typemill. Responsive, minimal and without any dependencies. It uses the system fonts Calibri and Helvetica. No JavaScript is used. author: Sebastian Schürmanns homepage: https://typemill.net @@ -27,6 +27,11 @@ forms: label: Different Design for Startpage checkboxlabel: Activate Special Startpage-Design + coverlogo: + type: checkbox + label: Logo on startpage + checkboxlabel: Show logo instead of title on startpage + start: type: text label: Label for Start Button