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
|
}
|