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
Bug
introduced
by
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 |