PageController.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. <?php
  2. namespace Typemill\Controllers;
  3. use Typemill\Models\Folder;
  4. use Typemill\Models\WriteCache;
  5. use Typemill\Models\WriteSitemap;
  6. use Typemill\Models\WriteYaml;
  7. use Typemill\Models\WriteMeta;
  8. use \Symfony\Component\Yaml\Yaml;
  9. use Typemill\Models\VersionCheck;
  10. use Typemill\Models\Markdown;
  11. use Typemill\Events\OnPagetreeLoaded;
  12. use Typemill\Events\OnBreadcrumbLoaded;
  13. use Typemill\Events\OnItemLoaded;
  14. use Typemill\Events\OnOriginalLoaded;
  15. use Typemill\Events\OnMetaLoaded;
  16. use Typemill\Events\OnMarkdownLoaded;
  17. use Typemill\Events\OnContentArrayLoaded;
  18. use Typemill\Events\OnHtmlLoaded;
  19. use Typemill\Extensions\ParsedownExtension;
  20. class PageController extends Controller
  21. {
  22. public function index($request, $response, $args)
  23. {
  24. /* Initiate Variables */
  25. $structure = false;
  26. $contentHTML = false;
  27. $item = false;
  28. $home = false;
  29. $breadcrumb = false;
  30. $settings = $this->c->get('settings');
  31. $pathToContent = $settings['rootPath'] . $settings['contentFolder'];
  32. $cache = new WriteCache();
  33. $uri = $request->getUri()->withUserInfo('');
  34. $base_url = $uri->getBaseUrl();
  35. $this->pathToContent = $pathToContent;
  36. try
  37. {
  38. # if the cached structure is still valid, use it
  39. if($cache->validate('cache', 'lastCache.txt', 600))
  40. {
  41. $structure = $this->getCachedStructure($cache);
  42. }
  43. if(!isset($structure) OR !$structure)
  44. {
  45. # if not, get a fresh structure of the content folder
  46. $structure = $this->getFreshStructure($pathToContent, $cache, $uri);
  47. # if there is no structure at all, the content folder is probably empty
  48. if(!$structure)
  49. {
  50. $content = '<h1>No Content</h1><p>Your content folder is empty.</p>';
  51. return $this->render($response, '/index.twig', array( 'content' => $content ));
  52. }
  53. elseif(!$cache->validate('cache', 'lastSitemap.txt', 86400))
  54. {
  55. # update sitemap
  56. $sitemap = new WriteSitemap();
  57. $sitemap->updateSitemap('cache', 'sitemap.xml', 'lastSitemap.txt', $structure, $uri->getBaseUrl());
  58. }
  59. }
  60. # dispatch event and let others manipulate the structure
  61. $structure = $this->c->dispatcher->dispatch('onPagetreeLoaded', new OnPagetreeLoaded($structure))->getData();
  62. }
  63. catch (Exception $e)
  64. {
  65. echo $e->getMessage();
  66. exit(1);
  67. }
  68. # get meta-Information
  69. $writeMeta = new WriteMeta();
  70. $theme = $settings['theme'];
  71. # check if there is a custom theme css
  72. $customcss = $writeMeta->checkFile('cache', $theme . '-custom.css');
  73. if($customcss)
  74. {
  75. $this->c->assets->addCSS($base_url . '/cache/' . $theme . '-custom.css');
  76. }
  77. $logo = false;
  78. if(isset($settings['logo']) && $settings['logo'] != '')
  79. {
  80. $logo = 'media/files/' . $settings['logo'];
  81. }
  82. $favicon = false;
  83. if(isset($settings['favicon']) && $settings['favicon'] != '')
  84. {
  85. $favicon = true;
  86. }
  87. # get the cached navigation here (structure without hidden files )
  88. $navigation = $cache->getCache('cache', 'navigation.txt');
  89. if(!$navigation)
  90. {
  91. # use the structure as navigation if there is no difference
  92. $navigation = $structure;
  93. }
  94. # if the user is on startpage
  95. if(empty($args))
  96. {
  97. $home = true;
  98. $item = Folder::getItemForUrl($navigation, $uri->getBasePath(), $uri->getBasePath());
  99. $urlRel = $uri->getBasePath();
  100. }
  101. else
  102. {
  103. # get the request url
  104. $urlRel = $uri->getBasePath() . '/' . $args['params'];
  105. # find the url in the content-item-tree and return the item-object for the file
  106. # important to use the structure here so it is found, even if the item is hidden.
  107. $item = Folder::getItemForUrl($structure, $urlRel, $uri->getBasePath());
  108. # if there is still no item, return a 404-page
  109. if(!$item)
  110. {
  111. return $this->render404($response, array(
  112. 'navigation' => $navigation,
  113. 'settings' => $settings,
  114. 'base_url' => $base_url,
  115. 'title' => false,
  116. 'content' => false,
  117. 'item' => false,
  118. 'breadcrumb' => false,
  119. 'metatabs' => false,
  120. 'image' => false,
  121. 'logo' => $logo,
  122. 'favicon' => $favicon
  123. ));
  124. }
  125. if(!$item->hide)
  126. {
  127. # get breadcrumb for page and set pages active
  128. # use navigation, the hidden pages won't get a breadcrumb
  129. $breadcrumb = Folder::getBreadcrumb($navigation, $item->keyPathArray);
  130. $breadcrumb = $this->c->dispatcher->dispatch('onBreadcrumbLoaded', new OnBreadcrumbLoaded($breadcrumb))->getData();
  131. # set pages active for navigation again
  132. # Folder::getBreadcrumb($navigation, $item->keyPathArray);
  133. # add the paging to the item
  134. $item = Folder::getPagingForItem($navigation, $item);
  135. }
  136. }
  137. if(isset($item->hide) && $item->hide)
  138. {
  139. # delete the paging elements
  140. $item->thisChapter = false;
  141. $item->nextItem = false;
  142. $item->prevItem = false;
  143. $breadcrumb = false;
  144. }
  145. # dispatch the item
  146. $item = $this->c->dispatcher->dispatch('onItemLoaded', new OnItemLoaded($item))->getData();
  147. # set the filepath
  148. $filePath = $pathToContent . $item->path;
  149. # check if url is a folder and add index.md
  150. if($item->elementType == 'folder')
  151. {
  152. $filePath = $filePath . DIRECTORY_SEPARATOR . 'index.md';
  153. # if folder is not hidden
  154. if(isset($item->hide) && !$item->hide)
  155. {
  156. # use the navigation instead of the structure so that hidden elements are erased
  157. $item = Folder::getItemForUrl($navigation, $urlRel, $uri->getBasePath());
  158. }
  159. }
  160. # read the content of the file
  161. $contentMD = file_exists($filePath) ? file_get_contents($filePath) : false;
  162. # dispatch the original content without plugin-manipulations for case anyone wants to use it
  163. $this->c->dispatcher->dispatch('onOriginalLoaded', new OnOriginalLoaded($contentMD));
  164. # makes sure that you always have the full meta with title, description and all the rest.
  165. $metatabs = $writeMeta->completePageMeta($contentMD, $settings, $item);
  166. # dispatch meta
  167. $metatabs = $this->c->dispatcher->dispatch('onMetaLoaded', new OnMetaLoaded($metatabs))->getData();
  168. # dispatch content
  169. $contentMD = $this->c->dispatcher->dispatch('onMarkdownLoaded', new OnMarkdownLoaded($contentMD))->getData();
  170. $itemUrl = isset($item->urlRel) ? $item->urlRel : false;
  171. /* initialize parsedown */
  172. $parsedown = new ParsedownExtension($settings['headlineanchors']);
  173. /* set safe mode to escape javascript and html in markdown */
  174. $parsedown->setSafeMode(true);
  175. /* parse markdown-file to content-array */
  176. $contentArray = $parsedown->text($contentMD, $itemUrl);
  177. $contentArray = $this->c->dispatcher->dispatch('onContentArrayLoaded', new OnContentArrayLoaded($contentArray))->getData();
  178. /* parse markdown-content-array to content-string */
  179. $contentHTML = $parsedown->markup($contentArray, $itemUrl);
  180. $contentHTML = $this->c->dispatcher->dispatch('onHtmlLoaded', new OnHtmlLoaded($contentHTML))->getData();
  181. /* extract the h1 headline*/
  182. $contentParts = explode("</h1>", $contentHTML);
  183. $title = isset($contentParts[0]) ? strip_tags($contentParts[0]) : $settings['title'];
  184. $contentHTML = isset($contentParts[1]) ? $contentParts[1] : $contentHTML;
  185. # get the first image from content array */
  186. $img_url = isset($metatabs['meta']['heroimage']) ? $metatabs['meta']['heroimage'] : false;
  187. $img_alt = isset($metatabs['meta']['heroimagealt']) ? $metatabs['meta']['heroimagealt'] : false;
  188. # get url and alt-tag for first image, if exists */
  189. if(!$img_url OR $img_url == '')
  190. {
  191. # extract first image from content
  192. $firstImageMD = $this->getFirstImage($contentArray);
  193. if($firstImageMD)
  194. {
  195. preg_match('#\((.*?)\)#', $firstImageMD, $img_url_result);
  196. $img_url = isset($img_url_result[1]) ? $img_url_result[1] : false;
  197. if($img_url)
  198. {
  199. preg_match('#\[(.*?)\]#', $firstImageMD, $img_alt_result);
  200. $img_alt = isset($img_alt_result[1]) ? $img_alt_result[1] : false;
  201. }
  202. }
  203. }
  204. $firstImage = array('img_url' => $base_url . '/' . $img_url, 'img_alt' => $img_alt);
  205. $route = empty($args) && isset($settings['themes'][$theme]['cover']) ? '/cover.twig' : '/index.twig';
  206. return $this->render($response, $route, [
  207. 'home' => $home,
  208. 'navigation' => $navigation,
  209. 'title' => $title,
  210. 'content' => $contentHTML,
  211. 'item' => $item,
  212. 'breadcrumb' => $breadcrumb,
  213. 'settings' => $settings,
  214. 'metatabs' => $metatabs,
  215. 'base_url' => $base_url,
  216. 'image' => $firstImage,
  217. 'logo' => $logo,
  218. 'favicon' => $favicon
  219. ]);
  220. }
  221. protected function getCachedStructure($cache)
  222. {
  223. return $cache->getCache('cache', 'structure.txt');
  224. }
  225. protected function getFreshStructure($pathToContent, $cache, $uri)
  226. {
  227. /* scan the content of the folder */
  228. $pagetree = Folder::scanFolder($pathToContent);
  229. /* if there is no content, render an empty page */
  230. if(count($pagetree) == 0)
  231. {
  232. return false;
  233. }
  234. # get the extended structure files with changes like navigation title or hidden pages
  235. $yaml = new writeYaml();
  236. $extended = $yaml->getYaml('cache', 'structure-extended.yaml');
  237. # create an array of object with the whole content of the folder
  238. $structure = Folder::getFolderContentDetails($pagetree, $extended, $uri->getBaseUrl(), $uri->getBasePath());
  239. # now update the extended structure
  240. if(!$extended)
  241. {
  242. $extended = $this->createExtended($this->pathToContent, $yaml, $structure);
  243. if(!empty($extended))
  244. {
  245. $yaml->updateYaml('cache', 'structure-extended.yaml', $extended);
  246. # we have to update the structure with extended again
  247. $structure = Folder::getFolderContentDetails($pagetree, $extended, $uri->getBaseUrl(), $uri->getBasePath());
  248. }
  249. }
  250. # cache structure
  251. $cache->updateCache('cache', 'structure.txt', 'lastCache.txt', $structure);
  252. if($extended && $this->containsHiddenPages($extended))
  253. {
  254. # generate the navigation (delete empty pages)
  255. $navigation = $this->createNavigationFromStructure($structure);
  256. # cache navigation
  257. $cache->updateCache('cache', 'navigation.txt', false, $navigation);
  258. }
  259. else
  260. {
  261. # make sure no separate navigation file is set
  262. $cache->deleteFileWithPath('cache' . DIRECTORY_SEPARATOR . 'navigation.txt');
  263. }
  264. # load and return the cached structure, because might be manipulated with navigation....
  265. return $this->getCachedStructure($cache);
  266. }
  267. protected function createExtended($contentPath, $yaml, $structure, $extended = NULL)
  268. {
  269. if(!$extended)
  270. {
  271. $extended = [];
  272. }
  273. foreach ($structure as $key => $item)
  274. {
  275. # $filename = ($item->elementType == 'folder') ? DIRECTORY_SEPARATOR . 'index.yaml' : $item->pathWithoutType . '.yaml';
  276. $filename = $item->pathWithoutType . '.yaml';
  277. if(file_exists($contentPath . $filename))
  278. {
  279. # read file
  280. $meta = $yaml->getYaml('content', $filename);
  281. $extended[$item->urlRelWoF]['hide'] = isset($meta['meta']['hide']) ? $meta['meta']['hide'] : false;
  282. $extended[$item->urlRelWoF]['navtitle'] = isset($meta['meta']['navtitle']) ? $meta['meta']['navtitle'] : '';
  283. }
  284. if ($item->elementType == 'folder')
  285. {
  286. $extended = $this->createExtended($contentPath, $yaml, $item->folderContent, $extended);
  287. }
  288. }
  289. return $extended;
  290. }
  291. protected function containsHiddenPages($extended)
  292. {
  293. foreach($extended as $element)
  294. {
  295. if(isset($element['hide']) && $element['hide'] === true)
  296. {
  297. return true;
  298. }
  299. }
  300. return false;
  301. }
  302. protected function createNavigationFromStructure($navigation)
  303. {
  304. foreach ($navigation as $key => $element)
  305. {
  306. if($element->hide === true)
  307. {
  308. unset($navigation[$key]);
  309. }
  310. elseif(isset($element->folderContent))
  311. {
  312. $navigation[$key]->folderContent = $this->createNavigationFromStructure($element->folderContent);
  313. }
  314. }
  315. return $navigation;
  316. }
  317. # 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
  318. protected function updateVersion($baseUrl)
  319. {
  320. /* check the latest public typemill version */
  321. $version = new VersionCheck();
  322. $latestVersion = $version->checkVersion($baseUrl);
  323. if($latestVersion)
  324. {
  325. /* store latest version */
  326. \Typemill\Settings::updateSettings(array('latestVersion' => $latestVersion));
  327. }
  328. }
  329. protected function getFirstImage(array $contentBlocks)
  330. {
  331. foreach($contentBlocks as $block)
  332. {
  333. /* is it a paragraph? */
  334. if(isset($block['name']) && $block['name'] == 'p')
  335. {
  336. if(isset($block['handler']['argument']) && substr($block['handler']['argument'], 0, 2) == '![' )
  337. {
  338. return $block['handler']['argument'];
  339. }
  340. }
  341. }
  342. return false;
  343. }
  344. }