PageController.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  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\Events\OnRestrictionsLoaded;
  21. use Typemill\Extensions\ParsedownExtension;
  22. class PageController extends Controller
  23. {
  24. public function index($request, $response, $args)
  25. {
  26. /* Initiate Variables */
  27. $structure = false;
  28. $contentHTML = false;
  29. $item = false;
  30. $home = false;
  31. $breadcrumb = false;
  32. $pathToContent = $this->settings['rootPath'] . $this->settings['contentFolder'];
  33. $cache = new WriteCache();
  34. $uri = $request->getUri()->withUserInfo('');
  35. $base_url = $uri->getBaseUrl();
  36. $this->pathToContent = $pathToContent;
  37. try
  38. {
  39. # if the cached structure is still valid, use it
  40. if($cache->validate('cache', 'lastCache.txt', 600))
  41. {
  42. $structure = $this->getCachedStructure($cache);
  43. }
  44. else
  45. {
  46. # dispatch message that the cache has been refreshed
  47. $this->c->dispatcher->dispatch('onCacheUpdated', new OnCacheUpdated(false));
  48. }
  49. if(!isset($structure) OR !$structure)
  50. {
  51. # if not, get a fresh structure of the content folder
  52. $structure = $this->getFreshStructure($pathToContent, $cache, $uri);
  53. # if there is no structure at all, the content folder is probably empty
  54. if(!$structure)
  55. {
  56. $content = '<h1>No Content</h1><p>Your content folder is empty.</p>';
  57. return $this->render($response, '/index.twig', array( 'content' => $content ));
  58. }
  59. elseif(!$cache->validate('cache', 'lastSitemap.txt', 86400))
  60. {
  61. # update sitemap
  62. $sitemap = new WriteSitemap();
  63. $sitemap->updateSitemap('cache', 'sitemap.xml', 'lastSitemap.txt', $structure, $uri->getBaseUrl());
  64. }
  65. }
  66. # dispatch event and let others manipulate the structure
  67. $structure = $this->c->dispatcher->dispatch('onPagetreeLoaded', new OnPagetreeLoaded($structure))->getData();
  68. }
  69. catch (Exception $e)
  70. {
  71. echo $e->getMessage();
  72. exit(1);
  73. }
  74. # get meta-Information
  75. $writeMeta = new WriteMeta();
  76. $theme = $this->settings['theme'];
  77. # check if there is a custom theme css
  78. $customcss = $writeMeta->checkFile('cache', $theme . '-custom.css');
  79. if($customcss)
  80. {
  81. $this->c->assets->addCSS($base_url . '/cache/' . $theme . '-custom.css');
  82. }
  83. $logo = false;
  84. if(isset($this->settings['logo']) && $this->settings['logo'] != '')
  85. {
  86. $logo = 'media/files/' . $this->settings['logo'];
  87. }
  88. $favicon = false;
  89. if(isset($this->settings['favicon']) && $this->settings['favicon'] != '')
  90. {
  91. $favicon = true;
  92. }
  93. # get the cached navigation here (structure without hidden files )
  94. $navigation = $cache->getCache('cache', 'navigation.txt');
  95. if(!$navigation)
  96. {
  97. # use the structure as navigation if there is no difference
  98. $navigation = $structure;
  99. }
  100. # if the user is on startpage
  101. $home = false;
  102. if(empty($args))
  103. {
  104. $home = true;
  105. $item = Folder::getItemForUrl($navigation, $uri->getBasePath(), $uri->getBaseUrl(), NULL, $home);
  106. $urlRel = $uri->getBasePath();
  107. }
  108. else
  109. {
  110. # get the request url, trim args so physical folders have no trailing slash
  111. $urlRel = $uri->getBasePath() . '/' . trim($args['params'], "/");
  112. # find the url in the content-item-tree and return the item-object for the file
  113. # important to use the structure here so it is found, even if the item is hidden.
  114. $item = Folder::getItemForUrl($structure, $urlRel, $uri->getBasePath());
  115. # if there is still no item, return a 404-page
  116. if(!$item)
  117. {
  118. return $this->render404($response, array(
  119. 'navigation' => $navigation,
  120. 'settings' => $this->settings,
  121. 'base_url' => $base_url,
  122. 'title' => false,
  123. 'content' => false,
  124. 'item' => false,
  125. 'breadcrumb' => false,
  126. 'metatabs' => false,
  127. 'image' => false,
  128. 'logo' => $logo,
  129. 'favicon' => $favicon
  130. ));
  131. }
  132. if(!$item->hide)
  133. {
  134. # get breadcrumb for page and set pages active
  135. # use navigation, the hidden pages won't get a breadcrumb
  136. $breadcrumb = Folder::getBreadcrumb($navigation, $item->keyPathArray);
  137. $breadcrumb = $this->c->dispatcher->dispatch('onBreadcrumbLoaded', new OnBreadcrumbLoaded($breadcrumb))->getData();
  138. # set pages active for navigation again
  139. # Folder::getBreadcrumb($navigation, $item->keyPathArray);
  140. # add the paging to the item
  141. $item = Folder::getPagingForItem($navigation, $item);
  142. }
  143. }
  144. if(isset($item->hide) && $item->hide)
  145. {
  146. # delete the paging elements
  147. $item->thisChapter = false;
  148. $item->nextItem = false;
  149. $item->prevItem = false;
  150. $breadcrumb = false;
  151. }
  152. # dispatch the item
  153. $item = $this->c->dispatcher->dispatch('onItemLoaded', new OnItemLoaded($item))->getData();
  154. # set the filepath
  155. $filePath = $pathToContent . $item->path;
  156. # check if url is a folder and add index.md
  157. if($item->elementType == 'folder')
  158. {
  159. $filePath = $filePath . DIRECTORY_SEPARATOR . 'index.md';
  160. # if folder is not hidden
  161. if(isset($item->hide) && !$item->hide)
  162. {
  163. # use the navigation instead of the structure so that hidden elements are erased
  164. $item = Folder::getItemForUrl($navigation, $urlRel, $uri->getBaseUrl(), NULL, $home);
  165. }
  166. }
  167. # read the content of the file
  168. $contentMD = file_exists($filePath) ? file_get_contents($filePath) : false;
  169. # dispatch the original content without plugin-manipulations for case anyone wants to use it
  170. $this->c->dispatcher->dispatch('onOriginalLoaded', new OnOriginalLoaded($contentMD));
  171. # makes sure that you always have the full meta with title, description and all the rest.
  172. $metatabs = $writeMeta->completePageMeta($contentMD, $this->settings, $item);
  173. # dispatch meta
  174. $metatabs = $this->c->dispatcher->dispatch('onMetaLoaded', new OnMetaLoaded($metatabs))->getData();
  175. # dispatch content
  176. $contentMD = $this->c->dispatcher->dispatch('onMarkdownLoaded', new OnMarkdownLoaded($contentMD))->getData();
  177. $itemUrl = isset($item->urlRel) ? $item->urlRel : false;
  178. /* initialize parsedown */
  179. $parsedown = new ParsedownExtension($base_url, $this->settings);
  180. /* set safe mode to escape javascript and html in markdown */
  181. $parsedown->setSafeMode(true);
  182. # check access restriction here
  183. $restricted = $this->checkRestrictions($metatabs['meta']);
  184. if($restricted)
  185. {
  186. # convert markdown into array of markdown block-elements
  187. $markdownBlocks = $parsedown->markdownToArrayBlocks($contentMD);
  188. # infos that plugins need to add restriction content
  189. $restrictions = [
  190. 'restricted' => $restricted,
  191. 'defaultContent' => true,
  192. 'markdownBlocks' => $markdownBlocks,
  193. ];
  194. # dispatch the data
  195. $restrictions = $this->c->dispatcher->dispatch('onRestrictionsLoaded', new OnRestrictionsLoaded( $restrictions ))->getData();
  196. # use the returned markdown
  197. $markdownBlocks = $restrictions['markdownBlocks'];
  198. # if no plugin has disabled the default behavior
  199. if($restrictions['defaultContent'])
  200. {
  201. # cut the restricted content
  202. $shortenedPage = $this->cutRestrictedContent($markdownBlocks);
  203. # check if there is customized content
  204. $restrictionnotice = $this->prepareRestrictionNotice();
  205. # add notice to shortened content
  206. $shortenedPage[] = $restrictionnotice;
  207. # Use the shortened page
  208. $markdownBlocks = $shortenedPage;
  209. }
  210. # finally transform the markdown blocks back to pure markdown text
  211. $contentMD = $parsedown->arrayBlocksToMarkdown($markdownBlocks);
  212. }
  213. /* parse markdown-file to content-array */
  214. $contentArray = $parsedown->text($contentMD);
  215. $contentArray = $this->c->dispatcher->dispatch('onContentArrayLoaded', new OnContentArrayLoaded($contentArray))->getData();
  216. /* parse markdown-content-array to content-string */
  217. $contentHTML = $parsedown->markup($contentArray);
  218. $contentHTML = $this->c->dispatcher->dispatch('onHtmlLoaded', new OnHtmlLoaded($contentHTML))->getData();
  219. /* extract the h1 headline*/
  220. $contentParts = explode("</h1>", $contentHTML, 2);
  221. $title = isset($contentParts[0]) ? strip_tags($contentParts[0]) : $this->settings['title'];
  222. $contentHTML = isset($contentParts[1]) ? $contentParts[1] : $contentHTML;
  223. # get the first image from content array */
  224. $img_url = isset($metatabs['meta']['heroimage']) ? $metatabs['meta']['heroimage'] : false;
  225. $img_alt = isset($metatabs['meta']['heroimagealt']) ? $metatabs['meta']['heroimagealt'] : false;
  226. # get url and alt-tag for first image, if exists */
  227. if(!$img_url OR $img_url == '')
  228. {
  229. # extract first image from content
  230. $firstImageMD = $this->getFirstImage($contentArray);
  231. if($firstImageMD)
  232. {
  233. preg_match('#\((.*?)\)#', $firstImageMD, $img_url_result);
  234. $img_url = isset($img_url_result[1]) ? $img_url_result[1] : false;
  235. if($img_url)
  236. {
  237. preg_match('#\[(.*?)\]#', $firstImageMD, $img_alt_result);
  238. $img_alt = isset($img_alt_result[1]) ? $img_alt_result[1] : false;
  239. }
  240. }
  241. elseif($logo)
  242. {
  243. $img_url = $logo;
  244. $pathinfo = pathinfo($this->settings['logo']);
  245. $img_alt = $pathinfo['filename'];
  246. }
  247. }
  248. $firstImage = false;
  249. if($img_url)
  250. {
  251. $firstImage = array('img_url' => $base_url . '/' . $img_url, 'img_alt' => $img_alt);
  252. }
  253. $route = empty($args) && isset($this->settings['themes'][$theme]['cover']) ? '/cover.twig' : '/index.twig';
  254. return $this->render($response, $route, [
  255. 'home' => $home,
  256. 'navigation' => $navigation,
  257. 'title' => $title,
  258. 'content' => $contentHTML,
  259. 'item' => $item,
  260. 'breadcrumb' => $breadcrumb,
  261. 'settings' => $this->settings,
  262. 'metatabs' => $metatabs,
  263. 'base_url' => $base_url,
  264. 'image' => $firstImage,
  265. 'logo' => $logo,
  266. 'favicon' => $favicon
  267. ]);
  268. }
  269. protected function getCachedStructure($cache)
  270. {
  271. return $cache->getCache('cache', 'structure.txt');
  272. }
  273. protected function getFreshStructure($pathToContent, $cache, $uri)
  274. {
  275. /* scan the content of the folder */
  276. $pagetree = Folder::scanFolder($pathToContent);
  277. /* if there is no content, render an empty page */
  278. if(count($pagetree) == 0)
  279. {
  280. return false;
  281. }
  282. # get the extended structure files with changes like navigation title or hidden pages
  283. $yaml = new writeYaml();
  284. $extended = $yaml->getYaml('cache', 'structure-extended.yaml');
  285. # create an array of object with the whole content of the folder
  286. $structure = Folder::getFolderContentDetails($pagetree, $extended, $uri->getBaseUrl(), $uri->getBasePath());
  287. # now update the extended structure
  288. if(!$extended)
  289. {
  290. $extended = $this->createExtended($this->pathToContent, $yaml, $structure);
  291. if(!empty($extended))
  292. {
  293. $yaml->updateYaml('cache', 'structure-extended.yaml', $extended);
  294. # we have to update the structure with extended again
  295. $structure = Folder::getFolderContentDetails($pagetree, $extended, $uri->getBaseUrl(), $uri->getBasePath());
  296. }
  297. }
  298. # cache structure
  299. $cache->updateCache('cache', 'structure.txt', 'lastCache.txt', $structure);
  300. if($extended && $this->containsHiddenPages($extended))
  301. {
  302. # generate the navigation (delete empty pages)
  303. $navigation = $this->createNavigationFromStructure($structure);
  304. # cache navigation
  305. $cache->updateCache('cache', 'navigation.txt', false, $navigation);
  306. }
  307. else
  308. {
  309. # make sure no separate navigation file is set
  310. $cache->deleteFileWithPath('cache' . DIRECTORY_SEPARATOR . 'navigation.txt');
  311. }
  312. # load and return the cached structure, because might be manipulated with navigation....
  313. return $this->getCachedStructure($cache);
  314. }
  315. protected function createExtended($contentPath, $yaml, $structure, $extended = NULL)
  316. {
  317. if(!$extended)
  318. {
  319. $extended = [];
  320. }
  321. foreach ($structure as $key => $item)
  322. {
  323. # $filename = ($item->elementType == 'folder') ? DIRECTORY_SEPARATOR . 'index.yaml' : $item->pathWithoutType . '.yaml';
  324. $filename = $item->pathWithoutType . '.yaml';
  325. if(file_exists($contentPath . $filename))
  326. {
  327. # read file
  328. $meta = $yaml->getYaml('content', $filename);
  329. $extended[$item->urlRelWoF]['hide'] = isset($meta['meta']['hide']) ? $meta['meta']['hide'] : false;
  330. $extended[$item->urlRelWoF]['navtitle'] = isset($meta['meta']['navtitle']) ? $meta['meta']['navtitle'] : '';
  331. }
  332. if ($item->elementType == 'folder')
  333. {
  334. $extended = $this->createExtended($contentPath, $yaml, $item->folderContent, $extended);
  335. }
  336. }
  337. return $extended;
  338. }
  339. protected function containsHiddenPages($extended)
  340. {
  341. foreach($extended as $element)
  342. {
  343. if(isset($element['hide']) && $element['hide'] === true)
  344. {
  345. return true;
  346. }
  347. }
  348. return false;
  349. }
  350. protected function createNavigationFromStructure($navigation)
  351. {
  352. foreach ($navigation as $key => $element)
  353. {
  354. if($element->hide === true)
  355. {
  356. unset($navigation[$key]);
  357. }
  358. elseif(isset($element->folderContent))
  359. {
  360. $navigation[$key]->folderContent = $this->createNavigationFromStructure($element->folderContent);
  361. }
  362. }
  363. return $navigation;
  364. }
  365. # 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
  366. protected function updateVersion($baseUrl)
  367. {
  368. /* check the latest public typemill version */
  369. $version = new VersionCheck();
  370. $latestVersion = $version->checkVersion($baseUrl);
  371. if($latestVersion)
  372. {
  373. /* store latest version */
  374. \Typemill\Settings::updateSettings(array('latestVersion' => $latestVersion));
  375. }
  376. }
  377. protected function getFirstImage(array $contentBlocks)
  378. {
  379. foreach($contentBlocks as $block)
  380. {
  381. /* is it a paragraph? */
  382. if(isset($block['name']) && $block['name'] == 'p')
  383. {
  384. if(isset($block['handler']['argument']) && substr($block['handler']['argument'], 0, 2) == '![' )
  385. {
  386. return $block['handler']['argument'];
  387. }
  388. }
  389. }
  390. return false;
  391. }
  392. # checks if a page has a restriction in meta and if the current user is blocked by that restriction
  393. protected function checkRestrictions($meta)
  394. {
  395. # check if content restrictions are active
  396. if(isset($this->settings['pageaccess']) && $this->settings['pageaccess'])
  397. {
  398. # check if page is restricted to certain user
  399. if(isset($meta['alloweduser']) && $meta['alloweduser'] && $meta['alloweduser'] !== '' )
  400. {
  401. $alloweduser = array_map('trim', explode(",", $meta['alloweduser']));
  402. if(isset($_SESSION['user']) && in_array($_SESSION['user'], $alloweduser))
  403. {
  404. # user has access to the page, so there are no restrictions
  405. return false;
  406. }
  407. # otherwise return array with type of restriction and allowed username
  408. return [ 'alloweduser' => $meta['alloweduser'] ];
  409. }
  410. # check if page is restricted to certain userrole
  411. if(isset($meta['allowedrole']) && $meta['allowedrole'] && $meta['allowedrole'] !== '' )
  412. {
  413. # var_dump($this->c->acl->inheritsRole('editor', 'member'));
  414. # die();
  415. if(
  416. isset($_SESSION['role'])
  417. AND (
  418. $_SESSION['role'] == 'administrator'
  419. OR $_SESSION['role'] == $meta['allowedrole']
  420. OR $this->c->acl->inheritsRole($_SESSION['role'], $meta['allowedrole'])
  421. )
  422. )
  423. {
  424. # role has access to page, so there are no restrictions
  425. return false;
  426. }
  427. return [ 'allowedrole' => $meta['allowedrole'] ];
  428. }
  429. }
  430. return false;
  431. }
  432. protected function cutRestrictedContent($markdown)
  433. {
  434. #initially add only the title of the page.
  435. $restrictedMarkdown = [$markdown[0]];
  436. unset($markdown[0]);
  437. if(isset($this->settings['hrdelimiter']) && $this->settings['hrdelimiter'] !== NULL )
  438. {
  439. foreach ($markdown as $block)
  440. {
  441. $firstCharacters = substr($block, 0, 3);
  442. if($firstCharacters == '---' OR $firstCharacters == '***')
  443. {
  444. return $restrictedMarkdown;
  445. }
  446. $restrictedMarkdown[] = $block;
  447. }
  448. # no delimiter found, so use the title only
  449. $restrictedMarkdown = [$restrictedMarkdown[0]];
  450. }
  451. return $restrictedMarkdown;
  452. }
  453. protected function prepareRestrictionNotice()
  454. {
  455. if( isset($this->settings['restrictionnotice']) && $this->settings['restrictionnotice'] != '' )
  456. {
  457. $restrictionNotice = $this->settings['restrictionnotice'];
  458. }
  459. else
  460. {
  461. $restrictionNotice = 'You are not allowed to access this content.';
  462. }
  463. if( isset($this->settings['wraprestrictionnotice']) && $this->settings['wraprestrictionnotice'] )
  464. {
  465. # standardize line breaks
  466. $text = str_replace(array("\r\n", "\r"), "\n", $restrictionNotice);
  467. # remove surrounding line breaks
  468. $text = trim($text, "\n");
  469. # split text into lines
  470. $lines = explode("\n", $text);
  471. $restrictionNotice = '';
  472. foreach($lines as $key => $line)
  473. {
  474. $restrictionNotice .= "!!!! " . $line . "\n";
  475. }
  476. }
  477. return $restrictionNotice;
  478. }
  479. }