diff --git a/content/index.md b/content/index.md index 07ddf3b..f89146c 100644 --- a/content/index.md +++ b/content/index.md @@ -1,6 +1,6 @@ -# Typemill +# Typemill -Typemill is a lightweight, flat-file CMS designed for simple, fast, and flexible website and eBook creation using Markdown. Create handbooks, documentations, manuals, reports, traditional websites, online novels, and more. +Typemill is a lightweight, flat-file CMS designed for simple, fast, and flexible website and eBook creation using Markdown. Create handbooks, documentation, manuals, reports, traditional websites, online novels, and more. Stay in the loop and subscribe to the [Typemill newsletter](https://typemill.net/news)! diff --git a/system/typemill/Controllers/ControllerApiGlobals.php b/system/typemill/Controllers/ControllerApiGlobals.php index b781908..9bd7c3f 100644 --- a/system/typemill/Controllers/ControllerApiGlobals.php +++ b/system/typemill/Controllers/ControllerApiGlobals.php @@ -5,7 +5,11 @@ namespace Typemill\Controllers; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; use Typemill\Models\Navigation; +use Typemill\Models\Validation; +use Typemill\Models\Content; +use Typemill\Models\Meta; use Typemill\Models\Sitemap; +use Typemill\Static\Translations; use Typemill\Models\StorageWrapper; class ControllerApiGlobals extends Controller @@ -46,6 +50,39 @@ class ControllerApiGlobals extends Controller return $response->withHeader('Content-Type', 'application/json')->withStatus(200); } + public function getNavigation(Request $request, Response $response, $args) + { + $params = $request->getQueryParams(); + + $urlinfo = $this->c->get('urlinfo'); + $langattr = $this->settings['langattr']; + $navigation = new Navigation(); + + if(isset($params['draft']) && $params['draft'] == true) + { + $contentnavi = $navigation->getFullDraftNavigation($urlinfo, $langattr); + } + else + { + $contentnavi = $navigation->getLiveNavigation($urlinfo, $langattr); + } + + if(!$contentnavi) + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('navigation not found'), + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(404); + } + + $response->getBody()->write(json_encode([ + 'navigation' => $contentnavi + ])); + + return $response->withHeader('Content-Type', 'application/json'); + } + public function clearNavigation(Request $request, Response $response) { $navigation = new Navigation(); @@ -59,6 +96,215 @@ class ControllerApiGlobals extends Controller return $response->withHeader('Content-Type', 'application/json')->withStatus(200);; } + public function getItemForUrl(Request $request, Response $response, $args) + { + $params = $request->getQueryParams(); + $validate = new Validation(); + $validInput = $validate->articleUrl($params); + if($validInput !== true) + { + $errors = $validate->returnFirstValidationErrors($validInput); + $response->getBody()->write(json_encode([ + 'message' => reset($errors), + 'errors' => $errors + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $urlinfo = $this->c->get('urlinfo'); + $langattr = $this->settings['langattr']; + $navigation = new Navigation(); + $item = $navigation->getItemForUrl($params['url'], $urlinfo, $langattr); + + if(!$item) + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('page not found'), + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(404); + } + + $response->getBody()->write(json_encode([ + 'item' => $item + ])); + + return $response->withHeader('Content-Type', 'application/json'); + } + + public function getItemsForSlug(Request $request, Response $response, $args) + { + $params = $request->getQueryParams(); + $validate = new Validation(); + $validInput = $validate->articleSlug($params); + if($validInput !== true) + { + $errors = $validate->returnFirstValidationErrors($validInput); + $response->getBody()->write(json_encode([ + 'message' => reset($errors), + 'errors' => $errors + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $urlinfo = $this->c->get('urlinfo'); + $langattr = $this->settings['langattr']; + $navigation = new Navigation(); + $items = $navigation->getItemsForSlug($params['slug'], $urlinfo, $langattr); + + if(!$items) + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('page not found'), + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(404); + } + + $response->getBody()->write(json_encode([ + 'items' => $items + ])); + + return $response->withHeader('Content-Type', 'application/json'); + } + + public function getArticleContent(Request $request, Response $response, $args) + { + $params = $request->getQueryParams(); + $validate = new Validation(); + $validInput = $validate->articleUrl($params); + if($validInput !== true) + { + $errors = $validate->returnFirstValidationErrors($validInput); + $response->getBody()->write(json_encode([ + 'message' => reset($errors), + 'errors' => $errors + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $urlinfo = $this->c->get('urlinfo'); + $langattr = $this->settings['langattr']; + $navigation = new Navigation(); + $item = $navigation->getItemForUrl($params['url'], $urlinfo, $langattr); + if(!$item) + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('page not found'), + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(404); + } + + # if user is not allowed to perform this action (e.g. not admin) + if(!$this->userroleIsAllowed($request->getAttribute('c_userrole'), 'content', 'read')) + { + # then check if user is the owner of this content + $meta = new Meta(); + $metadata = $meta->getMetaData($item); + if(!$this->userIsAllowed($request->getAttribute('c_username'), $metadata)) + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('You do not have enough rights.'), + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(403); + } + } + + # GET THE CONTENT + $content = new Content($urlinfo['baseurl'], $this->settings, $this->c->get('dispatcher')); + $markdown = $content->getLiveMarkdown($item); + + if(isset($params['draft']) && $params['draft'] == true) + { + # if draft is explicitly requested + $markdown = $content->getDraftMarkdown($item); + } + + if(!$markdown) + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('page not found'), + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(404); + } + + if(!is_array($markdown)) + { + $markdown = $content->markdownTextToArray($markdown); + } + $markdownHtml = $content->addDraftHtml($markdown); + + $response->getBody()->write(json_encode([ + 'content' => $markdownHtml + ])); + + return $response->withHeader('Content-Type', 'application/json'); + } + + public function getArticleMeta(Request $request, Response $response, $args) + { + $params = $request->getQueryParams(); + $validate = new Validation(); + $validInput = $validate->articleUrl($params); + if($validInput !== true) + { + $errors = $validate->returnFirstValidationErrors($validInput); + $response->getBody()->write(json_encode([ + 'message' => reset($errors), + 'errors' => $errors + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $urlinfo = $this->c->get('urlinfo'); + $langattr = $this->settings['langattr']; + $navigation = new Navigation(); + $item = $navigation->getItemForUrl($params['url'], $urlinfo, $langattr); + if(!$item) + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('page not found'), + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(404); + } + + # if user is not allowed to perform this action (e.g. not admin) + if(!$this->userroleIsAllowed($request->getAttribute('c_userrole'), 'content', 'read')) + { + # then check if user is the owner of this content + $meta = new Meta(); + $metadata = $meta->getMetaData($item); + if(!$this->userIsAllowed($request->getAttribute('c_username'), $metadata)) + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('You do not have enough rights.'), + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(403); + } + } + + # GET THE META + $meta = new Meta(); + $metadata = $meta->getMetaData($item); + $metadata = $meta->addMetaDefaults($metadata, $item, $this->settings['author']); +# $metadata = $meta->addMetaTitleDescription($metadata, $item, $markdown); + + $response->getBody()->write(json_encode([ + 'meta' => $metadata + ])); + + return $response->withHeader('Content-Type', 'application/json'); + } + public function showSecurityLog(Request $request, Response $response) { $storage = new StorageWrapper('\Typemill\Models\Storage'); @@ -154,7 +400,6 @@ class ControllerApiGlobals extends Controller return $response->withHeader('Content-Type', 'application/json')->withStatus(200); } - public function getTranslations(Request $request, Response $response) { $response->getBody()->write(json_encode([ diff --git a/system/typemill/Models/Navigation.php b/system/typemill/Models/Navigation.php index 3fb8d18..4652abc 100644 --- a/system/typemill/Models/Navigation.php +++ b/system/typemill/Models/Navigation.php @@ -140,6 +140,19 @@ class Navigation extends Folder return $result; } + public function getItemsForSlug($slug, $urlinfo, $langattr) + { + $draftNavigation = $this->getFullDraftNavigation($urlinfo, $langattr); + + if(!$draftNavigation) + { + return false; + } + + $items = $this->findItemsWithSlug($draftNavigation, $slug); + + return $items; + } public function getItemForUrl($url, $urlinfo, $langattr) { @@ -813,6 +826,26 @@ class Navigation extends Folder return $flat; } + # only used by public api + public function findItemsWithSlug($navigation, $slug, $result = NULL) + { + foreach($navigation as $key => $item) + { + # set item active, needed to move item in navigation + if($item->slug === $slug) + { + $result[] = $item; + } + elseif($item->elementType === "folder") + { + $result = self::findItemsWithSlug($item->folderContent, $slug, $result); + } + } + + return $result; + } + + # NOT IN USE ANYMORE BUT KEEP IT public function getItemWithUrl($navigation, $url, $result = NULL) { diff --git a/system/typemill/Models/Validation.php b/system/typemill/Models/Validation.php index df718fe..c786a43 100644 --- a/system/typemill/Models/Validation.php +++ b/system/typemill/Models/Validation.php @@ -522,6 +522,36 @@ class Validation return $v->errors(); } + public function articleUrl(array $params) + { + $v = new Validator($params); + + $v->rule('required', 'url'); + $v->rule('regex', 'url', '/^\/?[a-z0-9\-\/]+(?:\?[a-z0-9\-=&]*)?$/i'); + + if($v->validate()) + { + return true; + } + + return $v->errors(); + } + + public function articleSlug(array $params) + { + $v = new Validator($params); + + $v->rule('required', 'slug'); + $v->rule('regex', 'slug', '/^[a-z0-9-]+$/i'); + + if($v->validate()) + { + return true; + } + + return $v->errors(); + } + public function blockInput(array $params) { $v = new Validator($params); diff --git a/system/typemill/routes/api.php b/system/typemill/routes/api.php index 9e53916..6951252 100644 --- a/system/typemill/routes/api.php +++ b/system/typemill/routes/api.php @@ -23,10 +23,6 @@ use Typemill\Controllers\ControllerApiTestmail; $app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) { - # GLOBALS - $group->get('/systemnavi', ControllerApiGlobals::class . ':getSystemnavi')->setName('api.systemnavi.get')->add(new ApiAuthorization($acl, 'account', 'read')); # member - $group->get('/mainnavi', ControllerApiGlobals::class . ':getMainnavi')->setName('api.mainnavi.get')->add(new ApiAuthorization($acl, 'account', 'read')); # member - # SYSTEM $group->get('/settings', ControllerApiSystemSettings::class . ':getSettings')->setName('api.settings.get')->add(new ApiAuthorization($acl, 'system', 'read')); # manager $group->post('/settings', ControllerApiSystemSettings::class . ':updateSettings')->setName('api.settings.set')->add(new ApiAuthorization($acl, 'system', 'update')); # manager @@ -92,10 +88,19 @@ $app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) { $group->post('/meta', ControllerApiAuthorMeta::class . ':updateMeta')->setName('api.metadata.update')->add(new ApiAuthorization($acl, 'mycontent', 'update')); # KIXOTE - $group->delete('/clearnavigation', ControllerApiGlobals::class . ':clearNavigation')->setName('api.navigation.clear')->add(new ApiAuthorization($acl, 'system', 'update')); # manager $group->get('/securitylog', ControllerApiGlobals::class . ':showSecurityLog')->setName('api.securitylog.show')->add(new ApiAuthorization($acl, 'system', 'update')); # manager $group->delete('/securitylog', ControllerApiGlobals::class . ':deleteSecurityLog')->setName('api.securitylog.delete')->add(new ApiAuthorization($acl, 'system', 'update')); # manager $group->delete('/cache', ControllerApiGlobals::class . ':deleteCache')->setName('api.cache.delete')->add(new ApiAuthorization($acl, 'system', 'update')); # manager + $group->delete('/clearnavigation', ControllerApiGlobals::class . ':clearNavigation')->setName('api.navigation.clear')->add(new ApiAuthorization($acl, 'system', 'update')); # manager + + # API USED ONLY EXTERNALLY + $group->get('/systemnavi', ControllerApiGlobals::class . ':getSystemnavi')->setName('api.systemnavi.get')->add(new ApiAuthorization($acl, 'account', 'read')); # member + $group->get('/mainnavi', ControllerApiGlobals::class . ':getMainnavi')->setName('api.mainnavi.get')->add(new ApiAuthorization($acl, 'account', 'read')); # member + $group->get('/navigation', ControllerApiGlobals::class . ':getNavigation')->setName('api.navigation.get')->add(new ApiAuthorization($acl, 'content', 'read')); # author + $group->get('/article/items', ControllerApiGlobals::class . ':getItemsForSlug')->setName('api.articleitems.get')->add(new ApiAuthorization($acl, 'content', 'read')); # author + $group->get('/article/item', ControllerApiGlobals::class . ':getItemForUrl')->setName('api.articleitem.get')->add(new ApiAuthorization($acl, 'content', 'read')); # author + $group->get('/article/content', ControllerApiGlobals::class . ':getArticleContent')->setName('api.articlecontent.get')->add(new ApiAuthorization($acl, 'content', 'read')); # author + $group->get('/article/meta', ControllerApiGlobals::class . ':getArticleMeta')->setName('api.articlemeta.get')->add(new ApiAuthorization($acl, 'content', 'read')); # author })->add(new CorsHeadersMiddleware($settings, $urlinfo))->add(new ApiAuthentication());