ArticleApiController.php 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991
  1. <?php
  2. namespace Typemill\Controllers;
  3. use Slim\Http\Request;
  4. use Slim\Http\Response;
  5. use Typemill\Models\Folder;
  6. use Typemill\Models\Write;
  7. use Typemill\Models\WriteYaml;
  8. use Typemill\Models\WriteMeta;
  9. use Typemill\Extensions\ParsedownExtension;
  10. use Typemill\Events\OnPagePublished;
  11. use Typemill\Events\OnPageUnpublished;
  12. use Typemill\Events\OnPageDeleted;
  13. use Typemill\Events\OnPageSorted;
  14. use \URLify;
  15. class ArticleApiController extends ContentController
  16. {
  17. public function publishArticle(Request $request, Response $response, $args)
  18. {
  19. # get params from call
  20. $this->params = $request->getParams();
  21. $this->uri = $request->getUri()->withUserInfo('');
  22. # minimum permission is that user can publish his own content
  23. if(!$this->c->acl->isAllowed($_SESSION['role'], 'mycontent', 'publish'))
  24. {
  25. return $response->withJson(array('data' => false, 'errors' => ['message' => 'You are not allowed to publish content.']), 403);
  26. }
  27. # validate input only if raw mode
  28. if($this->params['raw'])
  29. {
  30. if(!$this->validateEditorInput()){ return $response->withJson($this->errors,422); }
  31. }
  32. # set structure
  33. if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); }
  34. # set item
  35. if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
  36. # if user has no right to update content from others (eg admin or editor)
  37. if(!$this->c->acl->isAllowed($_SESSION['role'], 'content', 'publish'))
  38. {
  39. # check ownership. This code should nearly never run, because there is no button/interface to trigger it.
  40. if(!$this->checkContentOwnership())
  41. {
  42. return $response->withJson(array('data' => false, 'errors' => ['message' => 'You are not allowed to publish content.']), 403);
  43. }
  44. }
  45. # set the status for published and drafted
  46. $this->setPublishStatus();
  47. # set path
  48. $this->setItemPath($this->item->fileType);
  49. # if raw mode, use the content from request
  50. if($this->params['raw'])
  51. {
  52. $this->content = '# ' . $this->params['title'] . "\r\n\r\n" . $this->params['content'];
  53. }
  54. else
  55. {
  56. # read content from file
  57. if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
  58. # If it is a draft, then create clean markdown content
  59. if(is_array($this->content))
  60. {
  61. # initialize parsedown extension
  62. $parsedown = new ParsedownExtension();
  63. # turn markdown into an array of markdown-blocks
  64. $this->content = $parsedown->arrayBlocksToMarkdown($this->content);
  65. }
  66. }
  67. # set path for the file (or folder)
  68. $this->setItemPath('md');
  69. # update the file
  70. if($this->write->writeFile($this->settings['contentFolder'], $this->path, $this->content))
  71. {
  72. # update the file
  73. $delete = $this->deleteContentFiles(['txt']);
  74. # update the internal structure
  75. $this->setStructure($draft = true, $cache = false);
  76. # update the public structure
  77. $this->setStructure($draft = false, $cache = false);
  78. # complete the page meta if title or description not set
  79. $writeMeta = new WriteMeta();
  80. $meta = $writeMeta->completePageMeta($this->content, $this->settings, $this->item);
  81. # dispatch event
  82. $page = ['content' => $this->content, 'meta' => $meta, 'item' => $this->item];
  83. $page = $this->c->dispatcher->dispatch('onPagePublished', new OnPagePublished($page))->getData();
  84. return $response->withJson(['success' => true, 'meta' => $page['meta']], 200);
  85. }
  86. else
  87. {
  88. return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404);
  89. }
  90. }
  91. public function unpublishArticle(Request $request, Response $response, $args)
  92. {
  93. # get params from call
  94. $this->params = $request->getParams();
  95. $this->uri = $request->getUri()->withUserInfo('');
  96. # minimum permission is that user can unpublish his own content
  97. if(!$this->c->acl->isAllowed($_SESSION['role'], 'mycontent', 'unpublish'))
  98. {
  99. return $response->withJson(array('data' => false, 'errors' => ['message' => 'You are not allowed to unpublish content.']), 403);
  100. }
  101. # set structure
  102. if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); }
  103. # set item
  104. if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
  105. # if user has no right to update content from others (eg admin or editor)
  106. if(!$this->c->acl->isAllowed($_SESSION['role'], 'content', 'unpublish'))
  107. {
  108. # check ownership. This code should nearly never run, because there is no button/interface to trigger it.
  109. if(!$this->checkContentOwnership())
  110. {
  111. return $response->withJson(array('data' => false, 'errors' => ['message' => 'You are not allowed to unpublish content.']), 403);
  112. }
  113. }
  114. # set the status for published and drafted
  115. $this->setPublishStatus();
  116. # check if draft exists, if not, create one.
  117. if(!$this->item->drafted)
  118. {
  119. # set path for the file (or folder)
  120. $this->setItemPath('md');
  121. # set content of markdown-file
  122. if(!$this->setContent()){ return $response->withJson($this->errors, 404); }
  123. # initialize parsedown extension
  124. $parsedown = new ParsedownExtension();
  125. # turn markdown into an array of markdown-blocks
  126. $contentArray = $parsedown->markdownToArrayBlocks($this->content);
  127. # encode the content into json
  128. $contentJson = json_encode($contentArray);
  129. # set path for the file (or folder)
  130. $this->setItemPath('txt');
  131. /* update the file */
  132. if(!$this->write->writeFile($this->settings['contentFolder'], $this->path, $contentJson))
  133. {
  134. return $response->withJson(['errors' => ['message' => 'Could not create a draft of the page. Please check if the folder is writable']], 404);
  135. }
  136. }
  137. # check if it is a folder and if the folder has published pages.
  138. $message = false;
  139. if($this->item->elementType == 'folder')
  140. {
  141. foreach($this->item->folderContent as $folderContent)
  142. {
  143. if($folderContent->status == 'published')
  144. {
  145. $message = 'There are published pages within this folder. The pages are not visible on your website anymore.';
  146. }
  147. }
  148. }
  149. # update the file
  150. $delete = $this->deleteContentFiles(['md']);
  151. if($delete)
  152. {
  153. # update the internal structure
  154. $this->setStructure($draft = true, $cache = false);
  155. # update the live structure
  156. $this->setStructure($draft = false, $cache = false);
  157. # dispatch event
  158. $this->c->dispatcher->dispatch('onPageUnpublished', new OnPageUnpublished($this->item));
  159. return $response->withJson(['success' => ['message' => $message]], 200);
  160. }
  161. else
  162. {
  163. return $response->withJson(['errors' => ['message' => "Could not delete some files. Please check if the files exists and are writable"]], 404);
  164. }
  165. }
  166. public function discardArticleChanges(Request $request, Response $response, $args)
  167. {
  168. # get params from call
  169. $this->params = $request->getParams();
  170. $this->uri = $request->getUri()->withUserInfo('');
  171. # minimum permission is that user is allowed to update his own content
  172. if(!$this->c->acl->isAllowed($_SESSION['role'], 'mycontent', 'update'))
  173. {
  174. return $response->withJson(array('data' => false, 'errors' => ['message' => 'You are not allowed to publish content.']), 403);
  175. }
  176. # set structure
  177. if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); }
  178. # set item
  179. if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
  180. # if user has no right to update content from others (eg admin or editor)
  181. if(!$this->c->acl->isAllowed($_SESSION['role'], 'content', 'update'))
  182. {
  183. # check ownership. This code should nearly never run, because there is no button/interface to trigger it.
  184. if(!$this->checkContentOwnership())
  185. {
  186. return $response->withJson(array('data' => false, 'errors' => ['message' => 'You are not allowed to update content.']), 403);
  187. }
  188. }
  189. # remove the unpublished changes
  190. $delete = $this->deleteContentFiles(['txt']);
  191. # set redirect url to edit page
  192. $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor'];
  193. if(isset($this->item->urlRelWoF))
  194. {
  195. $url = $url . $this->item->urlRelWoF;
  196. }
  197. # remove the unpublished changes
  198. $delete = $this->deleteContentFiles(['txt']);
  199. if($delete)
  200. {
  201. # update the backend structure
  202. $this->setStructure($draft = true, $cache = false);
  203. return $response->withJson(['data' => $this->structure, 'errors' => false, 'url' => $url], 200);
  204. }
  205. else
  206. {
  207. return $response->withJson(['data' => $this->structure, 'errors' => $this->errors], 404);
  208. }
  209. }
  210. public function deleteArticle(Request $request, Response $response, $args)
  211. {
  212. # get params from call
  213. $this->params = $request->getParams();
  214. $this->uri = $request->getUri()->withUserInfo('');
  215. # minimum permission is that user is allowed to delete his own content
  216. if(!$this->c->acl->isAllowed($_SESSION['role'], 'mycontent', 'delete'))
  217. {
  218. return $response->withJson(array('data' => false, 'errors' => ['message' => 'You are not allowed to delete content.']), 403);
  219. }
  220. # set url to base path initially
  221. $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor'];
  222. # set structure
  223. if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); }
  224. # set item
  225. if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
  226. # if user has no right to delete content from others (eg admin or editor)
  227. if(!$this->c->acl->isAllowed($_SESSION['role'], 'content', 'delete'))
  228. {
  229. # check ownership. This code should nearly never run, because there is no button/interface to trigger it.
  230. if(!$this->checkContentOwnership())
  231. {
  232. return $response->withJson(array('data' => false, 'errors' => ['message' => 'You are not allowed to delete content.']), 403);
  233. }
  234. }
  235. if($this->item->elementType == 'file')
  236. {
  237. $delete = $this->deleteContentFiles(['md','txt', 'yaml']);
  238. }
  239. elseif($this->item->elementType == 'folder')
  240. {
  241. $delete = $this->deleteContentFolder();
  242. }
  243. if($delete)
  244. {
  245. # check if it is a subfile or subfolder and set the redirect-url to the parent item
  246. if(count($this->item->keyPathArray) > 1)
  247. {
  248. # get the parent item
  249. $parentItem = Folder::getParentItem($this->structure, $this->item->keyPathArray);
  250. if($parentItem)
  251. {
  252. # an active file has been moved to another folder
  253. $url .= $parentItem->urlRelWoF;
  254. }
  255. }
  256. # update the live structure
  257. $this->setStructure($draft = false, $cache = false);
  258. # update the backend structure
  259. $this->setStructure($draft = true, $cache = false);
  260. # check if page is in extended structure and delete it
  261. $this->deleteFromExtended();
  262. # dispatch event
  263. $this->c->dispatcher->dispatch('onPageDeleted', new OnPageDeleted($this->item));
  264. return $response->withJson(array('data' => $this->structure, 'errors' => false, 'url' => $url), 200);
  265. }
  266. else
  267. {
  268. return $response->withJson(array('data' => $this->structure, 'errors' => $this->errors), 422);
  269. }
  270. }
  271. public function updateArticle(Request $request, Response $response, $args)
  272. {
  273. # get params from call
  274. $this->params = $request->getParams();
  275. $this->uri = $request->getUri()->withUserInfo('');
  276. # minimum permission is that user is allowed to update his own content
  277. if(!$this->c->acl->isAllowed($_SESSION['role'], 'mycontent', 'update'))
  278. {
  279. return $response->withJson(array('data' => false, 'errors' => ['message' => 'You are not allowed to update content.']), 403);
  280. }
  281. # validate input
  282. if(!$this->validateEditorInput()){ return $response->withJson($this->errors,422); }
  283. # set structure
  284. if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); }
  285. # set item
  286. if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
  287. # if user has no right to delete content from others (eg admin or editor)
  288. if(!$this->c->acl->isAllowed($_SESSION['role'], 'content', 'update'))
  289. {
  290. # check ownership. This code should nearly never run, because there is no button/interface to trigger it.
  291. if(!$this->checkContentOwnership())
  292. {
  293. return $response->withJson(array('data' => false, 'errors' => ['message' => 'You are not allowed to update content.']), 403);
  294. }
  295. }
  296. # set path for the file (or folder)
  297. $this->setItemPath('txt');
  298. # merge title with content for complete markdown document
  299. $updatedContent = '# ' . $this->params['title'] . "\r\n\r\n" . $this->params['content'];
  300. # initialize parsedown extension
  301. $parsedown = new ParsedownExtension();
  302. # turn markdown into an array of markdown-blocks
  303. $contentArray = $parsedown->markdownToArrayBlocks($updatedContent);
  304. # encode the content into json
  305. $contentJson = json_encode($contentArray);
  306. /* update the file */
  307. if($this->write->writeFile($this->settings['contentFolder'], $this->path, $contentJson))
  308. {
  309. # update the internal structure
  310. $this->setStructure($draft = true, $cache = false);
  311. return $response->withJson(['success'], 200);
  312. }
  313. else
  314. {
  315. return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404);
  316. }
  317. }
  318. public function sortArticle(Request $request, Response $response, $args)
  319. {
  320. # get params from call
  321. $this->params = $request->getParams();
  322. $this->uri = $request->getUri()->withUserInfo('');
  323. # minimum permission is that user is allowed to update his own content
  324. if(!$this->c->acl->isAllowed($_SESSION['role'], 'mycontent', 'update'))
  325. {
  326. return $response->withJson(array('data' => false, 'errors' => 'You are not allowed to update content.'), 403);
  327. }
  328. # url is only needed, if an active page is moved to another folder, so user has to be redirected to the new url
  329. $url = false;
  330. # set structure
  331. if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); }
  332. # validate input
  333. if(!$this->validateNavigationSort()){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Data not valid. Please refresh the page and try again.', 'url' => $url), 422); }
  334. # get the ids (key path) for item, old folder and new folder
  335. $itemKeyPath = explode('.', $this->params['item_id']);
  336. $parentKeyFrom = explode('.', $this->params['parent_id_from']);
  337. $parentKeyTo = explode('.', $this->params['parent_id_to']);
  338. # get the item from structure
  339. $item = Folder::getItemWithKeyPath($this->structure, $itemKeyPath);
  340. if(!$item){ return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not find this page. Please refresh and try again.', 'url' => $url), 404); }
  341. # needed for acl check
  342. $this->item = $item;
  343. # if user has no right to update content from others (eg admin or editor)
  344. if(!$this->c->acl->isAllowed($_SESSION['role'], 'content', 'update'))
  345. {
  346. # check ownership. This code should nearly never run, because there is no button/interface to trigger it.
  347. if(!$this->checkContentOwnership())
  348. {
  349. return $response->withJson(array('data' => $this->structure, 'errors' => 'You are not allowed to move that content.'), 403);
  350. }
  351. }
  352. # if an item is moved to the first level
  353. if($this->params['parent_id_to'] == 'navi')
  354. {
  355. # create empty and default values so that the logic below still works
  356. $newFolder = new \stdClass();
  357. $newFolder->path = '';
  358. $folderContent = $this->structure;
  359. }
  360. else
  361. {
  362. # get the target folder from structure
  363. $newFolder = Folder::getItemWithKeyPath($this->structure, $parentKeyTo);
  364. # get the content of the target folder
  365. $folderContent = $newFolder->folderContent;
  366. }
  367. # if the item has been moved within the same folder
  368. if($this->params['parent_id_from'] == $this->params['parent_id_to'])
  369. {
  370. # get key of item
  371. $itemKey = end($itemKeyPath);
  372. reset($itemKeyPath);
  373. # delete item from folderContent
  374. unset($folderContent[$itemKey]);
  375. }
  376. else
  377. {
  378. # rename links in extended file
  379. $this->renameExtended($item, $newFolder);
  380. # an active file has been moved to another folder, so send new url with response
  381. if($this->params['active'] == 'active')
  382. {
  383. $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor'] . $newFolder->urlRelWoF . '/' . $item->slug;
  384. }
  385. }
  386. # add item to newFolder
  387. array_splice($folderContent, $this->params['index_new'], 0, array($item));
  388. # initialize index
  389. $index = 0;
  390. # initialise write object
  391. $write = new Write();
  392. # iterate through the whole content of the new folder to rename the files
  393. $writeError = false;
  394. foreach($folderContent as $folderItem)
  395. {
  396. if(!$write->moveElement($folderItem, $newFolder->path, $index))
  397. {
  398. $writeError = true;
  399. }
  400. $index++;
  401. }
  402. if($writeError){ return $response->withJson(array('data' => $this->structure, 'errors' => ['message' => 'Something went wrong. Please refresh the page and check, if all folders and files are writable.'], 'url' => $url), 404); }
  403. # update the structure for editor
  404. $this->setStructure($draft = true, $cache = false);
  405. # get item for url and set it active again
  406. if(isset($this->params['url']))
  407. {
  408. $activeItem = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl());
  409. }
  410. # keep the internal structure for response
  411. $internalStructure = $this->structure;
  412. # update the structure for website
  413. $this->setStructure($draft = false, $cache = false);
  414. # dispatch event
  415. $this->c->dispatcher->dispatch('onPageSorted', new OnPageSorted($this->params));
  416. return $response->withJson(array('data' => $internalStructure, 'errors' => false, 'url' => $url));
  417. }
  418. public function createPost(Request $request, Response $response, $args)
  419. {
  420. # get params from call
  421. $this->params = $request->getParams();
  422. $this->uri = $request->getUri()->withUserInfo('');
  423. # minimum permission is that user is allowed to update his own content
  424. if(!$this->c->acl->isAllowed($_SESSION['role'], 'mycontent', 'create'))
  425. {
  426. return $response->withJson(array('data' => false, 'errors' => ['message' => 'You are not allowed to create content.']), 403);
  427. }
  428. # url is only needed, if an active page is moved
  429. $url = false;
  430. # set structure
  431. if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); }
  432. # validate input
  433. if(!$this->validateNaviItem()){ return $response->withJson(array('data' => $this->structure, 'errors' => ['message' => 'Special Characters not allowed. Length between 1 and 60 chars.'], 'url' => $url), 422); }
  434. # get the ids (key path) for item, old folder and new folder
  435. $folderKeyPath = explode('.', $this->params['folder_id']);
  436. # get the item from structure
  437. $folder = Folder::getItemWithKeyPath($this->structure, $folderKeyPath);
  438. if(!$folder){ return $response->withJson(array('data' => $this->structure, 'errors' => ['message' => 'We could not find this page. Please refresh and try again.'], 'url' => $url), 404); }
  439. $name = $this->params['item_name'];
  440. $slug = URLify::filter(iconv(mb_detect_encoding($this->params['item_name'], mb_detect_order(), true), "UTF-8", $this->params['item_name']));
  441. $namePath = date("YmdHi") . '-' . $slug;
  442. $folderPath = 'content' . $folder->path;
  443. $content = json_encode(['# ' . $name, 'Content']);
  444. # initialise write object
  445. $write = new WriteYaml();
  446. # check, if name exists
  447. if($write->checkFile($folderPath, $namePath . '.txt') OR $write->checkFile($folderPath, $namePath . '.md'))
  448. {
  449. return $response->withJson(array('data' => $this->structure, 'errors' => 'There is already a page with this name. Please choose another name.', 'url' => $url), 404);
  450. }
  451. if(!$write->writeFile($folderPath, $namePath . '.txt', $content))
  452. {
  453. 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);
  454. }
  455. # get extended structure
  456. $extended = $write->getYaml('cache', 'structure-extended.yaml');
  457. # create the url for the item
  458. $urlWoF = $folder->urlRelWoF . '/' . $slug;
  459. # add the navigation name to the item htmlspecialchars needed for french language
  460. $extended[$urlWoF] = ['hide' => false, 'navtitle' => $name];
  461. # store the extended structure
  462. $write->updateYaml('cache', 'structure-extended.yaml', $extended);
  463. # update the structure for editor
  464. $this->setStructure($draft = true, $cache = false);
  465. $folder = Folder::getItemWithKeyPath($this->structure, $folderKeyPath);
  466. # activate this if you want to redirect after creating the page...
  467. # $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor'] . $folder->urlRelWoF . '/' . $slug;
  468. return $response->withJson(array('posts' => $folder, $this->structure, 'errors' => false, 'url' => $url));
  469. }
  470. public function createArticle(Request $request, Response $response, $args)
  471. {
  472. # get params from call
  473. $this->params = $request->getParams();
  474. $this->uri = $request->getUri()->withUserInfo('');
  475. # minimum permission is that user is allowed to update his own content
  476. if(!$this->c->acl->isAllowed($_SESSION['role'], 'mycontent', 'create'))
  477. {
  478. return $response->withJson(array('data' => false, 'errors' => ['message' => 'You are not allowed to create content.']), 403);
  479. }
  480. # url is only needed, if an active page is moved
  481. $url = false;
  482. # set structure
  483. if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); }
  484. # validate input
  485. if(!$this->validateNaviItem()){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Special Characters not allowed. Length between 1 and 60 chars.', 'url' => $url), 422); }
  486. # get the ids (key path) for item, old folder and new folder
  487. $folderKeyPath = explode('.', $this->params['folder_id']);
  488. # get the item from structure
  489. $folder = Folder::getItemWithKeyPath($this->structure, $folderKeyPath);
  490. if(!$folder){ return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not find this page. Please refresh and try again.', 'url' => $url), 404); }
  491. # Rename all files within the folder to make sure, that namings and orders are correct
  492. # get the content of the target folder
  493. $folderContent = $folder->folderContent;
  494. $name = $this->params['item_name'];
  495. $slug = URLify::filter(iconv(mb_detect_encoding($this->params['item_name'], mb_detect_order(), true), "UTF-8", $this->params['item_name']));
  496. # create the name for the new item
  497. # $nameParts = Folder::getStringParts($this->params['item_name']);
  498. # $name = implode("-", $nameParts);
  499. # $slug = $name;
  500. # initialize index
  501. $index = 0;
  502. # initialise write object
  503. $write = new WriteYaml();
  504. # iterate through the whole content of the new folder
  505. $writeError = false;
  506. foreach($folderContent as $folderItem)
  507. {
  508. # check, if the same name as new item, then return an error
  509. if($folderItem->slug == $slug)
  510. {
  511. return $response->withJson(array('data' => $this->structure, 'errors' => 'There is already a page with this name. Please choose another name.', 'url' => $url), 404);
  512. }
  513. if(!$write->moveElement($folderItem, $folder->path, $index))
  514. {
  515. $writeError = true;
  516. }
  517. $index++;
  518. }
  519. 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); }
  520. # add prefix number to the name
  521. $namePath = $index > 9 ? $index . '-' . $slug : '0' . $index . '-' . $slug;
  522. $folderPath = 'content' . $folder->path;
  523. # create default content
  524. $content = json_encode(['# ' . $name, 'Content']);
  525. if($this->params['type'] == 'file')
  526. {
  527. if(!$write->writeFile($folderPath, $namePath . '.txt', $content))
  528. {
  529. 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);
  530. }
  531. }
  532. elseif($this->params['type'] == 'folder')
  533. {
  534. if(!$write->checkPath($folderPath . DIRECTORY_SEPARATOR . $namePath))
  535. {
  536. 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);
  537. }
  538. $write->writeFile($folderPath . DIRECTORY_SEPARATOR . $namePath, 'index.txt', $content);
  539. # always redirect to a folder
  540. $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor'] . $folder->urlRelWoF . '/' . $slug;
  541. }
  542. # get extended structure
  543. $extended = $write->getYaml('cache', 'structure-extended.yaml');
  544. # create the url for the item
  545. $urlWoF = $folder->urlRelWoF . '/' . $slug;
  546. # add the navigation name to the item htmlspecialchars needed for french language
  547. $extended[$urlWoF] = ['hide' => false, 'navtitle' => $name];
  548. # store the extended structure
  549. $write->updateYaml('cache', 'structure-extended.yaml', $extended);
  550. # update the structure for editor
  551. $this->setStructure($draft = true, $cache = false);
  552. # get item for url and set it active again
  553. if(isset($this->params['url']))
  554. {
  555. $activeItem = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl());
  556. }
  557. # activate this if you want to redirect after creating the page...
  558. # $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor'] . $folder->urlRelWoF . '/' . $slug;
  559. return $response->withJson(array('data' => $this->structure, 'errors' => false, 'url' => $url));
  560. }
  561. public function createBaseItem(Request $request, Response $response, $args)
  562. {
  563. # get params from call
  564. $this->params = $request->getParams();
  565. $this->uri = $request->getUri()->withUserInfo('');
  566. # minimum permission is that user is allowed to update his own content
  567. if(!$this->c->acl->isAllowed($_SESSION['role'], 'mycontent', 'create'))
  568. {
  569. return $response->withJson(array('data' => false, 'errors' => 'You are not allowed to create content.'), 403);
  570. }
  571. # url is only needed, if an active page is moved
  572. $url = false;
  573. # set structure
  574. if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); }
  575. # validate input
  576. if(!$this->validateBaseNaviItem()){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Special Characters not allowed. Length between 1 and 20 chars.', 'url' => $url), 422); }
  577. # create the name for the new item
  578. # $nameParts = Folder::getStringParts($this->params['item_name']);
  579. # $name = implode("-", $nameParts);
  580. # $slug = $name;
  581. $name = $this->params['item_name'];
  582. $slug = URLify::filter(iconv(mb_detect_encoding($this->params['item_name'], mb_detect_order(), true), "UTF-8", $this->params['item_name']));
  583. # initialize index
  584. $index = 0;
  585. # initialise write object
  586. $write = new WriteYaml();
  587. # iterate through the whole content of the new folder
  588. $writeError = false;
  589. foreach($this->structure as $item)
  590. {
  591. # check, if the same name as new item, then return an error
  592. if($item->slug == $slug)
  593. {
  594. return $response->withJson(array('data' => $this->structure, 'errors' => 'There is already a page with this name. Please choose another name.', 'url' => $url), 422);
  595. }
  596. if(!$write->moveElement($item, '', $index))
  597. {
  598. $writeError = true;
  599. }
  600. $index++;
  601. }
  602. 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), 422); }
  603. # add prefix number to the name
  604. $namePath = $index > 9 ? $index . '-' . $slug : '0' . $index . '-' . $slug;
  605. $folderPath = 'content';
  606. # create default content
  607. # $content = json_encode(['# Add Title', 'Add Content']);
  608. $content = json_encode(['# ' . $name, 'Content']);
  609. if($this->params['type'] == 'file')
  610. {
  611. if(!$write->writeFile($folderPath, $namePath . '.txt', $content))
  612. {
  613. 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), 422);
  614. }
  615. }
  616. elseif($this->params['type'] == 'folder')
  617. {
  618. if(!$write->checkPath($folderPath . DIRECTORY_SEPARATOR . $namePath))
  619. {
  620. 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), 422);
  621. }
  622. $write->writeFile($folderPath . DIRECTORY_SEPARATOR . $namePath, 'index.txt', $content);
  623. # activate this if you want to redirect after creating the page...
  624. $url = $this->uri->getBaseUrl() . '/tm/content/' . $this->settings['editor'] . '/' . $slug;
  625. }
  626. # get extended structure
  627. $extended = $write->getYaml('cache', 'structure-extended.yaml');
  628. # create the url for the item
  629. $urlWoF = '/' . $slug;
  630. # add the navigation name to the item htmlspecialchars needed for frensh language
  631. $extended[$urlWoF] = ['hide' => false, 'navtitle' => $name];
  632. # store the extended structure
  633. $write->updateYaml('cache', 'structure-extended.yaml', $extended);
  634. # update the structure for editor
  635. $this->setStructure($draft = true, $cache = false);
  636. # get item for url and set it active again
  637. if(isset($this->params['url']))
  638. {
  639. $activeItem = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl());
  640. }
  641. return $response->withJson(array('data' => $this->structure, 'errors' => false, 'url' => $url));
  642. }
  643. public function getNavigation(Request $request, Response $response, $args)
  644. {
  645. # get params from call
  646. $this->params = $request->getParams();
  647. $this->uri = $request->getUri()->withUserInfo('');
  648. # set structure
  649. if(!$this->setStructure($draft = true, $cache = false)){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); }
  650. # set information for homepage
  651. $this->setHomepage();
  652. # get item for url and set it active again
  653. if(isset($this->params['url']))
  654. {
  655. $activeItem = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl());
  656. }
  657. return $response->withJson(array('data' => $this->structure, 'homepage' => $this->homepage, 'errors' => false));
  658. }
  659. public function getArticleMarkdown(Request $request, Response $response, $args)
  660. {
  661. /* get params from call */
  662. $this->params = $request->getParams();
  663. $this->uri = $request->getUri()->withUserInfo('');
  664. # minimum permission is that user is allowed to update his own content. This will completely disable the block-editor
  665. if(!$this->c->acl->isAllowed($_SESSION['role'], 'mycontent', 'update'))
  666. {
  667. return $response->withJson(array('data' => false, 'errors' => 'You are not allowed to edit content.'), 403);
  668. }
  669. # set structure
  670. if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
  671. /* set item */
  672. if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
  673. # if user has no right to delete content from others (eg admin or editor)
  674. if(!$this->c->acl->isAllowed($_SESSION['role'], 'content', 'update'))
  675. {
  676. # check ownership. This code should nearly never run, because there is no button/interface to trigger it.
  677. if(!$this->checkContentOwnership())
  678. {
  679. return $response->withJson(array('data' => false, 'errors' => 'You are not allowed to delete content.'), 403);
  680. }
  681. }
  682. # set the status for published and drafted
  683. $this->setPublishStatus();
  684. # set path
  685. $this->setItemPath($this->item->fileType);
  686. # read content from file
  687. if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
  688. $content = $this->content;
  689. if($content == '')
  690. {
  691. $content = [];
  692. }
  693. # if content is not an array, then transform it
  694. if(!is_array($content))
  695. {
  696. # initialize parsedown extension
  697. $parsedown = new ParsedownExtension();
  698. # turn markdown into an array of markdown-blocks
  699. $content = $parsedown->markdownToArrayBlocks($content);
  700. }
  701. # delete markdown from title
  702. if(isset($content[0]))
  703. {
  704. $content[0] = trim($content[0], "# ");
  705. }
  706. return $response->withJson(array('data' => $content, 'errors' => false));
  707. }
  708. public function getArticleHtml(Request $request, Response $response, $args)
  709. {
  710. /* get params from call */
  711. $this->params = $request->getParams();
  712. $this->uri = $request->getUri()->withUserInfo('');
  713. if(!$this->c->acl->isAllowed($_SESSION['role'], 'mycontent', 'update'))
  714. {
  715. return $response->withJson(array('data' => false, 'errors' => 'You are not allowed to edit content.'), 403);
  716. }
  717. # set structure
  718. if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
  719. /* set item */
  720. if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
  721. # if user has no right to delete content from others (eg admin or editor)
  722. if(!$this->c->acl->isAllowed($_SESSION['role'], 'content', 'update'))
  723. {
  724. # check ownership. This code should nearly never run, because there is no button/interface to trigger it.
  725. if(!$this->checkContentOwnership())
  726. {
  727. return $response->withJson(array('data' => false, 'errors' => 'You are not allowed to delete content.'), 403);
  728. }
  729. }
  730. # set the status for published and drafted
  731. $this->setPublishStatus();
  732. # set path
  733. $this->setItemPath($this->item->fileType);
  734. # read content from file
  735. if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
  736. $content = $this->content;
  737. if($content == '')
  738. {
  739. $content = [];
  740. }
  741. # initialize parsedown extension
  742. $parsedown = new ParsedownExtension();
  743. # fix footnotes in parsedown, might break with complicated footnotes
  744. $parsedown->setVisualMode();
  745. # if content is not an array, then transform it
  746. if(!is_array($content))
  747. {
  748. # turn markdown into an array of markdown-blocks
  749. $content = $parsedown->markdownToArrayBlocks($content);
  750. }
  751. # needed for ToC links
  752. $relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel;
  753. # flag for TOC
  754. $toc = false;
  755. # loop through mardkown-array and create html-blocks
  756. foreach($content as $key => $block)
  757. {
  758. # parse markdown-file to content-array
  759. $contentArray = $parsedown->text($block);
  760. if($block == '[TOC]')
  761. {
  762. $toc = $key;
  763. }
  764. # parse markdown-content-array to content-string
  765. $content[$key] = ['id' => $key, 'html' => $parsedown->markup($contentArray, $relurl)];
  766. }
  767. if($toc)
  768. {
  769. $tocMarkup = $parsedown->buildTOC($parsedown->headlines);
  770. $content[$toc] = ['id' => $toc, 'html' => $tocMarkup];
  771. }
  772. return $response->withJson(array('data' => $content, 'errors' => false));
  773. }
  774. }