From 9a4332c550741a8e4ec4d1adbf24fbf44e5004ec Mon Sep 17 00:00:00 2001
From: drugan <drugan@1578644.no-reply.drupal.org>
Date: Thu, 2 Mar 2017 20:17:45 +0200
Subject: [PATCH] Issue #2745123 by drugan, mondrake, Mile23, benjifisher,
 penyaskito, hgoto, othermachines, Jaypan, alexpott:
 Simpletest module crashes on cleanup, uninstall

---
 core/modules/simpletest/simpletest.module          |   61 ++++++++++++--
 .../src/Tests/UiCleanTemporaryDirectoriesTest.php  |   85 ++++++++++++++++++++
 .../testing_page_test/testing_page_test.info.yml   |    6 ++
 .../testing_page_test/testing_page_test.module     |   25 ++++++
 4 files changed, 170 insertions(+), 7 deletions(-)
 create mode 100644 core/modules/simpletest/src/Tests/UiCleanTemporaryDirectoriesTest.php
 create mode 100644 core/modules/simpletest/tests/modules/testing_page_test/testing_page_test.info.yml
 create mode 100644 core/modules/simpletest/tests/modules/testing_page_test/testing_page_test.module

diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index c9eed3b..4c4d630 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -684,16 +684,61 @@ function simpletest_clean_database() {
 
 /**
  * Finds all leftover temporary directories and removes them.
+ *
+ * @param string $directory
+ *   (optional) The relative path to directory of a particular test site. Must
+ *   to be of the following pattern: sites/simpletest/12345678. If ommited, all
+ *   the test sites' directories found in sites/simpletest will be removed.
+ *
+ * @return null|bool
+ *   Returns TRUE if all attempted to remove directories are removed, FALSE if
+ *   at least one directory was not removed successfully, NULL if there is
+ *   nothing to remove or the directory is not eligible for removing.
+ *
+ * @see file_unmanaged_delete()
+ * @see http://php.net/manual/en/function.unlink.php
  */
-function simpletest_clean_temporary_directories() {
+function simpletest_clean_temporary_directories($directory = NULL) {
+  $directories = [];
   $count = 0;
-  if (is_dir(DRUPAL_ROOT . '/sites/simpletest')) {
-    $files = scandir(DRUPAL_ROOT . '/sites/simpletest');
-    foreach ($files as $file) {
+  $result = NULL;
+  $simpletest_root = \Drupal::root() . '/sites/simpletest/';
+
+  if (is_dir($simpletest_root)) {
+    // If the $directory is not valid string or NULL then get the type of it for
+    // debugging purposes in the error message below.
+    $path = is_string($directory) && !empty($directory) ? $directory : gettype($directory);
+    // Do not recognize any, except expected directory pattern as wrong
+    // directory being passed accidentally may cause catastrophic consequences.
+    preg_match('/^(sites\/simpletest\/)(.*)/', $path, $matches);
+
+    if (!empty($matches[2]) && is_dir($simpletest_root . $matches[2])) {
+      $directories[] = $matches[2];
+    }
+    elseif ($directory === NULL) {
+      $directories = scandir($simpletest_root);
+    }
+    else {
+      drupal_set_message(t('The %path is not eligible for removing in %func().', ['%path' => $path, '%func' => __FUNCTION__]));
+    }
+
+    foreach ($directories as $file) {
       if ($file[0] != '.') {
-        $path = DRUPAL_ROOT . '/sites/simpletest/' . $file;
-        file_unmanaged_delete_recursive($path, array('Drupal\simpletest\TestBase', 'filePreDeleteCallback'));
-        $count++;
+        $path = $simpletest_root . $file;
+        // When the webserver runs with the same system user as the test
+        // runner, we can make read-only files writable again. If not, chmod
+        // will fail while the file deletion still works if file permissions
+        // have been configured correctly. Thus, we ignore any chmod errors.
+        $deleted = file_unmanaged_delete_recursive($path, function ($any_path) {
+          @chmod($any_path, 0700);
+        });
+        $result = $result === FALSE ? $result : $deleted;
+        if ($deleted) {
+          $count++;
+        }
+        else {
+          drupal_set_message(t('This directory is failed to be removed: @path.', ['@path' => $path]));
+        }
       }
     }
   }
@@ -704,6 +749,8 @@ function simpletest_clean_temporary_directories() {
   else {
     drupal_set_message(t('No temporary directories to remove.'));
   }
+
+  return $result;
 }
 
 /**
diff --git a/core/modules/simpletest/src/Tests/UiCleanTemporaryDirectoriesTest.php b/core/modules/simpletest/src/Tests/UiCleanTemporaryDirectoriesTest.php
new file mode 100644
index 0000000..77509f7
--- /dev/null
+++ b/core/modules/simpletest/src/Tests/UiCleanTemporaryDirectoriesTest.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\simpletest\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests removing the Simpletest temporary directories through UI.
+ *
+ * @group simpletest
+ */
+class UiCleanTemporaryDirectoriesTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('simpletest', 'testing_page_test',);
+
+  protected function setUp() {
+    parent::setUp();
+    // Create and log in an admin user.
+    $this->drupalLogin($this->drupalCreateUser(array('administer unit tests')));
+  }
+
+  /**
+   * Tests removing a temporary directory through the UI.
+   */
+  public function testCleanTemporaryDirectories() {
+    $simpletest = \Drupal::root() . '/sites/simpletest';
+    $test_site_directory = basename($this->siteDirectory);
+    $clean_environment = t('Clean environment');
+
+    $this->assertNull(simpletest_clean_temporary_directories('sites/simpletest/test_site_directory'), 'The sites/simpletest/test_site_directory folder does not exist.');
+
+    $this->recurseCopyTestSiteDirectory("$simpletest/$test_site_directory", "$simpletest/test_site_directory");
+    $original = scandir("$simpletest/$test_site_directory");
+    $copied = scandir("$simpletest/test_site_directory");
+
+    $this->assertEqual($original, $copied, 'The original and copied test site directories exist in the sites/simpletest folder and have the same structure.');
+
+    $this->drupalGet('admin/config/development/testing');
+
+    $this->assertFieldByXPath('//input', $clean_environment, 'Displayed the "Clean environment" button.');
+
+    $this->drupalPostForm(NULL, [], $clean_environment);
+
+    $this->assertText(t('Removed @count temporary directory.', ['@count' => 1]), 'Displayed a message: "Removed 1 temporary directory.".');
+    $this->assertNoText(t('This directory is failed to be removed:'), 'The message "This directory is failed to be removed:" is not displayed.');
+    $this->assertNull(simpletest_clean_temporary_directories('sites/simpletest/test_site_directory'), 'The sites/simpletest/test_site_directory folder does not exist any more.');
+  }
+
+  /**
+   * Creates the exact copy of the test site temporary directory.
+   *
+   * The permissions on the copied directory structure are intentionally lowered
+   * in order to check if the chmod() works as as expected in the
+   * simpletest_clean_temporary_directories() function.
+   */
+  protected function recurseCopyTestSiteDirectory($source, $destination) {
+    $directory = opendir($source);
+    if (!mkdir($destination)) {
+      throw new \Exception("The $destination directory cannot be created.");
+    }
+
+    while (($file = readdir($directory)) !== FALSE) {
+      if ($file != '.' && $file != '..') {
+        if (is_dir("$source/$file")) {
+          $this->recurseCopyTestSiteDirectory("$source/$file", "$destination/$file");
+        }
+        else {
+          if (!copy("$source/$file", "$destination/$file") || !chmod("$destination/$file", 0444)) {
+            throw new \Exception("The $file cannot be copied to $destination with 0444 permissions.");
+          }
+        }
+      }
+    }
+    closedir($directory);
+    if (!chmod($destination, 0555)) {
+      throw new \Exception("The permissions 0555 cannot be set on the $destination directory.");
+    }
+  }
+
+}
diff --git a/core/modules/simpletest/tests/modules/testing_page_test/testing_page_test.info.yml b/core/modules/simpletest/tests/modules/testing_page_test/testing_page_test.info.yml
new file mode 100644
index 0000000..33773ba
--- /dev/null
+++ b/core/modules/simpletest/tests/modules/testing_page_test/testing_page_test.info.yml
@@ -0,0 +1,6 @@
+name: 'Testing page test'
+type: module
+description: 'Provides customizations for the simpletest testing page.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/simpletest/tests/modules/testing_page_test/testing_page_test.module b/core/modules/simpletest/tests/modules/testing_page_test/testing_page_test.module
new file mode 100644
index 0000000..f52be67
--- /dev/null
+++ b/core/modules/simpletest/tests/modules/testing_page_test/testing_page_test.module
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @file
+ * Defines overrides for the simpletest testing page.
+ */
+
+/**
+ * Implements hook_form_alter().
+ */
+function testing_page_test_form_alter(&$form, $form_state, $form_id) {
+  if ($form_id == 'simpletest_test_form') {
+    $form['clean']['op']['#submit'] = ['testing_page_test_submit'];
+  }
+}
+
+/**
+ * Removes test site directory when the 'Clean environment' button is pressed.
+ */
+function testing_page_test_submit() {
+  $cleaned = simpletest_clean_temporary_directories('sites/simpletest/test_site_directory');
+  if ($cleaned !== TRUE) {
+    throw new \Exception("The sites/simpletest/test_site_directory cannot be removed because of unsufficient permissions.");
+  }
+}
-- 
1.7.9.5

