1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
|
<?php
namespace Drupal\media_library;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Site\Settings;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* A value object for the media library state.
*
* When the media library is opened it needs several parameters to work
* properly. These parameters are normally extracted from the current URL, then
* retrieved from and managed by the MediaLibraryState value object. The
* following parameters are required in order to open the media library:
* - media_library_opener_id: The ID of a container service which implements
* \Drupal\media_library\MediaLibraryOpenerInterface and is responsible for
* interacting with the media library on behalf of the "thing" (e.g., a field
* widget or text editor button) which opened it.
* - media_library_allowed_types: The media types available in the library can
* be restricted to a list of allowed types. This should be an array of media
* type IDs.
* - media_library_selected_type: The media library contains tabs to navigate
* between the different media types. The selected type contains the ID of the
* media type whose tab that should be opened.
* - media_library_remaining: When the opener wants to limit the amount of media
* items that can be selected, it can pass the number of remaining slots. When
* the number of remaining slots is a negative number, an unlimited amount of
* items can be selected.
*
* This object can also carry an optional opener-specific array of arbitrary
* values, under the media_library_opener_parameters key. These values are
* included in the hash generated by ::getHash(), so the end user cannot tamper
* with them either.
*
* @see \Drupal\media_library\MediaLibraryOpenerInterface
*/
class MediaLibraryState extends ParameterBag implements CacheableDependencyInterface {
/**
* {@inheritdoc}
*/
public function __construct(array $parameters = []) {
$this->validateRequiredParameters($parameters['media_library_opener_id'], $parameters['media_library_allowed_types'], $parameters['media_library_selected_type'], $parameters['media_library_remaining']);
$parameters += [
'media_library_opener_parameters' => [],
];
parent::__construct($parameters);
$this->set('hash', $this->getHash());
}
/**
* Creates a new MediaLibraryState object.
*
* @param string $opener_id
* The opener ID.
* @param string[] $allowed_media_type_ids
* The allowed media type IDs.
* @param string $selected_type_id
* The selected media type ID.
* @param int $remaining_slots
* The number of remaining items the user is allowed to select or add in the
* library.
* @param array $opener_parameters
* (optional) Any additional opener-specific parameter values.
*
* @return static
* A state object.
*/
public static function create($opener_id, array $allowed_media_type_ids, $selected_type_id, $remaining_slots, array $opener_parameters = []) {
$state = new static([
'media_library_opener_id' => $opener_id,
'media_library_allowed_types' => $allowed_media_type_ids,
'media_library_selected_type' => $selected_type_id,
'media_library_remaining' => $remaining_slots,
'media_library_opener_parameters' => $opener_parameters,
]);
return $state;
}
/**
* Get the media library state from a request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return static
* A state object.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* Thrown when the hash query parameter is invalid.
*/
public static function fromRequest(Request $request) {
$query = $request->query;
// Create a MediaLibraryState object through the create method to make sure
// all validation runs.
$state = static::create(
$query->get('media_library_opener_id'),
$query->all('media_library_allowed_types'),
$query->get('media_library_selected_type'),
$query->get('media_library_remaining'),
$query->all('media_library_opener_parameters')
);
// The request parameters need to contain a valid hash to prevent a
// malicious user modifying the query string to attempt to access
// inaccessible information.
if (!$state->isValidHash($query->get('hash'))) {
throw new BadRequestHttpException("Invalid media library parameters specified.");
}
// @todo Review parameters passed and remove irrelevant ones in
// https://www.drupal.org/i/3396650
// Once we have validated the required parameters, we restore the parameters
// from the request since there might be additional values.
$state->replace($query->all());
return $state;
}
/**
* Validates the required parameters for a new MediaLibraryState object.
*
* @param string $opener_id
* The media library opener service ID.
* @param string[] $allowed_media_type_ids
* The allowed media type IDs.
* @param string $selected_type_id
* The selected media type ID.
* @param int $remaining_slots
* The number of remaining items the user is allowed to select or add in the
* library.
*
* @throws \InvalidArgumentException
* If one of the passed arguments is missing or does not pass the
* validation.
*/
protected function validateRequiredParameters($opener_id, array $allowed_media_type_ids, $selected_type_id, $remaining_slots) {
// The opener ID must be a non-empty string.
if (!is_string($opener_id) || empty(trim($opener_id))) {
throw new \InvalidArgumentException('The opener ID parameter is required and must be a string.');
}
// The allowed media type IDs must be an array of non-empty strings.
if (empty($allowed_media_type_ids) || !is_array($allowed_media_type_ids)) {
throw new \InvalidArgumentException('The allowed types parameter is required and must be an array of strings.');
}
foreach ($allowed_media_type_ids as $allowed_media_type_id) {
if (!is_string($allowed_media_type_id) || empty(trim($allowed_media_type_id))) {
throw new \InvalidArgumentException('The allowed types parameter is required and must be an array of strings.');
}
}
// The selected type ID must be a non-empty string.
if (!is_string($selected_type_id) || empty(trim($selected_type_id))) {
throw new \InvalidArgumentException('The selected type parameter is required and must be a string.');
}
// The selected type ID must be present in the list of allowed types.
if (!in_array($selected_type_id, $allowed_media_type_ids, TRUE)) {
throw new \InvalidArgumentException('The selected type parameter must be present in the list of allowed types.');
}
// The remaining slots must be numeric.
if (!is_numeric($remaining_slots)) {
throw new \InvalidArgumentException('The remaining slots parameter is required and must be numeric.');
}
}
/**
* Get the hash for the state object.
*
* @return string
* The hashed parameters.
*/
public function getHash() {
// Create a hash from the required state parameters and the serialized
// optional opener-specific parameters. Sort the allowed types and
// opener parameters so that differences in order do not result in
// different hashes.
$allowed_media_type_ids = array_values($this->getAllowedTypeIds());
sort($allowed_media_type_ids);
$opener_parameters = $this->getOpenerParameters();
ksort($opener_parameters);
$hash = implode(':', [
$this->getOpenerId(),
implode(':', $allowed_media_type_ids),
$this->getSelectedTypeId(),
$this->getAvailableSlots(),
serialize($opener_parameters),
]);
return Crypt::hmacBase64($hash, \Drupal::service('private_key')->get() . Settings::getHashSalt());
}
/**
* Validate a hash for the state object.
*
* @param string $hash
* The hash to validate.
*
* @return string
* The hashed parameters.
*/
public function isValidHash($hash) {
return hash_equals($this->getHash(), $hash);
}
/**
* Returns the ID of the media library opener service.
*
* @return string
* The media library opener service ID.
*/
public function getOpenerId() {
return $this->get('media_library_opener_id');
}
/**
* Returns the media type IDs which can be selected.
*
* @return string[]
* The media type IDs.
*/
public function getAllowedTypeIds() {
return $this->all('media_library_allowed_types');
}
/**
* Returns the selected media type.
*
* @return string
* The selected media type.
*/
public function getSelectedTypeId() {
return $this->get('media_library_selected_type');
}
/**
* Determines if additional media items can be selected.
*
* @return bool
* TRUE if additional items can be selected, otherwise FALSE.
*/
public function hasSlotsAvailable() {
return $this->getAvailableSlots() !== 0;
}
/**
* Returns the number of additional media items that can be selected.
*
* When the value is not available in the URL the default is 0. When a
* negative integer is passed, an unlimited amount of media items can be
* selected.
*
* @return int
* The number of additional media items that can be selected.
*/
public function getAvailableSlots() {
return $this->getInt('media_library_remaining');
}
/**
* Returns all opener-specific parameter values.
*
* @return array
* An associative array of all opener-specific parameter values.
*/
public function getOpenerParameters() {
return $this->all('media_library_opener_parameters');
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['url.query_args'];
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return [];
}
}
|