PageController.php 13 KB

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