diff --git a/content/00-welcome/00-setup.md b/content/00-welcome/00-setup.md new file mode 100644 index 0000000..f30d915 --- /dev/null +++ b/content/00-welcome/00-setup.md @@ -0,0 +1,25 @@ +# Setup + +Congratulations! If you see this page, then the setup of the system has worked successfully!! You can now setup and configure your system, your themes and your plugins in the [settings-area](/tm/settings). + +Anyway, if you read this file in the source code and if you did not manage to setup the system successfully, then try the following. + +## If it does not work + +If you face any problems, then please make sure, that your system supports these features: + +- PHP version 7+. +- Apache server. +- The module `mod_rewrite` and `htaccess`. + +If you run a linux-system like Debian or Ubuntu, then please double check that `mod_rewrite` and `htaccess` are activated. Check this [issue on GitHub](https://github.com/typemill/typemill/issues/16) for help. + +Please make the following folders writable with permission 774 (you can use your ftp-software for it): + +- Cache +- Content +- Media +- Settings + +If you still get an error, then you can post an issue on [GitHub](https://github.com/typemill/typemill). + diff --git a/content/00-welcome/01-write-content.md b/content/00-welcome/01-write-content.md new file mode 100644 index 0000000..0f24144 --- /dev/null +++ b/content/00-welcome/01-write-content.md @@ -0,0 +1,41 @@ +# Write Content + +Typemill is a simple Flat File Content Management System (CMS). We (the community) work hard to provide the best author experience with easy and intuitive authoring tools. But Typemill is still in early development and it is likely that not everything will work perfectly out of the box. If you miss something or if you have ideas for improvements, then post a new issue on [GitHub](https://github.com/typemill/typemill/issues). + +## The Navigation + +You can create, structure and reorder all pages with the navigation on the left. To structure your content, you can create new folders and files with the "add item" button. To reorder the pages, just drag an item and drop it wherever you want. Play around with it and you will notice, that it works pretty similar to the folder- and file-system of your laptop. And in fact, this is exactly what Typemill does in the background: It stores your content in files and folders on the server. + +However, there are some limitations when you try to reorder elements. For example, you cannot move a complete folder to another folder. Click on the question-mark at the top of the navigation for detailed information. + +## The Editor + +You can create and format your content with the Markdown syntax, that is similar to the markup syntax of Wikipedia. If you are not familiar with Markdown, then please read the short [Markdown-tutorial](https://typemill.net/) in the documentation of Typemill. You can learn Markdown in less than 10 minutes and there is no easier and faster way to format your webpage. You will love it! + +Typemill provides two edit modes: The **raw mode** and the **visual mode**. You can switch between the modes in the publish-bar at the bottom of each page. The **raw mode** is the most robust way to create your content, because you write raw markdown into a simple textarea. The **visual mode** uses blocks and transforms each content block into a html-preview immediately. This means that you can directly see and check the formatted result. + +By default Typemill will use the **visual mode**. + +* You can change the default mode in the system settings. +* You can also switch each format button on and off in the system settings. + +## The Publish Bar + +The publish bar of Typemill is pretty intuitiv and sticks at the bottom of the screen so that you have always full control of the status of each page. Simply play around with it and you will quickly understand how it works. In short: + +* The green button "online" indicates, that your page is published and visible for your readers. +* You can depublish a page by clicking the green "online" button. The button turns grey with the label "offline" then. +* With the green button "Publish" you can either publish a page that is offline or you can publish some unpublished changes of the page. +* The publish-button is grey, if the page is online and if there are no unpublished changes. +* All buttons will change in real time, so you can always exactly see what is going on. +* To provide an easy status-overview of the whole website, Typemill marks all pages in the navigation on the left side as published (green), changed (orange) and unpublished (red). + +## Working with Drafts + +Ever tried to revise a published article in WordPress? Yes, it works, but if you click on "save", then all your changes are directly live. Typemill is much more flexible here and allows you to keep your original version live while you work on a **drafted Version** in the background. This is how Typemill handles it: + +* In **visual mode**: Typemill stores your changes in a new draft automatically as soon as you save any content-block. +* In **raw mode**: To store changes in a new draft, simply click on the "save draft"-button in the publish controller. +* You can work on a draft as long as you want without changing the live version. Your changes go live if you click the button "publish". +* In visual mode, you can also use the discard-button and go back to the published version. + diff --git a/content/00-welcome/02-get-help.md b/content/00-welcome/02-get-help.md new file mode 100644 index 0000000..148b2c6 --- /dev/null +++ b/content/00-welcome/02-get-help.md @@ -0,0 +1,10 @@ +# Get Help + +If you need any help, then please read the [documentation on typemill.net](https://typemill.net/typemill) first. Some short video-tutorials are in work right now. + +If you found a bug or if you have a question, then please open a new issue on [GitHub](https://github.com/typemill/typemill/issues). + +Do you need professional help, an individual theme or a special plugin? You can hire us at [Trendschau Digital](https://trendschau.net/typemill-development). + +[Contributions](https://github.com/typemill/typemill#contributors--supporters), [donations](https://www.paypal.me/typemill) and [feedback](https://github.com/typemill/typemill/issues) for this open source project are always welcome. + diff --git a/content/00-welcome/03-markdown-test.md b/content/00-welcome/03-markdown-test.md new file mode 100644 index 0000000..b6458bb --- /dev/null +++ b/content/00-welcome/03-markdown-test.md @@ -0,0 +1,304 @@ +# Markdown Reference and Test Page + +Markdown is a simple and universal syntax for text formatting. More and more writers switch to markdown, because they can format their text during the writing process without using any format-buttons. Once they are familiar with the markdown syntax, they can write formatted text much easier and faster than with any standard HTML-editor. + +Developers love markdown, because it is much cleaner and saver than HTML. And they can easily convert markdown to a lot of other document formats like HTML and others. + +If you develop a theme for TYPEMILL, please take care that all elements on this page are designed properly. + +## Table of Contents + +To create a table of contents, simply write `[TOC]` in a separate line. It will be replaced with a table of contents like this automatically. + +[TOC] + +## Headlines + +``` +Headlines are simply done with hash chars like this: +# First Level Headline +## Second Level Headline +### Third Level Headline +#### Fourth Level Headline +##### Fifth Level Headline +###### Sixth Level Headline +``` + +### Third Level Headline + +A third headline is more decent and lower prioritized than a second level headline. + +#### Fourth Level Headline + +A fourth level headline is more decent and lower prioritized than a third level headline. + +##### Fifth Level Headline + +A fifth level headline is more decent and lower prioritized than a fourth level headline. + +##### Sixth Level Headline + +A sixth level headline is more decent and lower prioritized than a fifths level headline. + +##Paragraph + +```` +A paragraph is a simple text-block separated with a new line above and below. +```` + +A paragraph is a simple text-block separated with a new line above and below. + +## Soft Linebreak + +```` +For a soft linebreak (eg. for dialoges in literature), add two spaces at the end of a line and use a simple return. +She said: "Hello" +He said: "again" +```` + +For a soft linebreak (eg. for dialoges in literature), add two spaces at the end of a line and use a simple return. + +She said: "Hello" +He said: "again" + +##Emphasis + +```` +For italic text use one *asterix* or one _underscore_. +For bold text use two **asterix** or two __underscores__. +```` + +For italic text use one *asterix* or one _underscore_. + +For bold text use two **asterix** or two __underscores__. + +##Lists + +```` +For an unordered list use a dash +- like +- this +Or use one asterix +* like +* this +For an ordered list use whatever number you want and add a dot: +1. like +1. this +```` + +For an unordered list use a dash + +- like +- this + +Or use one asterix + +* like +* this + +For an ordered list use whatever number you want and add a dot: + +1. like +2. this + +## Horizontal Rule + +``` +Easily created for example with three dashes like this: +--- +``` + +Easily created for example with three dashes like this: + +--- + +##Links + +```` +This is an ordinary [Link](http://typemill.net). +Links can also be [relative](/info). +You can also add a [title](http://typemill.net "typemill"). +You can even add [ids or classes](http://typemill.net){#myid .myclass}. +Or you can use a shortcut like http://typemill.net. +```` + +This is an ordinary [Link](http://typemill.net). + +Links can also be [relative](/info). + +You can also add a [title](http://typemill.net "typemill"). + +You can even add [ids or classes](http://typemill.net){#myid .myclass}. + +Or you can use a shortcut like http://typemill.net. + +##Images + +```` +The same rules as with links, but with a ! +![alt-text](media/markdown.png) +![alt-text](media/markdown.png "my title"){#myid .imgClass} +![alt-text](media/markdown.png "my title"){#myid .otherclass width=150px} +```` + +The same rules as with links, but with a ! + +![alt-text](media/markdown.png) + +![alt-text](media/markdown.png "my title"){#myid .imgClass} + +![alt-text](media/markdown.png "my title"){#myid .otherclass width=150px} + +## Linked Images + +```` +You can link an image with a nested syntax like this: +[![alt-text](media/markdown.png)](https://typemill.net) +```` + +You can link an image with a nested syntax like this: + +[![alt-text](media/markdown.png){.imgClass}](https://typemill.net) + +## Image Position + +```` +You can controll the image position with the classes .left, .right and .middle like this: +![alt-text](media/markdown.png){.left} +![alt-text](media/markdown.png){.right} +![alt-text](media/markdown.png){.middle} +```` + +![image float left](media/markdown.png){.left} + +The first image should float on the left side of this paragraph. This might not work with all themes. If you are a theme developer, please ensure that you support the image classes "left", "right" and "middle". You can add these classes manually in the raw mode or you can assign them in the visual mode when you edit a picture (double click on it to open the dialog.) + +![image float right](media/markdown.png){.right} + +The second image should float on the right side of this paragraph. This might not work with all themes. If you are a theme developer, please ensure that you support the image classes "left", "right" and "middle". You can add these classes manually in the raw mode or you can assign them in the visual mode when you edit a picture (double click on it to open the dialog.) + +![image middle](media/markdown.png){.middle} + +The thirds image should be placed above this paragraph and centered to the middle of the content area. This might not work with all themes. If you are a theme developer, please ensure that you support the image classes "left", "right" and "middle". + +## Blockquote + +``` +There are always some women and men with wise words +> But I usually don't read them, to be honest. +``` + +There always some women and men with wise words + +> But I usually don't read them, to be honest. + +##Footnotes + +```` +You can write footnotes[^1] with markdown. +Scroll down to the end of the page[^2] and look for the footnotes. +Add the footnote text at the bottom of the page like this: +[^1]: Thank you for scrolling. +[^2]: This is the end of the page. +```` + +You can write footnotes[^1] with markdown. + +Scroll down to the end of the page[^2] and look for the footnotes. + +Footnotes won't work with the visual editor right now, so please use the raw mode for them. + +## Abbreviations + +```` +*[HTML]: Hyper Text Markup Language +*[W3C]: World Wide Web Consortium +```` + +You won't see the abbreviation directly, but if you write HTML or W3C somewhere, then you can see the tooltip with the explanation. + +*[HTML]: Hyper Text Markup Language + +*[W3C]: World Wide Web Consortium + +## Definition List + +```` +Apple +: Pomaceous fruit of plants of the genus Malus in the family Rosaceae. +Orange +: The fruit of an evergreen tree of the genus Citrus. +```` + +Apple +: Pomaceous fruit of plants of the genus Malus in +the family Rosaceae. + +Orange +: The fruit of an evergreen tree of the genus Citrus. + + + +## Tables + +```` +|name |usage | +|-----------|-----------| +| My Name | For Me | +| Your Name | For You | +```` + +| Name | Usage | +| --------- | ------- | +| My Name | For Me | +| Your Name | For You | + +## Code + +```` +Let us create some `` like this +```` + +Let us create some `` and now let us check, if a codeblock works: + +```` +Use four apostroph like this: +\```` + +\```` +```` + +## Math + +Please activate the math-plugin to use mathematical expressions with LaTeX syntax. You can choose between MathJax or the newer KaTeX library. MathJax is included from a CDN, KaTeX is included in the plugin. So if you don't want to fetch code from a CDN, use KaTeX instead. The markdown syntax in TYPEMILL is the same for both libraries. + +```` +Write inline math with \(...\) or $...$ syntax. +inline $x = \int_{0^1}^1(-b \pm \sqrt{b^2-4ac})/(2a)$ math +inline \(x = \int_{0^1}^1(-b \pm \sqrt{b^2-4ac})/(2a)\) math +```` + +inline $x = \int_{0^1}^1(-b \pm \sqrt{b^2-4ac})/(2a)$ math + +inline \(x = \int_{0^1}^1(-b \pm \sqrt{b^2-4ac})/(2a)\) math + +```` +Write display math with $$...$$ or \[...\] syntax. +$$ +x = \int_{0^1}^1(-b \pm \sqrt{b^2-4ac})/(2a) +$$ +\[ +x = \int_{0^1}^1(-b \pm \sqrt{b^2-4ac})/(2a) +\] +```` + +$$ +x = \int_{0^1}^1(-b \pm \sqrt{b^2-4ac})/(2a) +$$ + +[^1]: Thank you for scrolling. +[^2]: This is the end of the page. + diff --git a/content/00-welcome/index.md b/content/00-welcome/index.md new file mode 100644 index 0000000..9509085 --- /dev/null +++ b/content/00-welcome/index.md @@ -0,0 +1,4 @@ +# Welcome + +Great that you give Typemill a try!! Typemill is a small open source cms and a project in work. You will probably miss some important features, but I am working hard to add everything that is needed for a handy and productive writing-system. + diff --git a/system/Controllers/ContentApiController.php b/system/Controllers/ContentApiController.php index 3025288..8f9b6e6 100644 --- a/system/Controllers/ContentApiController.php +++ b/system/Controllers/ContentApiController.php @@ -13,6 +13,8 @@ use Typemill\Events\OnPagePublished; use Typemill\Events\OnPageUnpublished; use Typemill\Events\OnPageDeleted; use Typemill\Events\OnPageSorted; +use \URLify; + class ContentApiController extends ContentController { @@ -412,13 +414,14 @@ class ContentApiController extends ContentController return $response->withJson(array('data' => $internalStructure, 'errors' => false, 'url' => $url)); } - - public function createArticle(Request $request, Response $response, $args) + + + public function createPost(Request $request, Response $response, $args) { # get params from call $this->params = $request->getParams(); $this->uri = $request->getUri(); - + # url is only needed, if an active page is moved $url = false; @@ -426,7 +429,76 @@ class ContentApiController extends ContentController if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); } # validate input - if(!$this->validateNaviItem()){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Special Characters not allowed. Length between 1 and 20 chars.', 'url' => $url), 422); } + if(!$this->validateNaviItem()){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Special Characters not allowed. Length between 1 and 60 chars.', 'url' => $url), 422); } + + # get the ids (key path) for item, old folder and new folder + $folderKeyPath = explode('.', $this->params['folder_id']); + + # get the item from structure + $folder = Folder::getItemWithKeyPath($this->structure, $folderKeyPath); + + if(!$folder){ return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not find this page. Please refresh and try again.', 'url' => $url), 404); } + + $name = $this->params['item_name']; + $slug = URLify::filter(iconv(mb_detect_encoding($this->params['item_name'], mb_detect_order(), true), "UTF-8", $this->params['item_name'])); + $namePath = date("YmdHi") . '-' . $slug; + $folderPath = 'content' . $folder->path; + $content = json_encode(['# ' . $name, 'Content']); + + # initialise write object + $write = new WriteYaml(); + + # check, if name exists + if($write->checkFile($folderPath, $namePath . '.txt') OR $write->checkFile($folderPath, $namePath . '.md')) + { + return $response->withJson(array('data' => $this->structure, 'errors' => 'There is already a page with this name. Please choose another name.', 'url' => $url), 404); + } + + if(!$write->writeFile($folderPath, $namePath . '.txt', $content)) + { + return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not create the file. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404); + } + + + # get extended structure + $extended = $write->getYaml('cache', 'structure-extended.yaml'); + + # create the url for the item + $urlWoF = $folder->urlRelWoF . '/' . $slug; + + # add the navigation name to the item htmlspecialchars needed for frensh language + $extended[$urlWoF] = ['hide' => false, 'navtitle' => $name]; + + # store the extended structure + $write->updateYaml('cache', 'structure-extended.yaml', $extended); + + + # update the structure for editor + $this->setStructure($draft = true, $cache = false); + + $folder = Folder::getItemWithKeyPath($this->structure, $folderKeyPath); + + # activate this if you want to redirect after creating the page... + # $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor'] . $folder->urlRelWoF . '/' . $slug; + + return $response->withJson(array('posts' => $folder, $this->structure, 'errors' => false, 'url' => $url)); + } + + + public function createArticle(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri(); + + # url is only needed, if an active page is moved + $url = false; + + # set structure + if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); } + + # validate input + if(!$this->validateNaviItem()){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Special Characters not allowed. Length between 1 and 60 chars.', 'url' => $url), 422); } # get the ids (key path) for item, old folder and new folder $folderKeyPath = explode('.', $this->params['folder_id']); @@ -440,16 +512,19 @@ class ContentApiController extends ContentController # get the content of the target folder $folderContent = $folder->folderContent; + $name = $this->params['item_name']; + $slug = URLify::filter(iconv(mb_detect_encoding($this->params['item_name'], mb_detect_order(), true), "UTF-8", $this->params['item_name'])); + # create the name for the new item - $nameParts = Folder::getStringParts($this->params['item_name']); - $name = implode("-", $nameParts); - $slug = $name; + # $nameParts = Folder::getStringParts($this->params['item_name']); + # $name = implode("-", $nameParts); + # $slug = $name; # initialize index $index = 0; # initialise write object - $write = new Write(); + $write = new WriteYaml(); # iterate through the whole content of the new folder $writeError = false; @@ -472,13 +547,15 @@ class ContentApiController extends ContentController if($writeError){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Something went wrong. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404); } # add prefix number to the name - $namePath = $index > 9 ? $index . '-' . $name : '0' . $index . '-' . $name; +# $namePath = $index > 9 ? $index . '-' . $name : '0' . $index . '-' . $name; + $namePath = $index > 9 ? $index . '-' . $slug : '0' . $index . '-' . $slug; $folderPath = 'content' . $folder->path; - $title = implode(" ", $nameParts); +# $title = implode(" ", $nameParts); # create default content - $content = json_encode(['# ' . $title, 'Content']); +# $content = json_encode(['# ' . $title, 'Content']); + $content = json_encode(['# ' . $name, 'Content']); if($this->params['type'] == 'file') { @@ -494,8 +571,27 @@ class ContentApiController extends ContentController return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not create the folder. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404); } $write->writeFile($folderPath . DIRECTORY_SEPARATOR . $namePath, 'index.txt', $content); + + # always redirect to a folder + $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor'] . $folder->urlRelWoF . '/' . $slug; + } + + # get extended structure + $extended = $write->getYaml('cache', 'structure-extended.yaml'); + + # create the url for the item + $urlWoF = $folder->urlRelWoF . '/' . $slug; + + # add the navigation name to the item htmlspecialchars needed for frensh language + $extended[$urlWoF] = ['hide' => false, 'navtitle' => $name]; + + # store the extended structure + $write->updateYaml('cache', 'structure-extended.yaml', $extended); + + + # update the structure for editor $this->setStructure($draft = true, $cache = false); @@ -506,7 +602,7 @@ class ContentApiController extends ContentController } # activate this if you want to redirect after creating the page... - # $url = $this->uri->getBaseUrl() . '/tm/content' . $folder->urlRelWoF . '/' . $name; + # $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor'] . $folder->urlRelWoF . '/' . $slug; return $response->withJson(array('data' => $this->structure, 'errors' => false, 'url' => $url)); } @@ -527,15 +623,18 @@ class ContentApiController extends ContentController if(!$this->validateBaseNaviItem()){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Special Characters not allowed. Length between 1 and 20 chars.', 'url' => $url), 422); } # create the name for the new item - $nameParts = Folder::getStringParts($this->params['item_name']); - $name = implode("-", $nameParts); - $slug = $name; +# $nameParts = Folder::getStringParts($this->params['item_name']); +# $name = implode("-", $nameParts); +# $slug = $name; + + $name = $this->params['item_name']; + $slug = URLify::filter(iconv(mb_detect_encoding($this->params['item_name'], mb_detect_order(), true), "UTF-8", $this->params['item_name'])); # initialize index $index = 0; # initialise write object - $write = new Write(); + $write = new WriteYaml(); # iterate through the whole content of the new folder $writeError = false; @@ -558,11 +657,12 @@ class ContentApiController extends ContentController if($writeError){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Something went wrong. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404); } # add prefix number to the name - $namePath = $index > 9 ? $index . '-' . $name : '0' . $index . '-' . $name; + $namePath = $index > 9 ? $index . '-' . $slug : '0' . $index . '-' . $slug; $folderPath = 'content'; # create default content - $content = json_encode(['# Add Title', 'Add Content']); +# $content = json_encode(['# Add Title', 'Add Content']); + $content = json_encode(['# ' . $name, 'Content']); if($this->params['type'] == 'file') { @@ -578,8 +678,25 @@ class ContentApiController extends ContentController return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not create the folder. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404); } $write->writeFile($folderPath . DIRECTORY_SEPARATOR . $namePath, 'index.txt', $content); + + # activate this if you want to redirect after creating the page... + $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor'] . '/' . $slug; } + + # get extended structure + $extended = $write->getYaml('cache', 'structure-extended.yaml'); + + # create the url for the item + $urlWoF = '/' . $slug; + + # add the navigation name to the item htmlspecialchars needed for frensh language + $extended[$urlWoF] = ['hide' => false, 'navtitle' => $name]; + + # store the extended structure + $write->updateYaml('cache', 'structure-extended.yaml', $extended); + + # update the structure for editor $this->setStructure($draft = true, $cache = false); @@ -1006,8 +1123,6 @@ class ContentApiController extends ContentController 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, if title parse title. */ if($this->params['block_id'] == 0) @@ -1016,6 +1131,9 @@ class ContentApiController extends ContentController } else { + /* set safe mode to escape javascript and html in markdown */ + $parsedown->setSafeMode(true); + $blockArray = $parsedown->text($blockMarkdown); } diff --git a/system/Controllers/ContentController.php b/system/Controllers/ContentController.php index 3fe2765..78ea990 100644 --- a/system/Controllers/ContentController.php +++ b/system/Controllers/ContentController.php @@ -377,13 +377,18 @@ abstract class ContentController { $files = array_diff(scandir($path), array('.', '..')); - # check if there are folders first, then stop the operation + # check if there are published pages or folders inside, then stop the operation foreach ($files as $file) { if(is_dir(realpath($path) . DIRECTORY_SEPARATOR . $file)) { $this->errors = ['message' => 'Please delete the sub-folder first.']; } + + if(substr($file, -3) == '.md' ) + { + $this->errors = ['message' => 'Please unpublish all pages in the folder first.']; + } } if(!$this->errors) diff --git a/system/Controllers/MetaApiController.php b/system/Controllers/MetaApiController.php index 7a43c75..9e80c5e 100644 --- a/system/Controllers/MetaApiController.php +++ b/system/Controllers/MetaApiController.php @@ -18,12 +18,22 @@ class MetaApiController extends ContentController } # get the standard meta-definitions and the meta-definitions from plugins (same for all sites) - public function aggregateMetaDefinitions() + public function aggregateMetaDefinitions($folder = null) { $writeYaml = new writeYaml(); $metatabs = $writeYaml->getYaml('system' . DIRECTORY_SEPARATOR . 'author', 'metatabs.yaml'); + if($folder) + { + $metatabs['meta']['fields']['contains'] = [ + 'type' => 'radio', + 'label' => 'This folder contains:', + 'options' => ['pages' => 'PAGES (sort in navigation with drag & drop)', 'posts' => 'POSTS (sorted by publish date, for news or blogs)'], + 'class' => 'medium' + ]; + } + # loop through all plugins foreach($this->settings['plugins'] as $name => $plugin) { @@ -78,8 +88,20 @@ class MetaApiController extends ContentController $pagemeta = $writeYaml->getPageMetaDefaults($this->content, $this->settings, $this->item); } - # get global metadefinitions - $metadefinitions = $this->aggregateMetaDefinitions(); + # if item is a folder + if($this->item->elementType == "folder" && isset($this->item->contains)) + { + + $pagemeta['meta']['contains'] = isset($pagemeta['meta']['contains']) ? $pagemeta['meta']['contains'] : $this->item->contains; + + # get global metadefinitions + $metadefinitions = $this->aggregateMetaDefinitions($folder = true); + } + else + { + # get global metadefinitions + $metadefinitions = $this->aggregateMetaDefinitions(); + } $metadata = []; $metascheme = []; @@ -98,7 +120,7 @@ class MetaApiController extends ContentController # store the metascheme in cache for frontend $writeYaml->updateYaml('cache', 'metatabs.yaml', $metascheme); - return $response->withJson(array('metadata' => $metadata, 'metadefinitions' => $metadefinitions, 'errors' => false)); + return $response->withJson(array('metadata' => $metadata, 'metadefinitions' => $metadefinitions, 'item' => $this->item, 'errors' => false)); } public function updateArticleMeta(Request $request, Response $response, $args) @@ -117,8 +139,25 @@ class MetaApiController extends ContentController return $response->withJson($this->errors, 404); } - # load metadefinitions - $metaDefinitions = $this->aggregateMetaDefinitions(); + # set structure + if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); } + + # set item + if(!$this->setItem()){ return $response->withJson($this->errors, 404); } + + # if item is a folder + if($this->item->elementType == "folder") + { + $pagemeta['meta']['contains'] = isset($pagemeta['meta']['contains']) ? $pagemeta['meta']['contains'] : $this->item->contains; + + # get global metadefinitions + $metaDefinitions = $this->aggregateMetaDefinitions($folder = true); + } + else + { + # get global metadefinitions + $metaDefinitions = $this->aggregateMetaDefinitions(); + } # create validation object $validate = $this->getValidator(); @@ -147,12 +186,6 @@ class MetaApiController extends ContentController # return validation errors if($errors){ return $response->withJson(array('errors' => $errors),422); } - - # set structure - if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); } - - # set item - if(!$this->setItem()){ return $response->withJson($this->errors, 404); } $writeYaml = new writeYaml(); @@ -167,6 +200,50 @@ 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'])) + { + # update the time + $metaInput['time'] = date('H-i-s', time()); + + # if it is a post, then rename the post + if($this->item->elementType == "file" && strlen($this->item->order) == 12) + { + # create file-prefix with date + $datetime = $metaInput['manualdate'] . '-' . $metaInput['time']; + $datetime = implode(explode('-', $datetime)); + $datetime = substr($datetime,0,12); + + # create the new filename + $pathWithoutFile = str_replace($this->item->originalName, "", $this->item->path); + $newPathWithoutType = $pathWithoutFile . $datetime . '-' . $this->item->slug; + + $writeYaml->renamePost($this->item->pathWithoutType, $newPathWithoutType); + + # recreate the draft structure + $this->setStructure($draft = true, $cache = false); + + # update item + $this->setItem(); + } + } + + # 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'])) + { + $structure = true; + + if($metaInput['contains'] == "posts") + { + $writeYaml->transformPagesToPosts($this->item); + } + if($metaInput['contains'] == "pages") + { + $writeYaml->transformPostsToPages($this->item); + } + } + # normalize the meta-input $metaInput['navtitle'] = (isset($metaInput['navtitle']) && $metaInput['navtitle'] !== null )? $metaInput['navtitle'] : ''; $metaInput['hide'] = (isset($metaInput['hide']) && $metaInput['hide'] !== null) ? $metaInput['hide'] : false; @@ -179,9 +256,8 @@ class MetaApiController extends ContentController $structure = true; } - - # check if navtitle or hide-value has been changed elseif( + # check if navtitle or hide-value has been changed ($metaPage['meta']['navtitle'] != $metaInput['navtitle']) OR ($metaPage['meta']['hide'] != $metaInput['hide']) @@ -192,21 +268,6 @@ class MetaApiController extends ContentController $structure = true; } - - if($structure) - { - # store the file - $writeYaml->updateYaml('cache', 'structure-extended.yaml', $extended); - - # recreate the draft structure - $this->setStructure($draft = true, $cache = false); - - # set item in navigation active again - $activeItem = Folder::getItemForUrl($this->structure, $this->item->urlRel, $this->uri->getBaseUrl()); - - # send new structure to frontend - $structure = $this->structure; - } } # add the new/edited metadata @@ -215,8 +276,26 @@ class MetaApiController extends ContentController # store the metadata $writeYaml->updateYaml($this->settings['contentFolder'], $this->item->pathWithoutType . '.yaml', $meta); + if($structure) + { + # store the extended file + $writeYaml->updateYaml('cache', 'structure-extended.yaml', $extended); + + # recreate the draft structure + $this->setStructure($draft = true, $cache = false); + + # update item + $this->setItem(); + + # set item in navigation active again + $activeItem = Folder::getItemForUrl($this->structure, $this->item->urlRel, $this->uri->getBaseUrl()); + + # send new structure to frontend + $structure = $this->structure; + } + # return with the new metadata - return $response->withJson(array('metadata' => $metaInput, 'structure' => $structure, 'errors' => false)); + return $response->withJson(array('metadata' => $metaInput, 'structure' => $structure, 'item' => $this->item, 'errors' => false)); } } diff --git a/system/Controllers/PageController.php b/system/Controllers/PageController.php index 9b1e2df..929ca2e 100644 --- a/system/Controllers/PageController.php +++ b/system/Controllers/PageController.php @@ -63,7 +63,8 @@ class PageController extends Controller $sitemap->updateSitemap('cache', 'sitemap.xml', 'lastSitemap.txt', $structure, $uri->getBaseUrl()); /* check and update the typemill-version in the user settings */ - $this->updateVersion($uri->getBaseUrl()); + # this version check is not needed + # $this->updateVersion($uri->getBaseUrl()); } } @@ -249,7 +250,7 @@ class PageController extends Controller /* cache structure */ $cache->updateCache('cache', 'structure.txt', 'lastCache.txt', $structure); - if($this->containsHiddenPages($extended)) + if($extended && $this->containsHiddenPages($extended)) { # generate the navigation (delete empty pages) $navigation = $this->createNavigationFromStructure($structure); @@ -295,6 +296,7 @@ class PageController extends Controller return $navigation; } + # not in use, stored the latest version in user settings, but that does not make sense because checkd on the fly with api in admin protected function updateVersion($baseUrl) { /* check the latest public typemill version */ diff --git a/system/Controllers/SettingsController.php b/system/Controllers/SettingsController.php index f0a4aa5..ebe6d3f 100644 --- a/system/Controllers/SettingsController.php +++ b/system/Controllers/SettingsController.php @@ -136,15 +136,18 @@ class SettingsController extends Controller /* add the preview image */ $img = getcwd() . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . $themeName . DIRECTORY_SEPARATOR . $themeName; - $jpg = $img . '.jpg'; - $png = $img . '.png'; - $img = file_exists($jpg) ? $jpg : false; - if(!$img) + + $image = false; + if(file_exists($img . '.jpg')) { - $img = file_exists($png) ? $png : false; + $image = $themeName . '.jpg'; + } + if(file_exists($img . '.png')) + { + $image = $themeName . '.png'; } - $themedata[$themeName]['img'] = $img; + $themedata[$themeName]['img'] = $image; } /* add the users for navigation */ diff --git a/system/Extensions/TwigLanguageExtension.php b/system/Extensions/TwigLanguageExtension.php index 4fc9fda..1c91529 100644 --- a/system/Extensions/TwigLanguageExtension.php +++ b/system/Extensions/TwigLanguageExtension.php @@ -38,7 +38,7 @@ class TwigLanguageExtension extends \Twig_Extension $string = strtoupper( $string ); //translates the string - $translated_label = $this->labels[$string]; + $translated_label = isset($this->labels[$string]) ? $this->labels[$string] : null; // if the string is not present, set the original string if( empty($translated_label) ){ diff --git a/system/Models/Field.php b/system/Models/Field.php index 712a1ea..a989c4e 100644 --- a/system/Models/Field.php +++ b/system/Models/Field.php @@ -77,7 +77,8 @@ class Field /* defines additional data, that are allowed for fields */ private $helpers = array( 'help', - 'description' + 'description', + 'fieldsize' ); public function __construct($fieldName, array $fieldConfigs) diff --git a/system/Models/Folder.php b/system/Models/Folder.php index c233c5a..a006db8 100644 --- a/system/Models/Folder.php +++ b/system/Models/Folder.php @@ -91,11 +91,13 @@ class Folder return $folderContent; } + /* * Transforms array of folder item into an array of item-objects with additional information for each item * vars: multidimensional array with folder- and file-names * returns: array of objects. Each object contains information about an item (file or folder). - */ + */ + public static function getFolderContentDetails(array $folderContent, $extended, $baseUrl, $fullSlugWithFolder = NULL, $fullSlugWithoutFolder = NULL, $fullPath = NULL, $keyPath = NULL, $chapter = NULL) { $contentDetails = []; @@ -129,13 +131,14 @@ class Folder $item->originalName = $key; $item->elementType = 'folder'; + $item->contains = self::getFolderContentType($name, $fullPath . DIRECTORY_SEPARATOR . $key . DIRECTORY_SEPARATOR . 'index.yaml'); $item->status = $status; $item->fileType = $fileType; $item->order = count($nameParts) > 1 ? array_shift($nameParts) : NULL; $item->name = implode(" ",$nameParts); $item->name = iconv(mb_detect_encoding($item->name, mb_detect_order(), true), "UTF-8", $item->name); $item->slug = implode("-",$nameParts); - $item->slug = URLify::filter(iconv(mb_detect_encoding($item->slug, mb_detect_order(), true), "UTF-8", $item->slug)); + $item->slug = URLify::filter(iconv(mb_detect_encoding($item->slug, mb_detect_order(), true), "UTF-8", $item->slug)); $item->path = $fullPath . DIRECTORY_SEPARATOR . $key; $item->pathWithoutType = $fullPath . DIRECTORY_SEPARATOR . $key . DIRECTORY_SEPARATOR . 'index'; $item->urlRelWoF = $fullSlugWithoutFolder . '/' . $item->slug; @@ -156,6 +159,12 @@ class Folder $item->hide = ($extended[$item->urlRelWoF]['hide'] === true) ? true : false; } + # sort posts in descending order + if($item->contains == "posts") + { + rsort($name); + } + $item->folderContent = self::getFolderContentDetails($name, $extended, $baseUrl, $item->urlRel, $item->urlRelWoF, $item->path, $item->keyPath, $item->chapter); } else @@ -192,7 +201,7 @@ class Folder $item->name = implode(" ",$nameParts); $item->name = iconv(mb_detect_encoding($item->name, mb_detect_order(), true), "UTF-8", $item->name); $item->slug = implode("-",$nameParts); - $item->slug = URLify::filter(iconv(mb_detect_encoding($item->slug, mb_detect_order(), true), "UTF-8", $item->slug)); + $item->slug = URLify::filter(iconv(mb_detect_encoding($item->slug, mb_detect_order(), true), "UTF-8", $item->slug)); $item->path = $fullPath . DIRECTORY_SEPARATOR . $name; $item->pathWithoutType = $fullPath . DIRECTORY_SEPARATOR . $nameWithoutType; $item->key = $iteration; @@ -221,6 +230,44 @@ class Folder return $contentDetails; } + public static function getFolderContentType($folder, $yamlpath) + { + # check if folder is empty or has only index.yaml-file. This is a rare case so make it quick and dirty + if(count($folder) == 1) + { + # check if in folder yaml file contains "posts", then return posts + $folderyamlpath = getcwd() . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $yamlpath; + + $fileContent = false; + if(file_exists($folderyamlpath)) + { + $fileContent = file_get_contents($folderyamlpath); + } + + if($fileContent && strpos($fileContent, 'contains: posts') !== false) + { + return 'posts'; + } + return 'pages'; + } + else + { + $file = $folder[0]; + $nameParts = self::getStringParts($file); + $order = count($nameParts) > 1 ? array_shift($nameParts) : NULL; + $order = substr($order, 0, 7); + + if(\DateTime::createFromFormat('Ymd', $order) !== FALSE) + { + return "posts"; + } + else + { + return "pages"; + } + } + } + public static function getItemForUrl($folderContentDetails, $url, $baseUrl, $result = NULL) { diff --git a/system/Models/Validation.php b/system/Models/Validation.php index bcea4ca..d5ed19a 100644 --- a/system/Models/Validation.php +++ b/system/Models/Validation.php @@ -48,6 +48,16 @@ class Validation return false; }, 'wrong password'); + Validator::addRule('navigation', function($field, $value, array $params, array $fields) + { + $format = '/[@#^*()=\[\]{};:"\\|,.<>\/]/'; + if ( preg_match($format, $value)) + { + return false; + } + return true; + }, 'contains special characters'); + Validator::addRule('noSpecialChars', function($field, $value, array $params, array $fields) { $format = '/[!@#$%^&*()_+=\[\]{};\':"\\|,.<>\/?]/'; @@ -283,11 +293,12 @@ class Validation public function navigationItem(array $params) { $v = new Validator($params); - + $v->rule('required', ['folder_id', 'item_name', 'type', 'url']); $v->rule('regex', 'folder_id', '/^[0-9.]+$/i'); - $v->rule('noSpecialChars', 'item_name'); - $v->rule('lengthBetween', 'item_name', 1, 40); +# $v->rule('noSpecialChars', 'item_name'); + $v->rule('navigation', 'item_name'); + $v->rule('lengthBetween', 'item_name', 1, 60); $v->rule('in', 'type', ['file', 'folder']); if($v->validate()) @@ -305,7 +316,8 @@ class Validation $v = new Validator($params); $v->rule('required', ['item_name', 'type', 'url']); - $v->rule('noSpecialChars', 'item_name'); +# $v->rule('noSpecialChars', 'item_name'); + $v->rule('navigation', 'item_name'); $v->rule('lengthBetween', 'item_name', 1, 40); $v->rule('in', 'type', ['file', 'folder']); @@ -397,7 +409,7 @@ class Validation case "text": $v->rule('noHTML', $fieldName); $v->rule('lengthMax', $fieldName, 500); - $v->rule('regex', $fieldName, '/^[\pL0-9_ \-\.\?\!\/\:]*$/u'); +# $v->rule('regex', $fieldName, '/^[\pL0-9_ \-\.\?\!\/\:]*$/u'); break; case "textarea": $v->rule('noHTML', $fieldName); diff --git a/system/Models/VersionCheck.php b/system/Models/VersionCheck.php index 59d62d6..5c1b43e 100644 --- a/system/Models/VersionCheck.php +++ b/system/Models/VersionCheck.php @@ -2,6 +2,8 @@ namespace Typemill\Models; +# this check is not in use anymore (was in use to check and store latest version in user settings on page refresh) + class VersionCheck { function checkVersion($url) @@ -15,11 +17,13 @@ class VersionCheck $context = stream_context_create($opts); - if(false === ($version = @file_get_contents('http://typemill.net/api/v1/checkversion', false, $context))) + if(false === ($version = @file_get_contents('https://typemill.net/api/v1/checkversion', false, $context))) { return false; } $version = json_decode($version); + die(); + return $version->system->typemill; } } \ No newline at end of file diff --git a/system/Models/Write.php b/system/Models/Write.php index 5ea2a5c..c963815 100644 --- a/system/Models/Write.php +++ b/system/Models/Write.php @@ -110,7 +110,7 @@ class Write return false; } - public function moveElement($item, $folderPath, $index) + public function moveElement($item, $folderPath, $index, $date = null) { $filetypes = array('md', 'txt', 'yaml'); @@ -159,4 +159,35 @@ class Write return $result; } + + public function renamePost($oldPathWithoutType, $newPathWithoutType) + { + $filetypes = array('md', 'txt', 'yaml'); + + $oldPath = $this->basePath . 'content' . $oldPathWithoutType; + $newPath = $this->basePath . 'content' . $newPathWithoutType; + + $result = true; + + foreach($filetypes as $filetype) + { + $oldFilePath = $oldPath . '.' . $filetype; + $newFilePath = $newPath . '.' . $filetype; + + #check if file with filetype exists and rename + if($oldFilePath != $newFilePath && file_exists($oldFilePath)) + { + if(@rename($oldFilePath, $newFilePath)) + { + $result = $result; + } + else + { + $result = false; + } + } + } + + return $result; + } } \ No newline at end of file diff --git a/system/Models/WriteYaml.php b/system/Models/WriteYaml.php index a46a2b6..283ee2d 100644 --- a/system/Models/WriteYaml.php +++ b/system/Models/WriteYaml.php @@ -115,6 +115,7 @@ class WriteYaml extends Write 'description' => $description, 'author' => $author, 'created' => date("Y-m-d"), + 'time' => date("H-i-s"), ] ]; @@ -162,4 +163,109 @@ class WriteYaml extends Write return $meta; } + + + public function transformPagesToPosts($folder){ + + $filetypes = array('md', 'txt', 'yaml'); + + foreach($folder->folderContent as $page) + { + # create old filename without filetype + $oldFile = $this->basePath . 'content' . $page->pathWithoutType; + + # set default date + $date = date('Y-m-d', time()); + $time = date('H-i', time()); + + $meta = $this->getYaml('content', $page->pathWithoutType . '.yaml'); + + if($meta) + { + # get dates from meta + if(isset($meta['meta']['manualdate'])){ $date = $meta['meta']['manualdate']; } + elseif(isset($meta['meta']['created'])){ $date = $meta['meta']['created']; } + elseif(isset($meta['meta']['modified'])){ $date = $meta['meta']['modified']; } + + # set time + if(isset($meta['meta']['time'])) + { + $time = $meta['meta']['time']; + } + } + + $datetime = $date . '-' . $time; + $datetime = implode(explode('-', $datetime)); + $datetime = substr($datetime,0,12); + + # create new file-name without filetype + $newFile = $this->basePath . 'content' . $folder->path . DIRECTORY_SEPARATOR . $datetime . '-' . $page->slug; + + $result = true; + + foreach($filetypes as $filetype) + { + $oldFilePath = $oldFile . '.' . $filetype; + $newFilePath = $newFile . '.' . $filetype; + + #check if file with filetype exists and rename + if($oldFilePath != $newFilePath && file_exists($oldFilePath)) + { + if(@rename($oldFilePath, $newFilePath)) + { + $result = $result; + } + else + { + $result = false; + } + } + } + } + } + + public function transformPostsToPages($folder){ + + $filetypes = array('md', 'txt', 'yaml'); + $index = 0; + + foreach($folder->folderContent as $page) + { + # create old filename without filetype + $oldFile = $this->basePath . 'content' . $page->pathWithoutType; + + $order = $index; + + if($index < 10) + { + $order = '0' . $index; + } + + # create new file-name without filetype + $newFile = $this->basePath . 'content' . $folder->path . DIRECTORY_SEPARATOR . $order . '-' . $page->slug; + + $result = true; + + foreach($filetypes as $filetype) + { + $oldFilePath = $oldFile . '.' . $filetype; + $newFilePath = $newFile . '.' . $filetype; + + #check if file with filetype exists and rename + if($oldFilePath != $newFilePath && file_exists($oldFilePath)) + { + if(@rename($oldFilePath, $newFilePath)) + { + $result = $result; + } + else + { + $result = false; + } + } + } + + $index++; + } + } } \ No newline at end of file diff --git a/system/Routes/Api.php b/system/Routes/Api.php index 5734659..a81adbd 100644 --- a/system/Routes/Api.php +++ b/system/Routes/Api.php @@ -13,6 +13,7 @@ $app->post('/api/v1/article/html', ContentApiController::class . ':getArticleHtm $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'])); diff --git a/system/system.php b/system/system.php index f82f40d..c01da87 100644 --- a/system/system.php +++ b/system/system.php @@ -129,7 +129,6 @@ $container['assets'] = function($c) return new \Typemill\Assets($c['request']->getUri()->getBaseUrl()); }; - /************************ * DECIDE FOR SESSION * ************************/