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