Version 1
[yaffs-website] / web / core / lib / Drupal / Core / Updater / Updater.php
1 <?php
2
3 namespace Drupal\Core\Updater;
4
5 use Drupal\Component\Utility\Unicode;
6 use Drupal\Core\FileTransfer\FileTransferException;
7 use Drupal\Core\FileTransfer\FileTransfer;
8
9 /**
10  * Defines the base class for Updaters used in Drupal.
11  */
12 class Updater {
13
14   /**
15    * Directory to install from.
16    *
17    * @var string
18    */
19   public $source;
20
21   /**
22    * The root directory under which new projects will be copied.
23    *
24    * @var string
25    */
26   protected $root;
27
28   /**
29    * Constructs a new updater.
30    *
31    * @param string $source
32    *   Directory to install from.
33    * @param string $root
34    *   The root directory under which the project will be copied to if it's a
35    *   new project. Usually this is the app root (the directory in which the
36    *   Drupal site is installed).
37    */
38   public function __construct($source, $root) {
39     $this->source = $source;
40     $this->root = $root;
41     $this->name = self::getProjectName($source);
42     $this->title = self::getProjectTitle($source);
43   }
44
45   /**
46    * Returns an Updater of the appropriate type depending on the source.
47    *
48    * If a directory is provided which contains a module, will return a
49    * ModuleUpdater.
50    *
51    * @param string $source
52    *   Directory of a Drupal project.
53    * @param string $root
54    *   The root directory under which the project will be copied to if it's a
55    *   new project. Usually this is the app root (the directory in which the
56    *   Drupal site is installed).
57    *
58    * @return \Drupal\Core\Updater\Updater
59    *   A new Drupal\Core\Updater\Updater object.
60    *
61    * @throws \Drupal\Core\Updater\UpdaterException
62    */
63   public static function factory($source, $root) {
64     if (is_dir($source)) {
65       $updater = self::getUpdaterFromDirectory($source);
66     }
67     else {
68       throw new UpdaterException(t('Unable to determine the type of the source directory.'));
69     }
70     return new $updater($source, $root);
71   }
72
73   /**
74    * Determines which Updater class can operate on the given directory.
75    *
76    * @param string $directory
77    *   Extracted Drupal project.
78    *
79    * @return string
80    *   The class name which can work with this project type.
81    *
82    * @throws \Drupal\Core\Updater\UpdaterException
83    */
84   public static function getUpdaterFromDirectory($directory) {
85     // Gets a list of possible implementing classes.
86     $updaters = drupal_get_updaters();
87     foreach ($updaters as $updater) {
88       $class = $updater['class'];
89       if (call_user_func([$class, 'canUpdateDirectory'], $directory)) {
90         return $class;
91       }
92     }
93     throw new UpdaterException(t('Cannot determine the type of project.'));
94   }
95
96   /**
97    * Determines what the most important (or only) info file is in a directory.
98    *
99    * Since there is no enforcement of which info file is the project's "main"
100    * info file, this will get one with the same name as the directory, or the
101    * first one it finds.  Not ideal, but needs a larger solution.
102    *
103    * @param string $directory
104    *   Directory to search in.
105    *
106    * @return string
107    *   Path to the info file.
108    */
109   public static function findInfoFile($directory) {
110     $info_files = file_scan_directory($directory, '/.*\.info.yml$/');
111     if (!$info_files) {
112       return FALSE;
113     }
114     foreach ($info_files as $info_file) {
115       if (Unicode::substr($info_file->filename, 0, -9) == drupal_basename($directory)) {
116         // Info file Has the same name as the directory, return it.
117         return $info_file->uri;
118       }
119     }
120     // Otherwise, return the first one.
121     $info_file = array_shift($info_files);
122     return $info_file->uri;
123   }
124
125   /**
126    * Get Extension information from directory.
127    *
128    * @param string $directory
129    *   Directory to search in.
130    *
131    * @return array
132    *   Extension info.
133    *
134    * @throws \Drupal\Core\Updater\UpdaterException
135    *   If the info parser does not provide any info.
136    */
137   protected static function getExtensionInfo($directory) {
138     $info_file = static::findInfoFile($directory);
139     $info = \Drupal::service('info_parser')->parse($info_file);
140     if (empty($info)) {
141       throw new UpdaterException(t('Unable to parse info file: %info_file.', ['%info_file' => $info_file]));
142     }
143
144     return $info;
145   }
146
147   /**
148    * Gets the name of the project directory (basename).
149    *
150    * @todo It would be nice, if projects contained an info file which could
151    *   provide their canonical name.
152    *
153    * @param string $directory
154    *
155    * @return string
156    *   The name of the project.
157    */
158   public static function getProjectName($directory) {
159     return drupal_basename($directory);
160   }
161
162   /**
163    * Returns the project name from a Drupal info file.
164    *
165    * @param string $directory
166    *   Directory to search for the info file.
167    *
168    * @return string
169    *   The title of the project.
170    *
171    * @throws \Drupal\Core\Updater\UpdaterException
172    */
173   public static function getProjectTitle($directory) {
174     $info_file = self::findInfoFile($directory);
175     $info = \Drupal::service('info_parser')->parse($info_file);
176     if (empty($info)) {
177       throw new UpdaterException(t('Unable to parse info file: %info_file.', ['%info_file' => $info_file]));
178     }
179     return $info['name'];
180   }
181
182   /**
183    * Stores the default parameters for the Updater.
184    *
185    * @param array $overrides
186    *   An array of overrides for the default parameters.
187    *
188    * @return array
189    *   An array of configuration parameters for an update or install operation.
190    */
191   protected function getInstallArgs($overrides = []) {
192     $args = [
193       'make_backup' => FALSE,
194       'install_dir' => $this->getInstallDirectory(),
195       'backup_dir'  => $this->getBackupDir(),
196     ];
197     return array_merge($args, $overrides);
198   }
199
200   /**
201    * Updates a Drupal project and returns a list of next actions.
202    *
203    * @param \Drupal\Core\FileTransfer\FileTransfer $filetransfer
204    *   Object that is a child of FileTransfer. Used for moving files
205    *   to the server.
206    * @param array $overrides
207    *   An array of settings to override defaults; see self::getInstallArgs().
208    *
209    * @return array
210    *   An array of links which the user may need to complete the update
211    *
212    * @throws \Drupal\Core\Updater\UpdaterException
213    * @throws \Drupal\Core\Updater\UpdaterFileTransferException
214    */
215   public function update(&$filetransfer, $overrides = []) {
216     try {
217       // Establish arguments with possible overrides.
218       $args = $this->getInstallArgs($overrides);
219
220       // Take a Backup.
221       if ($args['make_backup']) {
222         $this->makeBackup($filetransfer, $args['install_dir'], $args['backup_dir']);
223       }
224
225       if (!$this->name) {
226         // This is bad, don't want to delete the install directory.
227         throw new UpdaterException(t('Fatal error in update, cowardly refusing to wipe out the install directory.'));
228       }
229
230       // Make sure the installation parent directory exists and is writable.
231       $this->prepareInstallDirectory($filetransfer, $args['install_dir']);
232
233       if (is_dir($args['install_dir'] . '/' . $this->name)) {
234         // Remove the existing installed file.
235         $filetransfer->removeDirectory($args['install_dir'] . '/' . $this->name);
236       }
237
238       // Copy the directory in place.
239       $filetransfer->copyDirectory($this->source, $args['install_dir']);
240
241       // Make sure what we just installed is readable by the web server.
242       $this->makeWorldReadable($filetransfer, $args['install_dir'] . '/' . $this->name);
243
244       // Run the updates.
245       // @todo Decide if we want to implement this.
246       $this->postUpdate();
247
248       // For now, just return a list of links of things to do.
249       return $this->postUpdateTasks();
250     }
251     catch (FileTransferException $e) {
252       throw new UpdaterFileTransferException(t('File Transfer failed, reason: @reason', ['@reason' => strtr($e->getMessage(), $e->arguments)]));
253     }
254   }
255
256   /**
257    * Installs a Drupal project, returns a list of next actions.
258    *
259    * @param \Drupal\Core\FileTransfer\FileTransfer $filetransfer
260    *   Object that is a child of FileTransfer.
261    * @param array $overrides
262    *   An array of settings to override defaults; see self::getInstallArgs().
263    *
264    * @return array
265    *   An array of links which the user may need to complete the install.
266    *
267    * @throws \Drupal\Core\Updater\UpdaterFileTransferException
268    */
269   public function install(&$filetransfer, $overrides = []) {
270     try {
271       // Establish arguments with possible overrides.
272       $args = $this->getInstallArgs($overrides);
273
274       // Make sure the installation parent directory exists and is writable.
275       $this->prepareInstallDirectory($filetransfer, $args['install_dir']);
276
277       // Copy the directory in place.
278       $filetransfer->copyDirectory($this->source, $args['install_dir']);
279
280       // Make sure what we just installed is readable by the web server.
281       $this->makeWorldReadable($filetransfer, $args['install_dir'] . '/' . $this->name);
282
283       // Potentially enable something?
284       // @todo Decide if we want to implement this.
285       $this->postInstall();
286       // For now, just return a list of links of things to do.
287       return $this->postInstallTasks();
288     }
289     catch (FileTransferException $e) {
290       throw new UpdaterFileTransferException(t('File Transfer failed, reason: @reason', ['@reason' => strtr($e->getMessage(), $e->arguments)]));
291     }
292   }
293
294   /**
295    * Makes sure the installation parent directory exists and is writable.
296    *
297    * @param \Drupal\Core\FileTransfer\FileTransfer $filetransfer
298    *   Object which is a child of FileTransfer.
299    * @param string $directory
300    *   The installation directory to prepare.
301    *
302    * @throws \Drupal\Core\Updater\UpdaterException
303    */
304   public function prepareInstallDirectory(&$filetransfer, $directory) {
305     // Make the parent dir writable if need be and create the dir.
306     if (!is_dir($directory)) {
307       $parent_dir = dirname($directory);
308       if (!is_writable($parent_dir)) {
309         @chmod($parent_dir, 0755);
310         // It is expected that this will fail if the directory is owned by the
311         // FTP user. If the FTP user == web server, it will succeed.
312         try {
313           $filetransfer->createDirectory($directory);
314           $this->makeWorldReadable($filetransfer, $directory);
315         }
316         catch (FileTransferException $e) {
317           // Probably still not writable. Try to chmod and do it again.
318           // @todo Make a new exception class so we can catch it differently.
319           try {
320             $old_perms = substr(sprintf('%o', fileperms($parent_dir)), -4);
321             $filetransfer->chmod($parent_dir, 0755);
322             $filetransfer->createDirectory($directory);
323             $this->makeWorldReadable($filetransfer, $directory);
324             // Put the permissions back.
325             $filetransfer->chmod($parent_dir, intval($old_perms, 8));
326           }
327           catch (FileTransferException $e) {
328             $message = t($e->getMessage(), $e->arguments);
329             $throw_message = t('Unable to create %directory due to the following: %reason', ['%directory' => $directory, '%reason' => $message]);
330             throw new UpdaterException($throw_message);
331           }
332         }
333         // Put the parent directory back.
334         @chmod($parent_dir, 0555);
335       }
336     }
337   }
338
339   /**
340    * Ensures that a given directory is world readable.
341    *
342    * @param \Drupal\Core\FileTransfer\FileTransfer $filetransfer
343    *   Object which is a child of FileTransfer.
344    * @param string $path
345    *   The file path to make world readable.
346    * @param bool $recursive
347    *   If the chmod should be applied recursively.
348    */
349   public function makeWorldReadable(&$filetransfer, $path, $recursive = TRUE) {
350     if (!is_executable($path)) {
351       // Set it to read + execute.
352       $new_perms = substr(sprintf('%o', fileperms($path)), -4, -1) . "5";
353       $filetransfer->chmod($path, intval($new_perms, 8), $recursive);
354     }
355   }
356
357   /**
358    * Performs a backup.
359    *
360    * @param \Drupal\Core\FileTransfer\FileTransfer $filetransfer
361    *   Object which is a child of FileTransfer.
362    * @param string $from
363    *   The file path to copy from.
364    * @param string $to
365    *   The file path to copy to.
366    *
367    * @todo Not implemented: https://www.drupal.org/node/2474355
368    */
369   public function makeBackup(FileTransfer $filetransfer, $from, $to) {
370   }
371
372   /**
373    * Returns the full path to a directory where backups should be written.
374    */
375   public function getBackupDir() {
376     return \Drupal::service('stream_wrapper_manager')->getViaScheme('temporary')->getDirectoryPath();
377   }
378
379   /**
380    * Performs actions after new code is updated.
381    */
382   public function postUpdate() {
383   }
384
385   /**
386    * Performs actions after installation.
387    */
388   public function postInstall() {
389   }
390
391   /**
392    * Returns an array of links to pages that should be visited post operation.
393    *
394    * @return array
395    *   Links which provide actions to take after the install is finished.
396    */
397   public function postInstallTasks() {
398     return [];
399   }
400
401   /**
402    * Returns an array of links to pages that should be visited post operation.
403    *
404    * @return array
405    *   Links which provide actions to take after the update is finished.
406    */
407   public function postUpdateTasks() {
408     return [];
409   }
410
411 }