FileTransferAuthorizeForm.php 12 KB
Newer Older
1 2 3 4 5
<?php

namespace Drupal\Core\FileTransfer\Form;

use Drupal\Core\Form\FormBase;
6
use Drupal\Core\Form\FormStateInterface;
7
use Drupal\Core\Render\Element;
8
use Symfony\Component\DependencyInjection\ContainerInterface;
9
use Symfony\Component\HttpFoundation\Response;
10 11 12

/**
 * Provides the file transfer authorization form.
13 14
 *
 * @internal
15 16 17
 */
class FileTransferAuthorizeForm extends FormBase {

18 19 20 21 22
  /**
   * The app root.
   *
   * @var string
   */
23
  protected $root;
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38

  /**
   * Constructs a new FileTransferAuthorizeForm object.
   *
   * @param string $root
   *   The app root.
   */
  public function __construct($root) {
    $this->root = $root;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
39
    return new static($container->getParameter('app.root'));
40 41
  }

42 43 44
  /**
   * {@inheritdoc}
   */
45
  public function getFormId() {
46 47 48 49 50 51
    return 'authorize_filetransfer_form';
  }

  /**
   * {@inheritdoc}
   */
52
  public function buildForm(array $form, FormStateInterface $form_state) {
53
    // Get all the available ways to transfer files.
54 55
    $available_backends = $this->getRequest()->getSession()->get('authorize_filetransfer_info', []);
    if (empty($available_backends)) {
56
      $this->messenger()->addError($this->t('Unable to continue, no available methods of file transfer'));
57
      return [];
58 59 60
    }

    if (!$this->getRequest()->isSecure()) {
61
      $form['information']['https_warning'] = [
62
        '#prefix' => '<div class="messages messages--error">',
63
        '#markup' => $this->t('WARNING: You are not using an encrypted connection, so your password will be sent in plain text. <a href=":https-link">Learn more</a>.', [':https-link' => 'https://www.drupal.org/https-information']),
64
        '#suffix' => '</div>',
65
      ];
66 67 68
    }

    // Decide on a default backend.
69
    $authorize_filetransfer_default = $form_state->getValue(['connection_settings', 'authorize_filetransfer_default']);
70
    if (!$authorize_filetransfer_default) {
71 72 73
      $authorize_filetransfer_default = key($available_backends);
    }

74
    $form['information']['main_header'] = [
75 76 77
      '#prefix' => '<h3>',
      '#markup' => $this->t('To continue, provide your server connection details'),
      '#suffix' => '</h3>',
78
    ];
79 80

    $form['connection_settings']['#tree'] = TRUE;
81
    $form['connection_settings']['authorize_filetransfer_default'] = [
82 83 84 85
      '#type' => 'select',
      '#title' => $this->t('Connection method'),
      '#default_value' => $authorize_filetransfer_default,
      '#weight' => -10,
86
    ];
87 88 89 90 91 92 93 94

    /*
     * Here we create two submit buttons. For a JS enabled client, they will
     * only ever see submit_process. However, if a client doesn't have JS
     * enabled, they will see submit_connection on the first form (when picking
     * what filetransfer type to use, and submit_process on the second one (which
     * leads to the actual operation).
     */
95
    $form['submit_connection'] = [
96 97 98 99 100
      '#prefix' => "<br style='clear:both'/>",
      '#name' => 'enter_connection_settings',
      '#type' => 'submit',
      '#value' => $this->t('Enter connection settings'),
      '#weight' => 100,
101
    ];
102

103
    $form['submit_process'] = [
104 105 106 107
      '#name' => 'process_updates',
      '#type' => 'submit',
      '#value' => $this->t('Continue'),
      '#weight' => 100,
108
    ];
109 110 111 112

    // Build a container for each connection type.
    foreach ($available_backends as $name => $backend) {
      $form['connection_settings']['authorize_filetransfer_default']['#options'][$name] = $backend['title'];
113
      $form['connection_settings'][$name] = [
114
        '#type' => 'container',
115 116 117 118 119 120 121
        '#attributes' => ['class' => ["filetransfer-$name", 'filetransfer']],
        '#states' => [
          'visible' => [
            'select[name="connection_settings[authorize_filetransfer_default]"]' => ['value' => $name],
          ],
        ],
      ];
122 123
      // We can't use #prefix on the container itself since then the header won't
      // be hidden and shown when the containers are being manipulated via JS.
124 125 126
      $form['connection_settings'][$name]['header'] = [
        '#markup' => '<h4>' . $this->t('@backend connection settings', ['@backend' => $backend['title']]) . '</h4>',
      ];
127 128 129 130

      $form['connection_settings'][$name] += $this->addConnectionSettings($name);

      // Start non-JS code.
131
      if ($form_state->getValue(['connection_settings', 'authorize_filetransfer_default']) == $name) {
132 133

        // Change the submit button to the submit_process one.
134
        $form['submit_process']['#attributes'] = [];
135 136 137 138 139 140 141 142
        unset($form['submit_connection']);

        // Activate the proper filetransfer settings form.
        $form['connection_settings'][$name]['#attributes']['style'] = 'display:block';
        // Disable the select box.
        $form['connection_settings']['authorize_filetransfer_default']['#disabled'] = TRUE;

        // Create a button for changing the type of connection.
143
        $form['connection_settings']['change_connection_type'] = [
144 145 146 147
          '#name' => 'change_connection_type',
          '#type' => 'submit',
          '#value' => $this->t('Change connection type'),
          '#weight' => -5,
148 149
          '#attributes' => ['class' => ['filetransfer-change-connection-type']],
        ];
150 151 152 153 154 155 156 157 158
      }
      // End non-JS code.
    }
    return $form;
  }

  /**
   * {@inheritdoc}
   */
159
  public function validateForm(array &$form, FormStateInterface $form_state) {
160 161
    // Only validate the form if we have collected all of the user input and are
    // ready to proceed with updating or installing.
162
    if ($form_state->getTriggeringElement()['#name'] != 'process_updates') {
163 164 165
      return;
    }

166 167 168
    if ($form_connection_settings = $form_state->getValue('connection_settings')) {
      $backend = $form_connection_settings['authorize_filetransfer_default'];
      $filetransfer = $this->getFiletransfer($backend, $form_connection_settings[$backend]);
169 170
      try {
        if (!$filetransfer) {
171
          throw new \Exception("The connection protocol '$backend' does not exist.");
172 173 174 175 176 177
        }
        $filetransfer->connect();
      }
      catch (\Exception $e) {
        // The format of this error message is similar to that used on the
        // database connection form in the installer.
178
        $form_state->setErrorByName('connection_settings', $this->t('Failed to connect to the server. The server reports the following message: <p class="error">@message</p> For more help adding or updating code on your server, see the <a href=":handbook_url">handbook</a>.', [
179
          '@message' => $e->getMessage(),
180
          ':handbook_url' => 'https://www.drupal.org/docs/8/extending-drupal-8/overview',
181
        ]));
182 183 184 185 186 187 188
      }
    }
  }

  /**
   * {@inheritdoc}
   */
189
  public function submitForm(array &$form, FormStateInterface $form_state) {
190
    $form_connection_settings = $form_state->getValue('connection_settings');
191
    switch ($form_state->getTriggeringElement()['#name']) {
192 193 194
      case 'process_updates':

        // Save the connection settings to the DB.
195
        $filetransfer_backend = $form_connection_settings['authorize_filetransfer_default'];
196 197 198 199 200 201

        // If the database is available then try to save our settings. We have
        // to make sure it is available since this code could potentially (will
        // likely) be called during the installation process, before the
        // database is set up.
        try {
202
          $filetransfer = $this->getFiletransfer($filetransfer_backend, $form_connection_settings[$filetransfer_backend]);
203 204

          // Now run the operation.
205 206 207 208
          $response = $this->runOperation($filetransfer);
          if ($response instanceof Response) {
            $form_state->setResponse($response);
          }
209 210 211 212 213 214 215 216 217
        }
        catch (\Exception $e) {
          // If there is no database available, we don't care and just skip
          // this part entirely.
        }

        break;

      case 'enter_connection_settings':
218
        $form_state->setRebuild();
219 220 221
        break;

      case 'change_connection_type':
222
        $form_state->setRebuild();
223
        $form_state->unsetValue(['connection_settings', 'authorize_filetransfer_default']);
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
        break;
    }
  }

  /**
   * Gets a FileTransfer class for a specific transfer method and settings.
   *
   * @param $backend
   *   The FileTransfer backend to get the class for.
   * @param $settings
   *   Array of settings for the FileTransfer.
   *
   * @return \Drupal\Core\FileTransfer\FileTransfer|bool
   *   An instantiated FileTransfer object for the requested method and settings,
   *   or FALSE if there was an error finding or instantiating it.
   */
240
  protected function getFiletransfer($backend, $settings = []) {
241
    $filetransfer = FALSE;
242 243 244 245
    $info = $this->getRequest()->getSession()->get('authorize_filetransfer_info', []);
    if (!empty($info[$backend])) {
      if (class_exists($info[$backend]['class'])) {
        $filetransfer = $info[$backend]['class']::factory($this->root, $settings);
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
      }
    }
    return $filetransfer;
  }

  /**
   * Generates the Form API array for a given connection backend's settings.
   *
   * @param string $backend
   *   The name of the backend (e.g. 'ftp', 'ssh', etc).
   *
   * @return array
   *   Form API array of connection settings for the given backend.
   *
   * @see hook_filetransfer_backends()
   */
  protected function addConnectionSettings($backend) {
263 264
    $defaults = [];
    $form = [];
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309

    // Create an instance of the file transfer class to get its settings form.
    $filetransfer = $this->getFiletransfer($backend);
    if ($filetransfer) {
      $form = $filetransfer->getSettingsForm();
    }
    // Fill in the defaults based on the saved settings, if any.
    $this->setConnectionSettingsDefaults($form, NULL, $defaults);
    return $form;
  }

  /**
   * Sets the default settings on a file transfer connection form recursively.
   *
   * The default settings for the file transfer connection forms are saved in
   * the database. The settings are stored as a nested array in the case of a
   * settings form that has details or otherwise uses a nested structure.
   * Therefore, to properly add defaults, we need to walk through all the
   * children form elements and process those defaults recursively.
   *
   * @param $element
   *   Reference to the Form API form element we're operating on.
   * @param $key
   *   The key for our current form element, if any.
   * @param array $defaults
   *   The default settings for the file transfer backend we're operating on.
   */
  protected function setConnectionSettingsDefaults(&$element, $key, array $defaults) {
    // If we're operating on a form element which isn't a details, and we have
    // a default setting saved, stash it in #default_value.
    if (!empty($key) && isset($defaults[$key]) && isset($element['#type']) && $element['#type'] != 'details') {
      $element['#default_value'] = $defaults[$key];
    }
    // Now, we walk through all the child elements, and recursively invoke
    // ourselves on each one. Since the $defaults settings array can be nested
    // (because of #tree, any values inside details will be nested), if
    // there's a subarray of settings for the form key we're currently
    // processing, pass in that subarray to the recursive call. Otherwise, just
    // pass on the whole $defaults array.
    foreach (Element::children($element) as $child_key) {
      $this->setConnectionSettingsDefaults($element[$child_key], $child_key, ((isset($defaults[$key]) && is_array($defaults[$key])) ? $defaults[$key] : $defaults));
    }
  }

  /**
310
   * Runs the operation specified in 'authorize_operation' session property.
311 312 313
   *
   * @param $filetransfer
   *   The FileTransfer object to use for running the operation.
314 315 316 317 318
   *
   * @return \Symfony\Component\HttpFoundation\Response|null
   *   The result of running the operation. If this is an instance of
   *   \Symfony\Component\HttpFoundation\Response the calling code should use
   *   that response for the current page request.
319 320
   */
  protected function runOperation($filetransfer) {
321
    $operation = $this->getRequest()->getSession()->remove('authorize_operation');
322

323
    require_once $operation['file'];
324
    return call_user_func_array($operation['callback'], array_merge([$filetransfer], $operation['arguments']));
325 326 327
  }

}