Chris@0: source = $source; Chris@0: $this->root = $root; Chris@0: $this->name = self::getProjectName($source); Chris@0: $this->title = self::getProjectTitle($source); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns an Updater of the appropriate type depending on the source. Chris@0: * Chris@0: * If a directory is provided which contains a module, will return a Chris@0: * ModuleUpdater. Chris@0: * Chris@0: * @param string $source Chris@0: * Directory of a Drupal project. Chris@0: * @param string $root Chris@0: * The root directory under which the project will be copied to if it's a Chris@0: * new project. Usually this is the app root (the directory in which the Chris@0: * Drupal site is installed). Chris@0: * Chris@0: * @return \Drupal\Core\Updater\Updater Chris@0: * A new Drupal\Core\Updater\Updater object. Chris@0: * Chris@0: * @throws \Drupal\Core\Updater\UpdaterException Chris@0: */ Chris@0: public static function factory($source, $root) { Chris@0: if (is_dir($source)) { Chris@0: $updater = self::getUpdaterFromDirectory($source); Chris@0: } Chris@0: else { Chris@0: throw new UpdaterException(t('Unable to determine the type of the source directory.')); Chris@0: } Chris@0: return new $updater($source, $root); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Determines which Updater class can operate on the given directory. Chris@0: * Chris@0: * @param string $directory Chris@0: * Extracted Drupal project. Chris@0: * Chris@0: * @return string Chris@0: * The class name which can work with this project type. Chris@0: * Chris@0: * @throws \Drupal\Core\Updater\UpdaterException Chris@0: */ Chris@0: public static function getUpdaterFromDirectory($directory) { Chris@0: // Gets a list of possible implementing classes. Chris@0: $updaters = drupal_get_updaters(); Chris@0: foreach ($updaters as $updater) { Chris@0: $class = $updater['class']; Chris@0: if (call_user_func([$class, 'canUpdateDirectory'], $directory)) { Chris@0: return $class; Chris@0: } Chris@0: } Chris@0: throw new UpdaterException(t('Cannot determine the type of project.')); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Determines what the most important (or only) info file is in a directory. Chris@0: * Chris@0: * Since there is no enforcement of which info file is the project's "main" Chris@0: * info file, this will get one with the same name as the directory, or the Chris@0: * first one it finds. Not ideal, but needs a larger solution. Chris@0: * Chris@0: * @param string $directory Chris@0: * Directory to search in. Chris@0: * Chris@0: * @return string Chris@0: * Path to the info file. Chris@0: */ Chris@0: public static function findInfoFile($directory) { Chris@0: $info_files = file_scan_directory($directory, '/.*\.info.yml$/'); Chris@0: if (!$info_files) { Chris@0: return FALSE; Chris@0: } Chris@0: foreach ($info_files as $info_file) { Chris@18: if (mb_substr($info_file->filename, 0, -9) == \Drupal::service('file_system')->basename($directory)) { Chris@0: // Info file Has the same name as the directory, return it. Chris@0: return $info_file->uri; Chris@0: } Chris@0: } Chris@0: // Otherwise, return the first one. Chris@0: $info_file = array_shift($info_files); Chris@0: return $info_file->uri; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Get Extension information from directory. Chris@0: * Chris@0: * @param string $directory Chris@0: * Directory to search in. Chris@0: * Chris@0: * @return array Chris@0: * Extension info. Chris@0: * Chris@0: * @throws \Drupal\Core\Updater\UpdaterException Chris@0: * If the info parser does not provide any info. Chris@0: */ Chris@0: protected static function getExtensionInfo($directory) { Chris@0: $info_file = static::findInfoFile($directory); Chris@0: $info = \Drupal::service('info_parser')->parse($info_file); Chris@0: if (empty($info)) { Chris@0: throw new UpdaterException(t('Unable to parse info file: %info_file.', ['%info_file' => $info_file])); Chris@0: } Chris@0: Chris@0: return $info; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the name of the project directory (basename). Chris@0: * Chris@0: * @todo It would be nice, if projects contained an info file which could Chris@0: * provide their canonical name. Chris@0: * Chris@0: * @param string $directory Chris@0: * Chris@0: * @return string Chris@0: * The name of the project. Chris@0: */ Chris@0: public static function getProjectName($directory) { Chris@18: return \Drupal::service('file_system')->basename($directory); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the project name from a Drupal info file. Chris@0: * Chris@0: * @param string $directory Chris@0: * Directory to search for the info file. Chris@0: * Chris@0: * @return string Chris@0: * The title of the project. Chris@0: * Chris@0: * @throws \Drupal\Core\Updater\UpdaterException Chris@0: */ Chris@0: public static function getProjectTitle($directory) { Chris@0: $info_file = self::findInfoFile($directory); Chris@0: $info = \Drupal::service('info_parser')->parse($info_file); Chris@0: if (empty($info)) { Chris@0: throw new UpdaterException(t('Unable to parse info file: %info_file.', ['%info_file' => $info_file])); Chris@0: } Chris@0: return $info['name']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Stores the default parameters for the Updater. Chris@0: * Chris@0: * @param array $overrides Chris@0: * An array of overrides for the default parameters. Chris@0: * Chris@0: * @return array Chris@0: * An array of configuration parameters for an update or install operation. Chris@0: */ Chris@0: protected function getInstallArgs($overrides = []) { Chris@0: $args = [ Chris@0: 'make_backup' => FALSE, Chris@0: 'install_dir' => $this->getInstallDirectory(), Chris@0: 'backup_dir' => $this->getBackupDir(), Chris@0: ]; Chris@0: return array_merge($args, $overrides); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Updates a Drupal project and returns a list of next actions. Chris@0: * Chris@0: * @param \Drupal\Core\FileTransfer\FileTransfer $filetransfer Chris@0: * Object that is a child of FileTransfer. Used for moving files Chris@0: * to the server. Chris@0: * @param array $overrides Chris@0: * An array of settings to override defaults; see self::getInstallArgs(). Chris@0: * Chris@0: * @return array Chris@0: * An array of links which the user may need to complete the update Chris@0: * Chris@0: * @throws \Drupal\Core\Updater\UpdaterException Chris@0: * @throws \Drupal\Core\Updater\UpdaterFileTransferException Chris@0: */ Chris@0: public function update(&$filetransfer, $overrides = []) { Chris@0: try { Chris@0: // Establish arguments with possible overrides. Chris@0: $args = $this->getInstallArgs($overrides); Chris@0: Chris@0: // Take a Backup. Chris@0: if ($args['make_backup']) { Chris@0: $this->makeBackup($filetransfer, $args['install_dir'], $args['backup_dir']); Chris@0: } Chris@0: Chris@0: if (!$this->name) { Chris@0: // This is bad, don't want to delete the install directory. Chris@0: throw new UpdaterException(t('Fatal error in update, cowardly refusing to wipe out the install directory.')); Chris@0: } Chris@0: Chris@0: // Make sure the installation parent directory exists and is writable. Chris@0: $this->prepareInstallDirectory($filetransfer, $args['install_dir']); Chris@0: Chris@0: if (is_dir($args['install_dir'] . '/' . $this->name)) { Chris@0: // Remove the existing installed file. Chris@0: $filetransfer->removeDirectory($args['install_dir'] . '/' . $this->name); Chris@0: } Chris@0: Chris@0: // Copy the directory in place. Chris@0: $filetransfer->copyDirectory($this->source, $args['install_dir']); Chris@0: Chris@0: // Make sure what we just installed is readable by the web server. Chris@0: $this->makeWorldReadable($filetransfer, $args['install_dir'] . '/' . $this->name); Chris@0: Chris@0: // Run the updates. Chris@0: // @todo Decide if we want to implement this. Chris@0: $this->postUpdate(); Chris@0: Chris@0: // For now, just return a list of links of things to do. Chris@0: return $this->postUpdateTasks(); Chris@0: } Chris@0: catch (FileTransferException $e) { Chris@0: throw new UpdaterFileTransferException(t('File Transfer failed, reason: @reason', ['@reason' => strtr($e->getMessage(), $e->arguments)])); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Installs a Drupal project, returns a list of next actions. Chris@0: * Chris@0: * @param \Drupal\Core\FileTransfer\FileTransfer $filetransfer Chris@0: * Object that is a child of FileTransfer. Chris@0: * @param array $overrides Chris@0: * An array of settings to override defaults; see self::getInstallArgs(). Chris@0: * Chris@0: * @return array Chris@0: * An array of links which the user may need to complete the install. Chris@0: * Chris@0: * @throws \Drupal\Core\Updater\UpdaterFileTransferException Chris@0: */ Chris@0: public function install(&$filetransfer, $overrides = []) { Chris@0: try { Chris@0: // Establish arguments with possible overrides. Chris@0: $args = $this->getInstallArgs($overrides); Chris@0: Chris@0: // Make sure the installation parent directory exists and is writable. Chris@0: $this->prepareInstallDirectory($filetransfer, $args['install_dir']); Chris@0: Chris@0: // Copy the directory in place. Chris@0: $filetransfer->copyDirectory($this->source, $args['install_dir']); Chris@0: Chris@0: // Make sure what we just installed is readable by the web server. Chris@0: $this->makeWorldReadable($filetransfer, $args['install_dir'] . '/' . $this->name); Chris@0: Chris@0: // Potentially enable something? Chris@0: // @todo Decide if we want to implement this. Chris@0: $this->postInstall(); Chris@0: // For now, just return a list of links of things to do. Chris@0: return $this->postInstallTasks(); Chris@0: } Chris@0: catch (FileTransferException $e) { Chris@0: throw new UpdaterFileTransferException(t('File Transfer failed, reason: @reason', ['@reason' => strtr($e->getMessage(), $e->arguments)])); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Makes sure the installation parent directory exists and is writable. Chris@0: * Chris@0: * @param \Drupal\Core\FileTransfer\FileTransfer $filetransfer Chris@0: * Object which is a child of FileTransfer. Chris@0: * @param string $directory Chris@0: * The installation directory to prepare. Chris@0: * Chris@0: * @throws \Drupal\Core\Updater\UpdaterException Chris@0: */ Chris@0: public function prepareInstallDirectory(&$filetransfer, $directory) { Chris@0: // Make the parent dir writable if need be and create the dir. Chris@0: if (!is_dir($directory)) { Chris@0: $parent_dir = dirname($directory); Chris@0: if (!is_writable($parent_dir)) { Chris@0: @chmod($parent_dir, 0755); Chris@0: // It is expected that this will fail if the directory is owned by the Chris@0: // FTP user. If the FTP user == web server, it will succeed. Chris@0: try { Chris@0: $filetransfer->createDirectory($directory); Chris@0: $this->makeWorldReadable($filetransfer, $directory); Chris@0: } Chris@0: catch (FileTransferException $e) { Chris@0: // Probably still not writable. Try to chmod and do it again. Chris@0: // @todo Make a new exception class so we can catch it differently. Chris@0: try { Chris@0: $old_perms = substr(sprintf('%o', fileperms($parent_dir)), -4); Chris@0: $filetransfer->chmod($parent_dir, 0755); Chris@0: $filetransfer->createDirectory($directory); Chris@0: $this->makeWorldReadable($filetransfer, $directory); Chris@0: // Put the permissions back. Chris@0: $filetransfer->chmod($parent_dir, intval($old_perms, 8)); Chris@0: } Chris@0: catch (FileTransferException $e) { Chris@0: $message = t($e->getMessage(), $e->arguments); Chris@0: $throw_message = t('Unable to create %directory due to the following: %reason', ['%directory' => $directory, '%reason' => $message]); Chris@0: throw new UpdaterException($throw_message); Chris@0: } Chris@0: } Chris@0: // Put the parent directory back. Chris@0: @chmod($parent_dir, 0555); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Ensures that a given directory is world readable. Chris@0: * Chris@0: * @param \Drupal\Core\FileTransfer\FileTransfer $filetransfer Chris@0: * Object which is a child of FileTransfer. Chris@0: * @param string $path Chris@0: * The file path to make world readable. Chris@0: * @param bool $recursive Chris@0: * If the chmod should be applied recursively. Chris@0: */ Chris@0: public function makeWorldReadable(&$filetransfer, $path, $recursive = TRUE) { Chris@0: if (!is_executable($path)) { Chris@0: // Set it to read + execute. Chris@0: $new_perms = substr(sprintf('%o', fileperms($path)), -4, -1) . "5"; Chris@0: $filetransfer->chmod($path, intval($new_perms, 8), $recursive); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Performs a backup. Chris@0: * Chris@0: * @param \Drupal\Core\FileTransfer\FileTransfer $filetransfer Chris@0: * Object which is a child of FileTransfer. Chris@0: * @param string $from Chris@0: * The file path to copy from. Chris@0: * @param string $to Chris@0: * The file path to copy to. Chris@0: * Chris@0: * @todo Not implemented: https://www.drupal.org/node/2474355 Chris@0: */ Chris@0: public function makeBackup(FileTransfer $filetransfer, $from, $to) { Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the full path to a directory where backups should be written. Chris@0: */ Chris@0: public function getBackupDir() { Chris@0: return \Drupal::service('stream_wrapper_manager')->getViaScheme('temporary')->getDirectoryPath(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Performs actions after new code is updated. Chris@0: */ Chris@0: public function postUpdate() { Chris@0: } Chris@0: Chris@0: /** Chris@0: * Performs actions after installation. Chris@0: */ Chris@0: public function postInstall() { Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns an array of links to pages that should be visited post operation. Chris@0: * Chris@0: * @return array Chris@0: * Links which provide actions to take after the install is finished. Chris@0: */ Chris@0: public function postInstallTasks() { Chris@0: return []; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns an array of links to pages that should be visited post operation. Chris@0: * Chris@0: * @return array Chris@0: * Links which provide actions to take after the update is finished. Chris@0: */ Chris@0: public function postUpdateTasks() { Chris@0: return []; Chris@0: } Chris@0: Chris@0: }