Updater.php 13 KB

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