Completed
Push — master ( af3b8b...4e6eb5 )
by Alexander
39:22 queued 36:28
created

ContentNegotiator::negotiate()   B

Complexity

Conditions 7
Paths 36

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 7

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 12
cts 12
cp 1
rs 8.8333
c 0
b 0
f 0
cc 7
nc 36
nop 0
crap 7
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\filters;
9
10
use Yii;
11
use yii\base\ActionFilter;
12
use yii\base\BootstrapInterface;
13
use yii\web\BadRequestHttpException;
14
use yii\web\Request;
15
use yii\web\Response;
16
use yii\web\UnsupportedMediaTypeHttpException;
17
18
/**
19
 * ContentNegotiator supports response format negotiation and application language negotiation.
20
 *
21
 * When the [[formats|supported formats]] property is specified, ContentNegotiator will support response format
22
 * negotiation based on the value of the GET parameter [[formatParam]] and the `Accept` HTTP header.
23
 * If a match is found, the [[Response::format]] property will be set as the chosen format.
24
 * The [[Response::acceptMimeType]] as well as [[Response::acceptParams]] will also be updated accordingly.
25
 *
26
 * When the [[languages|supported languages]] is specified, ContentNegotiator will support application
27
 * language negotiation based on the value of the GET parameter [[languageParam]] and the `Accept-Language` HTTP header.
28
 * If a match is found, the [[\yii\base\Application::language]] property will be set as the chosen language.
29
 *
30
 * You may use ContentNegotiator as a bootstrapping component as well as an action filter.
31
 *
32
 * The following code shows how you can use ContentNegotiator as a bootstrapping component. Note that in this case,
33
 * the content negotiation applies to the whole application.
34
 *
35
 * ```php
36
 * // in application configuration
37
 * use yii\web\Response;
38
 *
39
 * return [
40
 *     'bootstrap' => [
41
 *         [
42
 *             'class' => 'yii\filters\ContentNegotiator',
43
 *             'formats' => [
44
 *                 'application/json' => Response::FORMAT_JSON,
45
 *                 'application/xml' => Response::FORMAT_XML,
46
 *             ],
47
 *             'languages' => [
48
 *                 'en',
49
 *                 'de',
50
 *             ],
51
 *         ],
52
 *     ],
53
 * ];
54
 * ```
55
 *
56
 * The following code shows how you can use ContentNegotiator as an action filter in either a controller or a module.
57
 * In this case, the content negotiation result only applies to the corresponding controller or module, or even
58
 * specific actions if you configure the `only` or `except` property of the filter.
59
 *
60
 * ```php
61
 * use yii\web\Response;
62
 *
63
 * public function behaviors()
64
 * {
65
 *     return [
66
 *         [
67
 *             'class' => 'yii\filters\ContentNegotiator',
68
 *             'only' => ['view', 'index'],  // in a controller
69
 *             // if in a module, use the following IDs for user actions
70
 *             // 'only' => ['user/view', 'user/index']
71
 *             'formats' => [
72
 *                 'application/json' => Response::FORMAT_JSON,
73
 *             ],
74
 *             'languages' => [
75
 *                 'en',
76
 *                 'de',
77
 *             ],
78
 *         ],
79
 *     ];
80
 * }
81
 * ```
82
 *
83
 * @author Qiang Xue <[email protected]>
84
 * @since 2.0
85
 */
86
class ContentNegotiator extends ActionFilter implements BootstrapInterface
87
{
88
    /**
89
     * @var string the name of the GET parameter that specifies the response format.
90
     * Note that if the specified format does not exist in [[formats]], a [[UnsupportedMediaTypeHttpException]]
91
     * exception will be thrown.  If the parameter value is empty or if this property is null,
92
     * the response format will be determined based on the `Accept` HTTP header only.
93
     * @see formats
94
     */
95
    public $formatParam = '_format';
96
    /**
97
     * @var string the name of the GET parameter that specifies the [[\yii\base\Application::language|application language]].
98
     * Note that if the specified language does not match any of [[languages]], the first language in [[languages]]
99
     * will be used. If the parameter value is empty or if this property is null,
100
     * the application language will be determined based on the `Accept-Language` HTTP header only.
101
     * @see languages
102
     */
103
    public $languageParam = '_lang';
104
    /**
105
     * @var array list of supported response formats. The keys are MIME types (e.g. `application/json`)
106
     * while the values are the corresponding formats (e.g. `html`, `json`) which must be supported
107
     * as declared in [[\yii\web\Response::formatters]].
108
     *
109
     * If this property is empty or not set, response format negotiation will be skipped.
110
     */
111
    public $formats;
112
    /**
113
     * @var array a list of supported languages. The array keys are the supported language variants (e.g. `en-GB`, `en-US`),
114
     * while the array values are the corresponding language codes (e.g. `en`, `de`) recognized by the application.
115
     *
116
     * Array keys are not always required. When an array value does not have a key, the matching of the requested language
117
     * will be based on a language fallback mechanism. For example, a value of `en` will match `en`, `en_US`, `en-US`, `en-GB`, etc.
118
     *
119
     * If this property is empty or not set, language negotiation will be skipped.
120
     */
121
    public $languages;
122
    /**
123
     * @var Request the current request. If not set, the `request` application component will be used.
124
     */
125
    public $request;
126
    /**
127
     * @var Response the response to be sent. If not set, the `response` application component will be used.
128
     */
129
    public $response;
130
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    public function bootstrap($app)
136
    {
137
        $this->negotiate();
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143 3
    public function beforeAction($action)
144
    {
145 3
        $this->negotiate();
146 2
        return true;
147
    }
148
149
    /**
150
     * Negotiates the response format and application language.
151
     */
152 3
    public function negotiate()
153
    {
154 3
        $request = $this->request ?: Yii::$app->getRequest();
155 3
        $response = $this->response ?: Yii::$app->getResponse();
156 3
        if (!empty($this->formats)) {
157 2
            if (\count($this->formats) > 1) {
158 2
                $response->getHeaders()->add('Vary', 'Accept');
159
            }
160 2
            $this->negotiateContentType($request, $response);
161
        }
162 2
        if (!empty($this->languages)) {
163 2
            if (\count($this->languages) > 1) {
164 2
                $response->getHeaders()->add('Vary', 'Accept-Language');
165
            }
166 2
            Yii::$app->language = $this->negotiateLanguage($request);
167
        }
168 2
    }
169
170
    /**
171
     * Negotiates the response format.
172
     * @param Request $request
173
     * @param Response $response
174
     * @throws BadRequestHttpException if an array received for GET parameter [[formatParam]].
175
     * @throws UnsupportedMediaTypeHttpException if none of the requested content types is accepted.
176
     */
177 2
    protected function negotiateContentType($request, $response)
178
    {
179 2
        if (!empty($this->formatParam) && ($format = $request->get($this->formatParam)) !== null) {
180 1
            if (is_array($format)) {
181 1
                throw new BadRequestHttpException("Invalid data received for GET parameter '{$this->formatParam}'.");
182
            }
183
184
            if (in_array($format, $this->formats)) {
185
                $response->format = $format;
186
                $response->acceptMimeType = null;
187
                $response->acceptParams = [];
188
                return;
189
            }
190
191
            throw new UnsupportedMediaTypeHttpException('The requested response format is not supported: ' . $format);
192
        }
193
194 1
        $types = $request->getAcceptableContentTypes();
195 1
        if (empty($types)) {
196 1
            $types['*/*'] = [];
197
        }
198
199 1
        foreach ($types as $type => $params) {
200 1
            if (isset($this->formats[$type])) {
201
                $response->format = $this->formats[$type];
202
                $response->acceptMimeType = $type;
0 ignored issues
show
Documentation Bug introduced by
It seems like $type can also be of type integer. However, the property $acceptMimeType is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
203
                $response->acceptParams = $params;
204 1
                return;
205
            }
206
        }
207
208 1
        foreach ($this->formats as $type => $format) {
209 1
            $response->format = $format;
210 1
            $response->acceptMimeType = $type;
211 1
            $response->acceptParams = [];
212 1
            break;
213
        }
214
215 1
        if (isset($types['*/*'])) {
216 1
            return;
217
        }
218
219
        throw new UnsupportedMediaTypeHttpException('None of your requested content types is supported.');
220
    }
221
222
    /**
223
     * Negotiates the application language.
224
     * @param Request $request
225
     * @return string the chosen language
226
     */
227 2
    protected function negotiateLanguage($request)
228
    {
229 2
        if (!empty($this->languageParam) && ($language = $request->get($this->languageParam)) !== null) {
230 1
            if (is_array($language)) {
231
                // If an array received, then skip it and use the first of supported languages
232 1
                return reset($this->languages);
233
            }
234
            if (isset($this->languages[$language])) {
235
                return $this->languages[$language];
236
            }
237
            foreach ($this->languages as $key => $supported) {
238
                if (is_int($key) && $this->isLanguageSupported($language, $supported)) {
239
                    return $supported;
240
                }
241
            }
242
243
            return reset($this->languages);
244
        }
245
246 1
        foreach ($request->getAcceptableLanguages() as $language) {
247
            if (isset($this->languages[$language])) {
248
                return $this->languages[$language];
249
            }
250
            foreach ($this->languages as $key => $supported) {
251
                if (is_int($key) && $this->isLanguageSupported($language, $supported)) {
252
                    return $supported;
253
                }
254
            }
255
        }
256
257 1
        return reset($this->languages);
258
    }
259
260
    /**
261
     * Returns a value indicating whether the requested language matches the supported language.
262
     * @param string $requested the requested language code
263
     * @param string $supported the supported language code
264
     * @return bool whether the requested language is supported
265
     */
266
    protected function isLanguageSupported($requested, $supported)
267
    {
268
        $supported = str_replace('_', '-', strtolower($supported));
269
        $requested = str_replace('_', '-', strtolower($requested));
270
        return strpos($requested . '-', $supported . '-') === 0;
271
    }
272
}
273