Completed
Push — master ( 55b06d...9f2a87 )
by Alexander
35:57
created

framework/filters/ContentNegotiator.php (1 issue)

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');
0 ignored issues
show
The method getHeaders() does not exist on yii\console\Response. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

158
                $response->/** @scrutinizer ignore-call */ 
159
                           getHeaders()->add('Vary', 'Accept');
Loading history...
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;
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