From ea7f8776a55190251c65266f6684a71f7c97aaab Mon Sep 17 00:00:00 2001
From: Bernd Oliver Suenderhauf <bos@suenderhauf.de>
Date: Mon, 15 Apr 2019 20:25:57 +0200
Subject: [PATCH] Issue #3046765 by Pancho, wizonesolutions: Discourage
 public:// scheme for storing populated PDFs

---
 config/install/fillpdf.settings.yml           |  4 +
 config/schema/fillpdf.schema.yml              | 10 ++-
 css/fillpdf.form.css                          | 12 +++
 fillpdf.install                               | 56 ++++++++++++-
 fillpdf.libraries.yml                         |  5 ++
 src/Controller/HandlePdfController.php        | 24 +++---
 src/Entity/FillPdfForm.php                    | 68 ++++++++-------
 src/FillPdfAdminFormHelperInterface.php       |  7 +-
 src/Form/FillPdfFormForm.php                  | 84 +++++++++++++++----
 src/Form/FillPdfOverviewForm.php              |  2 +-
 src/Form/FillPdfSettingsForm.php              | 68 ++++++++++-----
 src/Service/FillPdfAdminFormHelper.php        | 20 ++---
 .../Functional/FillPdfFormDeleteFormTest.php  |  2 +-
 tests/src/Functional/FillPdfFormFormTest.php  | 79 +++++++++++++++++
 .../Functional/FillPdfSettingsFormTest.php    | 50 +++++------
 tests/src/Functional/FillPdfTestBase.php      |  2 +-
 .../src/Functional/FillPdfUploadTestBase.php  | 24 ++++--
 .../Functional/HandlePdfControllerTest.php    | 49 ++++++++++-
 tests/src/Functional/PdfPopulationTest.php    |  2 +-
 tests/src/Traits/TestFillPdfTrait.php         | 41 +++++----
 20 files changed, 461 insertions(+), 148 deletions(-)
 create mode 100644 css/fillpdf.form.css
 create mode 100644 fillpdf.libraries.yml

diff --git a/config/install/fillpdf.settings.yml b/config/install/fillpdf.settings.yml
index 22e2228..b8819ab 100644
--- a/config/install/fillpdf.settings.yml
+++ b/config/install/fillpdf.settings.yml
@@ -1,3 +1,7 @@
+allowed_schemes:
+  - private
+template_scheme: ''
+
 remote_protocol: https
 
 # Should not contain a protocol. That's what the above is for.
diff --git a/config/schema/fillpdf.schema.yml b/config/schema/fillpdf.schema.yml
index e02dc28..375a721 100644
--- a/config/schema/fillpdf.schema.yml
+++ b/config/schema/fillpdf.schema.yml
@@ -10,9 +10,15 @@ fillpdf.settings:
     remote_endpoint:
       type: string
       label: 'FillPDF Service endpoint URL without protocol'
-    scheme:
+    allowed_schemes:
+      type: sequence
+      label: 'Allowed storage schemes'
+      sequence:
+        type: string
+        label: 'Allowed storage scheme'
+    template_scheme:
       type: string
-      label: 'Filesystem scheme for uploads'
+      label: 'Template storage scheme'
     local_service_endpoint:
       type: string
       label: 'Local FillPDF PDF API endpoint'
diff --git a/css/fillpdf.form.css b/css/fillpdf.form.css
new file mode 100644
index 0000000..6ffcbeb
--- /dev/null
+++ b/css/fillpdf.form.css
@@ -0,0 +1,12 @@
+#edit-storage .form-wrapper {
+  display: inline-block;
+  vertical-align: top;
+}
+
+#edit-storage .form-item {
+  margin-bottom: 0;
+}
+
+#edit-scheme-wrapper {
+  padding-right: 1em;
+}
diff --git a/fillpdf.install b/fillpdf.install
index 85fdba9..f32b4b3 100644
--- a/fillpdf.install
+++ b/fillpdf.install
@@ -1,12 +1,16 @@
 <?php
+
 /**
  * @file
  * Install functions for FillPDF.
  */
 
-use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\StreamWrapper\StreamWrapperInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\fillpdf\Entity\FillPdfForm;
 use Drupal\fillpdf\Service\FillPdfAdminFormHelper;
+use Drupal\Core\Link;
 
 /**
  * Add scheme field to FillPdfForm.
@@ -127,3 +131,53 @@ function fillpdf_update_8106() {
 
   return t('Default FillPDF Service endpoint updated to fillpdf.io/xmlrpc.php.');
 }
+
+/**
+ * Update storage scheme configuration.
+ */
+function fillpdf_update_8107() {
+  $config = \Drupal::configFactory()->getEditable('fillpdf.settings');
+
+  // Rename the 'scheme' key to 'template_scheme'.
+  if ($config->get('template_scheme') === NULL) {
+    $config->set('template_scheme', $config->get('scheme'));
+  }
+  $config->clear('scheme');
+
+  // Initialize the new 'allowed_schemes' key with all currently available
+  // storage schemes.
+  if ($config->get('allowed_schemes') === NULL) {
+    $available_schemes = array_keys(\Drupal::service('stream_wrapper_manager')->getWrappers(StreamWrapperInterface::WRITE_VISIBLE));
+    $config->set('allowed_schemes', $available_schemes)->save();
+  }
+  $config->save();
+
+  return new TranslatableMarkup('All currently available file storage schemes were added to FillPDF configuration. Please review at %link.', [
+    '%link' => Link::createFromRoute(new TranslatableMarkup('FillPDF settings'), 'fillpdf.forms_admin'),
+  ]);
+}
+
+/**
+ * Update stored FillPDF forms to the changed storage logic.
+ */
+function fillpdf_update_8108() {
+  $fillpdf_forms = FillPdfForm::loadMultiple();
+
+  // Previously, populated PDF files were stored in the filesystem, if a
+  // 'destination_path' is set. Now it is stored, if 'scheme' is set, which
+  // previously was always the case. So for preexisting FillPdfForms without
+  // a 'destination_path', we need to unset 'scheme' to ensure nothing changes.
+  $updated_ids = [];
+  foreach ($fillpdf_forms as $id => $form) {
+    if (empty($form->get('destination_path'))) {
+      $status = $form->set('scheme', NULL)->save();
+      if ($status === SAVED_UPDATED) {
+        $updated_ids[] = $id;
+      }
+    }
+  }
+
+  return new TranslatableMarkup('The following FillPDF forms were updated: %list.', [
+    '%list' => implode(', ', $updated_ids),
+  ]);
+}
diff --git a/fillpdf.libraries.yml b/fillpdf.libraries.yml
new file mode 100644
index 0000000..9fd14fc
--- /dev/null
+++ b/fillpdf.libraries.yml
@@ -0,0 +1,5 @@
+form:
+  version: VERSION
+  css:
+    theme:
+      css/fillpdf.form.css: {}
diff --git a/src/Controller/HandlePdfController.php b/src/Controller/HandlePdfController.php
index 99d9b4b..de76bae 100644
--- a/src/Controller/HandlePdfController.php
+++ b/src/Controller/HandlePdfController.php
@@ -298,14 +298,16 @@ class HandlePdfController extends ControllerBase {
 
     // Determine the appropriate action for the PDF.
     $scheme = $fillpdf_form->getStorageScheme();
-    $available_schemes = array_keys(\Drupal::service('stream_wrapper_manager')->getWrappers(StreamWrapperInterface::WRITE_VISIBLE));
+    $is_available = array_key_exists($scheme, \Drupal::service('stream_wrapper_manager')->getWrappers(StreamWrapperInterface::WRITE_VISIBLE));
+    $is_allowed = in_array($scheme, \Drupal::config('fillpdf.settings')->get('allowed_schemes') ?: []);
 
-    // The configured file storage scheme is no longer available.
-    if (!empty($scheme) && !in_array($scheme, $available_schemes)) {
-      $id = $fillpdf_form->id();
-      // @todo: We can simpify this once an admin_title is #required,
+    if (empty($scheme)) {
+      $action_plugin_id = 'download';
+    }
+    elseif (!$is_available || !$is_allowed) {
+      // @todo: We don't need the ID once an admin_title is #required,
       // see https://www.drupal.org/project/fillpdf/issues/3040776.
-      $label = $fillpdf_form->label() . " ({$id})";
+      $label = $fillpdf_form->label() . " ({$fillpdf_form->id()})";
       $this->getLogger('fillpdf')->critical('Saving a generated PDF file in unavailable storage scheme %scheme failed.', [
         '%scheme' => "$scheme://",
       ]);
@@ -315,17 +317,13 @@ class HandlePdfController extends ControllerBase {
           '@link' => Link::fromTextAndUrl($label, $fillpdf_form->toUrl())->toString(),
         ]));
       }
-      // Unset the scheme to make sure the file is only sent to the browser.
-      $scheme = NULL;
+      // Make sure the file is only sent to the browser.
+      $action_plugin_id = 'download';
     }
-
-    if (!empty($scheme) && !empty($fillpdf_form->getStoragePath())) {
+    else {
       $redirect = !empty($fillpdf_form->destination_redirect->value);
       $action_plugin_id = $redirect ? 'redirect' : 'save';
     }
-    else {
-      $action_plugin_id = 'download';
-    }
 
     // @todo: Remove in FillPDF 5.x. The filename is not part of the context and
     // is separately available anyway.
diff --git a/src/Entity/FillPdfForm.php b/src/Entity/FillPdfForm.php
index d674465..5fb5c0d 100644
--- a/src/Entity/FillPdfForm.php
+++ b/src/Entity/FillPdfForm.php
@@ -6,6 +6,8 @@ use Drupal\Core\Entity\ContentEntityBase;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\StreamWrapper\StreamWrapperInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Url;
 use Drupal\fillpdf\FillPdfFormInterface;
 use Drupal\fillpdf\Service\FillPdfAdminFormHelper;
@@ -92,41 +94,32 @@ class FillPdfForm extends ContentEntityBase implements FillPdfFormInterface {
     $fields['default_entity_id'] = BaseFieldDefinition::create('integer');
 
     $fields['destination_path'] = BaseFieldDefinition::create('string')
-      ->setLabel(t('Where to save generated PDFs'))
-      ->setDescription(t("<p>By default, filled PDFs are not saved to disk; they are simply sent
-      directly to the browser for download. Enter a path here to change this behavior (tokens allowed).
-      <strong>Warning! Unless you include the &download=1 flag in the FillPDF URL, PDFs will only
-      be saved to disk <em>and won't</em> be sent to the browser as well.</strong></p><p>The path
-      you specify must be in the following format:<br />
-        <ul>
-          <li><code>path/to/directory</code> (path will be treated as relative to
-          your <em>fillpdf</em> files subdirectory)</li>
-          filesystem)</li>
-        </ul>
-      Note that you are responsible for ensuring that the user under which PHP is running can write to this path. Do not include a trailing slash.</p>"))
+      ->setLabel(t('Destination path'))
+      ->setDescription(new TranslatableMarkup('You may specify a subdirectory for storing filled PDFs. This field supports tokens.'))
       ->setDisplayOptions('form', [
-        'type' => 'string',
-        'weight' => 20,
+        'type' => 'string_textfield',
+        'weight' => 21,
+        'settings' => [
+          'size' => 38,
+        ],
       ]);
 
     // @todo: add post_save_redirect field for where to send the browser by default after they generate a PDF
 
     $fields['scheme'] = BaseFieldDefinition::create('list_string')
-      ->setLabel('Storage system for generated PDFs')
-      ->setDescription(t('This setting is used as the storage/download method for generated PDFs. The use of public files is more efficient, but does not provide any access control. Changing this setting will require you to migrate associated files and data yourself and is not recommended after you have uploaded a template.'))
+      ->setLabel('File storage')
       ->setSettings([
-        'allowed_values_function' => [get_called_class(), 'getSchemeAllowedValues'],
+        'allowed_values_function' => [get_called_class(), 'getStorageSchemeOptions'],
       ])
-      ->setDefaultValueCallback(get_called_class() . '::getSchemeDefaultValue')
-      ->setRequired(TRUE)
+      ->setDefaultValueCallback(static::class . '::getStorageSchemeDefault')
       ->setDisplayOptions('form', [
-        'type' => 'options_buttons',
-        'weight' => 25,
+        'type' => 'options_select',
+        'weight' => 20,
       ]);
 
     $fields['destination_redirect'] = BaseFieldDefinition::create('boolean')
       ->setLabel(t('Redirect browser directly to saved PDF'))
-      ->setDescription(t("<strong>This setting is applicable only if <em>Where to save generated PDFs</em> is set.</strong> Instead of redirecting your visitors to the front page, it will redirect them directly to the PDF. However, if you pass Drupal's <em>destination</em> query string parameter, that will override this setting."))
+      ->setDescription(t("Instead of redirecting your visitors to the front page, this will redirect them directly to the PDF. However, if you pass Drupal's <em>destination</em> query string parameter, that will override this setting."))
       ->setDisplayOptions('form', [
         'type' => 'boolean_checkbox',
         'weight' => 30,
@@ -196,11 +189,15 @@ class FillPdfForm extends ContentEntityBase implements FillPdfFormInterface {
    *
    * @see ::baseFieldDefinitions()
    *
-   * @return array
-   *   Stream wrapper descriptions, keyed by scheme.
+   * @return string[]
+   *   Associative array of storage scheme descriptions, keyed by the scheme.
    */
-  public static function getSchemeAllowedValues() {
-    return \Drupal::service('fillpdf.admin_form_helper')->schemeOptions();
+  public static function getStorageSchemeOptions() {
+    $options = [];
+    foreach (self::getAllowedStorageSchemes() as $scheme) {
+      $options[$scheme] = $scheme . '://';
+    }
+    return $options;
   }
 
   /**
@@ -209,10 +206,23 @@ class FillPdfForm extends ContentEntityBase implements FillPdfFormInterface {
    * @see ::baseFieldDefinitions()
    *
    * @return string
-   *   The system's default scheme.
+   *   The initial default storage scheme.
+   */
+  public static function getStorageSchemeDefault() {
+    $allowed = self::getAllowedStorageSchemes();
+    return array_key_exists('private', $allowed) ? 'private' : key($allowed);
+  }
+
+  /**
+   * Gets a list of all storage schemes that are both available and allowed.
+   *
+   * @return string[]
+   *   List of storage schemes that are both available and allowed.
    */
-  public static function getSchemeDefaultValue() {
-    return \Drupal::config('system.file')->get('default_scheme');
+  protected static function getAllowedStorageSchemes() {
+    $available = array_keys(\Drupal::service('stream_wrapper_manager')->getWrappers(StreamWrapperInterface::WRITE_VISIBLE));
+    $allowed = \Drupal::config('fillpdf.settings')->get('allowed_schemes') ?: [];
+    return array_intersect($available, $allowed);
   }
 
 }
diff --git a/src/FillPdfAdminFormHelperInterface.php b/src/FillPdfAdminFormHelperInterface.php
index 745004f..7a94166 100644
--- a/src/FillPdfAdminFormHelperInterface.php
+++ b/src/FillPdfAdminFormHelperInterface.php
@@ -21,14 +21,17 @@ interface FillPdfAdminFormHelperInterface {
   public function getAdminTokenForm($token_types = 'all');
 
   /**
-   * Returns acceptable file scheme options for use with FAPI radio buttons.
+   * Returns available file storage options for use with FAPI radio buttons.
    *
    * Any visible, writeable wrapper can potentially be used.
    *
+   * @param array $label_templates
+   *   (optional) Associative array of label templates keyed by scheme name.
+   *
    * @return array
    *   Stream wrapper descriptions, keyed by scheme.
    */
-  public function schemeOptions();
+  public function schemeOptions(array $label_templates = []);
 
   /**
    * Returns all FillPdfForms with template PDFs stored in a particular scheme.
diff --git a/src/Form/FillPdfFormForm.php b/src/Form/FillPdfFormForm.php
index 1844464..02d64db 100644
--- a/src/Form/FillPdfFormForm.php
+++ b/src/Form/FillPdfFormForm.php
@@ -158,9 +158,9 @@ class FillPdfFormForm extends ContentEntityForm {
       ],
     ];
 
-    $is_ajax = $form_state->isRebuilding();
-
-    if ($is_ajax) {
+    // On AJAX-triggered rebuild, work with the user input instead of previously
+    // stored values.
+    if ($form_state->isRebuilding()) {
       $default_entity_type = $form_state->getValue('default_entity_type');
       $default_entity_id = $form_state->getValue('default_entity_id');
     }
@@ -169,6 +169,7 @@ class FillPdfFormForm extends ContentEntityForm {
       $default_entity_id = count($stored_default_entity_id) ? $stored_default_entity_id->first()->value : NULL;
     }
 
+    // If a default entity type is set, allow selecting a default entity, too.
     if ($default_entity_type) {
       $default_entity = $default_entity_id ? $this->entityTypeManager->getStorage($default_entity_type)->load($default_entity_id) : NULL;
 
@@ -205,9 +206,10 @@ class FillPdfFormForm extends ContentEntityForm {
           'event' => 'autocompleteclose autocompletechange',
           'wrapper' => 'test-entity-wrapper',
           'progress' => ['type' => 'none'],
-        ]
+        ],
       ];
     }
+    // No default entity type set, so just provide a wrapper for AJAX replace.
     else {
       $form['default_entity_id'] = [
         '#type' => 'hidden',
@@ -234,7 +236,7 @@ class FillPdfFormForm extends ContentEntityForm {
       ],
     ];
 
-    $upload_location = FillPdf::buildFileUri($this->config('fillpdf.settings')->get('scheme'), 'fillpdf');
+    $upload_location = FillPdf::buildFileUri($this->config('fillpdf.settings')->get('template_scheme'), 'fillpdf');
     if (!file_prepare_directory($upload_location, FILE_CREATE_DIRECTORY + FILE_MODIFY_PERMISSIONS)) {
       $this->messenger()->addError($this->t('The directory %directory does not exist or is not writable. Please check permissions.', [
         '%directory' => $this->fileSystem->realpath($upload_location),
@@ -276,22 +278,76 @@ class FillPdfFormForm extends ContentEntityForm {
       '#weight' => $pdf_info_weight,
     ];
 
+    $available_schemes = $form['scheme']['widget']['#options'];
+    // If only one option is available, this is 'none', so there's nothing to
+    // chose.
+    if (count($available_schemes) == 1) {
+      $form['scheme']['#type'] = 'hidden';
+      $form['destination_path']['#type'] = 'hidden';
+      $form['destination_redirect']['#type'] = 'hidden';
+    }
+    // Otherwise show the 'Storage and download' section.
+    else {
+      $form['storage_download'] = [
+        '#type' => 'fieldset',
+        '#title' => $this->t('Storage and download'),
+        '#weight' => $form['pdf_info']['#weight'] + 1,
+        '#open' => TRUE,
+        '#attached' => [
+          'library' => ['fillpdf/form'],
+        ],
+      ];
+
+      $form['storage_download']['storage'] = [
+        '#type' => 'container',
+      ];
+
+      // @todo: Check for empty value after Core issue is fixed.
+      // See: https://www.drupal.org/project/drupal/issues/1585930
+      $states_no_scheme = [
+        ':input[name="scheme"]' => ['value' => '_none'],
+      ];
+      $form['scheme']['#group'] = 'storage';
+      $form['destination_path']['#group'] = 'storage';
+      $form['destination_path']['widget']['0']['value']['#field_prefix'] = 'fillpdf/';
+      $form['destination_path']['#states'] = [
+        'invisible' => $states_no_scheme,
+      ];
+      $form['destination_path']['token_tree'] = $this->adminFormHelper->getAdminTokenForm();
+      $description = $this->t('If filled PDFs should be automatically saved to disk, chose a file storage');
+      $description .= isset($available_schemes['public']) ? '; ' . $this->t('note that %public storage does not provide any access control.', [
+        '%public' => 'public://',
+      ]) : '.';
+      $description .= ' ' . $this->t('Otherwise, filled PDFs are sent to the browser for download.');
+      $form['storage_download']['storage']['description_scheme_none'] = [
+        '#type' => 'item',
+        '#description' => $description,
+        '#weight' => 22,
+        '#states' => [
+          'visible' => $states_no_scheme,
+        ],
+      ];
+      $form['storage_download']['storage']['description_scheme_set'] = [
+        '#type' => 'item',
+        '#description' => $this->t('As PDFs are saved to disk, make sure you include the <em>&download=1</em> flag to send them to the browser as well.'),
+        '#weight' => 23,
+        '#states' => [
+          'invisible' => $states_no_scheme,
+        ],
+      ];
+
+      $form['destination_redirect']['#group'] = 'storage_download';
+      $form['destination_redirect']['#states'] = [
+        'invisible' => $states_no_scheme,
+      ];
+    }
 
-    $additional_setting_set = $entity->destination_path->value || $entity->destination_redirect->value;
     $form['additional_settings'] = [
       '#type' => 'details',
       '#title' => $this->t('Additional settings'),
       '#weight' => $form['pdf_info']['#weight'] + 1,
-      '#open' => $additional_setting_set,
     ];
-
-    $form['destination_path']['#group'] = 'additional_settings';
-    $form['destination_path']['token_tree'] = $this->adminFormHelper->getAdminTokenForm([$entity->default_entity_type->value]);
-
-    $form['scheme']['#group'] = 'additional_settings';
-    $form['destination_redirect']['#group'] = 'additional_settings';
     $form['replacements']['#group'] = 'additional_settings';
-    $form['replacements']['#weight'] = 1;
 
     // @todo: Add a button to let them attempt re-parsing if it failed.
     $form['fillpdf_fields']['fields'] = FillPdf::embedView('fillpdf_form_fields',
diff --git a/src/Form/FillPdfOverviewForm.php b/src/Form/FillPdfOverviewForm.php
index e59969d..360825e 100644
--- a/src/Form/FillPdfOverviewForm.php
+++ b/src/Form/FillPdfOverviewForm.php
@@ -141,7 +141,7 @@ class FillPdfOverviewForm extends FillPdfAdminFormBase {
       return $form;
     }
 
-    $upload_location = FillPdf::buildFileUri($this->config('fillpdf.settings')->get('scheme'), 'fillpdf');
+    $upload_location = FillPdf::buildFileUri($this->config('fillpdf.settings')->get('template_scheme'), 'fillpdf');
     if (!file_prepare_directory($upload_location, FILE_CREATE_DIRECTORY + FILE_MODIFY_PERMISSIONS)) {
       $this->messenger()->addError($this->t('The directory %directory does not exist or is not writable. Please check permissions.', [
         '%directory' => $this->fileSystem->realpath($upload_location),
diff --git a/src/Form/FillPdfSettingsForm.php b/src/Form/FillPdfSettingsForm.php
index c12f909..3b184c3 100644
--- a/src/Form/FillPdfSettingsForm.php
+++ b/src/Form/FillPdfSettingsForm.php
@@ -116,19 +116,40 @@ class FillPdfSettingsForm extends ConfigFormBase {
 
     $config = $this->config('fillpdf.settings');
 
-    $config_scheme = $config->get('scheme');
-    $schemes = $this->adminFormHelper->schemeOptions();
+    // Get available scheme options.
+    $scheme_options = $this->adminFormHelper->schemeOptions([
+      'public' => $this->t('@scheme (discouraged)'),
+      'private' => $this->t('@scheme (recommended)'),
+    ]);
+    $form['allowed_schemes'] = [
+      '#type' => 'checkboxes',
+      '#title' => $this->t('Allowed file storages'),
+      '#default_value' => array_intersect(array_keys($scheme_options), $config->get('allowed_schemes')),
+      '#options' => $scheme_options,
+      '#description' => $this->t("You may choose one or more file storages to be available for storing generated PDF files with actual entity data; note that %public does not provide any access control.<br />If you don't choose any file storage, generated PDFs may only be sent to the browser instead of being stored.", [
+        '%public' => $this->t('Public files'),
+      ]),
+    ];
 
+    $form['advanced_storage'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Advanced storage settings'),
+    ];
+    $file_default_scheme = file_default_scheme();
+    $template_scheme_options = $this->adminFormHelper->schemeOptions([
+      $file_default_scheme => $this->t('@scheme (site default)'),
+    ]);
+    $template_scheme = $config->get('template_scheme');
     // Set an error if the previously configured scheme doesn't exist anymore.
-    if ($config_scheme && !array_key_exists($config_scheme, $schemes)) {
-      $error_message = $this->t('Your previously used file scheme %previous_scheme is no longer available on this Drupal site, see the %system_settings. Please reset your default to an existing file scheme.', [
-        '%previous_scheme' => $config_scheme . '://',
+    if ($template_scheme && !array_key_exists($template_scheme, $template_scheme_options)) {
+      $error_message = $this->t('Your previously used file storage %previous_scheme is no longer available on this Drupal site, see the %system_settings. Please reset your default to an existing file storage.', [
+        '%previous_scheme' => $template_scheme . '://',
         '%system_settings' => Link::createFromRoute($this->t('File system settings'), 'system.file_system_settings')->toString(),
       ]);
 
       // @todo: It would be helpful if we could use EntityQuery instead, see
       // https://www.drupal.org/project/fillpdf/issues/3043508.
-      $map = $this->adminFormHelper->getFormsByTemplateScheme($config_scheme);
+      $map = $this->adminFormHelper->getFormsByTemplateScheme($template_scheme);
       if ($count = count($map)) {
         $forms = FillPdfForm::loadMultiple(array_keys($map));
         $items = [];
@@ -150,15 +171,19 @@ class FillPdfSettingsForm extends ConfigFormBase {
       }
 
       $this->messenger()->addError(new FormattableMarkup($error_message, []));
-      $this->logger('fillpdf')->critical('File scheme %previous_scheme is no longer available.' . $count ? " $count FillPDF forms are defunct." : '');
+      $this->logger('fillpdf')->critical('File storage %previous_scheme is no longer available.' . $count ? " $count FillPDF forms are defunct." : '', [
+        '%previous_scheme' => $template_scheme . '://',
+      ]);
     }
 
-    $form['scheme'] = [
+    $form['advanced_storage']['template_scheme'] = [
       '#type' => 'radios',
-      '#title' => $this->t('Template download method'),
-      '#default_value' => array_key_exists($config_scheme, $schemes) ? $config_scheme : $this->config('system.file')->get('default_scheme'),
-      '#options' => $schemes,
-      '#description' => $this->t('This setting is used as the download method for uploaded templates. The use of public files is more efficient, but does not provide any access control. Changing this setting will require you to migrate associated files and data yourself and is not recommended after you have uploaded a template.'),
+      '#title' => $this->t('Template storage'),
+      '#default_value' => array_key_exists($template_scheme, $template_scheme_options) ? $template_scheme : $file_default_scheme,
+      '#options' => $template_scheme_options,
+      '#description' => $this->t('This setting is used as the storage for uploaded templates; note that the use of %public is more efficient, but does not provide any access control.<br />Changing this setting will require you to migrate associated files and data yourself and is not recommended after you have uploaded a template.', [
+        '%public' => t('Public files'),
+      ]),
     ];
 
     $form['backend'] = [
@@ -292,12 +317,16 @@ class FillPdfSettingsForm extends ConfigFormBase {
         break;
     }
 
-    $uri = FillPdf::buildFileUri($form_state->getValue('scheme'), 'fillpdf');
-    if (!file_prepare_directory($uri, FILE_CREATE_DIRECTORY + FILE_MODIFY_PERMISSIONS)) {
-      $error_message = $this->t('Could not automatically create the subdirectory %directory. Please check permissions before trying again.', [
-        '%directory' => $this->fileSystem->realpath($uri),
-      ]);
-      $form_state->setErrorByName('scheme', $error_message);
+    $template_scheme = $form_state->getValue('template_scheme');
+    $schemes_to_prepare = array_filter($form_state->getValue('allowed_schemes')) + [$template_scheme => $template_scheme];
+    foreach ($schemes_to_prepare as $scheme) {
+      $uri = FillPdf::buildFileUri($scheme, 'fillpdf');
+      if (!file_prepare_directory($uri, FILE_CREATE_DIRECTORY + FILE_MODIFY_PERMISSIONS)) {
+        $error_message = $this->t('Could not automatically create the subdirectory %directory. Please check permissions before trying again.', [
+          '%directory' => $this->fileSystem->realpath($uri),
+        ]);
+        $form_state->setErrorByName('template_scheme', $error_message);
+      }
     }
   }
 
@@ -309,7 +338,8 @@ class FillPdfSettingsForm extends ConfigFormBase {
     $values = $form_state->getValues();
     $config = $this->config('fillpdf.settings');
 
-    $config->set('scheme', $values['scheme'])
+    $config->set('allowed_schemes', array_keys(array_filter($values['allowed_schemes'])))
+      ->set('template_scheme', $values['template_scheme'])
       ->set('backend', $values['backend']);
 
     switch ($values['backend']) {
diff --git a/src/Service/FillPdfAdminFormHelper.php b/src/Service/FillPdfAdminFormHelper.php
index 9fbbd06..d11f8e2 100644
--- a/src/Service/FillPdfAdminFormHelper.php
+++ b/src/Service/FillPdfAdminFormHelper.php
@@ -8,6 +8,7 @@ use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\StreamWrapper\StreamWrapperInterface;
 use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
 use Drupal\fillpdf\FillPdfAdminFormHelperInterface;
+use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
@@ -82,20 +83,15 @@ class FillPdfAdminFormHelper implements FillPdfAdminFormHelperInterface {
   /**
    * {@inheritdoc}
    */
-  public function schemeOptions() {
-    $site_default = $this->configFactory->get('system.file')->get('default_scheme');
-    $streams = $this->streamWrapperManager;
+  public function schemeOptions(array $label_templates = []) {
+    $stream_wrapper_manager = $this->streamWrapperManager;
 
     $options = [];
-    foreach (array_keys($streams->getWrappers(StreamWrapperInterface::WRITE_VISIBLE)) as $scheme) {
-      $name = $streams->getViaScheme($scheme)->getName();
-      $description = $streams->getViaScheme($scheme)->getDescription();
-      $string = '<strong>' . $name . '</strong>';
-      if ($scheme == $site_default) {
-        $string .= ' (' . new TranslatableMarkup('site default') . ')';
-      }
-      $string .= ': ' . $description;
-      $options[$scheme] = $string;
+    foreach (array_keys($stream_wrapper_manager->getWrappers(StreamWrapperInterface::WRITE_VISIBLE)) as $scheme) {
+      $label_template = array_key_exists($scheme, $label_templates) ? $label_templates[$scheme] : '@scheme';
+      $options[$scheme] = new FormattableMarkup($label_template, [
+        '@scheme' => new FormattableMarkup("<strong>@label</strong>", ['@label' => $stream_wrapper_manager->getViaScheme($scheme)->getName()]),
+      ]) . ': ' . $stream_wrapper_manager->getViaScheme($scheme)->getDescription();
     }
 
     return $options;
diff --git a/tests/src/Functional/FillPdfFormDeleteFormTest.php b/tests/src/Functional/FillPdfFormDeleteFormTest.php
index c756059..e13436c 100644
--- a/tests/src/Functional/FillPdfFormDeleteFormTest.php
+++ b/tests/src/Functional/FillPdfFormDeleteFormTest.php
@@ -25,7 +25,7 @@ class FillPdfFormDeleteFormTest extends BrowserTestBase {
   protected function setUp() {
     parent::setUp();
 
-    $this->configureBackend();
+    $this->configureFillPdf();
     $this->initializeUser();
   }
 
diff --git a/tests/src/Functional/FillPdfFormFormTest.php b/tests/src/Functional/FillPdfFormFormTest.php
index cc370ca..193d9b7 100644
--- a/tests/src/Functional/FillPdfFormFormTest.php
+++ b/tests/src/Functional/FillPdfFormFormTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\fillpdf\Functional;
 
+use Drupal\Core\Url;
 use Drupal\file\Entity\File;
 use Drupal\fillpdf\Entity\FillPdfForm;
 use Drupal\user\Entity\Role;
@@ -80,6 +81,84 @@ class FillPdfFormFormTest extends FillPdfUploadTestBase {
     $this->assertUploadPdfFile(self::OP_SAVE, TRUE);
   }
 
+  /**
+   * Tests the FillPdfForm entity's edit form.
+   */
+  public function testStorageSettings() {
+    $this->uploadTestPdf('fillpdf_test_v3.pdf');
+    $form_id = $this->getLatestFillPdfForm();
+    $previous_file_id = $this->getLastFileId();
+
+    $edit_form_url = Url::fromRoute('entity.fillpdf_form.edit_form', ['fillpdf_form' => $form_id]);
+    $generate_url = Url::fromRoute('fillpdf.populate_pdf', [], [
+      'query' => [
+        'fid' => $form_id,
+        'entity_id' => "node:{$this->testNodes[1]->id()}",
+      ],
+    ]);
+
+    // Check the initial storage settings.
+    $this->assertSession()->fieldValueEquals('scheme', '_none');
+    foreach (['- None -', 'private://', 'public://'] as $option) {
+      $this->assertSession()->optionExists('scheme', $option);
+    }
+    $this->assertSession()->fieldValueEquals('destination_path[0][value]', '');
+    $this->drupalGet($edit_form_url);
+
+    // Now hit the generation route and make sure the PDF file is *not* stored.
+    $this->drupalGet($generate_url);
+    $this->assertEquals($previous_file_id, $this->getLastFileId(), 'Generated file is not stored.');
+
+    // Set the 'public' scheme and see if the 'destination_path' field appears.
+    $this->drupalPostForm($edit_form_url, ['scheme' => 'public'], self::OP_SAVE);
+    $this->assertSession()->fieldValueEquals('scheme', 'public');
+    $this->assertSession()->pageTextContains('Destination path');
+
+    // Hit the generation route again and make sure this time the PDF file is
+    // stored in the public storage.
+    $this->drupalGet($generate_url);
+    $this->assertEquals(++$previous_file_id, $this->getLastFileId(), 'Generated file was stored.');
+    $this->assertStringStartsWith('public://', File::load($this->getLastFileId())->getFileUri());
+
+    // Now disallow the public scheme and reload.
+    $this->configureFillPdf(['allowed_schemes' => ['private']]);
+
+    // Reload and check if the public option has disappeared now.
+    $this->drupalGet($edit_form_url);
+    $this->assertSession()->fieldValueEquals('scheme', '_none');
+    foreach (['- None -', 'private://'] as $option) {
+      $this->assertSession()->optionExists('scheme', $option);
+    }
+    $this->assertSession()->optionNotExists('scheme', 'public://');
+
+    // Hit the generation route once more and make sure the scheme has been
+    // unset, so the PDF file is *not* stored.
+    $this->drupalGet($generate_url);
+    $this->assertEquals($previous_file_id, $this->getLastFileId(), 'Generated file is not stored.');
+
+    // Set the 'private' scheme.
+    $this->drupalPostForm($edit_form_url, ['scheme' => 'private'], self::OP_SAVE);
+    $this->assertSession()->fieldValueEquals('scheme', 'private');
+
+    // Hit the generation route again and make sure this time the PDF file is
+    // stored in the private storage.
+    $this->drupalGet($generate_url);
+    $this->assertEquals(++$previous_file_id, $this->getLastFileId(), 'Generated file was stored.');
+    $this->assertStringStartsWith('private://', File::load($this->getLastFileId())->getFileUri());
+
+    // Now disallow the private scheme as well and reload.
+    $this->configureFillPdf(['allowed_schemes' => []]);
+    $this->drupalGet($edit_form_url);
+
+    // Check if the whole storage settings section has disappeared now.
+    $this->assertSession()->pageTextNotContains('Storage and download');
+
+    // Hit the generation route one last time and make sure the PDF file is
+    // again *not* stored.
+    $this->drupalGet($generate_url);
+    $this->assertEquals($previous_file_id, $this->getLastFileId(), 'Generated file is not stored.');
+  }
+
   /**
    * Tests proper registration of managed_files.
    */
diff --git a/tests/src/Functional/FillPdfSettingsFormTest.php b/tests/src/Functional/FillPdfSettingsFormTest.php
index ac24735..341d4c0 100644
--- a/tests/src/Functional/FillPdfSettingsFormTest.php
+++ b/tests/src/Functional/FillPdfSettingsFormTest.php
@@ -42,7 +42,7 @@ class FillPdfSettingsFormTest extends BrowserTestBase {
     // 'fillpdf_service' backend.
     $this->drupalGet(Url::fromRoute('fillpdf.settings'));
     $this->assertSession()->pageTextContains('Public files (site default)');
-    $this->assertSession()->checkboxChecked('edit-scheme-public');
+    $this->assertSession()->checkboxChecked('edit-template-scheme-public');
     $this->assertSession()->checkboxChecked('edit-backend-fillpdf-service');
 
     // Now set the site default to 'private'.
@@ -55,21 +55,21 @@ class FillPdfSettingsFormTest extends BrowserTestBase {
     // while the backend should remain unchanged.
     $this->drupalGet(Url::fromRoute('fillpdf.settings'));
     $this->assertSession()->pageTextContains('Private files (site default)');
-    $this->assertSession()->checkboxChecked('edit-scheme-private');
+    $this->assertSession()->checkboxChecked('edit-template-scheme-private');
     $this->assertSession()->checkboxChecked('edit-backend-fillpdf-service');
   }
 
   /**
    * Tests the scheme settings with the 'dummy_remote' stream wrapper.
    */
-  public function testSettingsFormSchemeDummyRemote() {
+  public function testTemplateSchemeDummyRemote() {
     // FillPDF is not yet configured.
     // Verify the 'dummy_remote' stream wrapper is present on the form.
     $this->drupalGet(Url::fromRoute('fillpdf.settings'));
-    $this->assertSession()->elementExists('css', '#edit-scheme-dummy-remote');
+    $this->assertSession()->elementExists('css', '#edit-template-scheme-dummy-remote');
 
     // Programmatically configure 'dummy_remote' as new default scheme.
-    $this->configureBackend('dummy_remote');
+    $this->configureFillPdf(['template_scheme' => 'dummy_remote']);
 
     // Now uninstall the file_test module with the dummy stream wrappers.
     $this->assertTrue(\Drupal::service('module_installer')->uninstall(['file_test']), "Module 'file_test' has been uninstalled.");
@@ -77,26 +77,26 @@ class FillPdfSettingsFormTest extends BrowserTestBase {
 
     // Reload the page and verify that 'dummy_remote' is gone.
     $this->drupalGet(Url::fromRoute('fillpdf.settings'));
-    $this->assertSession()->elementNotExists('css', '#edit-scheme-dummy-remote');
-    $this->assertSession()->pageTextContains('Your previously used file scheme dummy_remote:// is no longer available');
+    $this->assertSession()->elementNotExists('css', '#edit-template-scheme-dummy-remote');
+    $this->assertSession()->pageTextContains('Your previously used file storage dummy_remote:// is no longer available');
   }
 
   /**
    * Tests the scheme settings with the 'private' stream wrapper.
    */
-  public function testSettingsFormSchemePrivate() {
+  public function testTemplateSchemePrivate() {
     // FillPDF is not yet configured.
     // Configure FillPDF with the 'test' backend and the site default scheme,
     // which at this point is 'public'.
-    $this->configureBackend();
+    $this->configureFillPdf();
 
     // Now on the settings form, switch to the 'private' scheme.
-    $this->drupalPostForm(Url::fromRoute('fillpdf.settings'), ['scheme' => 'private'], 'Save configuration');
+    $this->drupalPostForm(Url::fromRoute('fillpdf.settings'), ['template_scheme' => 'private'], 'Save configuration');
 
     // Verify the new values have been submitted *and* successfully saved.
     $this->assertSession()->pageTextContains('The configuration options have been saved.');
-    $this->assertSession()->fieldValueEquals('scheme', 'private');
-    $this->assertEqual($this->config('fillpdf.settings')->get('scheme'), 'private');
+    $this->assertSession()->fieldValueEquals('template_scheme', 'private');
+    $this->assertEqual($this->config('fillpdf.settings')->get('template_scheme'), 'private');
 
     // Now remove the private path from settings.php and rebuild the container.
     $this->writeSettings([
@@ -111,29 +111,29 @@ class FillPdfSettingsFormTest extends BrowserTestBase {
 
     // Reload the page to verify the 'private' scheme is gone.
     $this->drupalGet(Url::fromRoute('fillpdf.settings'));
-    $this->assertSession()->elementNotExists('css', '#edit-scheme-private');
-    $this->assertSession()->pageTextContains('Your previously used file scheme private:// is no longer available');
+    $this->assertSession()->elementNotExists('css', '#edit-template-scheme-private');
+    $this->assertSession()->pageTextContains('Your previously used file storage private:// is no longer available');
 
     // Verify that the site default scheme, which at this point is 'public', is
     // preselected but not yet saved in config.
-    $this->assertSession()->fieldValueEquals('scheme', \Drupal::config('system.file')->get('default_scheme'));
-    $this->assertEqual($this->config('fillpdf.settings')->get('scheme'), 'private');
+    $this->assertSession()->fieldValueEquals('template_scheme', file_default_scheme());
+    $this->assertEqual($this->config('fillpdf.settings')->get('template_scheme'), 'private');
   }
 
   /**
    * Tests the scheme settings with the 'public' stream wrapper.
    */
-  public function testSettingsFormSchemePublic() {
+  public function testTemplateSchemePublic() {
     // FillPDF is not yet configured.
     // Configure FillPDF with the 'test' backend and the site default scheme,
     // which at this point is 'public'.
-    $this->configureBackend();
+    $this->configureFillPdf();
 
     // On the settings page, verify the 'public' scheme is set both in the form
     // and in config.
     $this->drupalGet(Url::fromRoute('fillpdf.settings'));
-    $this->assertSession()->fieldValueEquals('scheme', 'public');
-    $this->assertEqual($this->config('fillpdf.settings')->get('scheme'), 'public');
+    $this->assertSession()->fieldValueEquals('template_scheme', 'public');
+    $this->assertEqual($this->config('fillpdf.settings')->get('template_scheme'), 'public');
 
     // Verify the subdirectory doesn't exist yet.
     $directory = 'public://fillpdf';
@@ -159,7 +159,7 @@ class FillPdfSettingsFormTest extends BrowserTestBase {
   /**
    * Tests the backend settings with the 'fillpdf_service' backend.
    */
-  public function testSettingsFormBackendFillPdfService() {
+  public function testBackendFillPdfService() {
     // FillPDF is not yet configured. The settings form is however initialized
     // with the 'fillpdf_service' backend. Save that configuration.
     $this->drupalPostForm(Url::fromRoute('fillpdf.settings'), NULL, 'Save configuration');
@@ -183,11 +183,11 @@ class FillPdfSettingsFormTest extends BrowserTestBase {
   /**
    * Tests the backend settings with the 'pdftk' backend.
    */
-  public function testSettingsFormBackendPdftk() {
+  public function testBackendPdftk() {
     // FillPDF is not yet configured.
     // Try configuring FillPDF with the 'pdftk' backend, yet an invalid path.
     $edit = [
-      'scheme' => 'private',
+      'template_scheme' => 'private',
       'backend' => 'pdftk',
       'pdftk_path' => 'xyz',
     ];
@@ -207,7 +207,7 @@ class FillPdfSettingsFormTest extends BrowserTestBase {
   /**
    * Tests the backend settings with the 'test' backend.
    */
-  public function testSettingsFormBackendTest() {
+  public function testBackendTest() {
     // FillPDF is not yet configured.
     // Go to the settings page and verify the autodetected 'test' backend is
     // present only once and with the form-altered label.
@@ -219,7 +219,7 @@ class FillPdfSettingsFormTest extends BrowserTestBase {
     // for the form-altered 'example_setting' and the unrelated
     // 'fillpdf_service_api_key'.
     $edit = [
-      'scheme' => 'private',
+      'template_scheme' => 'private',
       'backend' => 'test',
       'example_setting' => 'x',
       'fillpdf_service_api_key' => 'Invalid, just playing around.',
diff --git a/tests/src/Functional/FillPdfTestBase.php b/tests/src/Functional/FillPdfTestBase.php
index 893e06d..f152a88 100644
--- a/tests/src/Functional/FillPdfTestBase.php
+++ b/tests/src/Functional/FillPdfTestBase.php
@@ -65,7 +65,7 @@ abstract class FillPdfTestBase extends ImageFieldTestBase {
       'administer pdfs',
     ]);
 
-    $this->configureBackend();
+    $this->configureFillPdf();
 
     $this->backendServiceManager = $this->container->get('plugin.manager.fillpdf_backend_service');
 
diff --git a/tests/src/Functional/FillPdfUploadTestBase.php b/tests/src/Functional/FillPdfUploadTestBase.php
index b1bf7bc..3e9804d 100644
--- a/tests/src/Functional/FillPdfUploadTestBase.php
+++ b/tests/src/Functional/FillPdfUploadTestBase.php
@@ -24,22 +24,30 @@ abstract class FillPdfUploadTestBase extends FileFieldTestBase {
   protected $profile = 'minimal';
 
   /**
-   * @var Upload a file in the managed file widget.
+   * Upload a file in the managed file widget.
+   *
+   * @var string
    */
   const OP_UPLOAD = 'Upload';
 
   /**
-   * @var Remove a file from the managed file widget.
+   * Remove a file from the managed file widget.
+   *
+   * @var string
    */
   const OP_REMOVE = 'Remove';
 
   /**
-   * @var Create a new FillPdfForm. Submit button on FillPdfOverviewForm.
+   * Create a new FillPdfForm. Submit button on FillPdfOverviewForm.
+   *
+   * @var string
    */
   const OP_CREATE = 'Create';
 
   /**
-   * @var Save and update the FillPdfForm. Submit button on FillPdfFormForm.
+   * Save and update the FillPdfForm. Submit button on FillPdfFormForm.
+   *
+   * @var string
    */
   const OP_SAVE = 'Save';
 
@@ -56,7 +64,7 @@ abstract class FillPdfUploadTestBase extends FileFieldTestBase {
   protected function setUp() {
     parent::setUp();
 
-    $this->configureBackend();
+    $this->configureFillPdf();
     $this->initializeUser();
 
     $this->testNodes[1] = $this->createNode([
@@ -105,7 +113,7 @@ abstract class FillPdfUploadTestBase extends FileFieldTestBase {
    *   - FillPdfUploadTestBase::OP_UPLOAD (default),
    *   - FillPdfUploadTestBase::OP_CREATE, or
    *   - FillPdfUploadTestBase::OP_SAVE.
-   * @param boolean $filename_preexists
+   * @param bool $filename_preexists
    *   (optional) Whether the testfile has previously been uploaded, so a file
    *   with the same filename preexists. Defaults to FALSE.
    */
@@ -153,7 +161,7 @@ abstract class FillPdfUploadTestBase extends FileFieldTestBase {
 
         // Make sure the file is permanent and correctly placed.
         $this->assertFileIsPermanent($new_file);
-        $expected_file_uri = FillPdf::buildFileUri($this->config('fillpdf.settings')->get('scheme'), "fillpdf/{$new_filename}");
+        $expected_file_uri = FillPdf::buildFileUri($this->config('fillpdf.settings')->get('template_scheme'), "fillpdf/{$new_filename}");
         $this->assertEquals($new_file->getFileUri(), $expected_file_uri);
         break;
 
@@ -164,7 +172,7 @@ abstract class FillPdfUploadTestBase extends FileFieldTestBase {
 
         // Make sure the file is permanent and correctly placed.
         $this->assertFileIsPermanent($new_file);
-        $expected_file_uri = FillPdf::buildFileUri($this->config('fillpdf.settings')->get('scheme'), "fillpdf/{$new_filename}");
+        $expected_file_uri = FillPdf::buildFileUri($this->config('fillpdf.settings')->get('template_scheme'), "fillpdf/{$new_filename}");
         $this->assertEquals($new_file->getFileUri(), $expected_file_uri);
         break;
     }
diff --git a/tests/src/Functional/HandlePdfControllerTest.php b/tests/src/Functional/HandlePdfControllerTest.php
index 899537e..844e5c9 100644
--- a/tests/src/Functional/HandlePdfControllerTest.php
+++ b/tests/src/Functional/HandlePdfControllerTest.php
@@ -25,6 +25,7 @@ class HandlePdfControllerTest extends FillPdfUploadTestBase {
     $form_id = $this->getLatestFillPdfForm();
     $edit = [
       'title[0][value]' => '[current-date:html_year]-[user:account-name]-[node:title].pdf',
+      'scheme' => 'public',
       'destination_path[0][value]' => '[current-date:html_year]-[user:account-name]-[node:title]',
     ];
     $this->drupalPostForm("admin/structure/fillpdf/{$form_id}", $edit, 'Save');
@@ -102,10 +103,56 @@ class HandlePdfControllerTest extends FillPdfUploadTestBase {
     }
   }
 
+  /**
+   * Tests handling of an no longer allowed storage scheme.
+   */
+  public function testStorageSchemeDisallowed() {
+    $this->uploadTestPdf('fillpdf_test_v3.pdf');
+    $form_id = $this->getLatestFillPdfForm();
+    $previous_file_id = $this->getLastFileId();
+    $edit = [
+      'admin_title[0][value]' => 'Scheme test',
+      'scheme' => 'public',
+      'destination_path[0][value]' => 'test',
+    ];
+    $this->drupalPostForm(NULL, $edit, 'Save');
+
+    $fillpdf_route = Url::fromRoute('fillpdf.populate_pdf', [], [
+      'query' => [
+        'fid' => $form_id,
+      ],
+    ]);
+
+    // Hit the generation route. Make sure we are redirected to the front page.
+    $this->drupalGet($fillpdf_route);
+    $this->assertSession()->addressNotEquals('/fillpdf');
+    $this->assertSession()->statusCodeEquals(200);
+    // Get back to the front page and make sure the file was stored in the
+    // private storage.
+    $this->drupalGet('<front>');
+    $this->assertSession()->pageTextNotContains('File storage scheme public:// is unavailable');
+    $this->assertEquals(++$previous_file_id, $this->getLastFileId(), 'Generated file was stored.');
+    $this->assertStringStartsWith('public://', File::load($this->getLastFileId())->getFileUri());
+
+    // Now disallow the public scheme.
+    $this->configureFillPdf(['allowed_schemes' => ['private']]);
+
+    // Hit the generation route again. This time we should be redirected to the
+    // PDF file. Make sure no PHP error occured.
+    $this->drupalGet($fillpdf_route);
+    $this->assertSession()->addressEquals('/fillpdf');
+    $this->assertSession()->statusCodeEquals(200);
+    // Get back to the front page and check if an error was set, and we didn't
+    // try to store the file.
+    $this->drupalGet('<front>');
+    $this->assertSession()->pageTextContains("File storage scheme public:// is unavailable, so a PDF file generated from FillPDF form Scheme test ($form_id) could not be stored.");
+    $this->assertEquals($previous_file_id, $this->getLastFileId(), 'Generated file was not stored.');
+  }
+
   /**
    * Tests handling of an unavailable storage scheme.
    */
-  public function testStorageSchemePrivate() {
+  public function testStorageSchemeUnavailable() {
     $this->uploadTestPdf('fillpdf_test_v3.pdf');
     $form_id = $this->getLatestFillPdfForm();
     $previous_file_id = $this->getLastFileId();
diff --git a/tests/src/Functional/PdfPopulationTest.php b/tests/src/Functional/PdfPopulationTest.php
index e187d2f..7ac1b28 100644
--- a/tests/src/Functional/PdfPopulationTest.php
+++ b/tests/src/Functional/PdfPopulationTest.php
@@ -31,7 +31,7 @@ class PdfPopulationTest extends FillPdfTestBase {
   protected function setUp() {
     parent::setUp();
 
-    $this->configureBackend();
+    $this->configureFillPdf();
 
     $this->testNode = Node::load(
       $this->uploadNodeImage(
diff --git a/tests/src/Traits/TestFillPdfTrait.php b/tests/src/Traits/TestFillPdfTrait.php
index 3252e39..e2bf3a5 100644
--- a/tests/src/Traits/TestFillPdfTrait.php
+++ b/tests/src/Traits/TestFillPdfTrait.php
@@ -10,28 +10,32 @@ namespace Drupal\Tests\fillpdf\Traits;
 trait TestFillPdfTrait {
 
   /**
-   * Configures the FillPdf test scheme and backend.
+   * Configures schemes and backend.
    *
-   * @param string $scheme
-   *   (optional) The file system scheme to use for PDF templates. Defaults
-   *   to the site default, which initially is 'public'.
-   * @param string $backend
-   *   (optional) The backend to use. Defaults to 'test'.
+   * @param array $configuration
+   *   (optional) Associative array containing configuration to be set. This may
+   *   contain the following keys:
+   *   - 'allowed_schemes': string[] (default: ['public', 'private']
+   *   - 'template_scheme': string (default: the site default)
+   *   - 'backend': string (default: 'test')
    */
-  protected function configureBackend($scheme = 'default', $backend = 'test') {
+  protected function configureFillPdf(array $configuration = []) {
+    // Merge in defaults.
+    $configuration += [
+      'allowed_schemes' => ['public', 'private'],
+      'template_scheme' => file_default_scheme(),
+      'backend' => 'test',
+    ];
+
     /** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */
     $config_factory = $this->container->get('config.factory');
 
-    // Get the site default scheme.
-    if ($scheme == 'default') {
-      $scheme = $config_factory->get('system.file')->get('default_scheme');
-    }
-
     // Set FillPDF backend and scheme.
-    $fillpdf_settings = $config_factory->getEditable('fillpdf.settings')
-      ->set('scheme', $scheme)
-      ->set('backend', $backend);
-    $fillpdf_settings->save();
+    $config_factory->getEditable('fillpdf.settings')
+      ->set('allowed_schemes', $configuration['allowed_schemes'])
+      ->set('template_scheme', $configuration['template_scheme'])
+      ->set('backend', $configuration['backend'])
+      ->save();
   }
 
   /**
@@ -42,7 +46,7 @@ trait TestFillPdfTrait {
     // environment to run the Docker container at http://127.0.0.1:8085 if you
     // are developing for FillPDF and want to run this test.
     $edit = [
-      'scheme' => 'public',
+      'template_scheme' => 'public',
       'backend' => 'local_service',
       'local_service_endpoint' => 'http://127.0.0.1:8085',
     ];
@@ -60,7 +64,7 @@ trait TestFillPdfTrait {
   protected function configureFillPdfServiceBackend($api_key, $api_endpoint = 'https://www.fillpdf.io') {
     // Configure FillPDF Service.
     $edit = [
-      'scheme' => 'public',
+      'template_scheme' => 'public',
       'backend' => 'fillpdf_service_v2',
       'fillpdf_service_api_key' => $api_key,
       'fillpdf_service_api_endpoint' => $api_endpoint,
@@ -103,6 +107,7 @@ trait TestFillPdfTrait {
    * Gets the ID of the latest fillpdf_form stored.
    *
    * @return int
+   *   ID of the lates FillPdf Form stored.
    */
   protected function getLatestFillPdfForm() {
     $entity_query = $this->container->get('entity_type.manager')
-- 
GitLab