Cloud Platform
acquia-inc-examples-file.inc
Examples for IP address restriction
Sample code for settings.php includes and $conf settings that help you quickly
lock down an Acquia Cloud environment using basic auth and / or IP whitelisting.
- All site lockdown logic in located in acquia.inc
- All settings are in $conf variables.
- ``$conf['ah_basic_auth_credentials']`` An array of basic auth username /
password combinations
- ``$conf['ah_whitelist']`` An array of IP addresses to allow on to the site.
- ``$conf['ah_blacklist']`` An array of IP addresses that will be denied access to the site.
- ``$conf['ah_paths_no_cache']`` Paths we should explicitly never cache.
- ``$conf['ah_paths_skip_auth']`` Skip basic authentication for these paths.
- ``$conf['ah_restricted_paths']`` Paths which may not be accessed unless the user is on the IP whitelist.
- The site lockdown process happens by calling ``ac_protect_this_site();`` with defined $conf elements.
- Whitelist / blacklist IPs may use any of the following syntax:
- CIDR (100.0.0.3/4)
- Range (100.0.0.3-100.0.5.10)
- Wildcard (100.0.0.*)
- Single (100.0.0.1)
## Business Logic
- With no $conf values set, ``ac_protect_this_site();`` will do nothing.
- If the path is marked as restricted, all users not on the whitelist will receive access denied.
- If a user's IP is on the blacklist and **not** on the whitelist they will receive access denied.
- Filling ``$conf['ah_basic_auth_credentials']`` will result in all requests being requring an .htaccess log in.
- Securing the site requires entries in both ``$conf['ah_whitelist']`` **and** ``$conf['ah_restricted_paths']``
## Examples
#### Block access to non-whitelisted users on all pages of non-production environments.
```
$conf['ah_restricted_paths'] = array(
'*',
);
$conf['ah_whitelist'] = array(
'100.0.0.*',
'100.0.0.1/5',
);
if (file_exists('/var/www/site-php')) {
require('/var/www/site-php/{site}/{site}-settings.inc');
if(!defined('DRUPAL_ROOT')) {
define('DRUPAL_ROOT', getcwd());
}
if (file_exists(DRUPAL_ROOT . '/sites/acquia.inc')) {
if (isset($_ENV['AH_NON_PRODUCTION']) && $_ENV['AH_NON_PRODUCTION']) {
require DRUPAL_ROOT . '/sites/acquia.inc';
ac_protect_this_site();
}
}
}
```
#### Block access to user and admin pages on the production environment. Enforce .htaccess authentication on non-production. Allow access to an API path without authentication
```
if (file_exists('/var/www/site-php')) {
require('/var/www/site-php/{site}/{site}-settings.inc');
if(!defined('DRUPAL_ROOT')) {
define('DRUPAL_ROOT', getcwd());
}
if (file_exists(DRUPAL_ROOT . '/sites/acquia.inc')) {
if (isset($_ENV['AH_SITE_ENVIRONMENT'])) {
if ($_ENV['AH_SITE_ENVIRONMENT'] != 'prod') {
$conf['ah_basic_auth_credentials'] = array(
'Editor' => 'Password',
'Admin' => 'P455w0rd',
);
$conf['ah_paths_no_cache'] = array(
'api'
);
}
else {
$conf['ah_restricted_paths'] = array(
'user',
'user/*',
'admin',
'admin/*',
);
$conf['ah_whitelist'] = array(
'100.0.0.9',
'100.0.0.1/5',
);
}
require DRUPAL_ROOT . '/sites/acquia.inc';
ac_protect_this_site();
}
}
}
```
#### Blacklist known bad IPs on all environments
```
$conf['ah_blacklist'] = array(
'12.13.14.15',
);
if (file_exists('/var/www/site-php')) {
require('/var/www/site-php/{site}/{site}-settings.inc');
if(!defined('DRUPAL_ROOT')) {
define('DRUPAL_ROOT', getcwd());
}
if (file_exists(DRUPAL_ROOT . '/sites/acquia.inc')) {
require DRUPAL_ROOT . '/sites/acquia.inc';
ac_protect_this_site();
}
}
```
acquia-inc-sample.inc
<?php
/**
* @file
* Utilities for use in protecting an environment via basic auth or IP whitelist.
*/
function ac_protect_this_site() {
global $conf;
$client_ip = ip_address();
// Test if we are using drush (command-line interface)
$cli = drupal_is_cli();
// Default to not skipping the auth check
$skip_auth_check = FALSE;
// Is the user on the VPN? Default to FALSE.
$on_vpn = $cli ? TRUE : FALSE;
if (!empty($client_ip) && !empty($conf['ah_whitelist'])) {
$on_vpn = ah_ip_in_list($client_ip, $conf['ah_whitelist']);
$skip_auth_check = $skip_auth_check || $on_vpn;
}
// If the IP is not explicitly whitelisted check to see if the IP is blacklisted.
if (!$on_vpn && !empty($client_ip) && !empty($conf['ah_blacklist'])) {
if (ah_ip_in_list($client_ip, $conf['ah_blacklist'])) {
ah_page_403($client_ip);
}
}
// Check if we should skip auth check for this page.
if (ah_path_skip_auth()) {
$skip_auth_check = TRUE;
}
// Check if we should disable cache for this page.
if (ah_path_no_cache()) {
$conf['page_cache_maximum_age'] = 0;
}
// Is the page restricted to whitelist only? Default to FALSE.
$restricted_page = FALSE;
// Check to see whether this page is restricted.
if (!empty($conf['ah_restricted_paths']) && ah_paths_restrict()) {
$restricted_page = TRUE;
}
$protect_ip = !empty($conf['ah_whitelist']);
$protect_password = !empty($conf['ah_basic_auth_credentials']);
// Do not protect command line requests, e.g. Drush.
if ($cli) {
$protect_ip = FALSE;
$protect_password = FALSE;
}
// Un-comment to disable protection, e.g. for load tests.
// $skip_auth_check = TRUE;
// $on_vpn = TRUE;
// If not on whitelisted IP prevent access to protected pages.
if ($protect_ip && !$on_vpn && $restricted_page) {
ah_page_403($client_ip);
}
// If not skipping auth, check basic auth.
if ($protect_password && !$skip_auth_check) {
ah_check_basic_auth();
}
}
/**
* Output a 403 (forbidden access) response.
*/
function ah_page_403($client_ip) {
header('HTTP/1.0 403 Forbidden');
print "403 Forbidden: Access denied ($client_ip)";
exit;
}
/**
* Output a 401 (unauthorized) response.
*/
function ah_page_401($client_ip) {
header('WWW-Authenticate: Basic realm="This site is protected"');
header('HTTP/1.0 401 Unauthorized');
print "401 Unauthorized: Access denied ($client_ip)";
exit;
}
/**
* Check basic auth against allowed values.
*/
function ah_check_basic_auth() {
global $conf;
$authorized = FALSE;
$php_auth_user = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : NULL;
$php_auth_pw = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : NULL;
$credentials = isset($conf['ah_basic_auth_credentials']) ? $conf['ah_basic_auth_credentials'] : NULL;
if ($php_auth_user && $php_auth_pw && !empty($credentials)) {
if (isset($credentials[$php_auth_user]) && $credentials[$php_auth_user] == $php_auth_pw) {
$authorized = TRUE;
}
}
if ($authorized) {
return;
}
// Always fall back to 401.
ah_page_401(ip_address());
}
/**
* Determine if the current path is in the list of paths to not cache.
*/
function ah_path_no_cache() {
global $conf;
$q = isset($_GET['q']) ? $_GET['q'] : NULL;
$paths = isset($conf['ah_paths_no_cache']) ? $conf['ah_paths_no_cache'] : NULL;
if (!empty($q) && !empty($paths)) {
foreach ($paths as $path) {
if ($q == $path || strpos($q, $path) === 0) {
return TRUE;
}
}
}
}
/**
* Determine if the current path is in the list of paths on which to not check
* auth.
*/
function ah_path_skip_auth() {
global $conf;
$q = isset($_GET['q']) ? $_GET['q'] : NULL;
$paths = isset($conf['ah_paths_skip_auth']) ? $conf['ah_paths_skip_auth'] : NULL;
if (!empty($q) && !empty($paths)) {
foreach ($paths as $path) {
if ($q == $path || strpos($q, $path) === 0) {
return TRUE;
}
}
}
}
/**
* Check whether a path has been restricted.
*
*/
function ah_paths_restrict() {
global $conf;
if (isset($_GET['q'])) {
// Borrow some code from drupal_match_path()
foreach ($conf['ah_restricted_paths'] as &$path) {
$path = preg_quote($path, '/');
}
$paths = preg_replace('/\\\\\*/', '.*', $conf['ah_restricted_paths']);
$paths = '/^(' . join('|', $paths) . ')$/';
// If this is a restricted path, return TRUE.
if (preg_match($paths, $_GET['q'])) {
// Do not cache restricted paths
$conf['page_cache_maximum_age'] = 0;
return TRUE;
}
}
return FALSE;
}
/**
* Determine if the IP is within the ranges defined in the white/black list.
*/
function ah_ip_in_list($ip, $list) {
foreach ($list as $item) {
// Match IPs in CIDR format.
if (strpos($item, '/') !== false) {
list($range, $mask) = explode('/', $item);
// Take the binary form of the IP and range.
$ip_dec = ip2long($ip);
$range_dec = ip2long($range);
// Verify the given IPs are valid IPv4 addresses
if (!$ip_dec || !$range_dec) {
continue;
}
// Create the binary form of netmask.
$mask_dec = ~ (pow(2, (32 - $mask)) - 1);
// Run a bitwise AND to determine whether the IP and range exist
// within the same netmask.
if (($mask_dec & $ip_dec) == ($mask_dec & $range_dec)) {
return TRUE;
}
}
// Match against wildcard IPs or IP ranges.
elseif (strpos($item, '*') !== false || strpos($item, '-') !== false) {
// Construct a range from wildcard IPs
if (strpos($item, '*') !== false) {
$item = str_replace('*', 0, $item) . '-' . str_replace('*', 255, $item);
}
// Match against ranges by converting to long IPs.
list($start, $end) = explode('-', $item);
$start_dec = ip2long($start);
$end_dec = ip2long($end);
$ip_dec = ip2long($ip);
// Verify the given IPs are valid IPv4 addresses
if (!$start_dec || !$end_dec || !$ip_dec) {
continue;
}
if ($start_dec <= $ip_dec && $ip_dec <= $end_dec) {
return TRUE;
}
}
// Match against single IPs
elseif ($ip === $item) {
return TRUE;
}
}
return FALSE;
}
api-notification-example.php
<?php
// This example requires `league/oauth2-client` package.
// Run `composer require league/oauth2-client` before running.
require __DIR__ . '/vendor/autoload.php';
use League\OAuth2\Client\Provider\GenericProvider;
use GuzzleHttp\Client;
// The UUID of an application you want to create the database for.
$applicationUuid = 'APP-UUID';
$dbName = 'test_database_1';
// See https://docs.acquia.com/cloud-platform/develop/api/auth/
// for how to generate a client ID and Secret.
$clientId = 'API-KEY';
$clientSecret = 'API-SECRET';
$provider = new GenericProvider([
'clientId' => $clientId,
'clientSecret' => $clientSecret,
'urlAuthorize' => '',
'urlAccessToken' => 'https://accounts.acquia.com/api/auth/oauth/token',
'urlResourceOwnerDetails' => '',
]);
$client = new Client();
$provider->setHttpClient($client);
echo 'retrieving access token', PHP_EOL;
$accessToken = $provider->getAccessToken('client_credentials');
echo 'access token retrieved', PHP_EOL;
// Generate a request object using the access token.
$request = $provider->getAuthenticatedRequest(
'POST',
"https://cloud.acquia.com/api/applications/{$applicationUuid}/databases",
$accessToken,
[
'headers' => ['Content-Type' => 'application/json'],
'body' => json_encode(['name' => $dbName])
]
);
// Send the request.
echo 'requesting db create api', PHP_EOL;
$response = $client->send($request);
echo 'response parsing', PHP_EOL;
$responseBody = json_decode($response->getBody()->getContents(), true);
$notificationLink = $responseBody['_links']['notification']['href'];
$retryCount = 10;
echo 'start watching for notification status at ', $notificationLink, PHP_EOL;
do {
sleep(5);
// create notification request.
$request = $provider->getAuthenticatedRequest(
'GET',
$notificationLink,
$accessToken
);
echo 'requesting notification status', PHP_EOL;
$response = $client->send($request);
$responseBody = json_decode($response->getBody()->getContents(), true);
echo 'notification status: ', $responseBody['status'], PHP_EOL;
if ($responseBody['status'] === 'succeeded') {
echo 'Successfully created database.';
exit(0);
} elseif ($responseBody['status'] === 'failed') {
echo 'Failed to create database.';
exit(1);
} else {
echo 'retrying notification in 5 sec', PHP_EOL;
$retryCount--;
$retry = $retryCount > 0;
}
} while ($retry);
api-v2-auth.php
<?php
require __DIR__ . '/vendor/autoload.php';
use League\OAuth2\Client\Provider\GenericProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use GuzzleHttp\Client;
// See https://docs.acquia.com/cloud-platform/develop/api/auth/
// for how to generate a client ID and Secret.
$clientId = 'API Key';
$clientSecret = 'Api Secret';
$provider = new GenericProvider([
'clientId' => $clientId,
'clientSecret' => $clientSecret,
'urlAuthorize' => '',
'urlAccessToken' => 'https://accounts.acquia.com/api/auth/oauth/token',
'urlResourceOwnerDetails' => '',
]);
try {
// Try to get an access token using the client credentials grant.
$accessToken = $provider->getAccessToken('client_credentials');
// Generate a request object using the access token.
$request = $provider->getAuthenticatedRequest(
'GET',
'https://cloud.acquia.com/api/account',
$accessToken
);
// Send the request.
$client = new Client();
$response = $client->send($request);
$responseBody = $response->getBody();
} catch (IdentityProviderException $e) {
// Failed to get the access token.
exit($e->getMessage());
}
myisam_to_innodb.sh.inc
#!/bin/sh
# Script to load a database, doing some conversions along the way
# EDIT THESE
dbfilename='db-backup.sql.gz'
dbuser='root'
dbpassword='rootpassword'
dbname='mydatabase'
# Flag to say whether we want to convert from innoDB to MyISAM (1 == yes)
# It will only convert the tables matching the regexp
innodb_to_myisam=0
innodb_to_myisam_exclude_tables_regexp='^(locales_source|locales_target|menu_links|workbench_scheduler_types)$'
# Flag for converting MyISAM to InnoDB (1 == yes)
# It will only convert the tables matching the regexp
myisam_to_innodb=0
myisam_to_innodb_exclude_tables_regexp='^XXX$'
# Tables that will be created with structure only and NO data
no_data_import_tables_regexp='^(__ACQUIA_MONITORING|accesslog|batch|boost_cache|cache|cache_.*|history|queue|search_index|search_dataset|search_total|sessions|watchdog|panels_hash_database_cache|migrate_.*)$'
pv -p $dbfilename |gzip -d -c | awk -F'`' '
NR==1 {
# http://superuser.com/questions/246784/how-to-tune-mysql-for-restoration-from-mysql-dump
# TODO? http://www.palominodb.com/blog/2011/08/02/mydumper-myloader-fast-backup-and-restore ?
print "SET SQL_LOG_BIN=0;"
print "SET unique_checks=0;"
print "SET autocommit=0;"
print "SET foreign_key_checks=0;"
output=1;
}
{
start_of_line=substr($0,1,200);
# Detect beginning of table structure definition.
if (index(start_of_line, "-- Table structure for table")==1) {
output=1
print "COMMIT;"
print "SET autocommit=0;"
current_db=$2
}
# Switch the engine from InnoDB to MyISAM : MUCHO FAST.
if (substr(start_of_line,1,8)==") ENGINE") {
if ('${innodb_to_myisam:-0}' == 1) {
if (current_db ~ /'"$innodb_to_myisam_exclude_tables_regexp"'/) {
print "Skipping InnoDB -> MyISAM for " current_db >"/dev/stderr"
} else {
gsub(/=InnoDB/, "=MyISAM", $0);
#gsub(/CHARSET=utf8/, "CHARSET=latin1", $0);
}
}
if ('${myisam_to_innodb:-0}' == 1) {
if (current_db ~ /'"$myisam_to_innodb_exclude_tables_regexp"'/) {
print "Skipping MyISAM -> InnoDB for " current_db >"/dev/stderr"
} else {
gsub(/=MyISAM/, "=InnoDB", $0);
}
}
}
# Detect beginning of table data dump.
if (index(start_of_line, "-- Dumping data for table")==1) {
if (current_db != $2) {
print "Internal problem: unexpected data, seems to come from table " $2 " whereas expected table " current_db;
current_db=$2
}
printf "\r Processing table " current_db > "/dev/stderr"
output=1
# Skip data in some tables
if (current_db ~ /'"$no_data_import_tables_regexp"'/) {
output=0
print "Skipping Data import (imported structure only) for " current_db >"/dev/stderr"
}
}
if (output==1) {
print
}
}
END {
print "COMMIT;"
}' |mysql -u$dbuser --password=$dbpassword $dbname
example.sitename.conf
[ req ]
default_bits = 4096
default_keyfile = private.key
distinguished_name = req_distinguished_name
req_extensions = req_ext # The extensions to add to the self signed cert
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = US
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = Massachusetts
localityName = Locality Name (eg, city)
localityName_default = Boston
organizationName = Organization Name (eg, company)
organizationName_default = Acquia
organizationalUnitName = Organizational Unit Name (department, division)
organizationalUnitName_default =
commonName = Common Name (e.g. server FQDN or YOUR name)
commonName_max = 64
commonName_default = localhost
emailAddress = Email Address (such as [email protected])
emailAddress_default =
[ req_ext ]
subjectAltName = @alt_names
[alt_names]
DNS.1 = www.example.com
DNS.2 = edit.example.com
memcache.yml
services:
# Replaces the default lock backend with a memcache implementation.
lock:
class: Drupal\Core\Lock\LockBackendInterface
factory: memcache.lock.factory:get
acsfd7.memcache.settings.php
<?php
/**
* @file
* Contains Drupal 7 Acquia memcache configuration to be added directly following the Acquia database require line
* (see https://docs.acquia.com/cloud-platform/manage/code/require-line/ for more info)
*/
if (getenv('AH_SITE_ENVIRONMENT') &&
isset($conf['memcache_servers'])
) {
$conf['memcache_extension'] = 'Memcached';
$conf['cache_backends'][] = 'sites/all/modules/contrib/memcache/memcache.inc';
$conf['cache_default_class'] = 'MemCacheDrupal';
$conf['cache_class_cache_form'] = 'DrupalDatabaseCache';
// Enable compression
$conf['memcache_options'][Memcached::OPT_COMPRESSION] = TRUE;
$conf['memcache_stampede_protection_ignore'] = array(
// Ignore some cids in 'cache_bootstrap'.
'cache_bootstrap' => array(
'module_implements',
'variables',
'lookup_cache',
'schema:runtime:*',
'theme_registry:runtime:*',
'_drupal_file_scan_cache',
),
// Ignore all cids in the 'cache' bin starting with 'i18n:string:'
'cache' => array(
'i18n:string:*',
),
// Disable stampede protection for the entire 'cache_path' and 'cache_rules'
// bins.
'cache_path',
'cache_rules',
);
# Move semaphore out of the database and into memory for performance purposes
$conf['lock_inc'] = 'sites/all/modules/contrib/memcache/memcache-lock.inc';
}
authsources.php
<?php
// This file is available at
// https://docs.acquia.com/resource/simplesaml/sources/
$config = array(
// This is a authentication source which handles admin authentication.
'admin' => array(
// The default is to use core:AdminPassword, but it can be replaced with
// any authentication source.
'core:AdminPassword',
),
'default-sp' => array(
'saml:SP',
// The entityID is the entityID of the SP that the IdP is expecting.
// This value must be exactly what the IdP is expecting. If the
// entityID is not set, it defaults to the URL of the SP's metadata.
// Don't declare an entityID for Site Factory.
'entityID' => 'SP EntityID',
// If the IdP requires the SP to hold a certificate, the location
// of the self-signed certificate.
// If you need to generate a SHA256 cert, see
// https://gist.github.com/guitarte/5745b94c6883eaddabfea68887ba6ee6
'certificate' => "../cert/saml.crt",
'privatekey' => "../cert/saml.pem",
'redirect.sign' => TRUE,
'redirect.validate' => TRUE,
// The entityID of the IdP.
// This is included in the metadata from the IdP.
'idp' => 'IdP EntityID',
// NameIDFormat is included in the metadata from the IdP
'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
// If the IdP does not pass any attributes, but provides a NameID in
// the authentication response, we can filter and add the value as an
// attribute.
// See https://simplesamlphp.org/docs/stable/saml:nameidattribute
'authproc' => array(
20 => array(
'class' => 'saml:NameIDAttribute',
'format' => '%V',
),
),
// The RelayState parameter needs to be set if SSL is terminated
// upstream. If you see the SAML response come back with
// https://example.com:80/saml_login, you likely need to set this.
// See https://github.com/simplesamlphp/simplesamlphp/issues/420
'RelayState' => 'https://' . $_SERVER['HTTP_HOST'] . '/saml_login',
// If working with ADFS, Microsoft may soon only allow SHA256 certs.
// You must specify signature.algorithm as SHA256.
// Defaults to SHA1 (http://www.w3.org/2000/09/xmldsig#rsa-sha1)
// See https://docs.microsoft.com/en-us/security/trusted-root/program-requirements
// 'signature.algorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
),
);
clam_av_script.sh.inc
#!/bin/bash
#
# Shell script to scan the default files directory with ClamAV
# Arguments:
# Email recipients: Comma separated list of email recipients wrapped in quotes
# Site environment: Site name and environment formatted like [site].[env]
#
SCAN_OUTPUT=/mnt/tmp/clamscan.log
EMAIL_RECIPIENTS=$1
SITE_ENV=$2
DATE=$(date)
CRON_OUTPUT=/var/log/sites/${SITE_ENV}/logs/$(hostname -s)/clamscan.log
if [ -d /mnt/gfs/${SITE_ENV} ]
then
{
echo -e "=============================\nStarting scan ${DATE}\n"
/usr/bin/clamscan -ri /mnt/gfs/${SITE_ENV}/sites/default/files > ${SCAN_OUTPUT}
echo -e "Checking output...\n"
cat ${SCAN_OUTPUT} | grep "FOUND"
if [ $? -eq 0 ] ; then
echo -e "FOUND VIRUS, SENDING EMAILS TO ${EMAIL_RECIPIENTS}.\n"
cat ${SCAN_OUTPUT} | mail -s "${DATE} ClamAV has detected a virus on your website files directory" "${EMAIL_RECIPIENTS}"
else
echo -e "CLEAN, NO VIRUSES FOUND.\n"
fi
echo -e "Done\n=============================\n"
} >> ${CRON_OUTPUT} 2>&1
else
echo "ERROR: directory /mnt/gfs/${SITE_ENV} is not a valid path. Please update your scheduled task with the correct [site].[env] as the second parameter"
fi
cloud-memcache-d7.php
/**
* @file
* Contains Drupal 7 Acquia memcache configuration to be added directly following the Acquia database require line
* (see https://docs.acquia.com/cloud-platform/manage/code/require-line/ for more info)
*/
if (getenv('AH_SITE_ENVIRONMENT') &&
isset($conf['memcache_servers'])
) {
$conf['memcache_extension'] = 'Memcached';
$conf['cache_backends'][] = 'sites/all/modules/contrib/memcache/memcache.inc';
$conf['cache_default_class'] = 'MemCacheDrupal';
$conf['cache_class_cache_form'] = 'DrupalDatabaseCache';
// Enable compression
$conf['memcache_options'][Memcached::OPT_COMPRESSION] = TRUE;
$conf['memcache_stampede_protection_ignore'] = array(
// Ignore some cids in 'cache_bootstrap'.
'cache_bootstrap' => array(
'module_implements',
'variables',
'lookup_cache',
'schema:runtime:*',
'theme_registry:runtime:*',
'_drupal_file_scan_cache',
),
// Ignore all cids in the 'cache' bin starting with 'i18n:string:'
'cache' => array(
'i18n:string:*',
),
// Disable stampede protection for the entire 'cache_path' and 'cache_rules'
// bins.
'cache_path',
'cache_rules',
);
# Move semaphore out of the database and into memory for performance purposes
$conf['lock_inc'] = 'sites/all/modules/contrib/memcache/memcache-lock.inc';
}
Default Memcached configuration
<?php
/**
* @file
* Contains caching configuration.
* Last change: 2022-08-01
*/
use Composer\Autoload\ClassLoader;
/**
* Use memcache as cache backend.
*
* Autoload memcache classes and service container in case module is not
* installed. Avoids the need to patch core and allows for overriding the
* default backend when installing Drupal.
*
* @see https://www.drupal.org/node/2766509
*/
// Determine if site is currently running under Acquia Cloud Next.
$is_acquia_cloud_next = (getenv("HOME") == "/home/clouduser");
if (getenv('AH_SITE_ENVIRONMENT') &&
array_key_exists('memcache', $settings) &&
array_key_exists('servers', $settings['memcache']) &&
!empty($settings['memcache']['servers']) &&
!$is_acquia_cloud_next
) {
// Check for PHP Memcached libraries.
$memcache_exists = class_exists('Memcache', FALSE);
$memcached_exists = class_exists('Memcached', FALSE);
$memcache_services_yml = DRUPAL_ROOT . '/modules/contrib/memcache/memcache.services.yml';
$memcache_module_is_present = file_exists($memcache_services_yml);
if ($memcache_module_is_present && ($memcache_exists || $memcached_exists)) {
// Use Memcached extension if available.
if ($memcached_exists) {
$settings['memcache']['extension'] = 'Memcached';
}
if (class_exists(ClassLoader::class)) {
$class_loader = new ClassLoader();
$class_loader->addPsr4('Drupal\\memcache\\', DRUPAL_ROOT . '/modules/contrib/memcache/src');
$class_loader->register();
$settings['container_yamls'][] = $memcache_services_yml;
// Acquia Default Settings for the memcache module
// Default settings for the Memcache module.
// Enable compression for PHP 7.
$settings['memcache']['options'][Memcached::OPT_COMPRESSION] = TRUE;
// Set key_prefix to avoid drush cr flushing all bins on multisite.
$settings['memcache']['key_prefix'] = $conf['acquia_hosting_site_info']['db']['name'] . '_';
// Decrease latency.
$settings['memcache']['options'][Memcached::OPT_TCP_NODELAY] = TRUE;
// Bootstrap cache.container with memcache rather than database.
$settings['bootstrap_container_definition'] = [
'parameters' => [],
'services' => [
'database' => [
'class' => 'Drupal\Core\Database\Connection',
'factory' => 'Drupal\Core\Database\Database::getConnection',
'arguments' => ['default'],
],
'settings' => [
'class' => 'Drupal\Core\Site\Settings',
'factory' => 'Drupal\Core\Site\Settings::getInstance',
],
'memcache.settings' => [
'class' => 'Drupal\memcache\MemcacheSettings',
'arguments' => ['@settings'],
],
'memcache.factory' => [
'class' => 'Drupal\memcache\Driver\MemcacheDriverFactory',
'arguments' => ['@memcache.settings'],
],
'memcache.timestamp.invalidator.bin' => [
'class' => 'Drupal\memcache\Invalidator\MemcacheTimestampInvalidator',
'arguments' => ['@memcache.factory', 'memcache_bin_timestamps', 0.001],
],
'memcache.backend.cache.container' => [
'class' => 'Drupal\memcache\DrupalMemcacheInterface',
'factory' => ['@memcache.factory', 'get'],
'arguments' => ['container'],
],
'cache_tags_provider.container' => [
'class' => 'Drupal\Core\Cache\DatabaseCacheTagsChecksum',
'arguments' => ['@database'],
],
'cache.container' => [
'class' => 'Drupal\memcache\MemcacheBackend',
'arguments' => [
'container',
'@memcache.backend.cache.container',
'@cache_tags_provider.container',
'@memcache.timestamp.invalidator.bin',
'@memcache.settings',
],
],
],
];
// Content Hub 2.x requires the Depcalc module which needs to use the database backend.
$settings['cache']['bins']['depcalc'] = 'cache.backend.database';
// Use memcache for bootstrap, discovery, config instead of fast chained
// backend to properly invalidate caches on multiple webs.
// See https://www.drupal.org/node/2754947
$settings['cache']['bins']['bootstrap'] = 'cache.backend.memcache';
$settings['cache']['bins']['discovery'] = 'cache.backend.memcache';
$settings['cache']['bins']['config'] = 'cache.backend.memcache';
// Use memcache as the default bin.
$settings['cache']['default'] = 'cache.backend.memcache';
}
}
}
Content Hub
ach-bulk-import-batch-functions.php
<?php
/**
* Process a subset of all the entities to be enqueued in a single request.
*
* @param $entity_type
* The entity type.
* @param $bundle
* The entity bundle.
* @param $bundle_key
* THe entity bundle key.
*/
function export_enqueue_entities($entity_type, $bundle, $entity_ids, &$context) {
/**
* Number of entities per iteration. Decrease this number if your site has
* too many dependencies per node.
*
* @var int $entities_per_iteration
*/
$entities_per_iteration = 5;
if (empty($context['sandbox'])) {
$context['sandbox']['progress'] = 0;
$context['sandbox']['max'] = count($entity_ids);
$context['results']['total'] = 0;
}
/** @var \Drupal\acquia_contenthub\EntityManager $entity_manager */
$entity_manager = \Drupal::service('acquia_contenthub.entity_manager');
/** @var \Drupal\acquia_contenthub\Controller\ContentHubEntityExportController $export_controller */
$export_controller = \Drupal::service('acquia_contenthub.acquia_contenthub_export_entities');
$slice_entity_ids = array_slice($entity_ids, $context['sandbox']['progress'], $entities_per_iteration);
$ids = array_values($slice_entity_ids);
if (!empty($ids)) {
$entities = \Drupal::entityTypeManager()
->getStorage($entity_type)
->loadMultiple($ids);
foreach ($entities as $entity) {
if ($entity_manager->isEligibleEntity($entity)) {
// Entity is eligible, then re-export.
$export_controller->exportEntities([$entity]);
}
}
}
$context['sandbox']['progress'] += count($ids);
$enqueued = implode(',', $ids);
$message = empty($enqueued) ? "Enqueuing '$entity_type' ($bundle) entities: No entities to queue." : "Enqueuing '$entity_type' ($bundle) entities with IDs: " . $enqueued . "\n";
$context['results']['total'] += count($ids);
$context['message'] = $message;
if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
}
}
function export_enqueue_finished($success, $results, $operations) {
// The 'success' parameter means no fatal PHP errors were detected. All
// other error management should be handled using 'results'.
if ($success) {
$message = 'Total number of enqueued entities: ' . $results['total'];
}
else {
$message = t('Finished with an error.');
}
drush_print($message);
}
content-hub-enqueue-entity-eligibility.php
<?php
namespace Drupal\acquia_contenthub_publisher\EventSubscriber\EnqueueEligibility;
use Drupal\acquia_contenthub_publisher\AcquiaContentHubPublisherEvents;
use Drupal\acquia_contenthub_publisher\Event\ContentHubEntityEligibilityEvent;
use Drupal\file\FileInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Subscribes to entity eligibility to prevent enqueueing temporary files.
*/
class FileIsTemporary implements EventSubscriberInterface {
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[AcquiaContentHubPublisherEvents::ENQUEUE_CANDIDATE_ENTITY][] = ['onEnqueueCandidateEntity', 50];
return $events;
}
/**
* Prevent temporary files from enqueueing.
*
* @param \Drupal\acquia_contenthub_publisher\Event\ContentHubEntityEligibilityEvent $event
* The event to determine entity eligibility.
*/
public function onEnqueueCandidateEntity(ContentHubEntityEligibilityEvent $event) {
// If this is a file with status = 0 (TEMPORARY FILE) do not export it.
// This is a check to avoid exporting temporary files.
$entity = $event->getEntity();
if ($entity instanceof FileInterface && $entity->isTemporary()) {
$event->setEligibility(FALSE);
$event->stopPropagation();
}
}
}
ach-bulk-import.php
<?php
/**
* @file
* Add entities from Content Hub to the Import Queue.
*
* Please locate this field in the 'scripts' directory as a sibling of docroot:
* <DOCROOT>/../scripts/ach-bulk-import.php
*
* To run the script, execute the drush command:
* $drush scr ../scripts/ach-bulk-import.php
*
* Make sure to enable the Import Queue before executing this script.
*
* Notes:
*
* 1) If you want to explicitly avoid importing a particular entity type, please
* add it to the list of $global_excluded_types.
* 2) By default importing includes all dependencies. To change this behavior
* change the variable $include_dependencies to FALSE.
* 3) You can decided whether to publish entities after importing them. To
* publish entities after importing, set variable $publishing_status to 1.
* Setting $publishing_status to 0 imports them as unpublished.
* 4) You can decide to use FIFO (first exported entities are imported first),
* or LIFO (last exported entities are imported first), according to the
* $fifo variable: $fifo = 1 uses FIFO, $fifo = 0 uses LIFO.
* 5) You can set the author of the nodes to be imported locally. Example: If
* you set the $uid = 1, it will import all nodes as administrator (author
* is administrator). Change it to specific UID to use as author.
*/
use Drupal\acquia_contenthub\ContentHubEntityDependency;
use Drupal\Component\Serialization\Json;
// Global exclusion of entity types.
$global_excluded_types = [
// 'redirect' => 'redirect',
];
// Include importing dependencies. By default it is "TRUE".
$include_dependencies = TRUE;
// Determine if we want to publish imported entities or not.
// 1: Publish entities, 0: Do not publish.
$publishing_status = 1;
// If TRUE, it will import from the last page to the first (FIFO: first entities
// exported will be the first to import), otherwise will use LIFO (Last exported
// entities will be imported first).
$fifo = TRUE;
// Determine the author UUID for the nodes to be created.
$uid = 1; // administrator.
$user = \Drupal\user\Entity\User::load($uid);
$author = $user->uuid();
/** @var \Drupal\acquia_contenthub\ContentHubEntitiesTracking $entities_tracking */
$entities_tracking = \Drupal::service('acquia_contenthub.acquia_contenthub_entities_tracking');
// Loading ClientManager to be able to execute requests to Content Hub and
// to check connection.
/** @var \Drupal\acquia_contenthub\Client\ClientManager $client_manager */
$client_manager = \Drupal::service('acquia_contenthub.client_manager');
$client = $client_manager->getConnection();
// The ImportEntityManager Service allows to import entities.
/** @var \Drupal\acquia_contenthub\ImportEntityManager $import_manager */
$import_manager = \Drupal::service("acquia_contenthub.import_entity_manager");
// List all the 'dependent' entities type IDs.
$dependent_entity_type_ids = ContentHubEntityDependency::getPostDependencyEntityTypes();
$excluded_types = array_merge($global_excluded_types, $dependent_entity_type_ids);
// Checks whether the import queue has been enabled.
$import_with_queue = \Drupal::config('acquia_contenthub.entity_config')->get('import_with_queue');
if (!$import_with_queue) {
drush_user_abort('Please enable the Import Queue.');
}
// Check if the site is connected to Content Hub.
if (!$client_manager->isConnected()) {
return;
}
$list = $client_manager->createRequest('listEntities', [[]]);
$total = floor($list['total'] / 1000) * 1000;
// Starting page.
$start = $fifo ? $total : 0;
// Step
$step = $fifo ? -1000 : 1000;
// Counter of queued entities.
$i = 0;
do {
// List all entities you want to import by modifying the $options array.
/*
* Example of how to structure the $options parameter:
*
* $options = [
* 'type' => 'node',
* 'origin' => '11111111-1111-1111-1111-111111111111',
* 'filters' => [
* 'status' => 1,
* 'title' => 'New*',
* 'body' => '/Boston/',
* ],
* ];
*
*/
$options = [
'start' => $start,
];
$list = $client_manager->createRequest('listEntities', [$options]);
foreach ($list['data'] as $entity) {
$i++;
// We do not want to import "dependent" entities.
// These 3 lines are not needed in this example, but if we are listing all
// entities, make sure to exclude dependent entities to be sent directly to
// the importRemoteEntity() method because you would not be sure if their
// host (parent) entity exist in the system yet.
if (in_array($entity['type'], $excluded_types)) {
drush_print("{$i}) Skipped entity type = {$entity['type']} , UUID = {$entity['uuid']} (Dependent or excluded entity type)");
continue;
}
// Do not import the entity if it has been previously imported and has the
// same "modified" flag, which means there are no new updates on the entity.
if ($imported_entity = $entities_tracking->loadImportedByUuid($entity['uuid'])) {
if ($imported_entity->getModified() === $entity['modified']) {
drush_print("{$i}) Skipped entity type = {$entity['type']} , UUID = {$entity['uuid']} (Entity already imported)");
continue;
}
}
// Add entity to import queue.
try {
$response = $import_manager->addEntityToImportQueue($entity['uuid'], $include_dependencies, $author, $publishing_status);
$status = Json::decode($response->getContent());
if (!empty($status['status']) && $status['status'] == 200) {
drush_print("{$i}) Entity added to import queue: type = {$entity['type']} , UUID = {$entity['uuid']}");
}
else {
drush_print("{$i}) ERROR: Cannot add entity to import queue: type = {$entity['type']} , UUID = {$entity['uuid']}");
}
} catch (\Drupal\Core\Entity\EntityStorageException $ex) {
drush_print("{$i}) ERROR: Failed to add entity to import queue: type = {$entity['type']} , UUID = {$entity['uuid']} [{$ex->getMessage()}]");
}
}
$start = $start + $step;
$exit_condition = $fifo ? $start >= 0 : $start <= $total;
} while ($exit_condition);
content-hub-publish-entities.php
<?php
namespace Drupal\acquia_contenthub_publisher\EventSubscriber\PublishEntities;
use Drupal\acquia_contenthub_publisher\AcquiaContentHubPublisherEvents;
use Drupal\acquia_contenthub_publisher\Event\ContentHubPublishEntitiesEvent;
use Drupal\acquia_contenthub_publisher\PublisherTracker;
use Drupal\Core\Database\Connection;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class RemoveUnmodifiedEntities implements EventSubscriberInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* RemoveUnmodifiedEntities constructor.
*
* @param \Drupal\Core\Database\Connection $database
* The database connection.
*/
public function __construct(Connection $database) {
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[AcquiaContentHubPublisherEvents::PUBLISH_ENTITIES][] = ['onPublishEntities', 1000];
return $events;
}
/**
* Removes unmodified entities before publishing.
*
* @param \Drupal\acquia_contenthub_publisher\Event\ContentHubPublishEntitiesEvent $event
*/
public function onPublishEntities(ContentHubPublishEntitiesEvent $event) {
$dependencies = $event->getDependencies();
$uuids = array_keys($dependencies);
$query = $this->database->select('acquia_contenthub_publisher_export_tracking', 't')
->fields('t', ['entity_uuid', 'hash']);
$query->condition('t.entity_uuid', $uuids, 'IN');
$query->condition('t.status', [PublisherTracker::CONFIRMED, PublisherTracker::EXPORTED], 'IN');
$results = $query->execute();
foreach ($results as $result) {
// Can't check it if it doesn't have a hash.
// @todo make this a query.
if (!$result->hash) {
continue;
}
$wrapper = $dependencies[$result->entity_uuid];
if ($wrapper->getHash() == $result->hash) {
$event->removeDependency($result->entity_uuid);
}
}
}
}
Personalization
ACSF-D8-settings-sample-factory-hook.php
<?php
/**
* @file
* Contains Environment variables.
*/
$ah_env = isset($_ENV['AH_SITE_ENVIRONMENT']) ? $_ENV['AH_SITE_ENVIRONMENT'] : NULL;
$ah_group = isset($_ENV['AH_SITE_GROUP']) ? $_ENV['AH_SITE_GROUP'] : NULL;
$is_ah_env = (bool) $ah_env;
$is_ah_prod_env = ($ah_env == 'prod' || $ah_env == '01live');
$is_ah_stage_env = ($ah_env == 'test' || $ah_env == '01test');
$is_ah_preview_env = ($ah_env == 'preview' || $ah_env == '01preview');
$is_ah_dev_cloud = (!empty($_SERVER['HTTP_HOST']) && strstr($_SERVER['HTTP_HOST'], 'devcloud'));
$is_ah_dev_env = (preg_match('/^dev[0-9]*$/', $ah_env) || $ah_env == '01dev');
$is_acsf = (!empty($ah_group) && file_exists("/mnt/files/$ah_group.$ah_env/files-private/sites.json"));
$acsf_db_name = $is_acsf ? $GLOBALS['gardens_site_settings']['conf']['acsf_db_name'] : NULL;
$is_local_env = !$is_ah_env;
$is_domain_a= (!empty($_SERVER['HTTP_HOST']) && strstr($_SERVER['HTTP_HOST'], 'domaina'));
$is_domain_b= (!empty($_SERVER['HTTP_HOST']) && strstr($_SERVER['HTTP_HOST'], 'domainb'));
/**
* @file
* Contains Acquia Lift and Content Hub configuration.
*/
if ($is_ah_env && $is_domain_a) {
switch ($ah_env) {
case '01live':
$config['acquia_lift.settings']['credential']['account_id'] = 'LIFTACCOUNT';
$config['acquia_lift.settings']['credential']['site_id'] = 'site_id_domain_a_prod'; //Set in Lift Profile Manager
$config['acquia_lift.settings']['credential']['content_origin'] = '12312312312312312312312'; //Same as origin below
// Configure these at at /admin/config/services/acquia-contenthub and run
// "drush cget acquia_contenthub.admin_settings --include-overridden" to get all the settings.
$config['acquia_contenthub.admin_settings']['api_key'] = '121231231231'; //Get from Profile Manager Customer Details
$config['acquia_contenthub.admin_settings']['secret_key'] = '123123123123123123123123123'; //Get from Profile Manager Customer Details
$config['acquia_contenthub.admin_settings']['client_name'] = 'site_id_domain_a_prod'; //Arbitrary but usually matches site_id
$config['acquia_contenthub.admin_settings']['origin'] = '12312312312312312312312';
$config['acquia_contenthub.admin_settings']['webhook'] = [
'uuid' => 'will be generated on /admin/config/services/acquia-contenthub',
'url' => 'create at /admin/config/services/acquia-contenthub',
'settings_url' => 'will be generated on /admin/config/services/acquia-contenthub',
];
break;
case '01test':
$config['acquia_lift.settings']['credential']['account_id'] = 'LIFTACCOUNT';
$config['acquia_lift.settings']['credential']['site_id'] = 'site_id_domain_a_test'; //Set in Lift Profile Manager
$config['acquia_lift.settings']['credential']['content_origin'] = '12312312312312312312312'; //Same as origin below
$config['acquia_contenthub.admin_settings']['api_key'] = '121231231231'; //Get from Profile Manager Customer Details
$config['acquia_contenthub.admin_settings']['secret_key'] = '123123123123123123123123123'; //Get from Profile Manager Customer Details
$config['acquia_contenthub.admin_settings']['client_name'] = 'site_id_domain_a_test'; //Arbitrary but usually matches site_id
$config['acquia_contenthub.admin_settings']['origin'] = '12312312312312312312312';
$config['acquia_contenthub.admin_settings']['webhook'] = [
'uuid' => 'will be generated on /admin/config/services/acquia-contenthub',
'url' => 'create at /admin/config/services/acquia-contenthub',
'settings_url' => 'will be generated on /admin/config/services/acquia-contenthub',
];
break;
case '01dev':
$config['acquia_lift.settings']['credential']['account_id'] = 'LIFTACCOUNT';
$config['acquia_lift.settings']['credential']['site_id'] = 'site_id_domain_a_dev'; //Set in Lift Profile Manager
$config['acquia_lift.settings']['credential']['content_origin'] = '12312312312312312312312'; //Same as origin below
$config['acquia_contenthub.admin_settings']['api_key'] = '121231231231'; //Get from Profile Manager Customer Details
$config['acquia_contenthub.admin_settings']['secret_key'] = '123123123123123123123123123'; //Get from Profile Manager Customer Details
$config['acquia_contenthub.admin_settings']['client_name'] = 'site_id_domain_a_dev'; //Arbitrary but usually matches site_id
$config['acquia_contenthub.admin_settings']['origin'] = '12312312312312312312312';
$config['acquia_contenthub.admin_settings']['webhook'] = [
'uuid' => 'will be generated on /admin/config/services/acquia-contenthub',
'url' => 'create at /admin/config/services/acquia-contenthub',
'settings_url' => 'will be generated on /admin/config/services/acquia-contenthub',
];
break;
}
}
if ($is_ah_env && $is_domain_b) {
switch ($ah_env) {
case '01live':
$config['acquia_lift.settings']['credential']['account_id'] = 'LIFTACCOUNT';
$config['acquia_lift.settings']['credential']['site_id'] = 'site_id_domain_b_prod'; //Set in Lift Profile Manager
$config['acquia_lift.settings']['credential']['content_origin'] = '12312312312312312312312'; //Same as origin below
$config['acquia_contenthub.admin_settings']['api_key'] = '121231231231'; //Get from Profile Manager Customer Details
$config['acquia_contenthub.admin_settings']['secret_key'] = '123123123123123123123123123'; //Get from Profile Manager Customer Details
$config['acquia_contenthub.admin_settings']['client_name'] = 'site_id_domain_b_prod'; //Arbitrary but usually matches site_id
$config['acquia_contenthub.admin_settings']['origin'] = '12312312312312312312312';
$config['acquia_contenthub.admin_settings']['webhook'] = [
'uuid' => 'will be generated on /admin/config/services/acquia-contenthub',
'url' => 'create at /admin/config/services/acquia-contenthub',
'settings_url' => 'will be generated on /admin/config/services/acquia-contenthub',
];
break;
case '01test':
$config['acquia_lift.settings']['credential']['account_id'] = 'LIFTACCOUNT';
$config['acquia_lift.settings']['credential']['site_id'] = 'site_id_domain_b_test'; //Set in Lift Profile Manager
$config['acquia_lift.settings']['credential']['content_origin'] = '12312312312312312312312'; //Same as origin below
$config['acquia_contenthub.admin_settings']['api_key'] = '121231231231'; //Get from Profile Manager Customer Details
$config['acquia_contenthub.admin_settings']['secret_key'] = '123123123123123123123123123'; //Get from Profile Manager Customer Details
$config['acquia_contenthub.admin_settings']['client_name'] = 'site_id_domain_b_test'; //Arbitrary but usually matches site_id
$config['acquia_contenthub.admin_settings']['origin'] = '12312312312312312312312';
$config['acquia_contenthub.admin_settings']['webhook'] = [
'uuid' => 'will be generated on /admin/config/services/acquia-contenthub',
'url' => 'create at /admin/config/services/acquia-contenthub',
'settings_url' => 'will be generated on /admin/config/services/acquia-contenthub',
];
break;
case '01dev':
$config['acquia_lift.settings']['credential']['account_id'] = 'LIFTACCOUNT';
$config['acquia_lift.settings']['credential']['site_id'] = 'site_id_domain_b_dev'; //Set in Lift Profile Manager
$config['acquia_lift.settings']['credential']['content_origin'] = '12312312312312312312312'; //Same as origin below
$config['acquia_contenthub.admin_settings']['api_key'] = '121231231231'; //Get from Profile Manager Customer Details
$config['acquia_contenthub.admin_settings']['secret_key'] = '123123123123123123123123123'; //Get from Profile Manager Customer Details
$config['acquia_contenthub.admin_settings']['client_name'] = 'site_id_domain_b_dev'; //Arbitrary but usually matches site_id
$config['acquia_contenthub.admin_settings']['origin'] = '12312312312312312312312';
$config['acquia_contenthub.admin_settings']['webhook'] = [
'uuid' => 'will be generated on /admin/config/services/acquia-contenthub',
'url' => 'create at /admin/config/services/acquia-contenthub',
'settings_url' => 'will be generated on /admin/config/services/acquia-contenthub',
];
break;
}
}
if ($is_local_env) {
$config['acquia_lift.settings']['credential']['customer_site'] = 'local';
$config['acquia_contenthub.admin_settings']['origin'] = 'Not connected';
}
D7-example-settings.php
<?php
if (isset($_ENV['AH_SITE_ENVIRONMENT'])) {
switch ($_ENV['AH_SITE_ENVIRONMENT']) {
case 'prod':
// Acquia Lift Unique Values - Create in Lift Profile Manager Admin > Manage Configuration Data > Customer Sites
$conf['acquia_lift_site_id'] = 'domain_prod'; //Unique
// Uncomment this if you only want Content Hub content from this environment to show in Experience Builder (value should match origin below)
//$conf['acquia_lift_content_origin'] = '';
// Acquia Content Hub Settings for dev - Create client_name in Content Hub module
$conf['content_hub_connector_client_name'] = 'domain_prod'; //Unique
$conf['content_hub_connector_origin'] = ''; //Unique
break;
case 'test':
// Acquia Lift Unique Values - Create in Lift Profile Manager Admin > Manage Configuration Data > Customer Sites
$conf['acquia_lift_site_id'] = 'domain_test'; //Unique
// Uncomment this if you only want Content Hub content from this environment to show in Experience Builder (value should match origin below)
//$conf['acquia_lift_content_origin'] = '';
// Acquia Content Hub Settings for dev - Create client_name in Content Hub module
$conf['content_hub_connector_client_name'] = 'domain_test'; //Unique
$conf['content_hub_connector_origin'] = ''; //Unique
break;
case 'dev':
// Acquia Lift Unique Values - Create in Lift Profile Manager Admin > Manage Configuration Data > Customer Sites
$conf['acquia_lift_site_id'] = 'domain_dev'; //Unique
// Uncomment this if you only want Content Hub content from this environment to show in Experience Builder (value should match origin below)
//$conf['acquia_lift_content_origin'] = '';
// Acquia Content Hub Settings for dev - Create client_name in Content Hub module
$conf['content_hub_connector_client_name'] = 'domain_dev'; //Unique
$conf['content_hub_connector_origin'] = ''; //Unique
}
}
D8-example-settings.php
<?php
if (isset($_ENV['AH_SITE_ENVIRONMENT'])) {
switch ($_ENV['AH_SITE_ENVIRONMENT']) {
case 'prod':
// Acquia Lift Unique Values - Create in Lift Profile Manager Admin > Manage Configuration Data > Customer Sites
$config['acquia_lift.settings']['credential']['site_id'] = 'mysite_prod';
// Uncomment this if you only want Content Hub content from this environment to show in Experience Builder
//$config['acquia_lift.settings']['credential']['content_origin'] = 'will be generated on /admin/config/services/acquia-contenthub';
// Acquia Content Hub Settings for prod - Create client_name in Content Hub module
// Run "drush cget acquia_contenthub.admin_settings --include-overridden" to get all the settings.
$config['acquia_contenthub.admin_settings']['client_name'] = 'create at /admin/config/services/acquia-contenthub';
$config['acquia_contenthub.admin_settings']['origin'] = 'will be generated on /admin/config/services/acquia-contenthub';
$config['acquia_contenthub.admin_settings']['webhook'] = [
'uuid' => 'will be generated on /admin/config/services/acquia-contenthub',
'url' => 'create at /admin/config/services/acquia-contenthub',
'settings_url' => 'will be generated on /admin/config/services/acquia-contenthub',
];
break;
case 'test':
// Acquia Lift Unique Values - Create in Lift Profile Manager Admin > Manage Configuration Data > Customer Sites
$config['acquia_lift.settings']['credential']['site_id'] = 'mysite_test';
// Uncomment this if you only want Content Hub content from this environment to show in Experience Builder
//$config['acquia_lift.settings']['credential']['content_origin'] = 'will be generated on /admin/config/services/acquia-contenthub';
// Acquia Content Hub Settings for test - Create client_name in Content Hub module
$config['acquia_contenthub.admin_settings']['client_name'] = 'create at /admin/config/services/acquia-contenthub';
$config['acquia_contenthub.admin_settings']['origin'] = 'will be generated on /admin/config/services/acquia-contenthub';
$config['acquia_contenthub.admin_settings']['webhook'] = [
'uuid' => 'will be generated on /admin/config/services/acquia-contenthub',
'url' => 'create at /admin/config/services/acquia-contenthub',
'settings_url' => 'will be generated on /admin/config/services/acquia-contenthub',
];
break;
case 'dev':
// Acquia Lift Unique Values - Create in Lift Profile Manager Admin > Manage Configuration Data > Customer Sites
$config['acquia_lift.settings']['credential']['site_id'] = 'mysite_dev';
// Uncomment this if you only want Content Hub content from this environment to show in Experience Builder
//$config['acquia_lift.settings']['credential']['content_origin'] = 'will be generated on /admin/config/services/acquia-contenthub';
// Acquia Content Hub Settings for dev - Create client_name in Content Hub module
$config['acquia_contenthub.admin_settings']['client_name'] = 'create at /admin/config/services/acquia-contenthub';
$config['acquia_contenthub.admin_settings']['origin'] = 'will be generated on /admin/config/services/acquia-contenthub';
$config['acquia_contenthub.admin_settings']['webhook'] = [
'uuid' => 'will be generated on /admin/config/services/acquia-contenthub',
'url' => 'create at /admin/config/services/acquia-contenthub',
'settings_url' => 'will be generated on /admin/config/services/acquia-contenthub',
];
break;
}
}
LiftWebJavaClient-HMACv1.java
package com.acquia.lift.examples;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.protocol.HttpContext;
/**
* Example Java client for talking to Lift Web API.
*
* Please note this class is for illustrative purposes only
* - it's not thread-safe
* - it does not clean resources completely after itself
* - may not be as performant as you'd want
*/
public class LiftWebJavaClient {
/**
* The API URL for Lift Web.
*/
private String apiUrl;
/**
* The Lift Web account name to use.
*/
private String accountId;
/**
* The access key to use for authorization.
*/
private String accessKey;
/**
* The secret key to use for authorization.
*/
private String secretKey;
/**
* The list of headers that can be used in the canonical request.
*/
private static final String[] HEADER_WHITE_LIST = { "Accept", "Host", "User-Agent" };
/**
* HMAC SHA1 algorithm constant
*/
private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
/**
* UTF8 encoding
*/
private static final String UTF8 = "UTF-8";
/**
* constructor
*
* @param accountId The name of the Lift Web account.
* @param apiUrl The URL to use for API calls.
* @param accessKey The access key to use for authorization.
* @param secretKey The secret key to use for authorization.
*/
private LiftWebJavaClient(String accountId, String apiUrl, String accessKey, String secretKey) {
this.accountId = accountId;
this.apiUrl = apiUrl;
this.accessKey = accessKey;
this.secretKey = secretKey;
}
/**
* Generates an endpoint for a particular section of the Lift Web API.
*
* @param path The endpoint path, e.g. 'segments' or 'events/my-event'
* @return String The endpoint to make calls to.
*/
protected String generateEndpoint(String path) {
return this.apiUrl + "/dashboard/rest/" + this.accountId + "/" + path;
}
/**
* Returns the canonical representation of a provided HTTP request.
*
* @param httpRequest request
* @return String The canonical representation of the request.
*/
private String canonicalizeRequest(HttpRequest httpRequest) throws Exception {
StringBuilder sb = new StringBuilder();
sb.append(httpRequest.getRequestLine().getMethod().toUpperCase()).append("\n");
for (String headerName : LiftWebJavaClient.HEADER_WHITE_LIST) {
Header header = httpRequest.getFirstHeader(headerName);
if (header != null) {
String lowercaseHeaderName = headerName.toLowerCase();
String trimmedHeaderValue = header.getValue().trim();
sb.append(lowercaseHeaderName).append(":").append(trimmedHeaderValue).append("\n");
}
}
URI uri = new URI(httpRequest.getRequestLine().getUri());
sb.append(uri.getPath());
String query = uri.getQuery();
if (query != null && query.trim().length() > 0) {
List<String> parameterNameValuePairs = Arrays.<String> asList(query.split("&"));
if (parameterNameValuePairs.size() > 0) {
Collections.sort(parameterNameValuePairs);
sb.append("?").append(parameterNameValuePairs.get(0));
for (int i = 1; i < parameterNameValuePairs.size(); i++) {
sb.append("&").append(parameterNameValuePairs.get(i));
}
}
}
return sb.toString();
}
/**
* calculates HMAC representation of the data using provided algorithm and key
*
*@param algorithm algorithm of choice; for us always SHA1
*@param data to hash
* @param key to has it with
*/
private String hashHMAC(String algorithm, String data, String key) throws Exception {
String result;
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(LiftWebJavaClient.UTF8),
algorithm);
Mac mac = Mac.getInstance(algorithm);
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(data.getBytes());
result = Base64.encodeBase64String(rawHmac);
return result;
}
/**
* adds 'Authorization' header to the request
*
* @param httpRequest request
*/
private void addAuthenticationCredentials(HttpRequest httpRequest) throws Exception {
// if access key is not provided, it means REST APIs are not authenticated
if (accessKey == null) {
return;
}
String canonical = canonicalizeRequest(httpRequest);
String hmac = hashHMAC(LiftWebJavaClient.HMAC_SHA1_ALGORITHM, canonical, this.secretKey);
String authorizationHeader = "HMAC " + this.accessKey + ":" + hmac;
System.out.println(authorizationHeader);
httpRequest.addHeader("Authorization", authorizationHeader);
}
/**
* when using Apache HTTP Client library, we need to correctly inject the authorization header
*
* @returns an HTTP Client that can be used to submit a request
*/
private CloseableHttpClient createHttpClient() {
return HttpClientBuilder.create().addInterceptorLast(new HttpRequestInterceptor() {
@Override
public void process(HttpRequest request, HttpContext context) throws HttpException,
IOException {
try {
addAuthenticationCredentials(request);
} catch(Exception e) {
throw new IOException(e.getMessage(), e);
}
}
}).build();
}
/**
* converts the response body into a string
*
* @param httpResponse
* @return String representation of the HTTP response body, if possible
*/
protected String readResponse(HttpResponse httpResponse) throws Exception {
HttpEntity entity = httpResponse.getEntity();
BufferedReader br = new BufferedReader(new InputStreamReader(entity.getContent(),
LiftWebJavaClient.UTF8));
StringBuilder body = new StringBuilder("");
String line = null;
while ((line = br.readLine()) != null) {
if (body.length() > 0) {
body.append("\n");
}
body.append(line);
}
br.close();
return body.toString().trim();
}
//
// EXAMPLES START HERE
//
/**
* these are several examples in the code
*/
public static void main(String[] args) throws Exception {
String accountId = "your_account_id";
String accessKey = "your_access_key"; // or null if the REST APIs are not authenticated
String secretKey = "your_secret_key";
String apiUrl = "your_apiUrl";
LiftWebJavaClient client = new LiftWebJavaClient(accountId, apiUrl, accessKey, secretKey);
// example 1 - get segments
{
String segmentsPath = "segments";
String url = client.generateEndpoint(segmentsPath);
HttpGet httpGet = new HttpGet(url);
try (CloseableHttpClient httpClient = client.createHttpClient()) {
HttpResponse httpResponse = httpClient.execute(httpGet);
int statusCode = httpResponse.getStatusLine().getStatusCode();
System.out.println(statusCode);
if (statusCode == 200) {
System.out.println(client.readResponse(httpResponse));
} else {
throw new Exception("status not HTTP OK"
+ httpResponse.getStatusLine().toString());
}
}
}
// example 2 - put event
{
String eventName = "LiftWebRESTEEventExample";
String eventType = "OTHER";
String eventsPath = "events/" + eventName + "?type=" + eventType;
String url = client.generateEndpoint(eventsPath);
HttpPut request = new HttpPut(url);
try (CloseableHttpClient httpClient = client.createHttpClient()) {
HttpResponse httpResponse = httpClient.execute(request);
int statusCode = httpResponse.getStatusLine().getStatusCode();
System.out.println(statusCode);
if (statusCode != 200) {
if (httpResponse.getStatusLine().getStatusCode() != 200) {
throw new Exception("status not HTTP OK"
+ httpResponse.getStatusLine().toString());
}
}
}
}
// example 3 - delete event
{
String eventName = "LiftWebRESTEEventExample";
String eventsPath = "events/" + eventName;
String url = client.generateEndpoint(eventsPath);
HttpDelete request = new HttpDelete(url);
try (CloseableHttpClient httpClient = client.createHttpClient()) {
HttpResponse httpResponse = httpClient.execute(request);
int statusCode = httpResponse.getStatusLine().getStatusCode();
System.out.println(statusCode);
if (statusCode != 200) {
if (httpResponse.getStatusLine().getStatusCode() != 200) {
throw new Exception("status not HTTP OK"
+ httpResponse.getStatusLine().toString());
}
}
}
}
}
}
LiftWebJavaClient-HMACv2.java
package com.acquia.lift.examples;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.protocol.HttpContext;
import com.acquia.http.HMACHttpRequestInterceptor;
import com.acquia.http.HMACHttpResponseInterceptor;
/**
* Example Java client for talking to Lift Web API.
*
* Please note this class is for illustrative purposes only
* - it's not thread-safe
* - it does not clean resources completely after itself
* - may not be as performant as you'd want
*/
public class LiftWebJavaClient {
/**
* The API URL for Lift Web.
*/
private String apiUrl;
/**
* The Lift Web account name to use.
*/
private String accountId;
/**
* The access key to use for authorization.
*/
private String accessKey;
/**
* The secret key to use for authorization.
*/
private String secretKey;
/**
* UTF8 encoding
*/
private static final String UTF8 = "UTF-8";
/**
* constructor
*
* @param accountId The name of the Lift Web account.
* @param apiUrl The URL to use for API calls.
* @param accessKey The access key to use for authorization.
* @param secretKey The secret key to use for authorization.
*/
private LiftWebJavaClient(String accountId, String apiUrl, String accessKey, String secretKey) {
this.accountId = accountId;
this.apiUrl = apiUrl;
this.accessKey = accessKey;
this.secretKey = secretKey;
}
/**
* Generates an endpoint for a particular section of the Lift Web API.
*
* @param path The endpoint path, e.g. 'segments' or 'events/my-event'
* @return String The endpoint to make calls to.
*/
protected String generateEndpoint(String path) {
return this.apiUrl + "/" + this.accountId + "/" + path;
}
/**
* when using Apache HTTP Client library, we need to correctly inject the authorization header
*
* @returns an HTTP Client that can be used to submit a request
*/
private CloseableHttpClient createHttpClient() {
HttpClientBuilder clientBuilder = HttpClientBuilder.create();
HMACHttpRequestInterceptor requestInterceptor = new HMACHttpRequestInterceptor("Acquia",
this.accessKey, this.secretKey, "SHA256") { //v2 only supports SHA256
@Override
public void process(HttpRequest request, HttpContext context)
throws HttpException, IOException {
super.process(request, context);
}
};
clientBuilder.addInterceptorLast(requestInterceptor);
HMACHttpResponseInterceptor responseInterceptor = new HMACHttpResponseInterceptor(
this.secretKey, "SHA256"); //v2 only supports SHA256
clientBuilder.addInterceptorLast(responseInterceptor);
return clientBuilder.build();
}
/**
* converts the response body into a string
*
* @param httpResponse
* @return String representation of the HTTP response body, if possible
*/
protected String readResponse(HttpResponse httpResponse) throws Exception {
HttpEntity entity = httpResponse.getEntity();
BufferedReader br = new BufferedReader(
new InputStreamReader(entity.getContent(), LiftWebJavaClient.UTF8));
StringBuilder body = new StringBuilder("");
String line = null;
while ((line = br.readLine()) != null) {
if (body.length() > 0) {
body.append("\n");
}
body.append(line);
}
br.close();
return body.toString().trim();
}
//
// EXAMPLES START HERE
//
/**
* these are several examples in the code
*/
public static void main(String[] args) throws Exception {
String accountId = "your_account_id";
String accessKey = "your_access_key"; // or null if the REST APIs are not authenticated
String secretKey = "your_secret_key";
String apiUrl = "your_apiUrl";
LiftWebJavaClient client = new LiftWebJavaClient(accountId, apiUrl, accessKey, secretKey);
// example 1 - get segments
{
String segmentsPath = "segments";
String url = client.generateEndpoint(segmentsPath);
HttpGet httpGet = new HttpGet(url);
try (CloseableHttpClient httpClient = client.createHttpClient()) {
HttpResponse httpResponse = httpClient.execute(httpGet);
int statusCode = httpResponse.getStatusLine().getStatusCode();
System.out.println(statusCode);
if (statusCode == 200) {
System.out.println(client.readResponse(httpResponse));
} else {
throw new Exception(
"status not HTTP OK" + httpResponse.getStatusLine().toString());
}
}
}
// example 2 - put event
{
String eventName = "LiftWebRESTEEventExample";
String eventType = "OTHER";
String eventsPath = "events/" + eventName + "?type=" + eventType;
String url = client.generateEndpoint(eventsPath);
HttpPut request = new HttpPut(url);
try (CloseableHttpClient httpClient = client.createHttpClient()) {
HttpResponse httpResponse = httpClient.execute(request);
int statusCode = httpResponse.getStatusLine().getStatusCode();
System.out.println(statusCode);
if (statusCode != 200) {
throw new Exception(
"status not HTTP OK" + httpResponse.getStatusLine().toString());
}
}
}
// example 3 - delete event
{
String eventName = "LiftWebRESTEEventExample";
String eventsPath = "events/" + eventName;
String url = client.generateEndpoint(eventsPath);
HttpDelete request = new HttpDelete(url);
try (CloseableHttpClient httpClient = client.createHttpClient()) {
HttpResponse httpResponse = httpClient.execute(request);
int statusCode = httpResponse.getStatusLine().getStatusCode();
System.out.println(statusCode);
if (statusCode != 200) {
throw new Exception(
"status not HTTP OK" + httpResponse.getStatusLine().toString());
}
}
}
}
}
LiftWebPHPClient.php
<?php
/**
* @file
* Example PHP client for talking to Lift Web API.
*/
class LiftWebPHPClient {
/**
* An http client for making calls to Lift Web.
*/
protected $httpClient;
/**
* The API URL for Lift Web.
*
* @var string
*/
protected $apiUrl;
/**
* The Lift Web account ID to use.
*
* @var string
*/
protected $accountId;
/**
* The access key to use for authorization.
*
* @var string
*/
protected $accessKey;
/**
* The secret key to use for authorization.
*
* @var string
*/
protected $secretKey;
/**
* The list of headers that can be used in the canonical request.
*
* @var array
*/
protected $headerWhitelist = array(
'Accept',
'Host',
'User-Agent'
);
/**
* The singleton instance.
*
* @var ALProfilesAPI
*/
private static $instance;
/**
* Singleton factory method.
*
* @param $account_id
* The ID of the Lift Web account.
* @param $api_url
* The URL to use for API calls.
* @param $access_key
* The access key to use for authorization.
* @param $secret_key
* The secret key to use for authorization.
*
* @return ALProfilesAPI
*/
public static function getInstance($account_id, $customer_site, $api_url, $access_key, $secret_key) {
if (empty(self::$instance)) {
self::$instance = new self($account_id, $customer_site, $api_url, $access_key, $secret_key);
}
return self::$instance;
}
/**
* Private constructor as this is a singleton.
*
* @param $account_id
* The ID of the Lift Web account.
* @param $api_url
* The URL to use for API calls.
* @param $access_key
* The access key to use for authorization.
* @param $secret_key
* The secret key to use for authorization.
*/
private function __construct($account_id, $site, $api_url, $access_key, $secret_key) {
$this->accountId = $account_id;
$this->customerSite = $site;
$this->apiUrl = $api_url;
$this->accessKey = $access_key;
$this->secretKey = $secret_key;
}
/**
* Returns an http client to use for Lift Web calls.
*/
protected function httpClient() {
if (!isset($this->httpClient)) {
$this->httpClient = new AcquiaLiftDrupalHttpClient();
}
return $this->httpClient;
}
/**
* Generates an endpoint for a particular section of the Lift Web API.
*
* @param string $path
* The endpoint path, e.g. 'segments' or 'events/my-event'
* @return string
* The endpoint to make calls to.
*/
protected function generateEndpoint($path) {
return $this->apiUrl . '/dashboard/rest/' . $this->accountId . '/' . $path;
}
/**
* Returns the canonical representation of a request.
*
* @param $method
* The request method, e.g. 'GET'.
* @param $path
* The path of the request, e.g. '/dashboard/rest/[ACCOUNTID]/segments'.
* @param array $parameters
* An array of request parameters.
* @param array $headers
* An array of request headers.
* @param bool $add_extra_headers
* Whether to add the extra headers that we know drupal_http_request will add
* to the request. Set to FALSE if the request will not be handled by
* drupal_http_request.
*
* @return string
* The canonical representation of the request.
*/
public function canonicalizeRequest($method, $url, $parameters = array(), $headers = array(), $add_extra_headers = TRUE) {
$parsed_url = parse_url($url);
$str = strtoupper($method) . "\n";
// Certain headers may get added to the actual request so we need to
// add them here.
if ($add_extra_headers && !isset($headers['User-Agent'])) {
$headers['User-Agent'] = 'Drupal (+http://drupal.org/)';
}
if ($add_extra_headers && !isset($headers['Host'])) {
$headers['Host'] = $parsed_url['host'] . (!empty($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
}
// Sort all header names alphabetically.
$header_names = array_keys($headers);
uasort($header_names, create_function('$a, $b', 'return strtolower($a) < strtolower($b) ? -1 : 1;'));
// Add each header (trimmed and lowercased) and value to the string, separated by
// a colon, and with a new line after each header:value pair.
foreach ($header_names as $header) {
if (!in_array($header, $this->headerWhitelist)) {
continue;
}
$str .= trim(strtolower($header)) . ':' . trim($headers[$header]) . "\n";
}
// Add the path.
$str .= $parsed_url['path'];
// Sort any parameters alphabetically and add them as a querystring to our string.
if (!empty($parameters)) {
ksort($parameters);
$first_param = key($parameters);
$str .= '?' . $first_param . '=' . array_shift($parameters);
foreach ($parameters as $key => $value) {
$str .= '&' . $key . '=' . $value;
}
}
return $str;
}
/**
* Returns a string to use for the 'Authorization' header.
*
* @return string
*/
public function getAuthHeader($method, $path, $parameters = array(), $headers = array()) {
$canonical = $this->canonicalizeRequest($method, $path, $parameters, $headers, is_a($this->httpClient(), 'AcquiaLiftDrupalHttpClient'));
$binary = hash_hmac('sha1', (string) $canonical, $this->secretKey, TRUE);
$hex = hash_hmac('sha1', (string) $canonical, $this->secretKey, FALSE);
$hmac = base64_encode($binary);
return 'HMAC ' . $this->accessKey . ':' . $hmac;
}
/**
* Example method that makes a call to the "example" endpoint.
*/
public function getMakeAPICall() {
// First get our Authorization header.
$headers = array('Accept' => 'application/json');
$url = $this->generateEndpoint('example');
$params = array();
if (!empty($this->customerSite)) {
$params['customerSite'] = $this->customerSite;
}
$auth_header = $this->getAuthHeader('GET', $url, $params, $headers);
$headers += array('Authorization' => $auth_header);
$querystring = empty($this->customerSite) ? '' : '?customerSite=' . rawurlencode($this->customerSite);
$response = $this->httpClient()->get($url . $querystring, $headers);
// Do something with the response.
}
}
Site Factory
acsf-backups.php
#!/usr/bin/env php
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
// Example script for making backups of several sites through the REST API.
// Two things are left up to the script user:
// - Including Guzzle, which is used by request();
// e.g. by doing: 'composer init; composer require guzzlehttp/guzzle'
require 'vendor/autoload.php';
// - Populating $config:
$config = [
// URL of a subsection inside the SF REST API; must end with sites/.
'url' => 'https://www.[CLIENT].acsitefactory.com/api/v1/sites/',
'api_user' => '',
'api_key' => '',
// Site IDs of the sites to process; can also be provided as CLI argument.
'sites' => [],
// Number of days before backups are deleted; can also be provided on ClI.
'backup_retention' => 30,
// Request parameter for /api/v1#List-sites.
'limit' => 100,
// The components of the websites to backup.
// Details: /api/v1#Create-a-site-backup.
// 'codebase' is excluded from the default components since those files would
// be the same in each site backup, and cannot be restored into the factory.
'components' => ['database', 'public files', 'private files', 'themes'],
];
if ($argc < 2 || $argc > 4 || !in_array($argv[1], array('backup-add', 'backup-del'), TRUE)) {
$help = <<<EOT
Usage: php application.php parameter [sites] [backup_retention=30].
Where:
- parameter is one of {backup-add, backup-del}
- [sites] is be either a comma separated list (e.g. 111,222,333) or 'all'
- [backup_retention] the number of days for which the backups should be retained. If passed this threshold they will be deleted when using backup-del command (defaults to 30 days)
EOT;
echo $help;
exit(1);
}
// Lower the 'limit' parameter to the maximum which the API allows.
if ($config['limit'] > 100) {
$config['limit'] = 100;
}
// Check if the list of sites in $config is to be overridden by the provided
// input. If the input is set to 'all' then fetch the list of sites using the
// Site Factory API, otherwise it should be a comma separated list of site IDs.
if ($argc >= 3) {
if ($argv[2] == 'all') {
$config['sites'] = get_all_sites($config);
}
else {
// Removing spaces.
$no_spaces = str_replace(' ', '', $argv[2]);
// Keeping only IDs that are valid.
$config['sites'] = array_filter(explode(',', $no_spaces), "id_check");
// Removing duplicates.
$config['sites'] = array_unique($config['sites']);
}
}
// Check if the backup_retention parameter is overwritten.
if ($argc >= 4 && id_check($argv[3])) {
$config['backup_retention'] = $argv[3];
}
// Helper; returns true if given ID is valid (numeric and > 0), false otherwise.
function id_check($id) {
return is_numeric($id) && $id > 0;
}
// Fetches the list of all sites using the Site Factory REST API.
function get_all_sites($config) {
// Starting from page 1.
$page = 1;
$sites = array();
printf("Getting all sites - Limit / request: %d\n", $config['limit']);
// Iterate through the paginated list until we get all sites, or
// an error occurs.
do {
printf("Getting sites page: %d\n", $page);
$method = 'GET';
$url = $config['url'] . "?limit=" . $config['limit'] . "&page=" . $page;
$has_another_page = FALSE;
$res = request($url, $method, $config);
if ($res->getStatusCode() != 200) {
echo "Error whilst fetching site list!\n";
exit(1);
}
$next_page_header = $res->getHeader('link');
$response = json_decode($res->getBody()->getContents());
// If the next page header is present and has a "next" link, we know we
// have another page.
if (!empty($next_page_header) && strpos($next_page_header[0], 'rel="next"') !== FALSE) {
$has_another_page = TRUE;
$page++;
}
foreach ($response->sites as $site) {
$sites[] = $site->id;
}
} while ($has_another_page);
return $sites;
}
// Helper function to return API user and key.
function get_request_auth($config) {
return [
'auth' => [$config['api_user'], $config['api_key']],
];
}
// Sends a request using the guzzle HTTP library; prints out any errors.
function request($url, $method, $config, $form_params = []) {
// We are setting http_errors => FALSE so that we can handle them ourselves.
// Otherwise, we cannot differentiate between different HTTP status codes
// since all 40X codes will just throw a ClientError exception.
$client = new Client(['http_errors' => FALSE]);
$parameters = get_request_auth($config);
if ($form_params) {
$parameters['form_params'] = $form_params;
}
try {
$res = $client->request($method, $url, $parameters);
return $res;
}
catch (RequestException $e) {
printf("Request exception!\nError message %s\n", $e->getMessage());
}
return NULL;
}
// Iterates through backups for a certain site and deletes them if they are
// past the backup_retention mark.
function backup_del($backups, $site_id, $config) {
// Iterating through existing backups for current site and deleting those
// that are X days old.
$time = $config['backup_retention'] . ' days ago';
foreach ($backups as $backup) {
$timestamp = $backup->timestamp;
if ($timestamp < strtotime($time)) {
printf("Deleting %s with backup (ID: %d).\n", $backup->label, $backup->id);
$method = 'DELETE';
$url = $config['url'] . $site_id . '/backups/' . $backup->id;
$res = request($url, $method, $config);
if (!$res || $res->getStatusCode() != 200) {
printf("Error! Whilst deleting backup ID %d. Please check the above messages for the full error.\n", $backup->id);
continue;
}
$task = json_decode($res->getBody()->getContents())->task_id;
printf("Deleting backup (ID: %d) with task ID %d.\n", $backup->id, $task);
}
else {
printf("Keeping %s since it was created sooner than %s (ID: %d).\n", $backup->label, $time, $backup->id);
}
}
}
// Creates or deletes backups depending on the operation given.
function backup($operation, $config) {
// Setting global operation endpoints and messages.
if ($operation === 'backup-add') {
$endpoint = '/backup';
$message = "Creating backup for site ID %d.\n";
$method = 'POST';
$form_params = [
'components' => $config['components'],
];
}
else {
// Unlike in other code, we do not paginate through backups, but we get the
// maximum for one request.
$endpoint = '/backups?limit=100';
$message = "Retrieving old backups for site ID %d.\n";
$method = 'GET';
$form_params = [];
}
// Iterating through the list of sites defined in secrets.php.
for ($i = 0; $i < count($config['sites']); $i++) {
// Sending API request.
$url = $config['url'] . $config['sites'][$i] . $endpoint;
$res = request($url, $method, $config, $form_params);
$message_site = sprintf($message, $config['sites'][$i]);
// If request returned an error, we show that and
// we continue with another site.
if (!$res) {
// An exception was thrown.
printf('Error whilst %s', $message_site);
printf("Please check the above messages for the full error.\n");
continue;
}
elseif ($res->getStatusCode() != 200) {
// If a site has no backups, it will return a 404.
if ($res->getStatusCode() == 404 && $operation == 'backup-del') {
printf("Site ID %d has no backups.\n", $config['sites'][$i]);
}
else {
printf('Error whilst %s', $message_site);
printf("HTTP code %d\n", $res->getStatusCode());
$body = json_decode($res->getBody()->getContents());
printf("Error message: %s\n", $body ? $body->message : '<empty>');
}
continue;
}
// All good here.
echo $message_site;
// For deleting backups, we have to iterate through the backups we get.
if ($operation == 'backup-del') {
backup_del(json_decode($res->getBody()->getContents())->backups, $config['sites'][$i], $config);
}
}
}
backup($argv[1], $config);
acsfd8+.memcache.settings.php
<?php
/**
* @file
* Contains caching configuration.
*/
use Composer\Autoload\ClassLoader;
/**
* Use memcache as cache backend.
*
* Autoload memcache classes and service container in case module is not
* installed. Avoids the need to patch core and allows for overriding the
* default backend when installing Drupal.
*
* @see https://www.drupal.org/node/2766509
*/
if (!function_exists('get_deployment_id')) {
function get_deployment_id() {
static $id = NULL;
if ($id == NULL) {
$site_settings = $GLOBALS['gardens_site_settings'];
$deployment_id_file = "/mnt/www/site-php/{$site_settings['site']}.{$site_settings['env']}/.vcs_head_ref";
if (is_readable($deployment_id_file)) {
$id = file_get_contents($deployment_id_file);
if ($id === FALSE) {
$id = NULL;
}
}
else {
$id = NULL;
}
}
return $id;
}
}
if (getenv('AH_SITE_ENVIRONMENT') &&
array_key_exists('memcache', $settings) &&
array_key_exists('servers', $settings['memcache']) &&
!empty($settings['memcache']['servers'])
) {
// Check for PHP Memcached libraries.
$memcache_exists = class_exists('Memcache', FALSE);
$memcached_exists = class_exists('Memcached', FALSE);
$memcache_services_yml = DRUPAL_ROOT . '/modules/contrib/memcache/memcache.services.yml';
$memcache_module_is_present = file_exists($memcache_services_yml);
if ($memcache_module_is_present && ($memcache_exists || $memcached_exists)) {
// Use Memcached extension if available.
if ($memcached_exists) {
$settings['memcache']['extension'] = 'Memcached';
}
if (class_exists(ClassLoader::class)) {
$class_loader = new ClassLoader();
$class_loader->addPsr4('Drupal\\memcache\\', DRUPAL_ROOT . '/modules/contrib/memcache/src');
$class_loader->register();
$settings['container_yamls'][] = $memcache_services_yml;
// Acquia Default Settings for the memcache module
// Default settings for the Memcache module.
// Enable compression for PHP 7.
$settings['memcache']['options'][Memcached::OPT_COMPRESSION] = TRUE;
// Set key_prefix to avoid drush cr flushing all bins on multisite.
$settings['memcache']['key_prefix'] = sprintf('%s%s_', $conf['acquia_hosting_site_info']['db']['name'], get_deployment_id());
// Decrease latency.
$settings['memcache']['options'][Memcached::OPT_TCP_NODELAY] = TRUE;
// Bootstrap cache.container with memcache rather than database.
$settings['bootstrap_container_definition'] = [
'parameters' => [],
'services' => [
'database' => [
'class' => 'Drupal\Core\Database\Connection',
'factory' => 'Drupal\Core\Database\Database::getConnection',
'arguments' => ['default'],
],
'settings' => [
'class' => 'Drupal\Core\Site\Settings',
'factory' => 'Drupal\Core\Site\Settings::getInstance',
],
'memcache.settings' => [
'class' => 'Drupal\memcache\MemcacheSettings',
'arguments' => ['@settings'],
],
'memcache.factory' => [
'class' => 'Drupal\memcache\Driver\MemcacheDriverFactory',
'arguments' => ['@memcache.settings'],
],
'memcache.timestamp.invalidator.bin' => [
'class' => 'Drupal\memcache\Invalidator\MemcacheTimestampInvalidator',
'arguments' => ['@memcache.factory', 'memcache_bin_timestamps', 0.001],
],
'memcache.backend.cache.container' => [
'class' => 'Drupal\memcache\DrupalMemcacheInterface',
'factory' => ['@memcache.factory', 'get'],
'arguments' => ['container'],
],
'cache_tags_provider.container' => [
'class' => 'Drupal\Core\Cache\DatabaseCacheTagsChecksum',
'arguments' => ['@database'],
],
'cache.container' => [
'class' => 'Drupal\memcache\MemcacheBackend',
'arguments' => [
'container',
'@memcache.backend.cache.container',
'@cache_tags_provider.container',
'@memcache.timestamp.invalidator.bin',
'@memcache.settings',
],
],
],
];
// Content Hub 2.x requires the Depcalc module which needs to use the database backend.
$settings['cache']['bins']['depcalc'] = 'cache.backend.database';
// Use memcache for bootstrap, discovery, config instead of fast chained
// backend to properly invalidate caches on multiple webs.
// See https://www.drupal.org/node/2754947
$settings['cache']['bins']['bootstrap'] = 'cache.backend.memcache';
$settings['cache']['bins']['discovery'] = 'cache.backend.memcache';
$settings['cache']['bins']['config'] = 'cache.backend.memcache';
// Use memcache as the default bin.
$settings['cache']['default'] = 'cache.backend.memcache';
}
}
}
api-dbupdate.txt
#!/bin/sh
## Initiate a code and database update from Site Factory
## Origin: http://docs.acquia.com/site-factory/extend/api/examples
# This script should primarily be used on non-production environments.
# Mandatory parameters:
# env : environment to run update on. Example: dev, pprod, qa2, test.
# - the api user must exist on this environment.
# - for security reasons, update of prod environment is *not*
# supported and must be performed manually through UI
# branch : branch/tag to update. Example: qa-build
# update_type : code or code,db
source $(dirname "$0")/includes/global-api-settings.inc.sh
env="$1"
branch="$2"
update_type="$3"
# add comma to "code,db" if not already entered
if [ "$update_type" == "code,db" ]
then
update_type="code, db"
fi
# Edit the following line, replacing [domain] with the appropriate
# part of your domain name.
curl "https://www.${env}-[domain].acsitefactory.com/api/v1/update" \
-v -u ${user}:${api_key} -k -X POST \
-H 'Content-Type: application/json' \
-d "{\"sites_ref\": \"${branch}\", \"sites_type\": \"${update_type}\"}"
acsf-cache-lifetime.php
<?php
/**
* @file
*
* This post-settings-php hook is created to conditionally set the cache
* lifetime of Drupal to be a value that is greater than 300 (5 minutes).
* It also does not let you set it to be lower than 5 minutes.
*
* This does not fire on Drush requests, as it interferes with site creation.
* It also means that drush will report back incorrect values for the
* cache lifetime, so using a real browser is the easiest way to validate
* what the current settings are.
*
* How to enable this for a site:
* - drush vset acsf_allow_override_page_cache 1
* - drush vset page_cache_maximum_age 3600
*/
if (!drupal_is_cli()) {
$result = db_query("SELECT value FROM {variable} WHERE name = 'acsf_allow_override_page_cache';")->fetchField();
if ($result) {
$acsf_allow_override_page_cache = unserialize($result);
if ($acsf_allow_override_page_cache) {
$result = db_query("SELECT value FROM {variable} WHERE name = 'page_cache_maximum_age';")->fetchField();
// An empty array indicates no value was set in the database, so we ignore
// the site.
if ($result) {
$page_cache_maximum_age = (int) unserialize($result);
if ($page_cache_maximum_age > 300) {
$conf['page_cache_maximum_age'] = $page_cache_maximum_age;
}
}
}
}
}
acsf-hook-tx-isolation.php
<?php
/**
* @file
* Example implementation of ACSF post-settings-php hook.
*
* @see https://docs.acquia.com/site-factory/extend/hooks
*/
// Changing the database transaction isolation level from `REPEATABLE-READ`
// to `READ-COMMITTED` to avoid/minimize the deadlocks.
// @see https://support-acquia.force.com/s/article/360005253954-Fixing-database-deadlocks
// for reference.
$databases['default']['default']['init_commands'] = [
'transaction_isolation' => 'SET SESSION transaction_isolation="READ-COMMITTED"',
];
if (file_exists('/var/www/site-php')) {
acquia_hosting_db_choose_active($conf['acquia_hosting_site_info']['db'], 'default', $databases, $conf);
}