Completed
Push — master ( 3e44d5...49b4bd )
by Rafael
06:52
created

endpointsAliasToRealNames()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 13
c 0
b 0
f 0
ccs 8
cts 8
cp 1
rs 10
cc 4
nc 4
nop 1
crap 4
1
<?php
2
3
/*******************************************************************************
4
 *  This file is part of the GraphQL Bundle package.
5
 *
6
 *  (c) YnloUltratech <[email protected]>
7
 *
8
 *  For the full copyright and license information, please view the LICENSE
9
 *  file that was distributed with this source code.
10
 ******************************************************************************/
11
12
namespace Ynlo\GraphQLBundle\Definition\Plugin;
13
14
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
15
use Ynlo\GraphQLBundle\Definition\ClassAwareDefinitionInterface;
16
use Ynlo\GraphQLBundle\Definition\DefinitionInterface;
17
use Ynlo\GraphQLBundle\Definition\ExecutableDefinitionInterface;
18
use Ynlo\GraphQLBundle\Definition\FieldsAwareDefinitionInterface;
19
use Ynlo\GraphQLBundle\Definition\ImplementorInterface;
20
use Ynlo\GraphQLBundle\Definition\InterfaceDefinition;
21
use Ynlo\GraphQLBundle\Definition\MutationDefinition;
22
use Ynlo\GraphQLBundle\Definition\NodeAwareDefinitionInterface;
23
use Ynlo\GraphQLBundle\Definition\QueryDefinition;
24
use Ynlo\GraphQLBundle\Definition\Registry\DefinitionRegistry;
25
use Ynlo\GraphQLBundle\Definition\Registry\Endpoint;
26
use Ynlo\GraphQLBundle\Model\NodeInterface;
27
28
class EndpointsDefinitionPlugin extends AbstractDefinitionPlugin
29
{
30
    /**
31
     * @var array
32
     */
33
    private $endpointAlias = [];
34
35
    /**
36
     * @var string
37
     */
38
    private $endpointDefault;
39
40
41
    /**
42
     * EndpointsDefinitionPlugin constructor.
43
     *
44
     * @param array $endpointsConfig
45
     */
46 3
    public function __construct(array $endpointsConfig)
47
    {
48 3
        $this->endpointAlias = $endpointsConfig['alias'] ?? [];
49 3
        $this->endpointDefault = $endpointsConfig['default'] ?? null;
50 3
    }
51
52
    /**
53
     * {@inheritDoc}
54
     */
55
    public function buildConfig(ArrayNodeDefinition $root): void
56
    {
57
        $root
58
            ->info('List of endpoints for queries and mutations')
59
            ->scalarPrototype();
60
    }
61
62
    /**
63
     * {@inheritDoc}
64
     */
65 3
    public function normalizeConfig(DefinitionInterface $definition, $config): array
66
    {
67 3
        $endpoints = $config['endpoints'] ?? [];
68
69
        //allow set only one endpoint in a simple string
70 3
        if (\is_string($endpoints)) {
71 2
            $endpoints = [$endpoints];
72
        }
73
74 3
        return $endpoints;
75
    }
76
77
    /**
78
     * {@inheritDoc}
79
     */
80 1
    public function configure(DefinitionInterface $definition, Endpoint $endpoint, array $config): void
81
    {
82 1
        if ($endpoint->getName() === DefinitionRegistry::DEFAULT_ENDPOINT) {
83
            return;
84
        }
85
86
        //ignore safe operations
87 1
        if (\in_array($definition->getName(), ['node', 'nodes'])) {
88
            return;
89
        }
90
91
        //apply default endpoint to all operations
92 1
        $endpoints = $this->normalizeConfig($definition, $definition->getMeta('endpoints'));
93 1
        if (!$endpoints && $this->endpointDefault) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $endpoints of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
94 1
            if ($definition instanceof QueryDefinition
95 1
                || ($definition instanceof ClassAwareDefinitionInterface
96 1
                    && is_subclass_of($definition->getClass(), NodeInterface::class, true))
97
            ) {
98 1
                $definition->setMeta('endpoints', ['endpoints' => [$this->endpointDefault]]);
99
            }
100
        }
101 1
    }
102
103
    /**
104
     * {@inheritDoc}
105
     */
106 1
    public function configureEndpoint(Endpoint $endpoint): void
107
    {
108 1
        if ($endpoint->getName() === DefinitionRegistry::DEFAULT_ENDPOINT) {
109
            return;
110
        }
111
112 1
        $forbiddenTypes = $this->getForbiddenTypes($endpoint);
113 1
        $this->processForbiddenTypes($endpoint, $forbiddenTypes);
114 1
    }
115
116 1
    protected function processForbiddenTypes(Endpoint $endpoint, $forbiddenTypes)
117
    {
118 1
        foreach ($endpoint->allQueries() as $queries) {
119 1
            $this->secureOperations($endpoint, $queries, $forbiddenTypes);
120
        }
121
122 1
        foreach ($endpoint->allMutations() as $mutations) {
123 1
            $this->secureOperations($endpoint, $mutations, $forbiddenTypes);
124
        }
125
126 1
        foreach ($endpoint->allTypes() as $type) {
127
            //remove implementations of forbidden interfaces
128 1
            if ($type instanceof ImplementorInterface) {
129 1
                foreach ($type->getInterfaces() as $interface) {
130 1
                    if (in_array($interface, $forbiddenTypes)) {
131 1
                        $type->removeInterface($interface);
132
                    }
133
                }
134
            }
135
            //remove fields related to forbidden interfaces
136 1
            if ($type instanceof FieldsAwareDefinitionInterface) {
137 1
                if ($type->getFields()) {
138 1
                    foreach ($type->getFields() as $field) {
139
                        //remove forbidden field
140 1
                        if (!$this->isGranted($endpoint, $field)) {
141 1
                            $type->removeField($field->getName());
142
                        }
143
144
                        //remove field related to forbidden type
145 1
                        $fieldType = $endpoint->hasType($field->getType()) ? $endpoint->getType($field->getType()) : null;
146 1
                        $fieldNodeType = $endpoint->hasType($field->getMeta('node')) ? $endpoint->getType($field->getMeta('node')) : null;
147 1
                        if (($fieldType && in_array($fieldType->getName(), $forbiddenTypes))
148 1
                            || ($fieldNodeType && in_array($fieldNodeType->getName(), $forbiddenTypes))) {
149 1
                            $type->removeField($field->getName());
150
                        }
151
152 1
                        foreach ($field->getArguments() as $argument) {
153
                            //remove forbidden argument
154 1
                            if (!$this->isGranted($endpoint, $argument)) {
155
                                $field->removeArgument($argument->getName());
156
                            }
157
158
                            //remove argument related to forbidden type
159 1
                            $argumentType = $endpoint->hasType($argument->getType()) ? $endpoint->getType($argument->getType()) : null;
160 1
                            if ($argumentType && \in_array($argumentType->getName(), $forbiddenTypes, true)) {
161 1
                                $field->removeArgument($argument->getName());
162
                            }
163
                        }
164
                    }
165
166
                    //after delete fields related to forbidden objects,
167
                    //verify if the object has at least one field
168
                    //otherwise mark this type as forbidden
169 1
                    if (!$type->getFields()) {
170 1
                        $forbiddenTypes[] = $type->getName();
171 1
                        $this->processForbiddenTypes($endpoint, $forbiddenTypes);
172
                    }
173
                }
174
            }
175
        }
176
177
        /** @var InterfaceDefinition $type */
178 1
        foreach ($endpoint->allInterfaces() as $type) {
179 1
            if ($type->getImplementors()) {
180 1
                foreach ($type->getImplementors() as $implementor) {
181 1
                    if (in_array($implementor, $forbiddenTypes)) {
182 1
                        $type->removeImplementor($implementor);
183
                    }
184
                }
185
186
                //after delete forbidden implementors
187
                //verify if the interface has at least one implementor
188
                //otherwise mark this interface as forbidden
189 1
                if (!$type->getImplementors()) {
190 1
                    $forbiddenTypes[] = $type->getName();
191 1
                    $this->processForbiddenTypes($endpoint, $forbiddenTypes);
192
                }
193
            }
194
        }
195
196 1
        foreach ($forbiddenTypes as $type) {
197 1
            $endpoint->removeType($type);
198
        }
199 1
    }
200
201
    /**
202
     * Remove
203
     *
204
     * @param Endpoint                      $endpoint
205
     * @param ExecutableDefinitionInterface $executableDefinition
206
     * @param array|string[]                $forbiddenTypes
207
     */
208 1
    protected function secureOperations(Endpoint $endpoint, ExecutableDefinitionInterface $executableDefinition, $forbiddenTypes)
209
    {
210 1
        $type = $endpoint->hasType($executableDefinition->getType()) ? $endpoint->getType($executableDefinition->getType()) : null;
211
212 1
        $node = null;
213
        //resolve the related node using interface
214 1
        if ($executableDefinition instanceof NodeAwareDefinitionInterface) {
0 ignored issues
show
introduced by
$executableDefinition is always a sub-type of Ynlo\GraphQLBundle\Defin...wareDefinitionInterface.
Loading history...
215 1
            $node = $endpoint->hasType($executableDefinition->getNode()) ? $endpoint->getType($executableDefinition->getNode()) : null;
216
        }
217
218
        //resolve related node using metadata
219 1
        if (!$node) {
220 1
            $node = $endpoint->hasType($executableDefinition->getMeta('node')) ? $endpoint->getType($executableDefinition->getMeta('node')) : null;
221
        }
222
223 1
        $granted = true;
224 1
        $endpoints = $this->normalizeConfig($executableDefinition, $executableDefinition->getMeta('endpoints', []));
225
226
        //if the operation has endpoints defined use that,
227
        //otherwise check by related type and node
228 1
        if ($endpoints) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $endpoints of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
229 1
            $granted = $this->isGranted($endpoint, $executableDefinition);
230 1
        } elseif (($type && \in_array($type->getName(), $forbiddenTypes, true))
231 1
                  || ($node && \in_array($node->getName(), $forbiddenTypes, true))) {
232 1
            $granted = false;
233
        }
234
235 1
        if (!$granted) {
236 1
            if ($executableDefinition instanceof MutationDefinition) {
237 1
                $endpoint->removeMutation($executableDefinition->getName());
238
            } else {
239 1
                $endpoint->removeQuery($executableDefinition->getName());
240
            }
241
        } else {
242 1
            foreach ($executableDefinition->getArguments() as $argument) {
243
                //remove forbidden argument
244
                if (!$this->isGranted($endpoint, $argument)) {
245
                    $executableDefinition->removeArgument($argument->getName());
246
                }
247
248
                //remove argument related to forbidden type
249
                $argumentType = $endpoint->hasType($argument->getType()) ? $endpoint->getType($argument->getType()) : null;
250
                if ($argumentType && \in_array($argumentType->getName(), $forbiddenTypes, true)) {
251
                    $executableDefinition->removeArgument($argument->getName());
252
                }
253
            }
254
        }
255 1
    }
256
257 1
    protected function getForbiddenTypes(Endpoint $endpoint)
258
    {
259 1
        $forbiddenTypes = [];
260 1
        foreach ($endpoint->allTypes() as $type) {
261 1
            if (!$this->isGranted($endpoint, $type)) {
262 1
                $forbiddenTypes[] = $type->getName();
263
            }
264
        }
265
266 1
        return $forbiddenTypes;
267
    }
268
269 1
    protected function isGranted(Endpoint $endpoint, DefinitionInterface $definition)
270
    {
271 1
        $endpoints = $this->normalizeConfig($definition, $definition->getMeta('endpoints', []));
272 1
        if ($endpoints) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $endpoints of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
273 1
            $endpoints = $this->endpointsAliasToRealNames($endpoints);
274
        }
275
276 1
        return empty($endpoints) || \in_array($endpoint->getName(), $endpoints);
277
    }
278
279
    /**
280
     * Given array of endpoints (containing alias) return the array of specific endpoints (without aliases)
281
     *
282
     * ["all"] => ["admin", "frontend"]
283
     *
284
     * @param array $endpoints
285
     *
286
     * @return array
287
     */
288 1
    protected function endpointsAliasToRealNames($endpoints)
289
    {
290 1
        foreach ($endpoints as $index => $endpointName) {
291 1
            foreach ($this->endpointAlias as $alias => $targets) {
292 1
                if ($alias === $endpointName) {
293 1
                    $targets = $this->endpointsAliasToRealNames($targets);
294 1
                    unset($endpoints[$index]);
295 1
                    $endpoints = array_merge($endpoints, $targets);
296
                }
297
            }
298
        }
299
300 1
        return $endpoints;
301
    }
302
}
303