Completed
Push — master ( 1ce40e...db4879 )
by Charles
03:04
created

Controller::getAccessControl()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 30
rs 8.8571
c 1
b 0
f 0
cc 3
eloc 17
nc 3
nop 0
1
<?php
2
3
namespace yrc\rest;
4
5
use yii\rest\Controller as RestController;
6
use yii\filters\Cors;
7
use yii\filters\AccessControl;
8
use yii\filters\RateLimiter;
9
use yii\filters\VerbFilter;
10
use yii\filters\ContentNegotiator;
11
use yii\web\HttpException;
12
use yii\web\ForbiddenHttpException;
13
use yrc\web\Response;
14
15
use Yii;
16
17
use ReflectionClass;
18
use ReflectionMethod;
19
20
/**
21
 * Implements Restful API controller interfaces
22
 * @class Controller
23
 */
24
class Controller extends RestController
25
{
26
    /**
27
     * Allowed HTTP verbs
28
     * @var array $httpVerbs
29
     */
30
    private $httpVerbs = ['post', 'get', 'delete', 'put', 'patch', 'options', 'head'];
31
    
32
    /**
33
     * Global access filter
34
     */
35
    public function beforeAction($action)
36
    {
37
        $parent = parent::beforeAction($action);
38
39
        // Check the global access control header
40
        if (!Yii::$app->yrc->checkAccessHeader(Yii::$app->request)) {
41
            throw new HttpException(401);
42
        }
43
44
        return $parent;
45
    }
46
47
    /**
48
     * RestController automatically applies HTTP verb filtering and CORS headers
49
     * @return array
50
     */
51
    public function behaviors()
52
    {
53
        $behaviors = parent::behaviors();
54
55
        $authenticator = false;
56
57
        if (isset($behaviors['authenticator'])) {
58
            $authenticator = $behaviors['authenticator'];
59
            unset($behaviors['authenticator']);
60
        }
61
62
        $behaviors['contentNegotiator'] = [
63
            'class' => ContentNegotiator::className(),
64
            'formats' => [
65
                'application/json' => Response::FORMAT_JSON,
66
                'application/vnd.25519+json' => Response::FORMAT_JSON25519,
67
                'application/xml' => Response::FORMAT_XML,
68
            ]
69
        ];
70
71
        $behaviors['corsFilter'] = [
72
            'class' => Cors::className(),
73
            'cors' => [
74
                'Origin' => ['*'],
75
                'Access-Control-Request-Method' => $this->getHttpVerbMethodsFromClass($this->actions()[$this->action->id]),
76
                'Access-Control-Request-Headers' => ['*'],
77
                'Access-Control-Expose-Headers' => [
78
                    'Access-Control-Allow-Origin',
79
                    'X-Pagination-Per-Page',
80
                    'X-Pagination-Total-Count',
81
                    'X-Pagination-Current-Page',
82
                    'X-Pagination-Page-Count',
83
                    'Allow',
84
                    'X-Rate-Limit-Limit',
85
                    'X-Rate-Limit-Remaining',
86
                    'X-Rate-Limit-Reset'
87
                ],
88
            ]
89
        ];
90
91
        $behaviors['verbs'] = [
92
            'class' => VerbFilter::className(),
93
            'actions' => $this->getVerbFilterActionMap()
94
        ];
95
96
        // Move authenticator after verbs and cors
97
        if ($authenticator != false) {
98
            $behaviors['authenticator'] = $authenticator;
99
        }
100
101
        $behaviors['rateLimiter'] = [
102
            'class' => RateLimiter::className(),
103
            'enableRateLimitHeaders' => true
104
        ];
105
106
        $access = $this->getAccessControl();
107
108
        if ($access !== null) {
109
            $behaviors['access'] = $access;
110
        }
111
112
        // Manually add the ACAO header because Yii2 is terrible at doing it
113
        header("Access-Control-Allow-Origin: " . \implode(',', $behaviors['corsFilter']['cors']['Origin']));
114
        return $behaviors;
0 ignored issues
show
Best Practice introduced by
The expression return $behaviors; seems to be an array, but some of its elements' types (null) are incompatible with the return type of the parent method yii\rest\Controller::behaviors of type array<string,array>.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
115
    }
116
117
    /**
118
     * Pulls the ACL list from the action
119
     * @return array
120
     */
121
    private function getAccessControl()
122
    {
123
        $access = [
124
            'class' => AccessControl::className(),
125
            'denyCallback' => function($rule, $action) {
0 ignored issues
show
Unused Code introduced by
The parameter $rule is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $action is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
126
                throw new ForbiddenHttpException(Yii::t('yrc', 'You do not have permission to access this resource'));
127
            }
128
        ];
129
130
        $acl = $this->action->acl;
131
        if ($acl === null) {
132
            return null;
133
        }
134
        
135
        foreach ($acl as $verb => $perms) {
136
            $access['rules'][] = [
137
                'allow' => true,
138
                'verbs' => [$verb],
139
                'roles' => $perms
140
            ];
141
        }
142
143
        // Allow HTTP Options
144
        $access['rules'][] = [
145
            'allow' => true,
146
            'verbs' => ['OPTIONS']
147
        ];
148
149
        return $access;
150
    }
151
152
    /**
153
     * Retrieves the HTTP verb list
154
     * @param string $class
155
     * @return array
156
     */
157
    private function getHttpVerbMethodsFromClass($class)
158
    {
159
        $result = [];
160
161
        if (is_array($class)) {
162
            $class = $class['class'];
163
        }
164
165
        // Fetch the public methods for the class then filter them out by the http verbs
166
        $reflection = new ReflectionClass($class);
167
        $methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
168
        foreach ($methods as $method) {
169
            if (\in_array($method->name, $this->httpVerbs)) {
170
                $result[] = $method->name;
171
            }
172
        }
173
174
        return $result;
175
    }
176
177
    /**
178
     * Convers self::actions() for automatic verb filtering
179
     * @return array
180
     */
181
    private function getVerbFilterActionMap()
182
    {
183
        $actions = $this->actions();
184
185
        // Only apply this filtering for ActionMapped Controllers
186
        if (empty($actions)) {
187
            return [];
188
        }
189
190
        $actionMap = [];
191
        
192
        // Iterate over all the actions, and automatically determine the methods implemented
193
        foreach ($actions as $actionName => $params) {
194
            static $class = null;
195
            if (is_array($params)) {
196
                $class = $params['class'];
197
            } else {
198
                $class = $params;
199
            }
200
201
            $actionMap[$actionName] = $this->getHttpVerbMethodsFromClass($class);
202
        }
203
204
        return $actionMap;
205
    }
206
}
207