annotate core/modules/file/tests/src/Functional/SaveUploadTest.php @ 5:12f9dff5fda9 tip

Update to Drupal core 8.7.1
author Chris Cannam
date Thu, 09 May 2019 15:34:47 +0100
parents a9cd425dd02b
children
rev   line source
Chris@4 1 <?php
Chris@4 2
Chris@4 3 namespace Drupal\Tests\file\Functional;
Chris@4 4
Chris@5 5 use Drupal\Component\Render\FormattableMarkup;
Chris@5 6 use Drupal\Core\File\FileSystemInterface;
Chris@5 7 use Drupal\Core\Url;
Chris@4 8 use Drupal\file\Entity\File;
Chris@4 9 use Drupal\Tests\TestFileCreationTrait;
Chris@4 10
Chris@4 11 /**
Chris@4 12 * Tests the file_save_upload() function.
Chris@4 13 *
Chris@4 14 * @group file
Chris@4 15 */
Chris@4 16 class SaveUploadTest extends FileManagedTestBase {
Chris@4 17
Chris@4 18 use TestFileCreationTrait {
Chris@4 19 getTestFiles as drupalGetTestFiles;
Chris@4 20 }
Chris@4 21
Chris@4 22 /**
Chris@4 23 * Modules to enable.
Chris@4 24 *
Chris@4 25 * @var array
Chris@4 26 */
Chris@4 27 public static $modules = ['dblog'];
Chris@4 28
Chris@4 29 /**
Chris@4 30 * An image file path for uploading.
Chris@4 31 *
Chris@4 32 * @var \Drupal\file\FileInterface
Chris@4 33 */
Chris@4 34 protected $image;
Chris@4 35
Chris@4 36 /**
Chris@4 37 * A PHP file path for upload security testing.
Chris@4 38 */
Chris@4 39 protected $phpfile;
Chris@4 40
Chris@4 41 /**
Chris@4 42 * The largest file id when the test starts.
Chris@4 43 */
Chris@4 44 protected $maxFidBefore;
Chris@4 45
Chris@4 46 /**
Chris@4 47 * Extension of the image filename.
Chris@4 48 *
Chris@4 49 * @var string
Chris@4 50 */
Chris@4 51 protected $imageExtension;
Chris@4 52
Chris@4 53 protected function setUp() {
Chris@4 54 parent::setUp();
Chris@4 55 $account = $this->drupalCreateUser(['access site reports']);
Chris@4 56 $this->drupalLogin($account);
Chris@4 57
Chris@4 58 $image_files = $this->drupalGetTestFiles('image');
Chris@4 59 $this->image = File::create((array) current($image_files));
Chris@4 60
Chris@4 61 list(, $this->imageExtension) = explode('.', $this->image->getFilename());
Chris@4 62 $this->assertTrue(is_file($this->image->getFileUri()), "The image file we're going to upload exists.");
Chris@4 63
Chris@4 64 $this->phpfile = current($this->drupalGetTestFiles('php'));
Chris@4 65 $this->assertTrue(is_file($this->phpfile->uri), 'The PHP file we are going to upload exists.');
Chris@4 66
Chris@5 67 $this->maxFidBefore = (int) \Drupal::entityQueryAggregate('file')->aggregate('fid', 'max')->execute()[0]['fid_max'];
Chris@4 68
Chris@4 69 // Upload with replace to guarantee there's something there.
Chris@4 70 $edit = [
Chris@4 71 'file_test_replace' => FILE_EXISTS_REPLACE,
Chris@4 72 'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->image->getFileUri()),
Chris@4 73 ];
Chris@4 74 $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
Chris@4 75 $this->assertResponse(200, 'Received a 200 response for posted test file.');
Chris@4 76 $this->assertRaw(t('You WIN!'), 'Found the success message.');
Chris@4 77
Chris@4 78 // Check that the correct hooks were called then clean out the hook
Chris@4 79 // counters.
Chris@4 80 $this->assertFileHooksCalled(['validate', 'insert']);
Chris@4 81 file_test_reset();
Chris@4 82 }
Chris@4 83
Chris@4 84 /**
Chris@4 85 * Test the file_save_upload() function.
Chris@4 86 */
Chris@4 87 public function testNormal() {
Chris@5 88 $max_fid_after = (int) \Drupal::entityQueryAggregate('file')->aggregate('fid', 'max')->execute()[0]['fid_max'];
Chris@4 89 $this->assertTrue($max_fid_after > $this->maxFidBefore, 'A new file was created.');
Chris@4 90 $file1 = File::load($max_fid_after);
Chris@4 91 $this->assertTrue($file1, 'Loaded the file.');
Chris@4 92 // MIME type of the uploaded image may be either image/jpeg or image/png.
Chris@4 93 $this->assertEqual(substr($file1->getMimeType(), 0, 5), 'image', 'A MIME type was set.');
Chris@4 94
Chris@4 95 // Reset the hook counters to get rid of the 'load' we just called.
Chris@4 96 file_test_reset();
Chris@4 97
Chris@4 98 // Upload a second file.
Chris@4 99 $image2 = current($this->drupalGetTestFiles('image'));
Chris@4 100 $edit = ['files[file_test_upload]' => \Drupal::service('file_system')->realpath($image2->uri)];
Chris@4 101 $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
Chris@4 102 $this->assertResponse(200, 'Received a 200 response for posted test file.');
Chris@4 103 $this->assertRaw(t('You WIN!'));
Chris@5 104 $max_fid_after = (int) \Drupal::entityQueryAggregate('file')->aggregate('fid', 'max')->execute()[0]['fid_max'];
Chris@4 105
Chris@4 106 // Check that the correct hooks were called.
Chris@4 107 $this->assertFileHooksCalled(['validate', 'insert']);
Chris@4 108
Chris@4 109 $file2 = File::load($max_fid_after);
Chris@4 110 $this->assertTrue($file2, 'Loaded the file');
Chris@4 111 // MIME type of the uploaded image may be either image/jpeg or image/png.
Chris@4 112 $this->assertEqual(substr($file2->getMimeType(), 0, 5), 'image', 'A MIME type was set.');
Chris@4 113
Chris@4 114 // Load both files using File::loadMultiple().
Chris@4 115 $files = File::loadMultiple([$file1->id(), $file2->id()]);
Chris@4 116 $this->assertTrue(isset($files[$file1->id()]), 'File was loaded successfully');
Chris@4 117 $this->assertTrue(isset($files[$file2->id()]), 'File was loaded successfully');
Chris@4 118
Chris@4 119 // Upload a third file to a subdirectory.
Chris@4 120 $image3 = current($this->drupalGetTestFiles('image'));
Chris@4 121 $image3_realpath = \Drupal::service('file_system')->realpath($image3->uri);
Chris@4 122 $dir = $this->randomMachineName();
Chris@4 123 $edit = [
Chris@4 124 'files[file_test_upload]' => $image3_realpath,
Chris@4 125 'file_subdir' => $dir,
Chris@4 126 ];
Chris@4 127 $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
Chris@4 128 $this->assertResponse(200, 'Received a 200 response for posted test file.');
Chris@4 129 $this->assertRaw(t('You WIN!'));
Chris@5 130 $this->assertTrue(is_file('temporary://' . $dir . '/' . trim(\Drupal::service('file_system')->basename($image3_realpath))));
Chris@4 131 }
Chris@4 132
Chris@4 133 /**
Chris@4 134 * Test extension handling.
Chris@4 135 */
Chris@4 136 public function testHandleExtension() {
Chris@4 137 // The file being tested is a .gif which is in the default safe list
Chris@4 138 // of extensions to allow when the extension validator isn't used. This is
Chris@4 139 // implicitly tested at the testNormal() test. Here we tell
Chris@4 140 // file_save_upload() to only allow ".foo".
Chris@4 141 $extensions = 'foo';
Chris@4 142 $edit = [
Chris@4 143 'file_test_replace' => FILE_EXISTS_REPLACE,
Chris@4 144 'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->image->getFileUri()),
Chris@4 145 'extensions' => $extensions,
Chris@4 146 ];
Chris@4 147
Chris@4 148 $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
Chris@4 149 $this->assertResponse(200, 'Received a 200 response for posted test file.');
Chris@4 150 $message = t('Only files with the following extensions are allowed:') . ' <em class="placeholder">' . $extensions . '</em>';
Chris@4 151 $this->assertRaw($message, 'Cannot upload a disallowed extension');
Chris@4 152 $this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.');
Chris@4 153
Chris@4 154 // Check that the correct hooks were called.
Chris@4 155 $this->assertFileHooksCalled(['validate']);
Chris@4 156
Chris@4 157 // Reset the hook counters.
Chris@4 158 file_test_reset();
Chris@4 159
Chris@4 160 $extensions = 'foo ' . $this->imageExtension;
Chris@4 161 // Now tell file_save_upload() to allow the extension of our test image.
Chris@4 162 $edit = [
Chris@4 163 'file_test_replace' => FILE_EXISTS_REPLACE,
Chris@4 164 'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->image->getFileUri()),
Chris@4 165 'extensions' => $extensions,
Chris@4 166 ];
Chris@4 167
Chris@4 168 $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
Chris@4 169 $this->assertResponse(200, 'Received a 200 response for posted test file.');
Chris@4 170 $this->assertNoRaw(t('Only files with the following extensions are allowed:'), 'Can upload an allowed extension.');
Chris@4 171 $this->assertRaw(t('You WIN!'), 'Found the success message.');
Chris@4 172
Chris@4 173 // Check that the correct hooks were called.
Chris@4 174 $this->assertFileHooksCalled(['validate', 'load', 'update']);
Chris@4 175
Chris@4 176 // Reset the hook counters.
Chris@4 177 file_test_reset();
Chris@4 178
Chris@4 179 // Now tell file_save_upload() to allow any extension.
Chris@4 180 $edit = [
Chris@4 181 'file_test_replace' => FILE_EXISTS_REPLACE,
Chris@4 182 'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->image->getFileUri()),
Chris@4 183 'allow_all_extensions' => TRUE,
Chris@4 184 ];
Chris@4 185 $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
Chris@4 186 $this->assertResponse(200, 'Received a 200 response for posted test file.');
Chris@4 187 $this->assertNoRaw(t('Only files with the following extensions are allowed:'), 'Can upload any extension.');
Chris@4 188 $this->assertRaw(t('You WIN!'), 'Found the success message.');
Chris@4 189
Chris@4 190 // Check that the correct hooks were called.
Chris@4 191 $this->assertFileHooksCalled(['validate', 'load', 'update']);
Chris@4 192 }
Chris@4 193
Chris@4 194 /**
Chris@4 195 * Test dangerous file handling.
Chris@4 196 */
Chris@4 197 public function testHandleDangerousFile() {
Chris@4 198 $config = $this->config('system.file');
Chris@4 199 // Allow the .php extension and make sure it gets renamed to .txt for
Chris@4 200 // safety. Also check to make sure its MIME type was changed.
Chris@4 201 $edit = [
Chris@4 202 'file_test_replace' => FILE_EXISTS_REPLACE,
Chris@4 203 'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->phpfile->uri),
Chris@4 204 'is_image_file' => FALSE,
Chris@4 205 'extensions' => 'php',
Chris@4 206 ];
Chris@4 207
Chris@4 208 $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
Chris@4 209 $this->assertResponse(200, 'Received a 200 response for posted test file.');
Chris@4 210 $message = t('For security reasons, your upload has been renamed to') . ' <em class="placeholder">' . $this->phpfile->filename . '.txt' . '</em>';
Chris@4 211 $this->assertRaw($message, 'Dangerous file was renamed.');
Chris@5 212 $this->assertSession()->pageTextContains('File name is php-2.php.txt.');
Chris@4 213 $this->assertRaw(t('File MIME type is text/plain.'), "Dangerous file's MIME type was changed.");
Chris@4 214 $this->assertRaw(t('You WIN!'), 'Found the success message.');
Chris@4 215
Chris@4 216 // Check that the correct hooks were called.
Chris@4 217 $this->assertFileHooksCalled(['validate', 'insert']);
Chris@4 218
Chris@4 219 // Ensure dangerous files are not renamed when insecure uploads is TRUE.
Chris@4 220 // Turn on insecure uploads.
Chris@4 221 $config->set('allow_insecure_uploads', 1)->save();
Chris@4 222 // Reset the hook counters.
Chris@4 223 file_test_reset();
Chris@4 224
Chris@4 225 $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
Chris@4 226 $this->assertResponse(200, 'Received a 200 response for posted test file.');
Chris@4 227 $this->assertNoRaw(t('For security reasons, your upload has been renamed'), 'Found no security message.');
Chris@5 228 $this->assertSession()->pageTextContains('File name is php-2.php.');
Chris@4 229 $this->assertRaw(t('You WIN!'), 'Found the success message.');
Chris@4 230
Chris@4 231 // Check that the correct hooks were called.
Chris@4 232 $this->assertFileHooksCalled(['validate', 'insert']);
Chris@4 233
Chris@4 234 // Turn off insecure uploads.
Chris@4 235 $config->set('allow_insecure_uploads', 0)->save();
Chris@4 236 }
Chris@4 237
Chris@4 238 /**
Chris@4 239 * Test file munge handling.
Chris@4 240 */
Chris@4 241 public function testHandleFileMunge() {
Chris@4 242 // Ensure insecure uploads are disabled for this test.
Chris@4 243 $this->config('system.file')->set('allow_insecure_uploads', 0)->save();
Chris@4 244 $this->image = file_move($this->image, $this->image->getFileUri() . '.foo.' . $this->imageExtension);
Chris@4 245
Chris@4 246 // Reset the hook counters to get rid of the 'move' we just called.
Chris@4 247 file_test_reset();
Chris@4 248
Chris@4 249 $extensions = $this->imageExtension;
Chris@4 250 $edit = [
Chris@4 251 'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->image->getFileUri()),
Chris@4 252 'extensions' => $extensions,
Chris@4 253 ];
Chris@4 254
Chris@4 255 $munged_filename = $this->image->getFilename();
Chris@4 256 $munged_filename = substr($munged_filename, 0, strrpos($munged_filename, '.'));
Chris@4 257 $munged_filename .= '_.' . $this->imageExtension;
Chris@4 258
Chris@4 259 $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
Chris@4 260 $this->assertResponse(200, 'Received a 200 response for posted test file.');
Chris@4 261 $this->assertRaw(t('For security reasons, your upload has been renamed'), 'Found security message.');
Chris@4 262 $this->assertRaw(t('File name is @filename', ['@filename' => $munged_filename]), 'File was successfully munged.');
Chris@4 263 $this->assertRaw(t('You WIN!'), 'Found the success message.');
Chris@4 264
Chris@4 265 // Check that the correct hooks were called.
Chris@4 266 $this->assertFileHooksCalled(['validate', 'insert']);
Chris@4 267
Chris@4 268 // Ensure we don't munge files if we're allowing any extension.
Chris@4 269 // Reset the hook counters.
Chris@4 270 file_test_reset();
Chris@4 271
Chris@4 272 $edit = [
Chris@4 273 'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->image->getFileUri()),
Chris@4 274 'allow_all_extensions' => TRUE,
Chris@4 275 ];
Chris@4 276
Chris@4 277 $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
Chris@4 278 $this->assertResponse(200, 'Received a 200 response for posted test file.');
Chris@4 279 $this->assertNoRaw(t('For security reasons, your upload has been renamed'), 'Found no security message.');
Chris@4 280 $this->assertRaw(t('File name is @filename', ['@filename' => $this->image->getFilename()]), 'File was not munged when allowing any extension.');
Chris@4 281 $this->assertRaw(t('You WIN!'), 'Found the success message.');
Chris@4 282
Chris@4 283 // Check that the correct hooks were called.
Chris@4 284 $this->assertFileHooksCalled(['validate', 'insert']);
Chris@4 285 }
Chris@4 286
Chris@4 287 /**
Chris@4 288 * Test renaming when uploading over a file that already exists.
Chris@4 289 */
Chris@4 290 public function testExistingRename() {
Chris@4 291 $edit = [
Chris@5 292 'file_test_replace' => FileSystemInterface::EXISTS_RENAME,
Chris@4 293 'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->image->getFileUri()),
Chris@4 294 ];
Chris@4 295 $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
Chris@4 296 $this->assertResponse(200, 'Received a 200 response for posted test file.');
Chris@4 297 $this->assertRaw(t('You WIN!'), 'Found the success message.');
Chris@5 298 $this->assertSession()->pageTextContains('File name is image-test_0.png.');
Chris@4 299
Chris@4 300 // Check that the correct hooks were called.
Chris@4 301 $this->assertFileHooksCalled(['validate', 'insert']);
Chris@4 302 }
Chris@4 303
Chris@4 304 /**
Chris@4 305 * Test replacement when uploading over a file that already exists.
Chris@4 306 */
Chris@4 307 public function testExistingReplace() {
Chris@4 308 $edit = [
Chris@4 309 'file_test_replace' => FILE_EXISTS_REPLACE,
Chris@4 310 'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->image->getFileUri()),
Chris@4 311 ];
Chris@4 312 $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
Chris@4 313 $this->assertResponse(200, 'Received a 200 response for posted test file.');
Chris@4 314 $this->assertRaw(t('You WIN!'), 'Found the success message.');
Chris@5 315 $this->assertSession()->pageTextContains('File name is image-test.png.');
Chris@4 316
Chris@4 317 // Check that the correct hooks were called.
Chris@4 318 $this->assertFileHooksCalled(['validate', 'load', 'update']);
Chris@4 319 }
Chris@4 320
Chris@4 321 /**
Chris@4 322 * Test for failure when uploading over a file that already exists.
Chris@4 323 */
Chris@4 324 public function testExistingError() {
Chris@4 325 $edit = [
Chris@4 326 'file_test_replace' => FILE_EXISTS_ERROR,
Chris@4 327 'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->image->getFileUri()),
Chris@4 328 ];
Chris@4 329 $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
Chris@4 330 $this->assertResponse(200, 'Received a 200 response for posted test file.');
Chris@4 331 $this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.');
Chris@4 332
Chris@4 333 // Check that the no hooks were called while failing.
Chris@4 334 $this->assertFileHooksCalled([]);
Chris@4 335 }
Chris@4 336
Chris@4 337 /**
Chris@4 338 * Test for no failures when not uploading a file.
Chris@4 339 */
Chris@4 340 public function testNoUpload() {
Chris@4 341 $this->drupalPostForm('file-test/upload', [], t('Submit'));
Chris@4 342 $this->assertNoRaw(t('Epic upload FAIL!'), 'Failure message not found.');
Chris@4 343 }
Chris@4 344
Chris@4 345 /**
Chris@4 346 * Tests for log entry on failing destination.
Chris@4 347 */
Chris@4 348 public function testDrupalMovingUploadedFileError() {
Chris@4 349 // Create a directory and make it not writable.
Chris@4 350 $test_directory = 'test_drupal_move_uploaded_file_fail';
Chris@5 351 /** @var \Drupal\Core\File\FileSystemInterface $file_system */
Chris@5 352 $file_system = \Drupal::service('file_system');
Chris@5 353 $file_system->mkdir('temporary://' . $test_directory, 0000);
Chris@4 354 $this->assertTrue(is_dir('temporary://' . $test_directory));
Chris@4 355
Chris@4 356 $edit = [
Chris@4 357 'file_subdir' => $test_directory,
Chris@5 358 'files[file_test_upload]' => $file_system->realpath($this->image->getFileUri()),
Chris@4 359 ];
Chris@4 360
Chris@4 361 \Drupal::state()->set('file_test.disable_error_collection', TRUE);
Chris@4 362 $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
Chris@4 363 $this->assertResponse(200, 'Received a 200 response for posted test file.');
Chris@4 364 $this->assertRaw(t('File upload error. Could not move uploaded file.'), 'Found the failure message.');
Chris@4 365 $this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.');
Chris@4 366
Chris@4 367 // Uploading failed. Now check the log.
Chris@4 368 $this->drupalGet('admin/reports/dblog');
Chris@4 369 $this->assertResponse(200);
Chris@4 370 $this->assertRaw(t('Upload error. Could not move uploaded file @file to destination @destination.', [
Chris@4 371 '@file' => $this->image->getFilename(),
Chris@4 372 '@destination' => 'temporary://' . $test_directory . '/' . $this->image->getFilename(),
Chris@4 373 ]), 'Found upload error log entry.');
Chris@4 374 }
Chris@4 375
Chris@5 376 /**
Chris@5 377 * Tests that filenames containing invalid UTF-8 are rejected.
Chris@5 378 */
Chris@5 379 public function testInvalidUtf8FilenameUpload() {
Chris@5 380 $this->drupalGet('file-test/upload');
Chris@5 381
Chris@5 382 // Filename containing invalid UTF-8.
Chris@5 383 $filename = "x\xc0xx.gif";
Chris@5 384
Chris@5 385 $page = $this->getSession()->getPage();
Chris@5 386 $data = [
Chris@5 387 'multipart' => [
Chris@5 388 [
Chris@5 389 'name' => 'file_test_replace',
Chris@5 390 'contents' => FileSystemInterface::EXISTS_RENAME,
Chris@5 391 ],
Chris@5 392 [
Chris@5 393 'name' => 'form_id',
Chris@5 394 'contents' => '_file_test_form',
Chris@5 395 ],
Chris@5 396 [
Chris@5 397 'name' => 'form_build_id',
Chris@5 398 'contents' => $page->find('hidden_field_selector', ['hidden_field', 'form_build_id'])->getAttribute('value'),
Chris@5 399 ],
Chris@5 400 [
Chris@5 401 'name' => 'form_token',
Chris@5 402 'contents' => $page->find('hidden_field_selector', ['hidden_field', 'form_token'])->getAttribute('value'),
Chris@5 403 ],
Chris@5 404 [
Chris@5 405 'name' => 'op',
Chris@5 406 'contents' => 'Submit',
Chris@5 407 ],
Chris@5 408 [
Chris@5 409 'name' => 'files[file_test_upload]',
Chris@5 410 'contents' => 'Test content',
Chris@5 411 'filename' => $filename,
Chris@5 412 ],
Chris@5 413 ],
Chris@5 414 'cookies' => $this->getSessionCookies(),
Chris@5 415 'http_errors' => FALSE,
Chris@5 416 ];
Chris@5 417
Chris@5 418 $this->assertFileNotExists('temporary://' . $filename);
Chris@5 419 // Use Guzzle's HTTP client directly so we can POST files without having to
Chris@5 420 // write them to disk. Not all filesystem support writing files with invalid
Chris@5 421 // UTF-8 filenames.
Chris@5 422 $response = $this->getHttpClient()->request('POST', Url::fromUri('base:file-test/upload')->setAbsolute()->toString(), $data);
Chris@5 423
Chris@5 424 $content = (string) $response->getBody();
Chris@5 425 $this->htmlOutput($content);
Chris@5 426 $error_text = new FormattableMarkup('The file %filename could not be uploaded because the name is invalid.', ['%filename' => $filename]);
Chris@5 427 $this->assertContains((string) $error_text, $content);
Chris@5 428 $this->assertContains('Epic upload FAIL!', $content);
Chris@5 429 $this->assertFileNotExists('temporary://' . $filename);
Chris@5 430 }
Chris@5 431
Chris@4 432 }