From be3dab8dfcf682393ee4a5f4e275529cf304f71a Mon Sep 17 00:00:00 2001
From: Darren Oh <darrenoh@30772.no-reply.drupal.org>
Date: Tue, 20 Feb 2018 17:38:59 -0500
Subject: [PATCH] Issue #1561866 by Darren Oh, mohit1604: Add support for
 built-in PHP session upload progress

---
 core/lib/Drupal/Component/Utility/Crypt.php   |  12 +-
 .../Drupal/Core/Session/SessionManager.php    |  50 +++++++-
 core/modules/file/file.es6.js                 |  10 +-
 core/modules/file/file.install                | 113 ++++++++++++------
 core/modules/file/file.js                     |   2 +-
 core/modules/file/file.module                 |   6 +-
 .../Controller/FileWidgetAjaxController.php   |  67 ++++++++++-
 core/modules/file/src/Element/ManagedFile.php |  10 ++
 8 files changed, 220 insertions(+), 50 deletions(-)

diff --git a/core/lib/Drupal/Component/Utility/Crypt.php b/core/lib/Drupal/Component/Utility/Crypt.php
index 6ebdc4aac8..583544eb01 100644
--- a/core/lib/Drupal/Component/Utility/Crypt.php
+++ b/core/lib/Drupal/Component/Utility/Crypt.php
@@ -154,18 +154,22 @@ public static function hashEquals($known_string, $user_string) {
   }
 
   /**
-   * Returns a URL-safe, base64 encoded string of highly randomized bytes.
+   * Returns a base64-encoded string of highly randomized bytes.
    *
-   * @param $count
+   * @param int $count
    *   The number of random bytes to fetch and base64 encode.
    *
+   * @param array|string $replace
+   *   Optional. An array or string to pass to str_replace(). This value will
+   *   replace "+", "/", and "=". Defaults to URL-safe characters.
+   *
    * @return string
    *   The base64 encoded result will have a length of up to 4 * $count.
    *
    * @see \Drupal\Component\Utility\Crypt::randomBytes()
    */
-  public static function randomBytesBase64($count = 32) {
-    return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode(static::randomBytes($count)));
+  public static function randomBytesBase64($count = 32, $replace = ['-', '_']) {
+    return str_replace(['+', '/', '='], $replace, base64_encode(static::randomBytes($count)));
   }
 
 }
diff --git a/core/lib/Drupal/Core/Session/SessionManager.php b/core/lib/Drupal/Core/Session/SessionManager.php
index 431959e995..2ef206be64 100644
--- a/core/lib/Drupal/Core/Session/SessionManager.php
+++ b/core/lib/Drupal/Core/Session/SessionManager.php
@@ -67,6 +67,20 @@ class SessionManager extends NativeSessionStorage implements SessionManagerInter
    */
   protected $writeSafeHandler;
 
+  /**
+   * The PHP session handler.
+   *
+   * @var string
+   */
+  protected $PHPSessionHandler;
+
+  /**
+   * The PHP session name.
+   *
+   * @var string
+   */
+  protected $PHPSessionName;
+
   /**
    * Constructs a new session manager instance.
    *
@@ -88,6 +102,11 @@ public function __construct(RequestStack $request_stack, Connection $connection,
     $this->requestStack = $request_stack;
     $this->connection = $connection;
 
+    // Save the PHP session info in case we need to retrieve session data that
+    // PHP stores before Drupal starts.
+    $this->PHPSessionHandler = session_module_name();
+    $this->PHPSessionName = session_name();
+
     parent::__construct($options, $handler, $metadata_bag);
 
     // @todo When not using the Symfony Session object, the list of bags in the
@@ -119,16 +138,16 @@ public function start() {
     }
 
     if (empty($result)) {
-      // Randomly generate a session identifier for this request. This is
-      // necessary because \Drupal\Core\TempStore\SharedTempStoreFactory::get()
-      // wants to know the future session ID of a lazily started session in
-      // advance.
+      // Randomly generate a valid PHP session identifier for this request. This
+      // is necessary because
+      // \Drupal\Core\TempStore\SharedTempStoreFactory::get() wants to know the
+      // future session ID of a lazily started session in advance.
       //
       // @todo: With current versions of PHP there is little reason to generate
       //   the session id from within application code. Consider using the
       //   default php session id instead of generating a custom one:
       //   https://www.drupal.org/node/2238561
-      $this->setId(Crypt::randomBytesBase64());
+      $this->setId(Crypt::randomBytesBase64(32, [',', '-']));
 
       // Initialize the session global and attach the Symfony session bags.
       $_SESSION = [];
@@ -226,7 +245,8 @@ public function regenerate($destroy = FALSE, $lifetime = NULL) {
       // Ensure the session is reloaded correctly.
       $this->startedLazy = TRUE;
     }
-    session_id(Crypt::randomBytesBase64());
+    // Generate a valid PHP session identifier.
+    session_id(Crypt::randomBytesBase64(32, [',', '-']));
 
     $this->getMetadataBag()->clearCsrfTokenSeed();
 
@@ -277,6 +297,24 @@ public function setWriteSafeHandler(WriteSafeSessionHandlerInterface $handler) {
     $this->writeSafeHandler = $handler;
   }
 
+  /**
+   * Returns the built-in PHP session handler.
+   *
+   * @return string
+   */
+  public function getPHPSessionHandler() {
+    return $this->PHPSessionHandler;
+  }
+
+  /**
+   * Returns the built-in PHP session name.
+   *
+   * @return string
+   */
+  public function getPHPSessionName() {
+    return $this->PHPSessionName;
+  }
+
   /**
    * Returns whether the current PHP process runs on CLI.
    *
diff --git a/core/modules/file/file.es6.js b/core/modules/file/file.es6.js
index dc8a807424..7406e468b0 100644
--- a/core/modules/file/file.es6.js
+++ b/core/modules/file/file.es6.js
@@ -261,13 +261,17 @@
       if ($progressId.length) {
         const originalName = $progressId.attr('name');
 
-        // Replace the name with the required identifier.
+        // Replace the name with the required identifier to ensure that this
+        // element sends its upload identifier. The upload identifier is the
+        // last string in the name that does not contain square brackets.
         $progressId.attr(
           'name',
-          originalName.match(/APC_UPLOAD_PROGRESS|UPLOAD_IDENTIFIER/)[0],
+          originalName.match(/[^\[\]]+(?!.*[^\[\]]+)/)[0],
         );
 
-        // Restore the original name after the upload begins.
+        // Restore the original name after the upload begins to prevent this
+        // element from sending its upload identifier with another element’s
+        // upload.
         setTimeout(() => {
           $progressId.attr('name', originalName);
         }, 1000);
diff --git a/core/modules/file/file.install b/core/modules/file/file.install
index 5140a2ef49..f79fc8f4cc 100644
--- a/core/modules/file/file.install
+++ b/core/modules/file/file.install
@@ -70,49 +70,94 @@ function file_requirements($phase) {
   // Check the server's ability to indicate upload progress.
   if ($phase == 'runtime') {
     $description = NULL;
+    $severity = NULL;
     $implementation = file_progress_implementation();
-    $server_software = \Drupal::request()->server->get('SERVER_SOFTWARE');
-
-    // Test the web server identity.
-    if (preg_match("/Nginx/i", $server_software)) {
-      $is_nginx = TRUE;
-      $is_apache = FALSE;
-      $fastcgi = FALSE;
-    }
-    elseif (preg_match("/Apache/i", $server_software)) {
-      $is_nginx = FALSE;
-      $is_apache = TRUE;
-      $fastcgi = strpos($server_software, 'mod_fastcgi') !== FALSE || strpos($server_software, 'mod_fcgi') !== FALSE;
+    if ($implementation == 'session') {
+      $value = t('Enabled (<a href="http://php.net/manual/en/session.upload-progress.php">PHP session upload progress</a>)');
+      $session_manager = \Drupal::service('session_manager');
+      if (!method_exists($session_manager, 'getPHPSessionName') || $session_manager->getPHPSessionName() != session_name()) {
+        $description = [
+          [
+            '#markup' => t('The <a href=":url">PHP session name</a> must be set to %session_name before Drupal starts in order to store file upload progress. It is currently set to %php_session_name.', [
+              ':url' => 'http://php.net/manual/en/session.configuration.php#ini.session.name',
+              '%session_name' => session_name(),
+              '%php_session_name' => $session_manager->getPHPSessionName(),
+            ]),
+          ],
+        ];
+        // Tell users how to configure the PHP session name, if we can.
+        if (substr(PHP_SAPI, 0, 6) == 'apache') {
+          // Settings can be configured in a .htaccess file if PHP is running as
+          // an Apache module.
+          $description[] = [
+            '#prefix' => ' ',
+            '#markup' => t('Add <code>php_value session.name @session_name</code> to the PHP @php-version settings of the .htaccess file in your Drupal root folder.', [
+              '@session_name' => session_name(),
+              '@php-version' => PHP_MAJOR_VERSION,
+            ]),
+          ];
+        }
+        elseif (substr(PHP_SAPI, 0, 3) == 'cgi') {
+          // PHP running as CGI cannot get settings from .htaccess files, but
+          // since 5.3.0 it supports .user.ini files.
+          $description[] = [
+            '#prefix' => ' ',
+            '#markup' => t('Add <code>session.name=@session_name</code> to a .<a href=":url">user.ini</a> file in your Drupal root folder.', [
+              '@session_name' => session_name(),
+              ':url' => 'http://php.net/manual/en/configuration.file.per-user.php',
+            ]),
+          ];
+        }
+      }
+      if (method_exists($session_manager, 'getPHPSessionName') && $session_manager->getPHPSessionName() != session_name()) {
+        $severity = REQUIREMENT_WARNING;
+      }
     }
     else {
-      $is_nginx = FALSE;
-      $is_apache = FALSE;
-      $fastcgi = FALSE;
-    }
+      $server_software = \Drupal::request()->server->get('SERVER_SOFTWARE');
 
-    if (!$is_apache && !$is_nginx) {
-      $value = t('Not enabled');
-      $description = t('Your server is not capable of displaying file upload progress. File upload progress requires an Apache server running PHP with mod_php or Nginx with PHP-FPM.');
-    }
-    elseif ($fastcgi) {
-      $value = t('Not enabled');
-      $description = t('Your server is not capable of displaying file upload progress. File upload progress requires PHP be run with mod_php or PHP-FPM and not as FastCGI.');
-    }
-    elseif (!$implementation) {
-      $value = t('Not enabled');
-      $description = t('Your server is capable of displaying file upload progress, but does not have the required libraries. It is recommended to install the <a href="http://pecl.php.net/package/uploadprogress">PECL uploadprogress library</a>.');
-    }
-    elseif ($implementation == 'apc') {
-      $value = t('Enabled (<a href="http://php.net/manual/apcu.configuration.php#ini.apcu.rfc1867">APC RFC1867</a>)');
-      $description = t('Your server is capable of displaying file upload progress using APC RFC1867. Note that only one upload at a time is supported. It is recommended to use the <a href="http://pecl.php.net/package/uploadprogress">PECL uploadprogress library</a> if possible.');
-    }
-    elseif ($implementation == 'uploadprogress') {
-      $value = t('Enabled (<a href="http://pecl.php.net/package/uploadprogress">PECL uploadprogress</a>)');
+      // Test the web server identity.
+      if (preg_match("/Nginx/i", $server_software)) {
+        $is_nginx = TRUE;
+        $is_apache = FALSE;
+        $fastcgi = FALSE;
+      }
+      elseif (preg_match("/Apache/i", $server_software)) {
+        $is_nginx = FALSE;
+        $is_apache = TRUE;
+        $fastcgi = strpos($server_software, 'mod_fastcgi') !== FALSE || strpos($server_software, 'mod_fcgi') !== FALSE;
+      }
+      else {
+        $is_nginx = FALSE;
+        $is_apache = FALSE;
+        $fastcgi = FALSE;
+      }
+
+      if (!$is_apache && !$is_nginx) {
+        $value = t('Not enabled');
+        $description = t('Your server is not capable of displaying file upload progress. File upload progress requires an Apache server running PHP with mod_php or Nginx with PHP-FPM.');
+      }
+      elseif ($fastcgi) {
+        $value = t('Not enabled');
+        $description = t('Your server is not capable of displaying file upload progress. File upload progress requires PHP be run with mod_php or PHP-FPM and not as FastCGI.');
+      }
+      elseif (!$implementation) {
+        $value = t('Not enabled');
+        $description = t('Your server is capable of displaying file upload progress, but does not have the required libraries. It is recommended to install the <a href="http://pecl.php.net/package/uploadprogress">PECL uploadprogress library</a>.');
+      }
+      elseif ($implementation == 'apc') {
+        $value = t('Enabled (<a href="http://php.net/manual/apcu.configuration.php#ini.apcu.rfc1867">APC RFC1867</a>)');
+        $description = t('Your server is capable of displaying file upload progress using APC RFC1867. Note that only one upload at a time is supported. It is recommended to use the <a href="http://pecl.php.net/package/uploadprogress">PECL uploadprogress library</a> if possible.');
+      }
+      elseif ($implementation == 'uploadprogress') {
+        $value = t('Enabled (<a href="http://pecl.php.net/package/uploadprogress">PECL uploadprogress</a>)');
+      }
     }
     $requirements['file_progress'] = [
       'title' => t('Upload progress'),
       'value' => $value,
       'description' => $description,
+      'severity' => $severity,
     ];
   }
 
diff --git a/core/modules/file/file.js b/core/modules/file/file.js
index 79aa8288c0..2f7b67a9bc 100644
--- a/core/modules/file/file.js
+++ b/core/modules/file/file.js
@@ -115,7 +115,7 @@
       if ($progressId.length) {
         var originalName = $progressId.attr('name');
 
-        $progressId.attr('name', originalName.match(/APC_UPLOAD_PROGRESS|UPLOAD_IDENTIFIER/)[0]);
+        $progressId.attr('name', originalName.match(/[^\[\]]+(?!.*[^\[\]]+)/)[0]);
 
         setTimeout(function () {
           $progressId.attr('name', originalName);
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index 92299e80ce..90d98acc05 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -1129,13 +1129,17 @@ function file_progress_implementation() {
     $implementation = FALSE;
 
     // We prefer the PECL extension uploadprogress because it supports multiple
-    // simultaneous uploads. APCu only supports one at a time.
+    // simultaneous uploads. APCu only supports one at a time. PHP session
+    // upload progress only works if the other two extensions are not enabled.
     if (extension_loaded('uploadprogress')) {
       $implementation = 'uploadprogress';
     }
     elseif (version_compare(PHP_VERSION, '7', '<') && extension_loaded('apc') && ini_get('apc.rfc1867')) {
       $implementation = 'apc';
     }
+    elseif (ini_get('session.upload_progress.enabled')) {
+      $implementation = 'session';
+    }
   }
   return $implementation;
 }
diff --git a/core/modules/file/src/Controller/FileWidgetAjaxController.php b/core/modules/file/src/Controller/FileWidgetAjaxController.php
index c958750443..0addaebb5c 100644
--- a/core/modules/file/src/Controller/FileWidgetAjaxController.php
+++ b/core/modules/file/src/Controller/FileWidgetAjaxController.php
@@ -3,11 +3,43 @@
 namespace Drupal\file\Controller;
 
 use Symfony\Component\HttpFoundation\JsonResponse;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Session\SessionManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Defines a controller to respond to file widget AJAX requests.
  */
-class FileWidgetAjaxController {
+class FileWidgetAjaxController implements ContainerInjectionInterface {
+
+  /**
+   * The PHP session handler.
+   *
+   * @var string
+   */
+  protected $PHPSessionHandler;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static($container->get('session_manager'));
+  }
+
+  /**
+   * Constructs a FileWidgetAjaxController object.
+   *
+   * @param \Drupal\Core\Session\SessionManagerInterface $session_manager
+   *   The session manager.
+   */
+  public function __construct(SessionManagerInterface $session_manager) {
+    if (method_exists($session_manager, 'getPHPSessionHandler')) {
+      $this->PHPSessionHandler = $session_manager->getPHPSessionHandler();
+    }
+    else {
+      $this->PHPSessionHandler = 'files';
+    }
+  }
 
   /**
    * Returns the progress status for a file upload process.
@@ -39,6 +71,39 @@ public function progress($key) {
         $progress['percentage'] = round(100 * $status['current'] / $status['total']);
       }
     }
+    elseif ($implementation == 'session') {
+      // PHP saves file upload data to the session before Drupal starts.  This
+      // works only if the default session name has been configured to match the
+      // one generated by Drupal so PHP can recognize the browser’s cookie
+      // before Drupal starts.
+
+      // Stop the Drupal session temporarily. Save the session handler and data
+      // so we can resume it later.
+      $save_handler = session_module_name();
+      $session = $_SESSION;
+      session_abort();
+
+      // Get upload status from the PHP session.
+      $status = [];
+      session_module_name($this->PHPSessionHandler);
+      // Fail silently if the PHP session handler generates an error.
+      @session_start();
+      $prefix = ini_get('session.upload_progress.prefix');
+      if (isset($_SESSION[$prefix . $key])) {
+        $status = $_SESSION[$prefix . $key];
+      }
+
+      // Stop the PHP session and resume the Drupal session.
+      session_abort();
+      session_module_name($save_handler);
+      session_start();
+      $_SESSION = $session;
+
+      if (isset($status['bytes_processed']) && !empty($status['content_length'])) {
+        $progress['message'] = t('Uploading... (@current of @total)', ['@current' => format_size($status['bytes_processed']), '@total' => format_size($status['content_length'])]);
+        $progress['percentage'] = round(100 * $status['bytes_processed'] / $status['content_length']);
+      }
+    }
 
     return new JsonResponse($progress);
   }
diff --git a/core/modules/file/src/Element/ManagedFile.php b/core/modules/file/src/Element/ManagedFile.php
index b26118916e..f81c44e410 100644
--- a/core/modules/file/src/Element/ManagedFile.php
+++ b/core/modules/file/src/Element/ManagedFile.php
@@ -296,6 +296,16 @@ public static function processManagedFile(&$element, FormStateInterface $form_st
           '#weight' => -20,
         ];
       }
+      elseif ($implementation == 'session') {
+        $element[ini_get('session.upload_progress.name')] = [
+          '#type' => 'hidden',
+          '#value' => $upload_progress_key,
+          '#attributes' => ['class' => ['file-progress']],
+          // Uploadprogress extension requires this field to be at the top of
+          // the form.
+          '#weight' => -20,
+        ];
+      }
 
       // Add the upload progress callback.
       $element['upload_button']['#ajax']['progress']['url'] = Url::fromRoute('file.ajax_progress', ['key' => $upload_progress_key]);
-- 
2.17.2 (Apple Git-113)

