Project

General

Profile

Paste
Download (11.7 KB) Statistics
| Branch: | Revision:

root / drupal7 / includes / filetransfer / filetransfer.inc @ 7dd8ec42

1
<?php
2

    
3
/*
4
 * Base FileTransfer class.
5
 *
6
 * Classes extending this class perform file operations on directories not
7
 * writable by the webserver. To achieve this, the class should connect back
8
 * to the server using some backend (for example FTP or SSH). To keep security,
9
 * the password should always be asked from the user and never stored. For
10
 * safety, all methods operate only inside a "jail", by default the Drupal root.
11
 */
12
abstract class FileTransfer {
13
  protected $username;
14
  protected $password;
15
  protected $hostname = 'localhost';
16
  protected $port;
17

    
18
  /**
19
   * The constructor for the UpdateConnection class. This method is also called
20
   * from the classes that extend this class and override this method.
21
   */
22
  function __construct($jail) {
23
    $this->jail = $jail;
24
  }
25

    
26
  /**
27
   * Classes that extend this class must override the factory() static method.
28
   *
29
   * @param string $jail
30
   *   The full path where all file operations performed by this object will
31
   *   be restricted to. This prevents the FileTransfer classes from being
32
   *   able to touch other parts of the filesystem.
33
   * @param array $settings
34
   *   An array of connection settings for the FileTransfer subclass. If the
35
   *   getSettingsForm() method uses any nested settings, the same structure
36
   *   will be assumed here.
37
   * @return object
38
   *   New instance of the appropriate FileTransfer subclass.
39
   */
40
  static function factory($jail, $settings) {
41
    throw new FileTransferException('FileTransfer::factory() static method not overridden by FileTransfer subclass.');
42
  }
43

    
44
  /**
45
   * Implementation of the magic __get() method.
46
   *
47
   * If the connection isn't set to anything, this will call the connect() method
48
   * and set it to and return the result; afterwards, the connection will be
49
   * returned directly without using this method.
50
   */
51
  function __get($name) {
52
    if ($name == 'connection') {
53
      $this->connect();
54
      return $this->connection;
55
    }
56

    
57
    if ($name == 'chroot') {
58
      $this->setChroot();
59
      return $this->chroot;
60
    }
61
  }
62

    
63
  /**
64
   * Connect to the server.
65
   */
66
  abstract protected function connect();
67

    
68
  /**
69
   * Copies a directory.
70
   *
71
   * @param $source
72
   *   The source path.
73
   * @param $destination
74
   *   The destination path.
75
   */
76
  public final function copyDirectory($source, $destination) {
77
    $source = $this->sanitizePath($source);
78
    $destination = $this->fixRemotePath($destination);
79
    $this->checkPath($destination);
80
    $this->copyDirectoryJailed($source, $destination);
81
  }
82

    
83
  /**
84
   * @see http://php.net/chmod
85
   *
86
   * @param string $path
87
   * @param long $mode
88
   * @param bool $recursive
89
   */
90
  public final function chmod($path, $mode, $recursive = FALSE) {
91
    if (!in_array('FileTransferChmodInterface', class_implements(get_class($this)))) {
92
      throw new FileTransferException('Unable to change file permissions');
93
    }
94
    $path = $this->sanitizePath($path);
95
    $path = $this->fixRemotePath($path);
96
    $this->checkPath($path);
97
    $this->chmodJailed($path, $mode, $recursive);
98
  }
99

    
100
  /**
101
   * Creates a directory.
102
   *
103
   * @param $directory
104
   *   The directory to be created.
105
   */
106
  public final function createDirectory($directory) {
107
    $directory = $this->fixRemotePath($directory);
108
    $this->checkPath($directory);
109
    $this->createDirectoryJailed($directory);
110
  }
111

    
112
  /**
113
   * Removes a directory.
114
   *
115
   * @param $directory
116
   *   The directory to be removed.
117
   */
118
  public final function removeDirectory($directory) {
119
    $directory = $this->fixRemotePath($directory);
120
    $this->checkPath($directory);
121
    $this->removeDirectoryJailed($directory);
122
  }
123

    
124
  /**
125
   * Copies a file.
126
   *
127
   * @param $source
128
   *   The source file.
129
   * @param $destination
130
   *   The destination file.
131
   */
132
  public final function copyFile($source, $destination) {
133
    $source = $this->sanitizePath($source);
134
    $destination = $this->fixRemotePath($destination);
135
    $this->checkPath($destination);
136
    $this->copyFileJailed($source, $destination);
137
  }
138

    
139
  /**
140
   * Removes a file.
141
   *
142
   * @param $destination
143
   *   The destination file to be removed.
144
   */
145
  public final function removeFile($destination) {
146
    $destination = $this->fixRemotePath($destination);
147
    $this->checkPath($destination);
148
    $this->removeFileJailed($destination);
149
  }
150

    
151
  /**
152
   * Checks that the path is inside the jail and throws an exception if not.
153
   *
154
   * @param $path
155
   *   A path to check against the jail.
156
   */
157
  protected final function checkPath($path) {
158
    $full_jail = $this->chroot . $this->jail;
159
    $full_path = drupal_realpath(substr($this->chroot . $path, 0, strlen($full_jail)));
160
    $full_path = $this->fixRemotePath($full_path, FALSE);
161
    if ($full_jail !== $full_path) {
162
      throw new FileTransferException('@directory is outside of the @jail', NULL, array('@directory' => $path, '@jail' => $this->jail));
163
    }
164
  }
165

    
166
  /**
167
   * Returns a modified path suitable for passing to the server.
168
   * If a path is a windows path, makes it POSIX compliant by removing the drive letter.
169
   * If $this->chroot has a value, it is stripped from the path to allow for
170
   * chroot'd filetransfer systems.
171
   *
172
   * @param $path
173
   * @param $strip_chroot
174
   *
175
   * @return string
176
   */
177
  protected final function fixRemotePath($path, $strip_chroot = TRUE) {
178
    $path = $this->sanitizePath($path);
179
    $path = preg_replace('|^([a-z]{1}):|i', '', $path); // Strip out windows driveletter if its there.
180
    if ($strip_chroot) {
181
      if ($this->chroot && strpos($path, $this->chroot) === 0) {
182
        $path = ($path == $this->chroot) ? '' : substr($path, strlen($this->chroot));
183
      }
184
    }
185
    return $path;
186
  }
187

    
188
  /**
189
  * Changes backslashes to slashes, also removes a trailing slash.
190
  *
191
  * @param string $path
192
  * @return string
193
  */
194
  function sanitizePath($path) {
195
    $path = str_replace('\\', '/', $path); // Windows path sanitization.
196
    if (substr($path, -1) == '/') {
197
      $path = substr($path, 0, -1);
198
    }
199
    return $path;
200
  }
201

    
202
  /**
203
   * Copies a directory.
204
   *
205
   * We need a separate method to make the $destination is in the jail.
206
   *
207
   * @param $source
208
   *   The source path.
209
   * @param $destination
210
   *   The destination path.
211
   */
212
  protected function copyDirectoryJailed($source, $destination) {
213
    if ($this->isDirectory($destination)) {
214
      $destination = $destination . '/' . drupal_basename($source);
215
    }
216
    $this->createDirectory($destination);
217
    foreach (new RecursiveIteratorIterator(new SkipDotsRecursiveDirectoryIterator($source), RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) {
218
      $relative_path = substr($filename, strlen($source));
219
      if ($file->isDir()) {
220
        $this->createDirectory($destination . $relative_path);
221
      }
222
      else {
223
        $this->copyFile($file->getPathName(), $destination . $relative_path);
224
      }
225
    }
226
  }
227

    
228
  /**
229
   * Creates a directory.
230
   *
231
   * @param $directory
232
   *   The directory to be created.
233
   */
234
  abstract protected function createDirectoryJailed($directory);
235

    
236
  /**
237
   * Removes a directory.
238
   *
239
   * @param $directory
240
   *   The directory to be removed.
241
   */
242
  abstract protected function removeDirectoryJailed($directory);
243

    
244
  /**
245
   * Copies a file.
246
   *
247
   * @param $source
248
   *   The source file.
249
   * @param $destination
250
   *   The destination file.
251
   */
252
  abstract protected function copyFileJailed($source, $destination);
253

    
254
  /**
255
   * Removes a file.
256
   *
257
   * @param $destination
258
   *   The destination file to be removed.
259
   */
260
  abstract protected function removeFileJailed($destination);
261

    
262
  /**
263
   * Checks if a particular path is a directory
264
   *
265
   * @param $path
266
   *   The path to check
267
   *
268
   * @return boolean
269
   */
270
  abstract public function isDirectory($path);
271

    
272
  /**
273
   * Checks if a particular path is a file (not a directory).
274
   *
275
   * @param $path
276
   *   The path to check
277
   *
278
   * @return boolean
279
   */
280
  abstract public function isFile($path);
281

    
282
  /**
283
   * Return the chroot property for this connection.
284
   *
285
   * It does this by moving up the tree until it finds itself. If successful,
286
   * it will return the chroot, otherwise FALSE.
287
   *
288
   * @return
289
   *   The chroot path for this connection or FALSE.
290
   */
291
  function findChroot() {
292
    // If the file exists as is, there is no chroot.
293
    $path = __FILE__;
294
    $path = $this->fixRemotePath($path, FALSE);
295
    if ($this->isFile($path)) {
296
      return FALSE;
297
    }
298

    
299
    $path = dirname(__FILE__);
300
    $path = $this->fixRemotePath($path, FALSE);
301
    $parts = explode('/', $path);
302
    $chroot = '';
303
    while (count($parts)) {
304
      $check = implode($parts, '/');
305
      if ($this->isFile($check . '/' . drupal_basename(__FILE__))) {
306
        // Remove the trailing slash.
307
        return substr($chroot, 0, -1);
308
      }
309
      $chroot .= array_shift($parts) . '/';
310
    }
311
    return FALSE;
312
  }
313

    
314
  /**
315
   * Sets the chroot and changes the jail to match the correct path scheme
316
   *
317
   */
318
  function setChroot() {
319
    $this->chroot = $this->findChroot();
320
    $this->jail = $this->fixRemotePath($this->jail);
321
  }
322

    
323
  /**
324
   * Returns a form to collect connection settings credentials.
325
   *
326
   * Implementing classes can either extend this form with fields collecting the
327
   * specific information they need, or override it entirely.
328
   */
329
  public function getSettingsForm() {
330
    $form['username'] = array(
331
      '#type' => 'textfield',
332
      '#title' => t('Username'),
333
    );
334
    $form['password'] = array(
335
      '#type' => 'password',
336
      '#title' => t('Password'),
337
      '#description' => t('Your password is not saved in the database and is only used to establish a connection.'),
338
    );
339
    $form['advanced'] = array(
340
      '#type' => 'fieldset',
341
      '#title' => t('Advanced settings'),
342
      '#collapsible' => TRUE,
343
      '#collapsed' => TRUE,
344
    );
345
    $form['advanced']['hostname'] = array(
346
      '#type' => 'textfield',
347
      '#title' => t('Host'),
348
      '#default_value' => 'localhost',
349
      '#description' => t('The connection will be created between your web server and the machine hosting the web server files. In the vast majority of cases, this will be the same machine, and "localhost" is correct.'),
350
    );
351
    $form['advanced']['port'] = array(
352
      '#type' => 'textfield',
353
      '#title' => t('Port'),
354
      '#default_value' => NULL,
355
    );
356
    return $form;
357
  }
358
}
359

    
360
/**
361
 * FileTransferException class.
362
 */
363
class FileTransferException extends Exception {
364
  public $arguments;
365

    
366
  function __construct($message, $code = 0, $arguments = array()) {
367
    parent::__construct($message, $code);
368
    $this->arguments = $arguments;
369
  }
370
}
371

    
372

    
373
/**
374
 * A FileTransfer Class implementing this interface can be used to chmod files.
375
 */
376
interface FileTransferChmodInterface {
377

    
378
  /**
379
   * Changes the permissions of the file / directory specified in $path
380
   *
381
   * @param string $path
382
   *   Path to change permissions of.
383
   * @param long $mode
384
   *   The new file permission mode to be passed to chmod().
385
   * @param boolean $recursive
386
   *   Pass TRUE to recursively chmod the entire directory specified in $path.
387
   */
388
  function chmodJailed($path, $mode, $recursive);
389
}
390

    
391
/**
392
 * Provides an interface for iterating recursively over filesystem directories.
393
 *
394
 * Manually skips '.' and '..' directories, since no existing method is
395
 * available in PHP 5.2.
396
 *
397
 * @todo Depreciate in favor of RecursiveDirectoryIterator::SKIP_DOTS once PHP
398
 *   5.3 or later is required.
399
 */
400
class SkipDotsRecursiveDirectoryIterator extends RecursiveDirectoryIterator {
401
  /**
402
   * Constructs a SkipDotsRecursiveDirectoryIterator
403
   *
404
   * @param $path
405
   *   The path of the directory to be iterated over.
406
   */
407
  function __construct($path) {
408
    parent::__construct($path);
409
    $this->skipdots();
410
  }
411

    
412
  function rewind() {
413
    parent::rewind();
414
    $this->skipdots();
415
  }
416

    
417
  function next() {
418
    parent::next();
419
    $this->skipdots();
420
  }
421

    
422
  protected function skipdots() {
423
    while ($this->isDot()) {
424
      parent::next();
425
    }
426
  }
427
}