class AcsfInitCommands in Acquia Cloud Site Factory Connector 8.2
Provides drush commands to set up a codebase for Acquia Cloud Site Factory.
This class' namespace choice is off but it is necessary. All the commands in this file are executed with the --include flag, and without reusing this namespace the commands would not be found.
@package Drush\Commands
Hierarchy
- class \Drush\Commands\AcsfInitCommands extends \Drush\Commands\DrushCommands
Expanded class hierarchy of AcsfInitCommands
File
- acsf_init/
src/ Commands/ AcsfInitCommands.php, line 21
Namespace
Drush\CommandsView source
class AcsfInitCommands extends DrushCommands {
/**
* Content that we need to add to customers' .htaccess files.
*/
const ACSF_HTACCESS_PATCH = 'RewriteCond %{REQUEST_URI} !/sites/g/apc_rebuild.php$';
/**
* The marker that tells us where we need to add our .htaccess patch.
*/
const ACSF_HTACCESS_PATCH_MARKER = 'RewriteRule "^(.+/.*|autoload)\\.php($|/)" - [F]';
/**
* A comment to include with the htaccess patch line.
*/
const ACSF_HTACCESS_PATCH_COMMENT = ' # ACSF requirement: allow access to apc_rebuild.php.';
/**
* Initial delimiter string to enclose code added by acsf-init.
*/
const ACSF_INIT_CODE_DELIMITER_START = '// ===== Added by acsf-init, please do not delete. Section start. =====';
/**
* Closing delimiter string to enclose code added by acsf-init.
*/
const ACSF_INIT_CODE_DELIMITER_END = '// ===== Added by acsf-init, please do not delete. Section end. =====';
/**
* Add the necessary classes.
*
* @hook pre-command *
*/
public function preInit() {
$path = dirname(dirname(dirname(__DIR__))) . '/src';
$classes = [
'AcsfException',
'AcsfInitException',
'AcsfInitHtaccessException',
];
foreach ($classes as $class) {
require_once "{$path}/{$class}.php";
}
}
/**
* Make this repository compatible with Acquia Site Factory.
*
* Installs/updates the non-standard Drupal components for this repository to
* be compatible with Acquia Site Factory. This command will update in place,
* so there is no harm in running it multiple times.
*
* @param array $options
* The command options supplied to the executed command.
*
* @throws \Drupal\acsf\AcsfException
* If the command cannot be executed.
*
* @command acsf-init
* @option skip-default-settings Do not edit the default settings.php file.
* Use this option when the edited default settings.php is causing issues in
* a local environment.
* @bootstrap root
*/
public function init(array $options = [
'skip-default-settings' => FALSE,
]) {
$drupal_root = realpath(DRUPAL_ROOT);
if (basename($drupal_root) !== 'docroot') {
// If for some reason we wanted to enable this command to run on directory
// structures where no /docroot exists, that's possible in theory... but
// we need to to audit / change the code in this file to
// - derive the repo root in a good way;
// - properly distinguish the repo root from the drupal root;
// - figure out what to do with remaining instances of "docroot", if any;
// - figure out what to do when drupal root and repo root are equal...
// like not copy hook files?
throw new AcsfException(dt("Drupal must be installed in a subdirectory of your code repository, named 'docroot'."));
}
$repo_root = dirname($drupal_root);
$skip_default_settings = $options['skip-default-settings'];
$this
->output()
->writeln(dt('Installing ACSF requirements.'));
// Create the required directories.
foreach ($this
->getRequiredDirs($repo_root) as $name => $dir) {
// Skip '../sites/default' if --skip-default-settings is set.
if ($skip_default_settings && $name == 'site env default') {
continue;
}
$this
->output()
->writeln(dt('Creating directory for !name at !dir', [
'!name' => $name,
'!dir' => $dir,
]));
if (!file_exists($dir)) {
if (mkdir($dir, 0755, TRUE)) {
$this
->logger()
->success(dt('Success'));
}
else {
$this
->logger()
->error(dt('Error'));
}
}
else {
$this
->logger()
->notice(dt('Already Exists'));
}
}
// Copy the required files.
$lib_path = sprintf('%s/lib', dirname(dirname(dirname(__FILE__))));
foreach ($this
->getRequiredFiles($repo_root) as $location) {
$file = $location['filename'];
$dest = sprintf('%s/%s', $location['dest'], $file);
// Some files only contain a destination as they are already in place.
if (isset($location['source']) && isset($location['dest'])) {
$source = sprintf('%s/%s/%s', $lib_path, $location['source'], $file);
$this
->output()
->writeln(dt('Copying !file to !dest.', [
'!file' => $source,
'!dest' => $dest,
]));
if (file_exists($dest)) {
$confirm = $this
->io()
->confirm(dt('Destination file exists, continue?'));
if ($confirm === FALSE) {
continue;
}
}
// Copy the file into the destination.
if (copy($source, $dest)) {
$this
->logger()
->success(dt('Copy Success: !file', [
'!file' => $file,
]));
}
else {
$this
->logger()
->error(dt('Copy Error: !file', [
'!file' => $file,
]));
}
// If the file exists, it could be set to 0444, so we have to ensure
// that it is writable before overwriting it. The copy would fail
// otherwise.
if (!is_writable($dest)) {
if (!chmod($dest, 0666)) {
$this
->logger()
->error(dt('Chmod Error: !file', [
'!file' => $file,
]));
}
}
}
// Chmod the file if required.
$mod = isset($location['mod']) ? $location['mod'] : FALSE;
if ($mod && chmod($dest, $mod)) {
$this
->logger()
->success(dt('Chmod Success: !file', [
'!file' => $file,
]));
}
elseif ($mod) {
$this
->logger()
->error(dt('Chmod Error: !file', [
'!file' => $file,
]));
}
}
try {
$this
->patchHtaccess();
} catch (AcsfInitHtaccessException $e) {
$this
->logger()
->error($e
->getMessage());
}
// The default settings.php file needs special handling. On the ACSF
// infrastructure our own business logic needs to execute while on ACE or on
// a local environment the default settings.php could be used to drive other
// sites. For this reason the ACSF specific code will be included in the
// file instead of rewriting it to contain only our code.
if (!$skip_default_settings) {
$this
->output()
->writeln(dt('Updating the default settings.php file with the ACSF specific business logic.'));
$edit_allowed = TRUE;
$default_settings_php_path = $repo_root . '/docroot/sites/default/settings.php';
if (file_exists($default_settings_php_path)) {
$edit_allowed = $this
->io()
->confirm(dt('Destination file exists, continue?'));
}
if ($edit_allowed !== FALSE) {
// If the file exists, it could be set to 0444, so we have to ensure
// that it is writable.
if (file_exists($default_settings_php_path) && !is_writable($default_settings_php_path)) {
if (!chmod($default_settings_php_path, 0666)) {
$this
->logger()
->error(dt('Chmod Error: !file', [
'!file' => $default_settings_php_path,
]));
}
}
if (file_exists($default_settings_php_path)) {
// If the current default settings.php file has the same content as
// acsf.legacy.default.settings.php then this file can be rewritten
// according to the new approach. A simple strict equality check
// should be enough since the acsf-init-verify checked the deployed
// files by comparing md5 hashes, so even a single character
// difference would have caused an error in the code deployment
// process.
$current_default_settings_php = file_get_contents($default_settings_php_path);
$legacy_default_settings_php = file_get_contents($lib_path . '/sites/default/acsf.legacy.default.settings.php');
if ($current_default_settings_php === $legacy_default_settings_php) {
$this
->defaultSettingsPhpCreate($default_settings_php_path);
}
else {
// Update the default settings.php file with the latest ACSF code.
$this
->defaultSettingsPhpUpdate($default_settings_php_path);
}
}
else {
// The default settings.php file does not exist yet, so create a new
// file with the necessary include.
$this
->defaultSettingsPhpCreate($default_settings_php_path);
}
}
}
// Verify that the files are in sync.
clearstatcache();
try {
$this
->initVerify($options);
$this
->output()
->writeln(dt("Be sure to commit any changes to your repository before deploying. This includes files like sites/default/settings.php; you can use 'git status --ignored' to make sure no changes are inadvertantly ignored by a custom git configuration."));
} catch (AcsfException $e) {
$this
->logger()
->error($e
->getMessage());
}
}
/**
* Verifies that acsf-init was successfully run in the current version.
*
* @param array $options
* The command options supplied to the executed command.
*
* @throws \Drupal\acsf\AcsfException
* If something is wrong with the current codebase.
*
* @command acsf-init-verify
* @option skip-default-settings Skip verifying the default settings.php file.
* @bootstrap root
*/
public function initVerify(array $options = [
'skip-default-settings' => NULL,
]) {
$drupal_root = realpath(DRUPAL_ROOT);
if (basename($drupal_root) !== 'docroot') {
// If for some reason we wanted to enable this command to run on directory
// structures where no /docroot exists, that's possible in theory... but
// we need to to audit / change the code in this file to
// - derive the repo root in a good way;
// - properly distinguish the repo root from the drupal root;
// - figure out what to do with remaining instances of "docroot", if any;
// - figure out what to do when drupal root and repo root are equal...
// like not copy hook files?
throw new AcsfException(dt("Drupal must be installed in a subdirectory of your code repository, named 'docroot'."));
}
$repo_root = dirname($drupal_root);
$skip_default_settings = $options['skip-default-settings'];
$lib_path = sprintf('%s/lib', dirname(dirname(dirname(__FILE__))));
$error = FALSE;
foreach ($this
->getRequiredFiles($repo_root) as $location) {
$file = $location['filename'];
$dest = sprintf('%s/%s', $location['dest'], $file);
// Some files only contain a destination as they are already in place.
if (isset($location['source']) && isset($location['dest'])) {
$source = sprintf('%s/%s/%s', $lib_path, $location['source'], $file);
if (!file_exists($dest)) {
$error = TRUE;
$this
->logger()
->error(dt('The file !file is missing.', [
'!file' => $file,
]));
}
elseif (md5_file($source) != md5_file($dest)) {
$error = TRUE;
$this
->logger()
->error(dt('The file !file is out of date.', [
'!file' => $file,
]));
}
}
// Verify the file is executable.
// Note: The approach here is to not check for the exact file perms (in
// other words to not test against the 'mod' element), since git - by
// design - does not respect anything beyond a simple executable bit, the
// other perms may be filesystem/OS-dependent, and can't be guaranteed to
// be consistent.
if (file_exists($dest) && !empty($location['test_executable'])) {
$dest_permissions = fileperms($dest);
// We do want to test the owner executable bit, and the group executable
// bit as well.
// e.g. to test whether the owner has execute permission, it is the case
// of testing with: 00000000 01000000 (which is 0100 in octal, 64 in
// decimal).
if (($dest_permissions & (0100 | 010)) != (0100 | 010)) {
$error = TRUE;
$this
->logger()
->error(dt('The file !file is not executable. Make this file executable for the owner and group, then commit it again.', [
'!file' => $file,
]));
}
}
}
if (!$this
->testHtaccessIsPatched()) {
$error = TRUE;
$this
->logger()
->error(dt('The .htaccess file has not been patched to allow access to apc_rebuild.php.'));
}
// Skip the default settings.php file if --skip-default-settings is set.
if (!$skip_default_settings) {
// Check that the default settings.php contains the necessary ACSF
// business logic.
$acsf_business_logic = $this
->defaultSettingsPhpIncludeGet();
// Break up the business logic by lines.
$acsf_business_logic_fragments = explode("\n", $acsf_business_logic);
// Examine each line in the business logic to make sure it appears in the
// file. This way minor indentation changes will not cause failure.
$missing_piece = FALSE;
$default_settings_php_contents = file_get_contents($repo_root . '/docroot/sites/default/settings.php');
foreach ($acsf_business_logic_fragments as $line) {
if (strpos($default_settings_php_contents, $line) === FALSE) {
$missing_piece = TRUE;
break;
}
}
if ($missing_piece) {
$error = TRUE;
$this
->logger()
->error(dt('The default settings.php file is out of date.'));
}
}
if ($error) {
throw new AcsfException(dt('Please run drush acsf-init to correct these problems and commit the resulting code changes.'));
}
else {
// The Site Factory code deployment uses this string to determine if the
// acsf-init has been properly run. If this is changed, also ensure that
// the check in VcsVerifyAcsf matches.
$this
->logger()
->success(dt('acsf-init required files ok'));
}
}
/**
* Connect a site to a factory by setting up the right database variables.
*
* @param array $options
* The command options supplied to the executed command.
*
* @throws \Drupal\acsf\AcsfException
* If the provided email address is invalid.
* @throws \InvalidArgumentException
* If one or more arguments are missing or invalid.
* @throws \Drush\Exceptions\UserAbortException
* If the customer does not allow the creation of a new account.
*
* @command acsf-connect-factory
* @aliases acsf-cf
* @option site-admin-mail The email address of the Site Factory admin /
* Gardens admin user. This is typically the "Site Factory admin" user on
* the factory. These email addresses have to match in order for the initial
* OpenID connection to bind these accounts.
* @option site-owner-name The name of the site owner.
* @option site-owner-mail The email address of the site owner.
* @option site-owner-roles A list of comma-separated roles (machine names)
* that should be granted to the site owner (optional).
* @usage drush acsf-connect-factory --site-admin-mail="user3@example.com"
* --site-owner-name="John Smith" --site-owner-mail="john.smith@example.com"
* Connect the site to the factory and sets the owner to John Smith.
* @bootstrap full
*/
public function connectFactory(array $options = [
'site-admin-mail' => NULL,
'site-owner-name' => NULL,
'site-owner-mail' => NULL,
'site-owner-roles' => NULL,
]) {
// Preliminary validation before starting to modify the database.
$site_admin_mail = trim($options['site-admin-mail']);
$site_owner_name = trim($options['site-owner-name']);
$site_owner_mail = trim($options['site-owner-mail']);
$site_owner_roles = array_filter(explode(',', $options['site-owner-roles']));
// Validate email addresses.
$validator = \Drupal::service('email.validator');
if (!$validator
->isValid($site_admin_mail)) {
throw new \InvalidArgumentException(dt('The site-admin-mail value is not a valid email address.'));
}
if (!$validator
->isValid($site_owner_mail)) {
throw new \InvalidArgumentException(dt('The site-owner-mail value is not a valid email address.'));
}
$user_storage = \Drupal::entityTypeManager()
->getStorage('user');
// Make sure there is no regular user account with the admin email address.
$site_admin_mail_accounts = $user_storage
->loadByProperties([
'mail' => $site_admin_mail,
]);
$site_admin_mail_account = reset($site_admin_mail_accounts);
if ($site_admin_mail_account && $site_admin_mail_account
->id() > 1) {
throw new AcsfException(dt('Unable to sync the admin account, the email address @mail is already used by the user account @uid.', [
'@mail' => $site_admin_mail,
'@uid' => $site_admin_mail_account
->id(),
]));
}
// The site owner's email address may have been changed on the factory (for
// instance, if the user updated their email address on the factory and the
// new email address has not yet been synced to the site). First, try to
// locate the user account by site-owner-mail.
$site_owner_accounts = $user_storage
->loadByProperties([
'mail' => $site_owner_mail,
]);
$site_owner_account = reset($site_owner_accounts);
if ($site_owner_account && $site_owner_account
->getAccountName() !== $site_owner_name) {
throw new AcsfException(dt('The site-owner-name value does not match the name of the user loaded by site-owner-mail.'));
}
// If the site owner user account is not found, try to locate it by
// site-owner-name.
if (!$site_owner_account) {
$site_owner_accounts = $user_storage
->loadByProperties([
'name' => $site_owner_name,
]);
$site_owner_account = reset($site_owner_accounts);
}
// If the site owner account is still not found then either the customer has
// made a typo or maybe there is going to be a new owner who needs a new
// account. Ask for confirmation to create a new account.
if (!$site_owner_account) {
if (!$this
->io()
->confirm(dt('The site owner name or email address that you provided does not correspond to any account on the site. Do you want to create a new account?'))) {
throw new UserAbortException();
}
}
// Clear all caches ahead of time so Drupal has a chance to rebuild
// registries.
drupal_flush_all_caches();
acsf_build_registry();
$this
->logger()
->info(dt('Cleared all caches.'));
// Set default settings for user accounts.
$admin_role_ids = \Drupal::EntityQuery('user_role')
->condition('is_admin', TRUE)
->execute();
// Take over uid 1 with our Site Factory admin user.
$admin_account = User::load(1);
// Create a new user if uid 1 doesn't exist.
if (!$admin_account) {
$admin_account = User::create([
'uid' => 1,
]);
}
// Ensure the default admin role is added to the account.
$admin_account
->addRole(reset($admin_role_ids));
// Set login time to avoid e-mail verification needed error.
$admin_account
->setLastLoginTime(1)
->setUsername('Site Factory admin')
->setEmail($site_admin_mail)
->setPassword(user_password())
->activate()
->save();
// Create or update site owner account.
// Prepare roles for site owner.
if (!$site_owner_account) {
$site_owner_account = User::create();
}
foreach ($site_owner_roles as $owner_role) {
if (Role::load($owner_role)) {
$site_owner_account
->addRole($owner_role);
}
else {
$this
->logger()
->warning(dt('The role @role does not exist; not adding it to the site owner.', [
'@role' => $owner_role,
]));
}
}
// Site owners also get the default administrator role.
$site_owner_account
->addRole(reset($admin_role_ids));
$site_owner_account
->setLastLoginTime(1)
->setUsername($site_owner_name)
->setEmail($site_owner_mail)
->setPassword(user_password())
->activate()
->save();
$this
->logger()
->info(dt('Synched Site Factory admin and site owner accounts.'));
// Remove acsf variable so that it can be repopulated with the right value
// on the next acsf-site-sync.
\Drupal::service('acsf.variable_storage')
->delete('acsf_site_info');
// Reset the local site data and run acsf-site-sync to fetch factory data
// about the site.
$site = AcsfSite::load();
$site
->clean();
\Drupal::service('acsf.commands')
->siteSync();
$this
->logger()
->info(dt('Executed acsf-site-sync to gather site data from factory and reset all acsf variables.'));
// Clear all caches.
drupal_flush_all_caches();
$this
->logger()
->info(dt('Cleared all caches.'));
// Send a theme event notification to the Factory.
\Drupal::service('acsf.theme_notification')
->sendNotification('site', 'create');
}
/**
* Remove Acquia Site Factory components from this repository.
*
* Uninstalls components that allow this Drupal repository to be compatible
* with Acquia Site Factory.
*
* @command acsf-uninstall
*
* @bootstrap root
*/
public function uninstall() {
$this
->output()
->writeln('Removing ACSF requirements.');
$drupal_root = realpath(DRUPAL_ROOT);
$repo_root = dirname($drupal_root);
if (basename($drupal_root) !== 'docroot') {
// We're not failing if Drupal is not installed in 'docroot' (as in
// acsf-init), so that files can still be removed from strange installs.
// hooks/ will be checked inside the docroot.
$repo_root = $drupal_root;
}
foreach ($this
->getRequiredFiles($repo_root) as $location) {
$file = $location['filename'];
$dest = sprintf('%s/%s', $location['dest'], $file);
// Some files only contain a destination as they are already in place.
if (isset($location['source']) && file_exists($dest)) {
$confirm = $this
->io()
->confirm(dt('Delete !file?', [
'!file' => $dest,
]));
if ($confirm === FALSE) {
continue;
}
if (unlink($dest)) {
$this
->logger()
->success(dt('Success'));
}
else {
$this
->logger()
->error(dt('Error'));
}
}
}
// Remove the ACSF specific business logic from the default setting.php.
if (file_exists($repo_root . '/docroot/sites/default/settings.php')) {
$default_settings_php_contents = file_get_contents($repo_root . '/docroot/sites/default/settings.php');
$default_settings_php_contents = preg_replace('/' . preg_quote(self::ACSF_INIT_CODE_DELIMITER_START, '/') . '.*?' . preg_quote(self::ACSF_INIT_CODE_DELIMITER_END, '/') . '/sm', '', $default_settings_php_contents);
file_put_contents($repo_root . '/docroot/sites/default/settings.php', $default_settings_php_contents);
}
}
/**
* Lists all required directories to create.
*/
public function getRequiredDirs($repo_root) {
return [
'cloud hooks' => sprintf('%s/hooks/common/post-db-copy', $repo_root),
'cloud hook deploy' => sprintf('%s/hooks/common/pre-web-activate', $repo_root),
'acquia hook dir' => sprintf('%s/hooks/acquia', $repo_root),
'cloud hook samples' => sprintf('%s/hooks/samples', $repo_root),
'site config logic' => sprintf('%s/sites/g', DRUPAL_ROOT),
'site env default' => sprintf('%s/sites/default', DRUPAL_ROOT),
];
}
/**
* Lists all required files to create/delete.
*/
public function getRequiredFiles($repo_root) {
// Array elements should use the following guidelines:
// - Use the 'source' element to indicate where the file should be copied
// from. Note: Some files do not have a source as they are already in
// place.
// - Use the 'dest' element to specify where the file will be copied to.
// - Use the 'mod' element to describe an octal for the file permissions -
// must be chmod() compatible. e.g. 0755
// - Use the 'test_executable' element to enforce testing executability of
// the file. "Executable" files are expected to be owner and group
// executable.
return [
[
'filename' => 'README.md',
'source' => 'cloud_hooks',
'dest' => sprintf('%s/hooks', $repo_root),
],
[
'filename' => '000-acquia_required_scrub.php',
'source' => 'cloud_hooks/common/post-db-copy',
'dest' => sprintf('%s/hooks/common/post-db-copy', $repo_root),
'mod' => 0750,
'test_executable' => TRUE,
],
[
'filename' => '000-acquia-deployment.php',
'source' => 'cloud_hooks/common/pre-web-activate',
'dest' => sprintf('%s/hooks/common/pre-web-activate', $repo_root),
'mod' => 0750,
'test_executable' => TRUE,
],
[
'filename' => 'db_connect.php',
'source' => 'cloud_hooks/acquia',
'dest' => sprintf('%s/hooks/acquia', $repo_root),
],
[
'filename' => 'uri.php',
'source' => 'cloud_hooks/acquia',
'dest' => sprintf('%s/hooks/acquia', $repo_root),
],
[
'filename' => 'acquia-cloud-site-factory-post-db.sh',
'source' => 'cloud_hooks/samples',
'dest' => sprintf('%s/hooks/samples', $repo_root),
],
[
'filename' => 'hello-world.sh',
'source' => 'cloud_hooks/samples',
'dest' => sprintf('%s/hooks/samples', $repo_root),
],
[
'filename' => 'sites.php',
'source' => 'sites',
'dest' => sprintf('%s/sites', DRUPAL_ROOT),
],
[
'filename' => 'apc_rebuild.php',
'source' => 'sites/g',
'dest' => sprintf('%s/sites/g', DRUPAL_ROOT),
],
[
'filename' => '.gitignore',
'source' => 'sites/g',
'dest' => sprintf('%s/sites/g', DRUPAL_ROOT),
],
// If we ever add functionality to 'drush acsf-init' to remove files which
// were copied by previous versions, then IF services.yml is still empty,
// it is also a candidate for removal. (If it's not empty anymore, please
// remove this comment.)
[
'filename' => 'services.yml',
'source' => 'sites/g',
'dest' => sprintf('%s/sites/g', DRUPAL_ROOT),
],
[
'filename' => 'settings.php',
'source' => 'sites/g',
'dest' => sprintf('%s/sites/g', DRUPAL_ROOT),
],
[
'filename' => 'SimpleRest.php',
'source' => 'sites/g',
'dest' => sprintf('%s/sites/g', DRUPAL_ROOT),
],
[
'filename' => 'sites.inc',
'source' => 'sites/g',
'dest' => sprintf('%s/sites/g', DRUPAL_ROOT),
],
[
'filename' => '.gitignore',
'source' => 'sites/default',
'dest' => sprintf('%s/sites/default', DRUPAL_ROOT),
],
[
'filename' => 'acsf.settings.php',
'source' => 'sites/default',
'dest' => sprintf('%s/sites/default', DRUPAL_ROOT),
],
];
}
/**
* Returns the path to the .htaccess file within the codebase.
*
* @return string
* The full absolute path to the .htaccess file.
*/
public function getHtaccessPath() {
return DRUPAL_ROOT . '/.htaccess';
}
/**
* Returns the code to be added to the default settings.php file.
*/
public function defaultSettingsPhpIncludeGet() {
// Heredoc does not handle constants.
$delimiter_start = self::ACSF_INIT_CODE_DELIMITER_START;
$delimiter_end = self::ACSF_INIT_CODE_DELIMITER_END;
return <<<INCLUDE
{<span class="php-variable">$delimiter_start</span>}
\$_acsf_infrastructure = include dirname(__FILE__) . '/acsf.settings.php';
if (\$_acsf_infrastructure === 'acsf-infrastructure') {
return;
}
{<span class="php-variable">$delimiter_end</span>}
INCLUDE;
}
/**
* Determines whether htaccess allows access to our apc_rebuild.php script.
*/
public function testHtaccessIsPatched() {
if (!file_exists($this
->getHtaccessPath()) || !($content = file_get_contents($this
->getHtaccessPath()))) {
// If the htaccess file does not exist or is empty, then it cannot forbid
// access to apc_rebuild.php, so we can consider this a pass, albeit a
// weird one.
return TRUE;
}
// If a customer was for some reason really sure that they did not
// want our line in their .htaccess file, then adding it verbatim, but
// commented-out would suffice.
return strpos($content, self::ACSF_HTACCESS_PATCH) !== FALSE;
}
/**
* Re-creates the default settings.php file.
*
* @param string $default_settings_php_path
* The path to the default settings.php file.
*/
private function defaultSettingsPhpCreate($default_settings_php_path) {
$result = file_put_contents($default_settings_php_path, "<?php\n\n" . $this
->defaultSettingsPhpIncludeGet() . "\n");
if ($result) {
$this
->logger()
->success(dt('File create success: sites/default/settings.php'));
}
else {
$this
->logger()
->error(dt('File create error: sites/default/settings.php'));
}
}
/**
* Updates the default settings.php with the ACSF specific script include.
*
* @param string $default_settings_php_path
* The path to the default settings.php file.
*/
private function defaultSettingsPhpUpdate($default_settings_php_path) {
$default_settings_php_contents = file_get_contents($default_settings_php_path);
// Check if the default settings.php contains our code block. The m modifier
// makes it possible to match text over multiple lines and the s modifier
// allows the . wildcard to match newline characters.
if (!preg_match('/' . preg_quote(self::ACSF_INIT_CODE_DELIMITER_START, '/') . '.*?' . preg_quote(self::ACSF_INIT_CODE_DELIMITER_END, '/') . '/ms', $default_settings_php_contents)) {
// Code block not detected: add it after the opening php tag.
$this
->logger()
->notice(dt('ACSF include not detected in sites/default/settings.php.'));
// Using preg_replace instead of str_replace to be able to control how
// many times the replace gets executed.
$default_settings_php_contents = preg_replace('/<\\?php/', "<?php\n\n" . $this
->defaultSettingsPhpIncludeGet() . "\n", $default_settings_php_contents, 1, $count);
if ($count === 0) {
$this
->logger()
->error(dt('Could not find <?php tag in sites/default/settings.php.'));
}
}
else {
// Code block found: update it with the latest version.
$this
->logger()
->notice(dt('ACSF include detected in sites/default/settings.php.'));
$default_settings_php_contents = preg_replace('/' . preg_quote(self::ACSF_INIT_CODE_DELIMITER_START, '/') . '.*?' . preg_quote(self::ACSF_INIT_CODE_DELIMITER_END, '/') . '/ms', $this
->defaultSettingsPhpIncludeGet(), $default_settings_php_contents);
}
$result = file_put_contents($default_settings_php_path, $default_settings_php_contents);
if ($result) {
$this
->logger()
->success(dt('File edit success: sites/default/settings.php'));
}
else {
$this
->logger()
->error(dt('File edit error: sites/default/settings.php'));
}
}
/**
* Patches the htaccess file to allow access to our apc_rebuild.php script.
*
* We do not use the unix patch utility here, as it might be too fussy about
* line numbers and context. We want to ensure that our line is present in
* the .htaccess file in an appropriate place, without caring too much about
* the content of surrounding lines. There will be extreme cases where this
* patching can fail if modifications to the customer's .htaccess file cause
* it to be wildly different to stock Drupal 8. In this case, they can
* manually add the line, and we will accept that manual modification.
* This doesn't seem like a situation we need to handle until we have at least
* one case. If a customer was for some reason really sure that they did not
* want our line in their .htaccess file, then adding it verbatim, but
* commented-out would suffice.
*
* @throws \Drupal\acsf\AcsfInitHtaccessException
* If the function couldn't patch the .htaccess file.
*/
private function patchHtaccess() {
if ($this
->testHtaccessIsPatched()) {
return;
}
$this
->output()
->writeln(dt('Patching .htaccess file.'));
// All the lines we've scanned so far up until the marker line.
$output_lines = [];
// The output as a string.
$output = '';
// Iterate line-by-line until we reach the line which our rule MUST precede.
$fp = fopen($this
->getHtaccessPath(), 'r+');
$marker_found = FALSE;
while (($line = fgets($fp, 4096)) !== FALSE) {
$output_lines[] = $line;
if (strpos($line, self::ACSF_HTACCESS_PATCH_MARKER) !== FALSE) {
$marker_found = TRUE;
// ... And then backtrack all the lines of comments that precede that
// marker line.
$marker_index = count($output_lines) - 1;
while ($marker_index > 0 && preg_match('/^\\s*#.*$/', $output_lines[$marker_index - 1])) {
$marker_index--;
}
if ($marker_index == 0) {
// This should never happen - it could only happen if the entire
// .htaccess file preceding the marker line were all comments. We
// cannot consider this valid, as the minimum requirements for
// rewrites are not met. We'd expect at a minimum
// <IfModule mod_rewrite.c> ... RewriteEngine on.
throw new AcsfInitHtaccessException('Reached the beginning of the file but was unable to find a place to insert the .htaccess patch. The .htaccess file can be manually handled to fix this error.');
}
// Insert our patch with preceding comment, before the marker index,
// i.e. after the first line seen which is not a comment.
// Also, prepending 2 whitespaces before the htaccess patch
// so that it's indented properly.
array_splice($output_lines, $marker_index, 0, [
self::ACSF_HTACCESS_PATCH_COMMENT . "\n",
' ' . self::ACSF_HTACCESS_PATCH . "\n",
]);
$output = implode('', $output_lines);
break;
}
}
if (!$marker_found) {
// We were unable to locate the marker that disallows access to PHP files
// - it may have been modified which makes it impossible to locate.
// Alternatively, it may have been removed entirely, which actually means
// we don't need to patch the file at all. @see
// drush_acsf_init_patch_htaccess() for info on how to convince the
// verification to accept the file.
throw new AcsfInitHtaccessException('Unable to locate the marker for patching the .htaccess file. This file will need manual patching to allow access to the apc_rebuild.php file.');
}
// Then append the rest of the file.
while (($line = fgets($fp, 4096)) !== FALSE) {
$output .= "{$line}";
}
file_put_contents($this
->getHtaccessPath(), $output);
if ($this
->testHtaccessIsPatched()) {
$this
->logger()
->info(dt('Successfully patched .htaccess file.'));
}
else {
$this
->logger()
->error(dt('Failed to patch .htaccess file.'));
}
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
AcsfInitCommands:: |
constant | Content that we need to add to customers' .htaccess files. | ||
AcsfInitCommands:: |
constant | A comment to include with the htaccess patch line. | ||
AcsfInitCommands:: |
constant | The marker that tells us where we need to add our .htaccess patch. | ||
AcsfInitCommands:: |
constant | Closing delimiter string to enclose code added by acsf-init. | ||
AcsfInitCommands:: |
constant | Initial delimiter string to enclose code added by acsf-init. | ||
AcsfInitCommands:: |
public | function | Connect a site to a factory by setting up the right database variables. | |
AcsfInitCommands:: |
private | function | Re-creates the default settings.php file. | |
AcsfInitCommands:: |
public | function | Returns the code to be added to the default settings.php file. | |
AcsfInitCommands:: |
private | function | Updates the default settings.php with the ACSF specific script include. | |
AcsfInitCommands:: |
public | function | Returns the path to the .htaccess file within the codebase. | |
AcsfInitCommands:: |
public | function | Lists all required directories to create. | |
AcsfInitCommands:: |
public | function | Lists all required files to create/delete. | |
AcsfInitCommands:: |
public | function | Make this repository compatible with Acquia Site Factory. | |
AcsfInitCommands:: |
public | function | Verifies that acsf-init was successfully run in the current version. | |
AcsfInitCommands:: |
private | function | Patches the htaccess file to allow access to our apc_rebuild.php script. | |
AcsfInitCommands:: |
public | function | Add the necessary classes. | |
AcsfInitCommands:: |
public | function | Determines whether htaccess allows access to our apc_rebuild.php script. | |
AcsfInitCommands:: |
public | function | Remove Acquia Site Factory components from this repository. |