Chris@16
|
1 <?php
|
Chris@16
|
2
|
Chris@16
|
3 namespace Drupal\Tests\image\Functional;
|
Chris@16
|
4
|
Chris@18
|
5 use Drupal\Core\File\FileSystemInterface;
|
Chris@16
|
6 use Drupal\image\Entity\ImageStyle;
|
Chris@16
|
7 use Drupal\language\Entity\ConfigurableLanguage;
|
Chris@16
|
8 use Drupal\Tests\BrowserTestBase;
|
Chris@16
|
9 use Drupal\Tests\TestFileCreationTrait;
|
Chris@16
|
10
|
Chris@16
|
11 /**
|
Chris@16
|
12 * Tests the functions for generating paths and URLs for image styles.
|
Chris@16
|
13 *
|
Chris@16
|
14 * @group image
|
Chris@16
|
15 */
|
Chris@16
|
16 class ImageStylesPathAndUrlTest extends BrowserTestBase {
|
Chris@16
|
17
|
Chris@16
|
18 use TestFileCreationTrait {
|
Chris@16
|
19 getTestFiles as drupalGetTestFiles;
|
Chris@16
|
20 compareFiles as drupalCompareFiles;
|
Chris@16
|
21 }
|
Chris@16
|
22
|
Chris@16
|
23 /**
|
Chris@16
|
24 * Modules to enable.
|
Chris@16
|
25 *
|
Chris@16
|
26 * @var array
|
Chris@16
|
27 */
|
Chris@16
|
28 public static $modules = ['image', 'image_module_test', 'language'];
|
Chris@16
|
29
|
Chris@16
|
30 /**
|
Chris@16
|
31 * The image style.
|
Chris@16
|
32 *
|
Chris@16
|
33 * @var \Drupal\image\ImageStyleInterface
|
Chris@16
|
34 */
|
Chris@16
|
35 protected $style;
|
Chris@16
|
36
|
Chris@16
|
37 /**
|
Chris@16
|
38 * {@inheritdoc}
|
Chris@16
|
39 */
|
Chris@16
|
40 protected function setUp() {
|
Chris@16
|
41 parent::setUp();
|
Chris@16
|
42
|
Chris@16
|
43 $this->style = ImageStyle::create([
|
Chris@16
|
44 'name' => 'style_foo',
|
Chris@16
|
45 'label' => $this->randomString(),
|
Chris@16
|
46 ]);
|
Chris@16
|
47 $this->style->save();
|
Chris@16
|
48
|
Chris@16
|
49 // Create a new language.
|
Chris@16
|
50 ConfigurableLanguage::createFromLangcode('fr')->save();
|
Chris@16
|
51 }
|
Chris@16
|
52
|
Chris@16
|
53 /**
|
Chris@16
|
54 * Tests \Drupal\image\ImageStyleInterface::buildUri().
|
Chris@16
|
55 */
|
Chris@16
|
56 public function testImageStylePath() {
|
Chris@16
|
57 $scheme = 'public';
|
Chris@16
|
58 $actual = $this->style->buildUri("$scheme://foo/bar.gif");
|
Chris@16
|
59 $expected = "$scheme://styles/" . $this->style->id() . "/$scheme/foo/bar.gif";
|
Chris@16
|
60 $this->assertEqual($actual, $expected, 'Got the path for a file URI.');
|
Chris@16
|
61
|
Chris@16
|
62 $actual = $this->style->buildUri('foo/bar.gif');
|
Chris@16
|
63 $expected = "$scheme://styles/" . $this->style->id() . "/$scheme/foo/bar.gif";
|
Chris@16
|
64 $this->assertEqual($actual, $expected, 'Got the path for a relative file path.');
|
Chris@16
|
65 }
|
Chris@16
|
66
|
Chris@16
|
67 /**
|
Chris@16
|
68 * Tests an image style URL using the "public://" scheme.
|
Chris@16
|
69 */
|
Chris@16
|
70 public function testImageStyleUrlAndPathPublic() {
|
Chris@16
|
71 $this->doImageStyleUrlAndPathTests('public');
|
Chris@16
|
72 }
|
Chris@16
|
73
|
Chris@16
|
74 /**
|
Chris@16
|
75 * Tests an image style URL using the "private://" scheme.
|
Chris@16
|
76 */
|
Chris@16
|
77 public function testImageStyleUrlAndPathPrivate() {
|
Chris@16
|
78 $this->doImageStyleUrlAndPathTests('private');
|
Chris@16
|
79 }
|
Chris@16
|
80
|
Chris@16
|
81 /**
|
Chris@16
|
82 * Tests an image style URL with the "public://" scheme and unclean URLs.
|
Chris@16
|
83 */
|
Chris@16
|
84 public function testImageStyleUrlAndPathPublicUnclean() {
|
Chris@16
|
85 $this->doImageStyleUrlAndPathTests('public', FALSE);
|
Chris@16
|
86 }
|
Chris@16
|
87
|
Chris@16
|
88 /**
|
Chris@16
|
89 * Tests an image style URL with the "private://" schema and unclean URLs.
|
Chris@16
|
90 */
|
Chris@16
|
91 public function testImageStyleUrlAndPathPrivateUnclean() {
|
Chris@16
|
92 $this->doImageStyleUrlAndPathTests('private', FALSE);
|
Chris@16
|
93 }
|
Chris@16
|
94
|
Chris@16
|
95 /**
|
Chris@16
|
96 * Tests an image style URL with the "public://" schema and language prefix.
|
Chris@16
|
97 */
|
Chris@16
|
98 public function testImageStyleUrlAndPathPublicLanguage() {
|
Chris@16
|
99 $this->doImageStyleUrlAndPathTests('public', TRUE, TRUE, 'fr');
|
Chris@16
|
100 }
|
Chris@16
|
101
|
Chris@16
|
102 /**
|
Chris@16
|
103 * Tests an image style URL with the "private://" schema and language prefix.
|
Chris@16
|
104 */
|
Chris@16
|
105 public function testImageStyleUrlAndPathPrivateLanguage() {
|
Chris@16
|
106 $this->doImageStyleUrlAndPathTests('private', TRUE, TRUE, 'fr');
|
Chris@16
|
107 }
|
Chris@16
|
108
|
Chris@16
|
109 /**
|
Chris@16
|
110 * Tests an image style URL with a file URL that has an extra slash in it.
|
Chris@16
|
111 */
|
Chris@16
|
112 public function testImageStyleUrlExtraSlash() {
|
Chris@16
|
113 $this->doImageStyleUrlAndPathTests('public', TRUE, TRUE);
|
Chris@16
|
114 }
|
Chris@16
|
115
|
Chris@16
|
116 /**
|
Chris@16
|
117 * Tests that an invalid source image returns a 404.
|
Chris@16
|
118 */
|
Chris@16
|
119 public function testImageStyleUrlForMissingSourceImage() {
|
Chris@16
|
120 $non_existent_uri = 'public://foo.png';
|
Chris@16
|
121 $generated_url = $this->style->buildUrl($non_existent_uri);
|
Chris@16
|
122 $this->drupalGet($generated_url);
|
Chris@16
|
123 $this->assertResponse(404, 'Accessing an image style URL with a source image that does not exist provides a 404 error response.');
|
Chris@16
|
124 }
|
Chris@16
|
125
|
Chris@16
|
126 /**
|
Chris@16
|
127 * Tests building an image style URL.
|
Chris@16
|
128 */
|
Chris@16
|
129 public function doImageStyleUrlAndPathTests($scheme, $clean_url = TRUE, $extra_slash = FALSE, $langcode = FALSE) {
|
Chris@16
|
130 $this->prepareRequestForGenerator($clean_url);
|
Chris@16
|
131
|
Chris@16
|
132 // Make the default scheme neither "public" nor "private" to verify the
|
Chris@16
|
133 // functions work for other than the default scheme.
|
Chris@16
|
134 $this->config('system.file')->set('default_scheme', 'temporary')->save();
|
Chris@16
|
135
|
Chris@16
|
136 // Create the directories for the styles.
|
Chris@16
|
137 $directory = $scheme . '://styles/' . $this->style->id();
|
Chris@18
|
138 $status = \Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
|
Chris@16
|
139 $this->assertNotIdentical(FALSE, $status, 'Created the directory for the generated images for the test style.');
|
Chris@16
|
140
|
Chris@16
|
141 // Override the language to build the URL for the correct language.
|
Chris@16
|
142 if ($langcode) {
|
Chris@16
|
143 $language_manager = \Drupal::service('language_manager');
|
Chris@16
|
144 $language = $language_manager->getLanguage($langcode);
|
Chris@16
|
145 $language_manager->setConfigOverrideLanguage($language);
|
Chris@16
|
146 }
|
Chris@16
|
147
|
Chris@16
|
148 // Create a working copy of the file.
|
Chris@16
|
149 $files = $this->drupalGetTestFiles('image');
|
Chris@16
|
150 $file = array_shift($files);
|
Chris@18
|
151 /** @var \Drupal\Core\File\FileSystemInterface $file_system */
|
Chris@18
|
152 $file_system = \Drupal::service('file_system');
|
Chris@18
|
153 $original_uri = $file_system->copy($file->uri, $scheme . '://', FileSystemInterface::EXISTS_RENAME);
|
Chris@16
|
154 // Let the image_module_test module know about this file, so it can claim
|
Chris@16
|
155 // ownership in hook_file_download().
|
Chris@16
|
156 \Drupal::state()->set('image.test_file_download', $original_uri);
|
Chris@16
|
157 $this->assertNotIdentical(FALSE, $original_uri, 'Created the generated image file.');
|
Chris@16
|
158
|
Chris@16
|
159 // Get the URL of a file that has not been generated and try to create it.
|
Chris@16
|
160 $generated_uri = $this->style->buildUri($original_uri);
|
Chris@16
|
161 $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
|
Chris@16
|
162 $generate_url = $this->style->buildUrl($original_uri, $clean_url);
|
Chris@16
|
163
|
Chris@16
|
164 // Make sure that language prefix is never added to the image style URL.
|
Chris@16
|
165 if ($langcode) {
|
Chris@16
|
166 $this->assertTrue(strpos($generate_url, "/$langcode/") === FALSE, 'Langcode was not found in the image style URL.');
|
Chris@16
|
167 }
|
Chris@16
|
168
|
Chris@16
|
169 // Ensure that the tests still pass when the file is generated by accessing
|
Chris@16
|
170 // a poorly constructed (but still valid) file URL that has an extra slash
|
Chris@16
|
171 // in it.
|
Chris@16
|
172 if ($extra_slash) {
|
Chris@16
|
173 $modified_uri = str_replace('://', ':///', $original_uri);
|
Chris@16
|
174 $this->assertNotEqual($original_uri, $modified_uri, 'An extra slash was added to the generated file URI.');
|
Chris@16
|
175 $generate_url = $this->style->buildUrl($modified_uri, $clean_url);
|
Chris@16
|
176 }
|
Chris@16
|
177 if (!$clean_url) {
|
Chris@16
|
178 $this->assertTrue(strpos($generate_url, 'index.php/') !== FALSE, 'When using non-clean URLS, the system path contains the script name.');
|
Chris@16
|
179 }
|
Chris@16
|
180 // Add some extra chars to the token.
|
Chris@16
|
181 $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', IMAGE_DERIVATIVE_TOKEN . '=Zo', $generate_url));
|
Chris@16
|
182 $this->assertResponse(404, 'Image was inaccessible at the URL with an invalid token.');
|
Chris@16
|
183 // Change the parameter name so the token is missing.
|
Chris@16
|
184 $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', 'wrongparam=', $generate_url));
|
Chris@16
|
185 $this->assertResponse(404, 'Image was inaccessible at the URL with a missing token.');
|
Chris@16
|
186
|
Chris@16
|
187 // Check that the generated URL is the same when we pass in a relative path
|
Chris@16
|
188 // rather than a URI. We need to temporarily switch the default scheme to
|
Chris@16
|
189 // match the desired scheme before testing this, then switch it back to the
|
Chris@16
|
190 // "temporary" scheme used throughout this test afterwards.
|
Chris@16
|
191 $this->config('system.file')->set('default_scheme', $scheme)->save();
|
Chris@16
|
192 $relative_path = file_uri_target($original_uri);
|
Chris@16
|
193 $generate_url_from_relative_path = $this->style->buildUrl($relative_path, $clean_url);
|
Chris@16
|
194 $this->assertEqual($generate_url, $generate_url_from_relative_path);
|
Chris@16
|
195 $this->config('system.file')->set('default_scheme', 'temporary')->save();
|
Chris@16
|
196
|
Chris@16
|
197 // Fetch the URL that generates the file.
|
Chris@16
|
198 $this->drupalGet($generate_url);
|
Chris@16
|
199 $this->assertResponse(200, 'Image was generated at the URL.');
|
Chris@16
|
200 $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.');
|
Chris@16
|
201 // assertRaw can't be used with string containing non UTF-8 chars.
|
Chris@16
|
202 $this->assertNotEmpty(file_get_contents($generated_uri), 'URL returns expected file.');
|
Chris@16
|
203 $image = $this->container->get('image.factory')->get($generated_uri);
|
Chris@16
|
204 $this->assertEqual($this->drupalGetHeader('Content-Type'), $image->getMimeType(), 'Expected Content-Type was reported.');
|
Chris@16
|
205 $this->assertEqual($this->drupalGetHeader('Content-Length'), $image->getFileSize(), 'Expected Content-Length was reported.');
|
Chris@16
|
206
|
Chris@16
|
207 // Check that we did not download the original file.
|
Chris@16
|
208 $original_image = $this->container->get('image.factory')
|
Chris@16
|
209 ->get($original_uri);
|
Chris@16
|
210 $this->assertNotEqual($this->drupalGetHeader('Content-Length'), $original_image->getFileSize());
|
Chris@16
|
211
|
Chris@16
|
212 if ($scheme == 'private') {
|
Chris@16
|
213 $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', 'Expires header was sent.');
|
Chris@16
|
214 $this->assertNotEqual(strpos($this->drupalGetHeader('Cache-Control'), 'no-cache'), FALSE, 'Cache-Control header contains \'no-cache\' to prevent caching.');
|
Chris@16
|
215 $this->assertEqual($this->drupalGetHeader('X-Image-Owned-By'), 'image_module_test', 'Expected custom header has been added.');
|
Chris@16
|
216
|
Chris@16
|
217 // Make sure that a second request to the already existing derivative
|
Chris@16
|
218 // works too.
|
Chris@16
|
219 $this->drupalGet($generate_url);
|
Chris@16
|
220 $this->assertResponse(200, 'Image was generated at the URL.');
|
Chris@16
|
221
|
Chris@16
|
222 // Check that the second request also returned the generated image.
|
Chris@16
|
223 $this->assertEqual($this->drupalGetHeader('Content-Length'), $image->getFileSize());
|
Chris@16
|
224
|
Chris@16
|
225 // Check that we did not download the original file.
|
Chris@16
|
226 $this->assertNotEqual($this->drupalGetHeader('Content-Length'), $original_image->getFileSize());
|
Chris@16
|
227
|
Chris@16
|
228 // Make sure that access is denied for existing style files if we do not
|
Chris@16
|
229 // have access.
|
Chris@16
|
230 \Drupal::state()->delete('image.test_file_download');
|
Chris@16
|
231 $this->drupalGet($generate_url);
|
Chris@16
|
232 $this->assertResponse(403, 'Confirmed that access is denied for the private image style.');
|
Chris@16
|
233
|
Chris@16
|
234 // Repeat this with a different file that we do not have access to and
|
Chris@16
|
235 // make sure that access is denied.
|
Chris@16
|
236 $file_noaccess = array_shift($files);
|
Chris@18
|
237 $original_uri_noaccess = $file_system->copy($file_noaccess->uri, $scheme . '://', FileSystemInterface::EXISTS_RENAME);
|
Chris@18
|
238 $generated_uri_noaccess = $scheme . '://styles/' . $this->style->id() . '/' . $scheme . '/' . $file_system->basename($original_uri_noaccess);
|
Chris@16
|
239 $this->assertFalse(file_exists($generated_uri_noaccess), 'Generated file does not exist.');
|
Chris@16
|
240 $generate_url_noaccess = $this->style->buildUrl($original_uri_noaccess);
|
Chris@16
|
241
|
Chris@16
|
242 $this->drupalGet($generate_url_noaccess);
|
Chris@16
|
243 $this->assertResponse(403, 'Confirmed that access is denied for the private image style.');
|
Chris@16
|
244 // Verify that images are not appended to the response.
|
Chris@16
|
245 // Currently this test only uses PNG images.
|
Chris@16
|
246 if (strpos($generate_url, '.png') === FALSE) {
|
Chris@16
|
247 $this->fail('Confirming that private image styles are not appended require PNG file.');
|
Chris@16
|
248 }
|
Chris@16
|
249 else {
|
Chris@16
|
250 // Check for PNG-Signature
|
Chris@16
|
251 // (cf. http://www.libpng.org/pub/png/book/chapter08.html#png.ch08.div.2)
|
Chris@16
|
252 // in the response body.
|
Chris@16
|
253 $raw = $this->getSession()->getPage()->getContent();
|
Chris@16
|
254 $this->assertFalse(strpos($raw, chr(137) . chr(80) . chr(78) . chr(71) . chr(13) . chr(10) . chr(26) . chr(10)));
|
Chris@16
|
255 }
|
Chris@16
|
256 }
|
Chris@16
|
257 else {
|
Chris@16
|
258 $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', 'Expires header was sent.');
|
Chris@16
|
259 $this->assertEqual(strpos($this->drupalGetHeader('Cache-Control'), 'no-cache'), FALSE, 'Cache-Control header contains \'no-cache\' to prevent caching.');
|
Chris@16
|
260
|
Chris@16
|
261 if ($clean_url) {
|
Chris@16
|
262 // Add some extra chars to the token.
|
Chris@16
|
263 $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', IMAGE_DERIVATIVE_TOKEN . '=Zo', $generate_url));
|
Chris@16
|
264 $this->assertResponse(200, 'Existing image was accessible at the URL with an invalid token.');
|
Chris@16
|
265 }
|
Chris@16
|
266 }
|
Chris@16
|
267
|
Chris@16
|
268 // Allow insecure image derivatives to be created for the remainder of this
|
Chris@16
|
269 // test.
|
Chris@16
|
270 $this->config('image.settings')
|
Chris@16
|
271 ->set('allow_insecure_derivatives', TRUE)
|
Chris@16
|
272 ->save();
|
Chris@16
|
273
|
Chris@16
|
274 // Create another working copy of the file.
|
Chris@16
|
275 $files = $this->drupalGetTestFiles('image');
|
Chris@16
|
276 $file = array_shift($files);
|
Chris@18
|
277 $original_uri = $file_system->copy($file->uri, $scheme . '://', FileSystemInterface::EXISTS_RENAME);
|
Chris@16
|
278 // Let the image_module_test module know about this file, so it can claim
|
Chris@16
|
279 // ownership in hook_file_download().
|
Chris@16
|
280 \Drupal::state()->set('image.test_file_download', $original_uri);
|
Chris@16
|
281
|
Chris@16
|
282 // Suppress the security token in the URL, then get the URL of a file that
|
Chris@16
|
283 // has not been created and try to create it. Check that the security token
|
Chris@16
|
284 // is not present in the URL but that the image is still accessible.
|
Chris@16
|
285 $this->config('image.settings')->set('suppress_itok_output', TRUE)->save();
|
Chris@16
|
286 $generated_uri = $this->style->buildUri($original_uri);
|
Chris@16
|
287 $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
|
Chris@16
|
288 $generate_url = $this->style->buildUrl($original_uri, $clean_url);
|
Chris@16
|
289 $this->assertIdentical(strpos($generate_url, IMAGE_DERIVATIVE_TOKEN . '='), FALSE, 'The security token does not appear in the image style URL.');
|
Chris@16
|
290 $this->drupalGet($generate_url);
|
Chris@16
|
291 $this->assertResponse(200, 'Image was accessible at the URL with a missing token.');
|
Chris@16
|
292
|
Chris@17
|
293 // Stop suppressing the security token in the URL.
|
Chris@16
|
294 $this->config('image.settings')->set('suppress_itok_output', FALSE)->save();
|
Chris@16
|
295 // Ensure allow_insecure_derivatives is enabled.
|
Chris@16
|
296 $this->assertEqual($this->config('image.settings')
|
Chris@16
|
297 ->get('allow_insecure_derivatives'), TRUE);
|
Chris@16
|
298 // Check that a security token is still required when generating a second
|
Chris@16
|
299 // image derivative using the first one as a source.
|
Chris@16
|
300 $nested_url = $this->style->buildUrl($generated_uri, $clean_url);
|
Chris@16
|
301 $matches_expected_url_format = (boolean) preg_match('/styles\/' . $this->style->id() . '\/' . $scheme . '\/styles\/' . $this->style->id() . '\/' . $scheme . '/', $nested_url);
|
Chris@16
|
302 $this->assertTrue($matches_expected_url_format, "URL for a derivative of an image style matches expected format.");
|
Chris@16
|
303 $nested_url_with_wrong_token = str_replace(IMAGE_DERIVATIVE_TOKEN . '=', 'wrongparam=', $nested_url);
|
Chris@16
|
304 $this->drupalGet($nested_url_with_wrong_token);
|
Chris@16
|
305 $this->assertResponse(404, 'Image generated from an earlier derivative was inaccessible at the URL with a missing token.');
|
Chris@16
|
306 // Check that this restriction cannot be bypassed by adding extra slashes
|
Chris@16
|
307 // to the URL.
|
Chris@16
|
308 $this->drupalGet(substr_replace($nested_url_with_wrong_token, '//styles/', strrpos($nested_url_with_wrong_token, '/styles/'), strlen('/styles/')));
|
Chris@16
|
309 $this->assertResponse(404, 'Image generated from an earlier derivative was inaccessible at the URL with a missing token, even with an extra forward slash in the URL.');
|
Chris@16
|
310 $this->drupalGet(substr_replace($nested_url_with_wrong_token, '////styles/', strrpos($nested_url_with_wrong_token, '/styles/'), strlen('/styles/')));
|
Chris@16
|
311 $this->assertResponse(404, 'Image generated from an earlier derivative was inaccessible at the URL with a missing token, even with multiple forward slashes in the URL.');
|
Chris@16
|
312 // Make sure the image can still be generated if a correct token is used.
|
Chris@16
|
313 $this->drupalGet($nested_url);
|
Chris@16
|
314 $this->assertResponse(200, 'Image was accessible when a correct token was provided in the URL.');
|
Chris@16
|
315
|
Chris@16
|
316 // Check that requesting a nonexistent image does not create any new
|
Chris@16
|
317 // directories in the file system.
|
Chris@16
|
318 $directory = $scheme . '://styles/' . $this->style->id() . '/' . $scheme . '/' . $this->randomMachineName();
|
Chris@16
|
319 $this->drupalGet(file_create_url($directory . '/' . $this->randomString()));
|
Chris@16
|
320 $this->assertFalse(file_exists($directory), 'New directory was not created in the filesystem when requesting an unauthorized image.');
|
Chris@16
|
321 }
|
Chris@16
|
322
|
Chris@16
|
323 }
|