diff --git a/_config/base-config.yml b/_config/base-config.yml index 18dd869..d3c3ee4 100755 --- a/_config/base-config.yml +++ b/_config/base-config.yml @@ -28,4 +28,4 @@ SilverLeague\IDEAnnotator\DataObjectAnnotator: enabled: true SilverStripe\UserForms\Form\UserForm: - show_labels: false + show_labels: true diff --git a/_config/graphql.yml b/_config/graphql.yml new file mode 100644 index 0000000..da26101 --- /dev/null +++ b/_config/graphql.yml @@ -0,0 +1,37 @@ +--- +Name: app-graphql +After: + - app-basics +Only: + classexists: 'SilverStripe\GraphQL\Schema\Schema' +--- + +SilverStripe\Core\Injector\Injector: + SilverStripe\GraphQL\Schema\Registry\PluginRegistry: + constructor: + - 'A2nt\CMSNiceties\GraphQL\URLLinkablePlugin' + +SilverStripe\Control\Director: + rules: + graphql: + Controller: '%$SilverStripe\GraphQL\Controller.default' + Schema: default + +SilverStripe\GraphQL\Schema\Schema: + schemas: + '*': + config: + max_query_nodes: 250 # default 500 + max_query_depth: 20 # default 15 + max_query_complexity: 200 # default unlimited + default: + src: + - app/_graphql + +SilverStripe\GraphQLDevTools\Controller: + # show two schemas + schemas: + - default + - admin + # default schema that is selected + default_schema: default diff --git a/_graphql/config.yml b/_graphql/config.yml new file mode 100644 index 0000000..56640f5 --- /dev/null +++ b/_graphql/config.yml @@ -0,0 +1,30 @@ +modelConfig: + Page: + plugins: + inheritance: true + operations: + read: + plugins: + readVersion: false + paginateList: false + readOne: + plugins: + getByLink: + after: filter + + DNADesign\Elemental\Models\ElementalArea: + plugins: + inheritance: true + operations: + read: + plugins: + readVersion: false + paginateList: false + DNADesign\Elemental\Models\BaseElement: + plugins: + inheritance: true + operations: + read: + plugins: + readVersion: false + paginateList: false diff --git a/src/GraphQL/_manifest_exclude b/_graphql/enums.yml similarity index 100% rename from src/GraphQL/_manifest_exclude rename to _graphql/enums.yml diff --git a/_graphql/models.yml b/_graphql/models.yml new file mode 100644 index 0000000..01b6a6e --- /dev/null +++ b/_graphql/models.yml @@ -0,0 +1,48 @@ +Page: + fields: + id: true + className: true + urlSegment: true + parentID: true + title: true + sort: true + CSSClass: true + MainContent: + type: String + showInMenus: Boolean + showInSearch: Boolean + link: + type: String + RequestLink: + type: String + children: "[Page]" + elementalArea: + type: ElementalArea + operations: + readOne: + plugins: + getByURL: + after: filter + getByLink: + after: filter + +DNADesign\Elemental\Models\ElementalArea: + fields: + id: true + elements: + plugins: + paginateList: false + operations: + read: true + readOne: true + +DNADesign\Elemental\Models\BaseElement: + fields: + id: true + title: true + showTitle: true + className: true + forTemplate: true + operations: + readOne: true + read: true diff --git a/_graphql/schema.yml b/_graphql/schema.yml new file mode 100644 index 0000000..745f791 --- /dev/null +++ b/_graphql/schema.yml @@ -0,0 +1,4 @@ +queries: + readMenu: + type: '[PageInterface]' + resolver: [ 'A2nt\CMSNiceties\GraphQL\MenuResolver', 'resolveMenu' ] diff --git a/_graphql/types.yml b/_graphql/types.yml new file mode 100644 index 0000000..e69de29 diff --git a/src/Extensions/PlaceholderFormExtension.php b/src/Extensions/PlaceholderFormExtension.php index 58dc55d..d0c8729 100755 --- a/src/Extensions/PlaceholderFormExtension.php +++ b/src/Extensions/PlaceholderFormExtension.php @@ -7,6 +7,8 @@ use SilverStripe\Core\Extension; use SilverStripe\Forms\CompositeField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\TextField; +use SilverStripe\Forms\TextareaField; +use SilverStripe\Forms\FormField; /** * Class \A2nt\CMSNiceties\Extensions\PlaceholderFormExtension @@ -24,7 +26,7 @@ class PlaceholderFormExtension extends Extension private function setPlaceholder($field) { - if (is_a($field, TextField::class)) { + if (is_a($field, TextField::class) || is_a($field, TextareaField::class)) { if (!$field->getAttribute('placeholder')) { $placeholder = $field->Title() .($field->hasClass('requiredField') ? '*' : ''); diff --git a/src/GraphQL/APIKeyAuthenticator.php b/src/GraphQL/APIKeyAuthenticator.php deleted file mode 100644 index cef6b97..0000000 --- a/src/GraphQL/APIKeyAuthenticator.php +++ /dev/null @@ -1,46 +0,0 @@ -getHeader('apikey') !== WebpackTemplateProvider::config()['GRAPHQL_API_KEY'] - ) { - if ($member && Permission::checkMember($member, 'CMS_ACCESS')) { - return $member; - } - - throw new ValidationException('Restricted resource', 401); - } - - return Member::get()->first(); - } - - public function isApplicable(HTTPRequest $request) - { - if ($request->param('Controller') === '%$SilverStripe\GraphQL\Controller.admin') { - return false; - } - - /*if($request->getHeader('apikey')){ - return true; - }*/ - return true; - return false; - } -} diff --git a/src/GraphQL/APIKeyMiddleware.php b/src/GraphQL/APIKeyMiddleware.php deleted file mode 100644 index da543b5..0000000 --- a/src/GraphQL/APIKeyMiddleware.php +++ /dev/null @@ -1,23 +0,0 @@ -getHeader('apikey') === WebpackTemplateProvider::config()['GRAPHQL_API_KEY']) { - return $next($schema, $query, $context, $params); - } - - throw new \Exception('Invalid API key token'); - } -} diff --git a/src/GraphQL/ElementTypeCreator.php b/src/GraphQL/ElementTypeCreator.php deleted file mode 100644 index 7e25119..0000000 --- a/src/GraphQL/ElementTypeCreator.php +++ /dev/null @@ -1,39 +0,0 @@ - 'element' - ]; - } - - public function fields() - { - return [ - '_id' => ['type' => Type::nonNull(Type::id()),'resolve' => static function ($object) { - return $object->ID; - }], - 'ID' => ['type' => Type::nonNull(Type::id())], - 'Title' => ['type' => Type::string()], - 'ParentID' => ['type' => Type::id()], - 'Render' => [ - 'type' => Type::string(), - 'resolve' => static function ($object, array $args, $context, ResolveInfo $info) { - return $object->getController()->forTemplate()->HTML(); - } - ], - ]; - } -} diff --git a/src/GraphQL/MemberTypeCreator.php b/src/GraphQL/MemberTypeCreator.php deleted file mode 100644 index e7d1af0..0000000 --- a/src/GraphQL/MemberTypeCreator.php +++ /dev/null @@ -1,31 +0,0 @@ - 'member' - ]; - } - - public function fields() - { - return [ - 'ID' => ['type' => Type::nonNull(Type::id())], - 'Email' => ['type' => Type::string()], - 'FirstName' => ['type' => Type::string()], - 'Surname' => ['type' => Type::string()], - ]; - } -} diff --git a/src/GraphQL/MenuResolver.php b/src/GraphQL/MenuResolver.php new file mode 100644 index 0000000..259fecf --- /dev/null +++ b/src/GraphQL/MenuResolver.php @@ -0,0 +1,37 @@ +filter('ParentID', '0'); + $results = self::getFields($pages); + return $results; + } + + protected static function getFields($pages): array + { + $results = []; + foreach ($pages as $p) { + $results[] = [ + 'id' => $p->ID, + 'title' => $p->Title, + 'children' => self::getFields($p->Children()), + ]; + } + + return $results; + } +} diff --git a/src/GraphQL/PageTypeCreator.php b/src/GraphQL/PageTypeCreator.php deleted file mode 100644 index 863c2f8..0000000 --- a/src/GraphQL/PageTypeCreator.php +++ /dev/null @@ -1,124 +0,0 @@ - 'page' - ]; - } - - public function fields() - { - $elementsConnection = Connection::create('Elements') - ->setConnectionType($this->manager->getType('element')) - ->setDescription('A list of the page elements') - ->setSortableFields(['ID', 'Title']); - - return [ - '_id' => ['type' => Type::nonNull(Type::id()),'resolve' => static function ($object) { - return $object->ID; - }], - 'ID' => ['type' => Type::nonNull(Type::id())], - 'Title' => ['type' => Type::string()], - 'Content' => ['type' => Type::string()], - 'Link' => ['type' => Type::string(), 'resolve' => static function ($object) { - return $object->Link(); - }], - 'URLSegment' => ['type' => Type::string()], - 'ParentID' => ['type' => Type::id()], - 'ClassName' => ['type' => Type::string()], - 'CSSClass' => ['type' => Type::string(), 'resolve' => static function ($object) { - return $object->CSSClass(); - }], - 'Summary' => ['type' => Type::string(), 'resolve' => static function ($object) { - return $object->Summary(); - }], - 'HTML' => ['type' => Type::string(), 'resolve' => static function ($object) { - // get action from request - $action = null; - - /** @var \Page $object */ - Director::set_current_page($object); - /** @var \PageController $controller */ - $controller = ModelAsController::controller_for($object); - - // find templates - $tpl = 'Page'; - $tpls = SSViewer::get_templates_by_class( - $object->ClassName, - ($action ? '_'.$action : ''), - \Page::class - ); - - foreach ($tpls as $tpl) { - if (is_array($tpl)) { - continue; - } - - $a_tpl = explode('\\', $tpl); - $last_name = array_pop($a_tpl); - $a_tpl[] = 'Layout'; - $a_tpl[] = $last_name; - $a_tpl = implode('\\', $a_tpl); - - if (SSViewer::hasTemplate($a_tpl)) { - break; - } - } - // - - $tpl = ($tpl !== 'Page') ? $tpl : 'Layout/Page'; - - $action = $action ? $action : 'index'; - /** @var HTTPRequest $request */ - $request = new HTTPRequest('GET', $object->AbsoluteLink()); - $request->setSession(new Session([])); - - // a little dirty way to make forms working - Controller::curr()->config()->set('url_segment', $object->AbsoluteLink()); - /*$controller->setRequest($request);*/ - //$request->getSession()->init($request); - - $controller->setRequest($request); - $controller->setAction($action); - //$controller->pushCurrent(); - $controller->doInit(); - - $layout = $controller->renderWith($tpl); - return $controller - ->customise(['Layout' => $layout]) - ->renderWith('GraphQLPage')->HTML(); - }], - 'Elements' => [ - 'type' => $elementsConnection->toType(), - 'args' => $elementsConnection->args(), - 'resolve' => static function ($object, array $args, $context, ResolveInfo $info) use ($elementsConnection) { - return $elementsConnection->resolveList( - $object->ElementalArea()->Elements(), - $args, - $context - ); - } - ] - ]; - } -} diff --git a/src/GraphQL/PaginatedReadMembersQueryCreator.php b/src/GraphQL/PaginatedReadMembersQueryCreator.php deleted file mode 100644 index ed0bdf9..0000000 --- a/src/GraphQL/PaginatedReadMembersQueryCreator.php +++ /dev/null @@ -1,46 +0,0 @@ -setConnectionType($this->manager->getType('member')) - ->setArgs([ - 'Email' => [ - 'type' => Type::string() - ] - ]) - ->setSortableFields(['ID', 'FirstName', 'Email']) - ->setConnectionResolver(static function ($object, array $args, $context, ResolveInfo $info) { - $member = Member::singleton(); - if (!$member->canView($context['currentUser'])) { - throw new \InvalidArgumentException(sprintf( - '%s view access not permitted', - Member::class - )); - } - $list = Member::get(); - - // Optional filtering by properties - if (isset($args['Email'])) { - $list = $list->filter('Email', $args['Email']); - } - - return $list; - }); - } -} diff --git a/src/GraphQL/PaginatedReadPagesQueryCreator.php b/src/GraphQL/PaginatedReadPagesQueryCreator.php deleted file mode 100644 index 31fb7ec..0000000 --- a/src/GraphQL/PaginatedReadPagesQueryCreator.php +++ /dev/null @@ -1,61 +0,0 @@ -setConnectionType($this->manager->getType('page')) - ->setArgs([ - 'Link' => [ - 'type' => Type::string() - ] - ]) - ->setSortableFields(['Sort']) - ->setConnectionResolver(static function ($object, array $args, $context, ResolveInfo $info) { - - if (isset($args['Link'])) { - $link = $args['Link']; - - if (SiteTree::has_extension('\TractorCow\Fluent\Extension\FluentSiteTreeExtension')) { - $arr = array_filter(explode('/', $args['Link'])); - - $locale = \TractorCow\Fluent\Model\Locale::get()->filter('URLSegment', array_shift($arr))->first(); - \TractorCow\Fluent\State\FluentState::singleton()->setLocale($locale->Locale); - - $link = implode('/', $arr); - } - - - $list = ArrayList::create(); - $page = SiteTree::get_by_link($link); - $list->add($page); - } - - /*$list = \Page::get(); - - // Optional filtering by properties - if (isset($args['ID'])) { - $list = $list->filter('ID', $args['ID']); - }*/ - - return $list; - }); - } -} diff --git a/src/GraphQL/ReadMembersQueryCreator.php b/src/GraphQL/ReadMembersQueryCreator.php deleted file mode 100644 index 8e25061..0000000 --- a/src/GraphQL/ReadMembersQueryCreator.php +++ /dev/null @@ -1,55 +0,0 @@ - 'readMembers' - ]; - } - - public function args() - { - return [ - 'Email' => ['type' => Type::string()] - ]; - } - - public function type() - { - return Type::listOf($this->manager->getType('member')); - } - - public function resolve($object, array $args, $context, ResolveInfo $info) - { - $member = Member::singleton(); - - if (!$member->canView($context['currentUser'])) { - throw new \InvalidArgumentException(sprintf( - '%s view access not permitted', - Member::class - )); - } - $list = Member::get(); - - // Optional filtering by properties - if (isset($args['Email'])) { - $list = $list->filter('Email', $args['Email']); - } - - return $list; - } -} diff --git a/src/GraphQL/URLLinkablePlugin.php b/src/GraphQL/URLLinkablePlugin.php new file mode 100644 index 0000000..96a8a8a --- /dev/null +++ b/src/GraphQL/URLLinkablePlugin.php @@ -0,0 +1,272 @@ +getModel()->getSourceClass(); + // Only site trees have the get_by_link capability + /*if ($class !== SiteTree::class && !is_subclass_of($class, SiteTree::class)) { + return; + }*/ + $singleFieldName = $this->config()->get('single_field_name'); + $listFieldName = $this->config()->get('list_field_name'); + $fieldName = $query->isList() ? $listFieldName : $singleFieldName; + $type = $query->isList() ? '[String]' : 'String'; + $query->addArg($fieldName, $type); + $query->addResolverAfterware( + $config['resolver'] ?? static::config()->get('resolver') + ); + } + + /** + * @param array $context + * @return callable + */ + public static function applyURLFilter($obj, array $args, array $context, ResolveInfo $info) + { + $url = self::getURL($args); + if (!$url) { + return $obj; + } + + $controller = self::getURLController($url); + + $obj = $controller->data(); + $obj->GraphQLContent = self::RenderTemplate($obj, $controller); + + $result = ArrayList::create(); + $result->push($obj); + + return $result; + } + + protected static function getURL($args) + { + $singleFieldName = static::config()->get('single_field_name'); + $listFieldName = static::config()->get('list_field_name'); + + $filterLink = $args['filter'][$singleFieldName] ?? ($args['filter'][$listFieldName] ?? null); + $argLink = $args[$singleFieldName] ?? ($args[$listFieldName] ?? null); + + $linkData = $filterLink ?: $argLink; + if (!$linkData) { + return false; + } + + $url = $linkData; + if ($url === '/') { + return '/home'; + } + + return $url; + } + + protected static function getURLController($url) + { + $curr = Controller::curr(); + $req = clone $curr->getRequest(); + + $req->setUrl($url); + $req->match('$URLSegment//$Action/$ID/$OtherID', true); + + $controller = ModelAsController::create(); + $controller->setRequest($req); + + // ContentController + $result = $controller->getNestedController(); + $result->setRequest($req); + + /** @var SiteTree $child */ + $action = $req->param('Action'); + + if ($action) { + $child = self::findChild($action, $result); + if ($child) { + $result = $child; + } + } + + return $result; + } + + // look recursively for a child page with URLSegment + protected static function findChild($action, $controller) + { + $req = $controller->getRequest(); + + $child = SiteTree::get()->filter([ + 'ParentID' => $controller->ID, + 'URLSegment' => $action, + ])->first(); + + if ($child) { + $req->shiftAllParams(); + $req->shift(); + + $controller = ModelAsController::controller_for($child); + $controller->setRequest($req); + $action = $req->param('Action'); + + if ($action) { + return self::findChild($action, $controller); + } + } + + return $controller; + } + + // AJAX/GraphQL helper + protected static function RenderTemplate($page, $ctl) + { + $object = $page; + $req = $ctl->getRequest(); + $actionParam = $req->param('Action'); + + Director::set_current_page($object); + + $match = self::findAction($ctl, $req); + $req->match($match['rule'], true); + $action = $match['action']; + + $action = ($action === 'handleAction') ? $actionParam : $action; + + $action = $action && $ctl->hasAction($action) ? $action : 'index'; + + + // find templates + $tpl = 'Page'; + $tpls = SSViewer::get_templates_by_class($object->ClassName, '', \Page::class); + + foreach ($tpls as $tpl) { + if (is_array($tpl)) { + continue; + } + + $a_tpl = explode('\\', $tpl); + $last_name = array_pop($a_tpl); + $a_tpl[] = 'Layout'; + $a_tpl[] = $last_name; + $a_tpl = implode('\\', $a_tpl); + + if (SSViewer::hasTemplate($a_tpl)) { + $tpl = $a_tpl; + break; + } + } + // + + $tpl = is_array($tpl) ? 'Page' : $tpl; + $tpl = ($tpl !== 'Page') ? $tpl : 'Layout/Page'; + + // a little dirty way to make forms working + Controller::curr()->config()->set('url_segment', $object->AbsoluteLink()); + $ctl->doInit(); + + $mResult = $ctl->$action($req); + if (is_array($mResult) || is_a($mResult, ArrayData::class)) { + $ctl->customise($mResult); + } + + + $layout = $ctl->renderWith([$tpl.'_'.$action, $tpl]); + + return $ctl + ->customise(['Layout' => $layout]) + ->renderWith('GraphQLPage'); + } + + protected static function findAction($controller, $request) + { + $handlerClass = $controller::class; + + // We stop after RequestHandler; in other words, at ViewableData + while ($handlerClass && $handlerClass != ViewableData::class) { + $urlHandlers = Config::inst()->get($handlerClass, 'url_handlers', Config::UNINHERITED); + + if ($urlHandlers) { + foreach ($urlHandlers as $rule => $action) { + if (isset($_REQUEST['debug_request'])) { + $class = static::class; + $remaining = $request->remaining(); + Debug::message("Testing '{$rule}' with '{$remaining}' on {$class}"); + } + + if ($request->match($rule, true)) { + if (isset($_REQUEST['debug_request'])) { + $class = static::class; + $latestParams = var_export($request->latestParams(), true); + Debug::message( + "Rule '{$rule}' matched to action '{$action}' on {$class}. " . "Latest request params: {$latestParams}" + ); + } + + return [ + 'rule' => $rule, + 'action' => $action, + ]; + } + } + } + + $handlerClass = get_parent_class($handlerClass ?? ''); + } + return null; + } +} diff --git a/src/Templates/DeferredRequirements.php b/src/Templates/DeferredRequirements.php index b776ef8..8c36931 100755 --- a/src/Templates/DeferredRequirements.php +++ b/src/Templates/DeferredRequirements.php @@ -36,9 +36,21 @@ class DeferredRequirements implements TemplateGlobalProvider 'DeferedJS' => 'loadJS', 'WebpackActive' => 'webpackActive', 'EmptyImgSrc' => 'emptyImageSrc', + 'HttpMethod' => 'httpMethod', ]; } + public static function httpMethod(): string + { + $ctl = Controller::curr(); + if (!$ctl) { + return null; + } + + $req = $ctl->getRequest(); + return ($req) ? $req->httpMethod() : null; + } + public static function Auto($class = false): string { $config = Config::inst()->get(self::class);