From 8885f37ea09db587dacc9e3344eac0e11a524e4d 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: Add support for built-in PHP
 session upload progress

---
 core/lib/Drupal/Core/Session/SessionManager.php    | 15 +++++
 core/modules/file/file.es6.js                      |  5 +-
 core/modules/file/file.install                     |  7 +++
 core/modules/file/file.js                          |  2 +-
 core/modules/file/file.module                      |  6 +-
 .../src/Controller/FileWidgetAjaxController.php    | 68 +++++++++++++++++++++-
 core/modules/file/src/Element/ManagedFile.php      | 10 ++++
 7 files changed, 108 insertions(+), 5 deletions(-)

diff --git a/core/lib/Drupal/Core/Session/SessionManager.php b/core/lib/Drupal/Core/Session/SessionManager.php
index 607103109d..34f1f3cd2c 100644
--- a/core/lib/Drupal/Core/Session/SessionManager.php
+++ b/core/lib/Drupal/Core/Session/SessionManager.php
@@ -67,6 +67,13 @@ class SessionManager extends NativeSessionStorage implements SessionManagerInter
    */
   protected $writeSafeHandler;
 
+  /**
+   * The PHP session handler.
+   *
+   * @var string
+   */
+  protected $PHPSessionHandler;
+
   /**
    * Constructs a new session manager instance.
    *
@@ -88,6 +95,10 @@ public function __construct(RequestStack $request_stack, Connection $connection,
     $this->requestStack = $request_stack;
     $this->connection = $connection;
 
+    // Save the PHP session handler in case we need to retrieve session data
+    // that PHP stores before Drupal starts the session.
+    $this->PHPSessionHandler = session_module_name();
+
     parent::__construct($options, $handler, $metadata_bag);
 
     // @todo When not using the Symfony Session object, the list of bags in the
@@ -273,6 +284,10 @@ public function setWriteSafeHandler(WriteSafeSessionHandlerInterface $handler) {
     $this->writeSafeHandler = $handler;
   }
 
+  public function getPHPSessionHandler() {
+    return $this->PHPSessionHandler;
+  }
+
   /**
    * 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 9ab5dc2da3..cddea2ccdd 100644
--- a/core/modules/file/file.es6.js
+++ b/core/modules/file/file.es6.js
@@ -222,8 +222,9 @@
       if ($progressId.length) {
         const originalName = $progressId.attr('name');
 
-        // Replace the name with the required identifier.
-        $progressId.attr('name', originalName.match(/APC_UPLOAD_PROGRESS|UPLOAD_IDENTIFIER/)[0]);
+        // Replace the name with the last string in the name that does not
+        // contain square brackets.
+        $progressId.attr('name', originalName.match(/[^\[\]]+(?!.*[^\[\]]+)/)[0]);
 
         // Restore the original name after the upload begins.
         setTimeout(() => {
diff --git a/core/modules/file/file.install b/core/modules/file/file.install
index 6274efc7c9..b4ad360a6b 100644
--- a/core/modules/file/file.install
+++ b/core/modules/file/file.install
@@ -109,6 +109,13 @@ function file_requirements($phase) {
     elseif ($implementation == 'uploadprogress') {
       $value = t('Enabled (<a href="http://pecl.php.net/package/uploadprogress">PECL uploadprogress</a>)');
     }
+    elseif ($implementation == 'session') {
+      $value = t('Enabled (<a href="http://php.net/manual/en/session.upload-progress.php">PHP session upload progress</a>)');
+      $description = t('The built-in PHP session handler stores file upload progress if the <a href="!url">PHP session name</a> is set to %session_name before Drupal starts.', array(
+        '!url' => 'http://php.net/manual/en/session.configuration.php#ini.session.name',
+        '%session_name' => session_name(),
+      ));
+    }
     $requirements['file_progress'] = [
       'title' => t('Upload progress'),
       'value' => $value,
diff --git a/core/modules/file/file.js b/core/modules/file/file.js
index 4d51bb0fa0..78df2840ce 100644
--- a/core/modules/file/file.js
+++ b/core/modules/file/file.js
@@ -116,7 +116,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 be1e136958..3e4fb43cbe 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -1075,13 +1075,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 twe 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..2fcd24be05 100644
--- a/core/modules/file/src/Controller/FileWidgetAjaxController.php
+++ b/core/modules/file/src/Controller/FileWidgetAjaxController.php
@@ -3,11 +3,38 @@
 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 session manager.
+   *
+   * @var \Drupal\Core\Session\SessionManagerInterface
+   */
+  protected $sessionManager;
+
+  /**
+   * {@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) {
+    $this->sessionManager = $session_manager;
+  }
 
   /**
    * Returns the progress status for a file upload process.
@@ -39,6 +66,45 @@ 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.
+
+      // Save the current session handler and data.
+      $save_handler = session_module_name();
+      $session = $_SESSION;
+
+      // Stop the current session without saving.
+      session_abort();
+
+      // Get upload status from the built-in PHP session data.
+      $status = array();
+      if (method_exists($this->sessionManager, 'getPHPSessionHandler')) {
+        session_module_name($this->sessionManager->getPHPSessionHandler());
+      }
+      else {
+        session_module_name('files');
+      }
+      session_start();
+      $prefix = ini_get('session.upload_progress.prefix');
+      if (isset($_SESSION[$prefix . $key])) {
+        $status = $_SESSION[$prefix . $key];
+      }
+
+      // Close the built-in PHP session without saving and restore the current
+      // 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 ca4e887a1b..fbc14bf493 100644
--- a/core/modules/file/src/Element/ManagedFile.php
+++ b/core/modules/file/src/Element/ManagedFile.php
@@ -292,6 +292,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.14.3 (Apple Git-98)

