View source
<?php
class AcquiaLiftAPI implements AcquiaLiftLearnReportDataSourceInterface {
const NAME_MAX_LENGTH = 255;
protected $api_url;
protected $api_public_key;
protected $api_private_key;
protected $lift_version;
protected $validate_response;
protected $lift_profiles_account_name;
private static $instance;
protected static $existing_tests;
protected $httpClient;
protected $logger = NULL;
public static function isSuccessful($code) {
return $code >= 200 && $code < 300;
}
public static function isClientError($code) {
return $code >= 400 && $code < 500;
}
public static function isServerError($code) {
return $code >= 500 && $code < 600;
}
protected static function mapBadResponseToExceptionClass($code) {
if (self::isSuccessful($code)) {
return NULL;
}
if (self::isClientError($code)) {
switch ($code) {
case 404:
return 'AcquiaLiftNotFoundException';
case 403:
return 'AcquiaLiftForbiddenException';
default:
return 'AcquiaLiftClientErrorException';
}
}
elseif (self::isServerError($code)) {
return 'AcquiaLiftServerErrorException';
}
return NULL;
}
public static function reset() {
self::$instance = NULL;
}
public static function getInstance($account_info) {
if (empty(self::$instance)) {
if (drupal_valid_test_ua()) {
$broken = variable_get('acquia_lift_web_test_broken_client', FALSE);
self::setTestInstance($broken);
return self::$instance;
}
if (empty($account_info['api_url']) || !valid_url($account_info['api_url'])) {
throw new AcquiaLiftCredsException('Acquia Lift API URL is missing or not a valid URL.');
}
$api_url = $account_info['api_url'];
$needs_scheme = strpos($api_url, '://') === FALSE;
if ($needs_scheme) {
global $is_https;
$url_scheme = $is_https ? 'https://' : 'http://';
$api_url = $url_scheme . $api_url;
}
if (substr($api_url, -1) === '/') {
$api_url = substr($api_url, 0, -1);
}
$acquia_lift_version = "undefined";
if (!empty($account_info['public_key'])) {
$public_key = $account_info['public_key'];
}
else {
throw new AcquiaLiftCredsException('Acquia Lift Public Key was not found.');
}
if (!empty($account_info['private_key'])) {
$private_key = $account_info['private_key'];
$private_key = base64_decode($private_key);
if ($private_key === FALSE) {
throw new AcquiaLiftCredsException(t('secret key could not be decoded as base64. Please validate your credentials or contact Acquia Support.'));
}
if (empty($private_key)) {
throw new AcquiaLiftCredsException(t('secret key cannot be empty. Please validate your credentials or contact Acquia Support.'));
}
}
else {
throw new AcquiaLiftCredsException('Acquia Lift Private Key was not found.');
}
if (!empty($account_info['acquia_lift_version'])) {
$acquia_lift_version = $account_info['acquia_lift_version'];
}
$validate_response = $account_info['validate_response'];
$lift_profiles_account_name = isset($account_info['profiles']['account_name']) ? $account_info['profiles']['account_name'] : '';
self::$instance = new self($api_url, $public_key, $private_key, $acquia_lift_version, $validate_response, $lift_profiles_account_name);
}
return self::$instance;
}
public static function setTestInstance($broken_http_client = FALSE, $simulate_client_side_breakage = FALSE) {
module_load_include('inc', 'acquia_lift', 'tests/acquia_lift.test_classes');
self::$instance = new self('http://api.example.com', 'test-api-key', 'test-private-key', '7.x', FALSE, 'my-lift-web-account');
$test_data = variable_get('acquia_lift_web_test_data', array());
self::$instance
->setHttpClient(new DummyAcquiaLiftHttpClient($broken_http_client, $test_data, $simulate_client_side_breakage));
self::$instance
->setLogger(new AcquiaLiftTestLogger(FALSE));
}
private function __construct($api_url, $public_key, $private_key, $acquia_lift_version, $validate_response = TRUE, $profiles_account_name = '') {
$this->api_url = $api_url;
$this->api_public_key = $public_key;
$this->api_private_key = $private_key;
$this->lift_version = $acquia_lift_version;
$this->validate_response = $validate_response;
$this->lift_profiles_account_name = $profiles_account_name;
}
protected function httpClient() {
if (!isset($this->httpClient)) {
$this->httpClient = new AcquiaLiftDrupalHttpClient();
}
return $this->httpClient;
}
public function setHttpClient(AcquiaLiftDrupalHttpClientInterface $client) {
$this->httpClient = $client;
}
public function getApiUrl() {
return $this->api_url;
}
public function getPublicKey() {
return $this->api_public_key;
}
public function getExistingAgentNames() {
if (!empty(self::$existing_tests)) {
return self::$existing_tests;
}
$campaigns = $this
->getCampaigns();
$names = array();
foreach ($campaigns as $campaign) {
$names[] = $campaign['id'];
}
self::$existing_tests = $names;
return $names;
}
public function getCampaigns() {
$url = $this
->generateEndpoint("campaigns");
$fail_msg = 'Could not retrieve agents from Lift';
$response = $this
->makeGetRequest($url, array(), $fail_msg);
if (empty($response) || isset($response['error'])) {
return array();
}
return $response;
}
public function getAgent($agent_name) {
$url = $this
->generateEndpoint("campaigns/{$agent_name}");
$fail_msg = 'Could not retrieve the specified agent from Lift';
$response = $this
->makeGetRequest($url, array(), $fail_msg);
if (!empty($response)) {
return $response;
}
return FALSE;
}
public function deleteAgent($agent_name) {
$url = $this
->generateEndpoint("campaigns/{$agent_name}");
$vars = array(
'agent' => $agent_name,
);
$success_msg = "The personalization {agent} has been deleted from Acquia Lift";
$fail_msg = "The personalization {agent} could not be deleted from Acquia Lift";
$this
->makeDeleteRequest($url, array(), $success_msg, $fail_msg, $vars);
}
public function getGoalsForAgent($agent_name) {
return array();
}
public function ping() {
$url = $this
->generateEndpoint("ping");
$fail_msg = 'Acquia Lift Testing Service could not be reached';
try {
$response = $this
->makeGetRequest($url, array(), $fail_msg);
if (!empty($response)) {
return TRUE;
}
} catch (Exception $e) {
return FALSE;
}
return FALSE;
}
public function saveCampaign($name, $title, $decision_name, $goals, $mab = FALSE, $control = 0, $explore = 0.2) {
$vars = array(
'agent' => $name,
);
$success_msg = 'The personalization {agent} was pushed to Acquia Lift';
$fail_msg = 'The personalization {agent} could not be pushed to Acquia Lift';
$path = "campaigns";
$url = $this
->generateEndpoint($path);
$agent = array(
'id' => $name,
'title' => $title,
'algorithm' => 'mab',
'decision_sets' => array(
$decision_name,
),
'goals' => $goals,
'traffic_fraction' => 1 - $control,
'explore_fraction' => $mab ? $explore : 1,
);
$this
->makePostRequest($url, array(), $agent, $success_msg, $fail_msg, $vars);
}
public function saveDecisionSet($name, $title, $options) {
$decision_set = array(
'id' => $name,
'title' => $title,
'decisions' => array(),
);
foreach ($options as $option) {
$decision_set['decisions'][] = array(
'external_id' => $option['option_id'],
);
}
$path = "decision_sets";
$url = $this
->generateEndpoint($path);
$vars = array(
'name' => $title,
);
$success_msg = 'The Decision Set {name} was pushed to Acquia Lift';
$fail_msg = 'The Decision Set {name} could not be pushed to Acquia Lift';
$this
->makePostRequest($url, $this
->getPutHeaders(), $decision_set, $success_msg, $fail_msg, $vars);
}
public function saveGoal($name) {
$goal = array(
'id' => $name,
'title' => $name,
'description' => $name,
);
$path = "goals";
$url = $this
->generateEndpoint($path);
$vars = array(
'name' => $name,
);
$success_msg = 'The Goal {name} was pushed to Acquia Lift';
$fail_msg = 'The Goal {name} could not be pushed to Acquia Lift';
$this
->makePostRequest($url, $this
->getPutHeaders(), $goal, $success_msg, $fail_msg, $vars);
}
public function getReportForDateRange($name, $from, $to) {
if ($from != $to && ($interval = date_diff(date_create($from), date_create($to)))) {
$days = $interval
->format('%d');
}
else {
$days = 1;
}
$date = new DateTime($from, new DateTimeZone("UTC"));
$from = $date
->format("Y-m-d\\TH:i:s\\Z");
$path = "report?campaign_id={$name}&from={$from}&days={$days}";
$url = $this
->generateEndpoint($path);
$vars = array(
'name' => $name,
);
$fail_msg = 'Could not retrieve report from Acquia Lift';
return $this
->makeGetRequest($url, $this
->getStandardHeaders(), $fail_msg, $vars);
}
public function getPersonalizationOverviewReport($name, $variations) {
if (empty($this->lift_profiles_account_name)) {
throw new AcquiaLiftCredsException('No account name for Lift Profiles configured');
}
$site_name = variable_get('acquia_lift_profiles_site_name', '');
if (empty($site_name)) {
$site_name = 'Drupal';
}
$path = "report/overview?account={$this->lift_profiles_account_name}&site={$site_name}&personalization={$name}";
foreach ($variations as $variation) {
$path .= "&variation={$variation}";
}
$url = $this
->generateEndpoint($path);
$vars = array(
'name' => $name,
);
$fail_msg = 'Could not retrieve report from Acquia Lift';
return $this
->makeGetRequest($url, $this
->getStandardHeaders(), $fail_msg, $vars);
}
public function getAudienceReport($name, $audience, $variations, $type, $from, $to, $goal = NULL) {
if ($type == 'target' && count($variations) > 1) {
throw new AcquiaLiftException("Can't retrieve targeting report for more than one variation");
}
if (empty($this->lift_profiles_account_name)) {
throw new AcquiaLiftCredsException('No account name for Lift Profiles configured');
}
$site_name = variable_get('acquia_lift_profiles_site_name', '');
if (empty($site_name)) {
$site_name = 'Drupal';
}
$tz = drupal_get_user_timezone();
$from_datetime = date_create("@{$from}", new DateTimeZone($tz));
$to_datetime = date_create("@{$to}", new DateTimeZone($tz));
$from_str = $from_datetime
->format('Y-m-d');
$to_str = $to_datetime
->format('Y-m-d');
$path = "report/{$type}?account={$this->lift_profiles_account_name}&site={$site_name}&personalization={$name}&audience={$audience}&from={$from_str}&to={$to_str}";
foreach ($variations as $variation) {
$path .= "&variation={$variation}";
}
if (!empty($goal)) {
$path .= "&goal={$goal}";
}
$url = $this
->generateEndpoint($path);
$vars = array(
'name' => $name,
);
$fail_msg = 'Could not retrieve report from Acquia Lift';
return $this
->makeGetRequest($url, $this
->getStandardHeaders(), $fail_msg, $vars);
}
protected function generateEndpoint($path, $admin = TRUE) {
$endpoint = $this->api_url;
if (substr($path, 0, 1) !== '/') {
$endpoint .= '/';
}
$endpoint .= $path;
return $endpoint;
}
function canonicalizeRequest($verb, $host, $path, $query_args = "", $auth_options = array(), $timestamp, $content_type, $payload_hash) {
$list = array();
$list[] = strtoupper($verb);
$list[] = $host;
$list[] = $path;
$list[] = $query_args;
ksort($auth_options);
$auth_options_list = array();
foreach ($auth_options as $id => $value) {
$auth_options_list[] = $id . "=" . $value;
}
$list[] = implode($auth_options_list, "&");
$list[] = $timestamp;
if (!empty($payload_hash)) {
$list[] = strtolower($content_type);
$list[] = $payload_hash;
}
return implode("\n", $list);
}
public function getAuthHeader($verb, $url, $headers = array(), $payload_hash = "", $nonce = "") {
$authorization = array();
$authorization["realm"] = "dice";
$authorization["id"] = $this->api_public_key;
$authorization["nonce"] = $nonce;
$authorization["version"] = "2.0";
$parsed_url = parse_url($url);
$host = $parsed_url['host'];
if (!empty($parsed_url['port'])) {
$host .= ":" . $parsed_url['port'];
}
$path = $parsed_url['path'];
$query_args = "";
if (isset($parsed_url['query'])) {
$query_args = $parsed_url['query'];
}
$timestamp = $headers['X-Authorization-Timestamp'];
$content_type = $headers['Content-Type'];
$message = $this
->canonicalizeRequest($verb, $host, $path, $query_args, $authorization, $timestamp, $content_type, $payload_hash);
$signature = base64_encode(hash_hmac('sha256', (string) $message, $this->api_private_key, TRUE));
$authorization["signature"] = $signature;
$auth_stringlist = array();
foreach ($authorization as $id => $value) {
$auth_stringlist[] = $id . "=" . "\"" . $value . "\"";
}
return "acquia-http-hmac" . " " . implode($auth_stringlist, ",");
}
public function getResponseSignature($nonce, $timestamp, $body = "") {
$list = array();
$list[] = $nonce;
$list[] = $timestamp;
$list[] = $body;
$message = implode("\n", $list);
return base64_encode(hash_hmac('sha256', (string) $message, $this->api_private_key, TRUE));
}
protected function prepareRequest($method, &$url, &$headers, $payload = "", $nonce = "", $timestamp) {
$id = uniqid();
if (!stristr($url, '?')) {
$url .= "?";
}
else {
$url .= "&";
}
$url .= 'request_id=' . $id;
$url .= '&client_id=' . $this->api_public_key;
$headers += array(
'User-Agent' => 'acquia_lift/' . $this->lift_version,
);
$headers += array(
'Content-Type' => 'application/json',
);
$headers += array(
'X-Authorization-Timestamp' => $timestamp,
);
$payload_hash = "";
if ($method != "GET" || $method != "HEAD") {
if (isset($payload) && !empty($payload)) {
$payload_hash = base64_encode(hash("sha256", $payload, TRUE));
$headers['X-Authorization-Content-SHA256'] = $payload_hash;
}
}
$auth_header = $this
->getAuthHeader($method, $url, $headers, $payload_hash, $nonce);
if (empty($auth_header)) {
throw new AcquiaLiftCredsException(t('Invalid authentication string - subscription keys expired or missing.'));
}
$headers += array(
'Authorization' => $auth_header,
);
}
protected static function nonce() {
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xfff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff));
}
protected function authenticateResponse($nonce, $timestamp, $response) {
if ($this->validate_response) {
if (!isset($response->headers['x-server-authorization-hmac-sha256'])) {
throw new Exception('Authentication of Acquia Lift Response not found for path');
}
$response_hmac = $this
->getResponseSignature($nonce, $timestamp, $response->data);
if ($response_hmac !== $response->headers['x-server-authorization-hmac-sha256']) {
throw new Exception('Authentication of Acquia Lift Response failed');
}
}
}
protected function makeGetRequest($url, $headers, $fail_msg, $vars = array()) {
$nonce = self::nonce();
$timestamp = time();
$this
->prepareRequest('GET', $url, $headers, "", $nonce, $timestamp);
$response = $this
->httpClient()
->get($url, $headers);
if ($response->code != 200) {
$this
->handleBadResponse($response->code, $fail_msg);
return array();
}
$this
->authenticateResponse($nonce, $timestamp, $response);
return json_decode($response->data, TRUE);
}
protected function makePutRequest($url, $headers = array(), $body = NULL, $success_msg = '', $fail_msg = '', $vars = array()) {
$payload = $this
->encodeBody($body);
$nonce = self::nonce();
$timestamp = time();
$this
->prepareRequest('PUT', $url, $headers, $payload, $nonce, $timestamp);
$response = $this
->httpClient()
->put($url, $headers, $payload);
if ($response->code == 200) {
$this
->logger()
->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
}
else {
$this
->handleBadResponse($response->code, $fail_msg, $vars);
}
$this
->authenticateResponse($nonce, $timestamp, $response);
return json_decode($response->data, TRUE);
}
protected function makePostRequest($url, $headers = array(), $body = NULL, $success_msg = '', $fail_msg = '', $vars = array()) {
$payload = $this
->encodeBody($body);
$nonce = self::nonce();
$timestamp = time();
$this
->prepareRequest('POST', $url, $headers, $payload, $nonce, $timestamp);
$response = $this
->httpClient()
->post($url, $headers, $payload);
if ($response->code == 200) {
$this
->logger()
->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
}
else {
$this
->handleBadResponse($response->code, $fail_msg, $vars);
}
$this
->authenticateResponse($nonce, $timestamp, $response);
return json_decode($response->data, TRUE);
}
protected function makeDeleteRequest($url, $headers = array(), $success_msg, $fail_msg, $vars) {
$nonce = self::nonce();
$timestamp = time();
$this
->prepareRequest('DELETE', $url, $headers, "", $nonce, $timestamp);
$response = $this
->httpClient()
->delete($url, $headers);
if ($response->code == 200) {
$this
->logger()
->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
}
else {
$this
->handleBadResponse($response->code, $fail_msg, $vars, TRUE, TRUE);
}
$this
->authenticateResponse($nonce, $timestamp, $response);
return json_decode($response->data, TRUE);
}
protected function encodeBody($body) {
if (is_string($body)) {
$data = $body;
}
else {
$data = drupal_json_encode($body);
}
return $data;
}
protected function getStandardHeaders() {
return array(
'Accept' => 'application/json',
);
}
protected function getPutHeaders() {
return array(
'Content-Type' => 'application/json; charset=utf-8',
'Accept' => 'application/json',
);
}
protected function logger() {
if ($this->logger !== NULL) {
return $this->logger;
}
return new PersonalizeLogger();
}
public function setLogger(PersonalizeLoggerInterface $logger) {
$this->logger = $logger;
}
public function handleBadResponse($response_code, $fail_msg, $vars = array(), $log_failure = TRUE, $soft_fail = FALSE) {
if ($exception_class = self::mapBadResponseToExceptionClass($response_code)) {
if ($log_failure) {
$logger = $this
->logger();
$logger
->log(PersonalizeLogLevel::ERROR, $fail_msg, $vars);
}
if (!$soft_fail) {
throw new $exception_class(t($fail_msg, $vars));
}
}
if (self::isSuccessful($response_code)) {
if ($log_failure) {
$this
->logger()
->log(PersonalizeLogLevel::WARNING, $fail_msg, $vars);
}
return;
}
if (!$soft_fail) {
throw new AcquiaLiftException(t($fail_msg, $vars));
}
}
public function ensureUniqueAgentName($agent_name, $max_length) {
if ($max_length > self::NAME_MAX_LENGTH) {
$max_length = self::NAME_MAX_LENGTH;
}
$agent_name = substr($agent_name, 0, $max_length);
$existing = $this
->getExistingAgentNames();
$index = 0;
$suffix = '';
while (in_array($agent_name . $suffix, $existing)) {
$suffix = '-' . $index;
while (strlen($agent_name . $suffix) > $max_length) {
$agent_name = substr($agent_name, 0, -1);
}
$index++;
}
$new_name = $agent_name . $suffix;
self::$existing_tests[] = $new_name;
return $new_name;
}
}