ArticleApiController.php 36 KB

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