diff --git a/system/Controllers/MetaApiController.php b/system/Controllers/MetaApiController.php index d785ae5..b8403b9 100644 --- a/system/Controllers/MetaApiController.php +++ b/system/Controllers/MetaApiController.php @@ -26,15 +26,17 @@ class MetaApiController extends ContentController $metatabs = $writeYaml->getYaml('system' . DIRECTORY_SEPARATOR . 'author', 'metatabs.yaml'); - # add radio buttons to choose posts or pages for folder. - if($folder) + # the fields for user or role based access + if(!isset($this->settings['pageaccess']) || $this->settings['pageaccess'] === NULL ) { - $metatabs['meta']['fields']['contains'] = [ - 'type' => 'radio', - 'label' => 'This folder contains:', - 'options' => ['pages' => 'PAGES (sort in navigation with drag & drop)', 'posts' => 'POSTS (sorted by publish date, for news or blogs)'], - 'class' => 'medium' - ]; + unset($metatabs['meta']['fields']['alloweduser']); + unset($metatabs['meta']['fields']['allowedrole']); + } + + # add radio buttons to choose posts or pages for folder. + if(!$folder) + { + unset($metatabs['meta']['fields']['contains']); } # loop through all plugins @@ -352,6 +354,7 @@ class MetaApiController extends ContentController return $response->withJson(array('metadata' => $metaInput, 'structure' => $structure, 'item' => $this->item, 'errors' => false)); } + # can be deleted ?? private function customfieldsPrepareForEdit($customfields) { # to edit fields in vue we have to transform the arrays in yaml into an array of objects like [{key: abc, value: xyz}{...}] @@ -374,6 +377,7 @@ class MetaApiController extends ContentController return $customfieldsForEdit; } + # can be deleted? private function customfieldsPrepareForSave($customfields, $arrayFeatureOn) { # we have to convert the incoming array of objects from vue [{key: abc, value: xyz}{...}] into key-value arrays for yaml. diff --git a/system/Controllers/PageController.php b/system/Controllers/PageController.php index b386a4f..c970a2a 100644 --- a/system/Controllers/PageController.php +++ b/system/Controllers/PageController.php @@ -19,6 +19,7 @@ use Typemill\Events\OnMetaLoaded; use Typemill\Events\OnMarkdownLoaded; use Typemill\Events\OnContentArrayLoaded; use Typemill\Events\OnHtmlLoaded; +use Typemill\Events\OnRestrictionsLoaded; use Typemill\Extensions\ParsedownExtension; class PageController extends Controller @@ -213,10 +214,50 @@ class PageController extends Controller /* set safe mode to escape javascript and html in markdown */ $parsedown->setSafeMode(true); + # check access restriction here + $restricted = $this->checkRestrictions($metatabs['meta']); + if($restricted) + { + # convert markdown into array of markdown block-elements + $markdownBlocks = $parsedown->markdownToArrayBlocks($contentMD); + + # infos that plugins need to add restriction content + $restrictions = [ + 'restricted' => $restricted, + 'defaultContent' => true, + 'markdownBlocks' => $markdownBlocks, + ]; + + # dispatch the data + $restrictions = $this->c->dispatcher->dispatch('onRestrictionsLoaded', new OnRestrictionsLoaded( $restrictions ))->getData(); + + # use the returned markdown + $markdownBlocks = $restrictions['markdownBlocks']; + + # if no plugin has disabled the default behavior + if($restrictions['defaultContent']) + { + # cut the restricted content + $shortenedPage = $this->cutRestrictedContent($markdownBlocks); + + # check if there is customized content + $restrictionnotice = ( isset($this->settings['restrictionnotice']) && $this->settings['restrictionnotice'] != '' ) ? $this->settings['restrictionnotice'] : 'You are not allowed to access this content.'; + + # add notice to shortened content + $shortenedPage[] = $restrictionnotice; + + # Use the shortened page + $markdownBlocks = $shortenedPage; + } + + # finally transform the markdown blocks back to pure markdown text + $contentMD = $parsedown->arrayBlocksToMarkdown($markdownBlocks); + } + /* parse markdown-file to content-array */ $contentArray = $parsedown->text($contentMD); $contentArray = $this->c->dispatcher->dispatch('onContentArrayLoaded', new OnContentArrayLoaded($contentArray))->getData(); - + /* parse markdown-content-array to content-string */ $contentHTML = $parsedown->markup($contentArray); $contentHTML = $this->c->dispatcher->dispatch('onHtmlLoaded', new OnHtmlLoaded($contentHTML))->getData(); @@ -426,4 +467,76 @@ class PageController extends Controller return false; } + + # checks if a page has a restriction in meta and if the current user is blocked by that restriction + protected function checkRestrictions($meta) + { + # check if content restrictions are active + if(isset($this->settings['pageaccess']) && $this->settings['pageaccess']) + { + + # check if page is restricted to certain user + if(isset($meta['alloweduser']) && $meta['alloweduser'] && $meta['alloweduser'] !== '' ) + { + if(isset($_SESSION['user']) && $_SESSION['user'] == $meta['alloweduser']) + { + # user has access to the page, so there are no restrictions + return false; + } + + # otherwise return array with type of restriction and allowed username + return [ 'alloweduser' => $meta['alloweduser'] ]; + } + + # check if page is restricted to certain userrole + if(isset($meta['allowedrole']) && $meta['allowedrole'] && $meta['allowedrole'] !== '' ) + { + # var_dump($this->c->acl->inheritsRole('editor', 'member')); + # die(); + if( + isset($_SESSION['role']) + AND ( + $_SESSION['role'] == 'administrator' + OR $_SESSION['role'] == $meta['allowedrole'] + OR $this->c->acl->inheritsRole($_SESSION['role'], $meta['allowedrole']) + ) + ) + { + # role has access to page, so there are no restrictions + return false; + } + + return [ 'allowedrole' => $meta['allowedrole'] ]; + } + + } + + return false; + + } + + protected function cutRestrictedContent($markdown) + { + #initially add only the title of the page. + $restrictedMarkdown = [$markdown[0]]; + unset($markdown[0]); + + if(isset($this->settings['hrdelimiter']) && $this->settings['hrdelimiter'] !== NULL ) + { + foreach ($markdown as $block) + { + $firstCharacters = substr($block, 0, 3); + if($firstCharacters == '---' OR $firstCharacters == '***') + { + return $restrictedMarkdown; + } + $restrictedMarkdown[] = $block; + } + + # no delimiter found, so use the title only + $restrictedMarkdown = [$restrictedMarkdown[0]]; + } + + return $restrictedMarkdown; + } } \ No newline at end of file diff --git a/system/Controllers/SettingsController.php b/system/Controllers/SettingsController.php index dfdbe75..24cd5e7 100644 --- a/system/Controllers/SettingsController.php +++ b/system/Controllers/SettingsController.php @@ -51,9 +51,6 @@ class SettingsController extends Controller # set navigation active $navigation['System']['active'] = true; - # set option for registered website - $options = ['' => 'all', 'registered' => 'registered users only']; - return $this->render($response, 'settings/system.twig', array( 'settings' => $settings, 'acl' => $this->c->acl, @@ -62,7 +59,6 @@ class SettingsController extends Controller 'languages' => $languages, 'locale' => $locale, 'formats' => $defaultSettings['formats'], - 'access' => $options, 'route' => $route->getName() )); } @@ -94,8 +90,11 @@ class SettingsController extends Controller 'language' => $newSettings['language'], 'langattr' => $newSettings['langattr'], 'editor' => $newSettings['editor'], - 'access' => $newSettings['access'], 'formats' => $newSettings['formats'], + 'access' => isset($newSettings['access']) ? true : null, + 'pageaccess' => isset($newSettings['pageaccess']) ? true : null, + 'hrdelimiter' => isset($newSettings['hrdelimiter']) ? true : null, + 'restrictionnotice' => $newSettings['restrictionnotice'], 'headlineanchors' => isset($newSettings['headlineanchors']) ? $newSettings['headlineanchors'] : null, 'displayErrorDetails' => isset($newSettings['displayErrorDetails']) ? true : null, 'twigcache' => isset($newSettings['twigcache']) ? true : null, diff --git a/system/Events/OnRestrictionsLoaded.php b/system/Events/OnRestrictionsLoaded.php new file mode 100644 index 0000000..fb0826e --- /dev/null +++ b/system/Events/OnRestrictionsLoaded.php @@ -0,0 +1,14 @@ +<?php + +namespace Typemill\Events; + +use Symfony\Component\EventDispatcher\Event; + +/** + * Event for page restrictions. + */ + +class OnRestrictionsLoaded extends BaseEvent +{ + +} \ No newline at end of file diff --git a/system/Extensions/TwigUserExtension.php b/system/Extensions/TwigUserExtension.php index 6c3e1cc..a7b088c 100644 --- a/system/Extensions/TwigUserExtension.php +++ b/system/Extensions/TwigUserExtension.php @@ -16,23 +16,12 @@ class TwigUserExtension extends \Twig_Extension public function isLoggedin() { - // configure session - ini_set('session.cookie_httponly', 1 ); - ini_set('session.use_strict_mode', 1); - ini_set('session.cookie_samesite', 'lax'); - session_name('typemill-session'); - - // start session - session_start(); - - if(isset($_SESSION['user'])) + if(isset($_SESSION['login']) && $_SESSION['login']) { return true; } - session_destroy(); return false; - } public function isRole($role) diff --git a/system/Models/Validation.php b/system/Models/Validation.php index 712c6db..f2e35d1 100644 --- a/system/Models/Validation.php +++ b/system/Models/Validation.php @@ -283,6 +283,8 @@ class Validation $v->rule('in', 'editor', ['raw', 'visual']); $v->rule('values_allowed', 'formats', $formats); $v->rule('in', 'copyright', $copyright); + $v->rule('noHTML', 'restrictionnotice'); + $v->rule('lengthBetween', 'restrictionnotice', 2, 1000 ); $v->rule('iplist', 'trustedproxies'); return $this->validationResult($v, $name); diff --git a/system/Settings.php b/system/Settings.php index 986dc22..bc1f7ff 100644 --- a/system/Settings.php +++ b/system/Settings.php @@ -159,6 +159,9 @@ class Settings 'author' => true, 'year' => true, 'access' => true, + 'pageaccess' => true, + 'hrdelimiter' => true, + 'restrictionnotice' => true, 'headlineanchors' => true, 'theme' => true, 'editor' => true, diff --git a/system/author/metatabs.yaml b/system/author/metatabs.yaml index b135492..3077f62 100644 --- a/system/author/metatabs.yaml +++ b/system/author/metatabs.yaml @@ -58,11 +58,26 @@ meta: label: Hide checkboxlabel: Hide page from navigation class: medium -# roles: -# type: select -# label: Show page to -# class: medium -# options: -# public: Public (standard) -# members: Members only (logged in) -# customers: Customers only (paying) \ No newline at end of file + allowedrole: + type: select + label: For access the user must have this minimum role + class: medium + options: + false: All + member: Member + author: Author + editor: Editor + administrator: Administrator + description: Select the lowest userrole. Higher roles will have access too. + alloweduser: + type: text + label: Only the following user has access + class: medium + description: Only this certain user will have access to this site. + contains: + type: radio + label: This folder contains + class: medium + options: + pages: PAGES (sort in navigation with drag & drop) + posts: POSTS (sorted by publish date, for news or blogs) \ No newline at end of file diff --git a/system/author/settings/system.twig b/system/author/settings/system.twig index bd99b07..3acf7d6 100644 --- a/system/author/settings/system.twig +++ b/system/author/settings/system.twig @@ -30,7 +30,7 @@ {% endif %} </div><div class="medium{{ errors.settings.author ? ' error' : '' }}"> <label for="settings[author]">{{ __('Author') }}</label> - <input type="text" name="settings[author]" id="author" pattern="[^()/><\]\{\}\?\$@#!*%§=[\\\x22;:|]{2,40}" value="{{ old.settings.author ? old.settings.author : __(settings.author) }}" title="{{ __('Use 2 to 40 characters') }} {{ __('Only the following special characters are allowed') }} a,b a.b a-b a_b a&b a+b" /> + <input type="text" name="settings[author]" id="author" pattern="[^()/><\]\{\}\?\$@#!*%§=[\\\x22;:|]{2,40}" value="{{ old.settings.author ? old.settings.author : settings.author }}" title="{{ __('Use 2 to 40 characters') }} {{ __('Only the following special characters are allowed') }} a,b a.b a-b a_b a&b a+b" /> {% if errors.settings.author %} <span class="error">{{ errors.settings.author | first }}</span> {% endif %} @@ -69,16 +69,6 @@ </div><div class="medium"> <label for="settings[sitemap]">{{ __('Google Sitemap') }} <small>({{ __('Readonly') }})</small></label> <input type="text" name="settings[sitemap]" id="sitemap" readonly value="{{ base_url }}/cache/sitemap.xml" /> - </div><div class="medium{{ errors.settings.access ? ' error' : '' }}"> - <label for="settings[access]">{{ __('Website visible for') }}</label> - <select name="settings[access]" id="access"> - {% for key,option in access %} - <option value="{{ key }}"{% if (key == old.settings.access or key == settings.access) %} selected{% endif %}>{{ __(option) }}</option> - {% endfor %} - </select> - {% if errors.settings.access %} - <span class="error">{{ errors.settings.access | first }}</span> - {% endif %} </div> <hr> <header class="headline"> @@ -149,6 +139,39 @@ {% endfor %} </div> <hr> + <header class="headline"> + <h2>{{ __('Access Control') }}</h2> + <p>{{ __('Limit the access for the whole website or for each page individually. If you activate the website restriction or the page restrictions, then sessions will be used in frontend.') }}</p> + </header> + <div class="large{{ errors.settings.access ? ' error' : '' }}"> + <label for="settings[access]">{{ __('Website Restriction') }}</label> + <label class="control-group">{{ __('Show the website only to authenticated users and redirect all other users to the login page.') }} + <input name="settings[access]" type="checkbox" {% if (settings.access or old.settings.access) %} checked {% endif %}> + <span class="checkmark"></span> + </label> + </div> + <div class="large{{ errors.settings.pageaccess ? ' error' : '' }}"> + <label for="settings[pageaccess]">{{ __('Page Restrictions - Activate') }}</label> + <label class="control-group">{{ __('Activate individual restrictions for pages in the meta-tab of each page.') }} + <input name="settings[pageaccess]" type="checkbox" {% if (settings.pageaccess or old.settings.pageaccess) %} checked {% endif %}> + <span class="checkmark"></span> + </label> + </div> + <div class="large{{ errors.settings.hrdelimiter ? ' error' : '' }}"> + <label for="settings[hrdelimiter]">{{ __('Page Restrictions - Cut Restricted Content') }}</label> + <label class="control-group">{{ __('Cut restricted content after the first hr-element on a page (per default content will be cut after title).') }} + <input name="settings[hrdelimiter]" type="checkbox" {% if (settings.hrdelimiter or old.settings.hrdelimiter) %} checked {% endif %}> + <span class="checkmark"></span> + </label> + </div> + <div class="large{{ errors.settings.restrictionnotice ? ' error' : '' }}"> + <label for="settings[restrictionnotice]">{{ __('Page Restrictions - Notice') }} <small>({{ __('use markdown') }})</small></label> + <textarea id="restrictionnotice" rows="8" name="settings[restrictionnotice]">{{ old.settings.restrictionnotice ? old.settings.restrictionnotice : settings.restrictionnotice }}</textarea> + {% if errors.settings.restrictionnotice %} + <span class="error">{{ errors.settings.restrictionnotice | first }}</span> + {% endif %} + </div> + <hr> <header class="headline"> <h2>{{ __('Developer') }}</h2> <p>{{ __('The following options are only for developers') }}</p> diff --git a/system/system.php b/system/system.php index 3bc913e..7bd9bad 100644 --- a/system/system.php +++ b/system/system.php @@ -181,11 +181,12 @@ $container['assets'] = function($c) use ($uri) ********************************/ # if website is restricted to registered user -if(isset($settings['settings']['access']) && $settings['settings']['access'] == 'registered') +if( ( isset($settings['settings']['access']) && $settings['settings']['access'] ) || ( isset($settings['settings']['pageaccess']) && $settings['settings']['pageaccess'] ) ) { # activate session for all routes $session_segments = [$uri->getPath()]; } + foreach($session_segments as $segment) { if(substr( $uri->getPath(), 0, strlen($segment) ) === $segment)