3f8fd6d36442899c64fed2abf4dc0f26436dd4e5
[yaffs-website] / web / core / lib / Drupal / Core / FileTransfer / Form / FileTransferAuthorizeForm.php
1 <?php
2
3 namespace Drupal\Core\FileTransfer\Form;
4
5 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 class FileTransferAuthorizeForm extends FormBase {
17
18   /**
19    * The app root.
20    *
21    * @var string
22    */
23   protected  $root;
24
25   /**
26    * Constructs a new FileTransferAuthorizeForm object.
27    *
28    * @param string $root
29    *   The app root.
30    */
31   public function __construct($root) {
32     $this->root = $root;
33   }
34
35   /**
36    * {@inheritdoc}
37    */
38   public static function create(ContainerInterface $container) {
39     return new static ($container->get('app.root'));
40   }
41
42   /**
43    * {@inheritdoc}
44    */
45   public function getFormId() {
46     return 'authorize_filetransfer_form';
47   }
48
49   /**
50    * {@inheritdoc}
51    */
52   public function buildForm(array $form, FormStateInterface $form_state) {
53     // Get all the available ways to transfer files.
54     if (empty($_SESSION['authorize_filetransfer_info'])) {
55       drupal_set_message($this->t('Unable to continue, no available methods of file transfer'), 'error');
56       return [];
57     }
58     $available_backends = $_SESSION['authorize_filetransfer_info'];
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       $authorize_filetransfer_default = key($available_backends);
72     }
73
74     $form['information']['main_header'] = [
75       '#prefix' => '<h3>',
76       '#markup' => $this->t('To continue, provide your server connection details'),
77       '#suffix' => '</h3>',
78     ];
79
80     $form['connection_settings']['#tree'] = TRUE;
81     $form['connection_settings']['authorize_filetransfer_default'] = [
82       '#type' => 'select',
83       '#title' => $this->t('Connection method'),
84       '#default_value' => $authorize_filetransfer_default,
85       '#weight' => -10,
86     ];
87
88     /*
89      * Here we create two submit buttons. For a JS enabled client, they will
90      * only ever see submit_process. However, if a client doesn't have JS
91      * enabled, they will see submit_connection on the first form (when picking
92      * what filetransfer type to use, and submit_process on the second one (which
93      * leads to the actual operation).
94      */
95     $form['submit_connection'] = [
96       '#prefix' => "<br style='clear:both'/>",
97       '#name' => 'enter_connection_settings',
98       '#type' => 'submit',
99       '#value' => $this->t('Enter connection settings'),
100       '#weight' => 100,
101     ];
102
103     $form['submit_process'] = [
104       '#name' => 'process_updates',
105       '#type' => 'submit',
106       '#value' => $this->t('Continue'),
107       '#weight' => 100,
108     ];
109
110     // Build a container for each connection type.
111     foreach ($available_backends as $name => $backend) {
112       $form['connection_settings']['authorize_filetransfer_default']['#options'][$name] = $backend['title'];
113       $form['connection_settings'][$name] = [
114         '#type' => 'container',
115         '#attributes' => ['class' => ["filetransfer-$name", 'filetransfer']],
116         '#states' => [
117           'visible' => [
118             'select[name="connection_settings[authorize_filetransfer_default]"]' => ['value' => $name],
119           ],
120         ],
121       ];
122       // We can't use #prefix on the container itself since then the header won't
123       // be hidden and shown when the containers are being manipulated via JS.
124       $form['connection_settings'][$name]['header'] = [
125         '#markup' => '<h4>' . $this->t('@backend connection settings', ['@backend' => $backend['title']]) . '</h4>',
126       ];
127
128       $form['connection_settings'][$name] += $this->addConnectionSettings($name);
129
130       // 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         unset($form['submit_connection']);
136
137         // Activate the proper filetransfer settings form.
138         $form['connection_settings'][$name]['#attributes']['style'] = 'display:block';
139         // Disable the select box.
140         $form['connection_settings']['authorize_filetransfer_default']['#disabled'] = TRUE;
141
142         // Create a button for changing the type of connection.
143         $form['connection_settings']['change_connection_type'] = [
144           '#name' => 'change_connection_type',
145           '#type' => 'submit',
146           '#value' => $this->t('Change connection type'),
147           '#weight' => -5,
148           '#attributes' => ['class' => ['filetransfer-change-connection-type']],
149         ];
150       }
151       // End non-JS code.
152     }
153     return $form;
154   }
155
156   /**
157    * {@inheritdoc}
158    */
159   public function validateForm(array &$form, FormStateInterface $form_state) {
160     // Only validate the form if we have collected all of the user input and are
161     // ready to proceed with updating or installing.
162     if ($form_state->getTriggeringElement()['#name'] != 'process_updates') {
163       return;
164     }
165
166     if ($form_connection_settings = $form_state->getValue('connection_settings')) {
167       $backend = $form_connection_settings['authorize_filetransfer_default'];
168       $filetransfer = $this->getFiletransfer($backend, $form_connection_settings[$backend]);
169       try {
170         if (!$filetransfer) {
171           throw new \Exception($this->t('The connection protocol %backend does not exist.', ['%backend' => $backend]));
172         }
173         $filetransfer->connect();
174       }
175       catch (\Exception $e) {
176         // The format of this error message is similar to that used on the
177         // 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 installing 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/documentation/install/modules-themes',
181         ]));
182       }
183     }
184   }
185
186   /**
187    * {@inheritdoc}
188    */
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       case 'process_updates':
193
194         // Save the connection settings to the DB.
195         $filetransfer_backend = $form_connection_settings['authorize_filetransfer_default'];
196
197         // If the database is available then try to save our settings. We have
198         // to make sure it is available since this code could potentially (will
199         // likely) be called during the installation process, before the
200         // database is set up.
201         try {
202           $filetransfer = $this->getFiletransfer($filetransfer_backend, $form_connection_settings[$filetransfer_backend]);
203
204           // Now run the operation.
205           $response = $this->runOperation($filetransfer);
206           if ($response instanceof Response) {
207             $form_state->setResponse($response);
208           }
209         }
210         catch (\Exception $e) {
211           // If there is no database available, we don't care and just skip
212           // this part entirely.
213         }
214
215         break;
216
217       case 'enter_connection_settings':
218         $form_state->setRebuild();
219         break;
220
221       case 'change_connection_type':
222         $form_state->setRebuild();
223         $form_state->unsetValue(['connection_settings', 'authorize_filetransfer_default']);
224         break;
225     }
226   }
227
228   /**
229    * Gets a FileTransfer class for a specific transfer method and settings.
230    *
231    * @param $backend
232    *   The FileTransfer backend to get the class for.
233    * @param $settings
234    *   Array of settings for the FileTransfer.
235    *
236    * @return \Drupal\Core\FileTransfer\FileTransfer|bool
237    *   An instantiated FileTransfer object for the requested method and settings,
238    *   or FALSE if there was an error finding or instantiating it.
239    */
240   protected function getFiletransfer($backend, $settings = []) {
241     $filetransfer = FALSE;
242     if (!empty($_SESSION['authorize_filetransfer_info'][$backend])) {
243       $backend_info = $_SESSION['authorize_filetransfer_info'][$backend];
244       if (class_exists($backend_info['class'])) {
245         $filetransfer = $backend_info['class']::factory($this->root, $settings);
246       }
247     }
248     return $filetransfer;
249   }
250
251   /**
252    * Generates the Form API array for a given connection backend's settings.
253    *
254    * @param string $backend
255    *   The name of the backend (e.g. 'ftp', 'ssh', etc).
256    *
257    * @return array
258    *   Form API array of connection settings for the given backend.
259    *
260    * @see hook_filetransfer_backends()
261    */
262   protected function addConnectionSettings($backend) {
263     $defaults = [];
264     $form = [];
265
266     // Create an instance of the file transfer class to get its settings form.
267     $filetransfer = $this->getFiletransfer($backend);
268     if ($filetransfer) {
269       $form = $filetransfer->getSettingsForm();
270     }
271     // Fill in the defaults based on the saved settings, if any.
272     $this->setConnectionSettingsDefaults($form, NULL, $defaults);
273     return $form;
274   }
275
276   /**
277    * Sets the default settings on a file transfer connection form recursively.
278    *
279    * The default settings for the file transfer connection forms are saved in
280    * the database. The settings are stored as a nested array in the case of a
281    * settings form that has details or otherwise uses a nested structure.
282    * Therefore, to properly add defaults, we need to walk through all the
283    * children form elements and process those defaults recursively.
284    *
285    * @param $element
286    *   Reference to the Form API form element we're operating on.
287    * @param $key
288    *   The key for our current form element, if any.
289    * @param array $defaults
290    *   The default settings for the file transfer backend we're operating on.
291    */
292   protected function setConnectionSettingsDefaults(&$element, $key, array $defaults) {
293     // If we're operating on a form element which isn't a details, and we have
294     // a default setting saved, stash it in #default_value.
295     if (!empty($key) && isset($defaults[$key]) && isset($element['#type']) && $element['#type'] != 'details') {
296       $element['#default_value'] = $defaults[$key];
297     }
298     // Now, we walk through all the child elements, and recursively invoke
299     // ourselves on each one. Since the $defaults settings array can be nested
300     // (because of #tree, any values inside details will be nested), if
301     // there's a subarray of settings for the form key we're currently
302     // processing, pass in that subarray to the recursive call. Otherwise, just
303     // pass on the whole $defaults array.
304     foreach (Element::children($element) as $child_key) {
305       $this->setConnectionSettingsDefaults($element[$child_key], $child_key, ((isset($defaults[$key]) && is_array($defaults[$key])) ? $defaults[$key] : $defaults));
306     }
307   }
308
309   /**
310    * Runs the operation specified in $_SESSION['authorize_operation'].
311    *
312    * @param $filetransfer
313    *   The FileTransfer object to use for running the operation.
314    *
315    * @return \Symfony\Component\HttpFoundation\Response|null
316    *   The result of running the operation. If this is an instance of
317    *   \Symfony\Component\HttpFoundation\Response the calling code should use
318    *   that response for the current page request.
319    */
320   protected function runOperation($filetransfer) {
321     $operation = $_SESSION['authorize_operation'];
322     unset($_SESSION['authorize_operation']);
323
324     require_once $operation['file'];
325     return call_user_func_array($operation['callback'], array_merge([$filetransfer], $operation['arguments']));
326   }
327
328 }