5 * Acquia Agent securely sends information to Acquia Network.
9 * XML-RPC errors defined by the Acquia Network.
11 define('SUBSCRIPTION_NOT_FOUND' , 1000);
12 define('SUBSCRIPTION_KEY_MISMATCH' , 1100);
13 define('SUBSCRIPTION_EXPIRED' , 1200);
14 define('SUBSCRIPTION_REPLAY_ATTACK' , 1300);
15 define('SUBSCRIPTION_KEY_NOT_FOUND' , 1400);
16 define('SUBSCRIPTION_MESSAGE_FUTURE' , 1500);
17 define('SUBSCRIPTION_MESSAGE_EXPIRED' , 1600);
18 define('SUBSCRIPTION_MESSAGE_INVALID' , 1700);
19 define('SUBSCRIPTION_VALIDATION_ERROR', 1800);
20 define('SUBSCRIPTION_PROVISION_ERROR' , 9000);
23 * Subscription message lifetime defined by the Acquia Network.
25 define('SUBSCRIPTION_MESSAGE_LIFETIME', 15*60);
28 * Implementation of hook_menu().
30 function acquia_agent_menu() {
31 $items['admin/config/system/acquia-agent'] = array(
32 'title' => 'Acquia Network settings',
33 'description' => 'Connect your site to the Acquia Network.',
34 'page callback' => 'acquia_agent_settings_page',
35 'file' => 'acquia_agent.pages.inc',
36 'access arguments' => array('administer site configuration'),
38 $items['admin/config/system/acquia-agent/refresh-status'] = array(
39 'title' => 'Manual update check',
40 'page callback' => 'acquia_agent_refresh_status',
41 'access arguments' => array('administer site configuration'),
42 'type' => MENU_CALLBACK
,
48 * Implement hook_page_alter().
50 function acquia_agent_page_alter(&$page) {
51 if (isset($page['page_top']['toolbar']) && user_access('administer site configuration')) {
52 $page['page_top']['toolbar']['#pre_render'][] = 'acquia_agent_toolbar_add_links';
57 * Pre-render function which dynamically adds links to the toolbar.
59 function acquia_agent_toolbar_add_links($toolbar) {
61 if (acquia_agent_subscription_is_active()) {
62 $subscription = acquia_agent_settings('acquia_subscription_data');
63 // Yes, this uses inline CSS, which sounds bad, but including a CSS file
64 // just for this sounds equally bad.
65 $icon = '<img src="'.
base_path() .
'misc/message-16-ok.png" alt="ok" style="vertical-align: middle;" />';
66 $link['title'] = t("!icon Subscription active (expires !date)", array('!icon' => $icon, '!date' => format_date(strtotime($subscription['expiration_date']['value']), 'custom', 'Y/n/j')));
67 $link['attributes']['class'][] = "acquia-active-subscription";
68 $link['attributes']['title'] = $subscription['product']['view'];
69 $link['href'] = $subscription['href'];
72 // Yes, this uses inline CSS, which sounds bad, but including a CSS file
73 // just for this sounds equally bad.
74 $icon = '<img src="'.
base_path() .
'misc/message-16-error.png" alt="error" style="vertical-align: middle;" />';
75 $link['title'] = t("!icon Subscription not active", array('!icon' => $icon));
76 $link['attributes']['class'][] = "acquia-inactive-subscription";
77 $link['href'] = 'http://acquia.com/network';
80 $toolbar['toolbar_user']['#links'] = array_merge(array('acquia_agent' => $link), $toolbar['toolbar_user']['#links']);
85 * Implementation of hook_init().
87 function acquia_agent_init() {
88 if ((arg(0) != 'overlay-ajax') &&
89 (arg(3) != 'acquia-agent') &&
91 user_access('administer site configuration') &&
92 (!acquia_agent_has_credentials())) {
93 $message = t('Get a <a href="@acquia-free">free 30 day trial</a> of Drupal support, enhanced content search, comment spam blocking and more. If you have an Acquia Network subscription, <a href="@settings">connect now</a>. You can turn this message off by disabling the Acquia Network <a href="@admin-modules">modules</a> too.', array('@acquia-free' => url('admin/config/system/acquia-agent'), '@settings' => url('admin/config/system/acquia-agent/connection'), '@admin-modules' => url('admin/modules', array('fragment' => 'edit-modules-acquia-network-connector'))));
94 drupal_set_message($message, 'warning', FALSE
);
99 * Implementation of hook_theme().
101 function acquia_agent_theme() {
103 'acquia_agent_banner_form' => array(
104 'render element' => 'form',
105 'file' => 'acquia_agent.pages.inc',
111 * Get subscription status from the Acquia Network, and store the result.
113 * This check also sends a heartbeat to the Acquia Network unless
114 * $params['no_heartbeat'] == 1.
116 function acquia_agent_check_subscription($params = array()) {
117 // Default return value is FALSE.
118 $subscription = FALSE
;
119 if (!acquia_agent_has_credentials()) {
120 // If there is not an identifier or key, delete any old subscription data.
121 variable_del('acquia_subscription_data');
124 // There is an identifier and key, so attempt communication.
126 // Include version number information.
127 acquia_agent_load_versions();
128 if (IS_ACQUIA_DRUPAL
) {
129 $params['version'] = ACQUIA_DRUPAL_VERSION
;
130 $params['series'] = ACQUIA_DRUPAL_SERIES
;
131 $params['branch'] = ACQUIA_DRUPAL_BRANCH
;
132 $params['revision'] = ACQUIA_DRUPAL_REVISION
;
134 // Include Acquia Search module version number.
135 if (module_exists('acquia_search')) {
136 foreach (array('acquia_search', 'apachesolr') as
$name) {
137 $info = system_get_info('module', $name);
138 // Send the version, or at least the core compatibility as a fallback.
139 $params['search_version'][$name] = isset($info['version']) ?
(string)$info['version'] : (string)$info['core'];
142 $data = acquia_agent_call('acquia.agent.subscription', $params);
143 $subscription['timestamp'] = REQUEST_TIME
;
144 if ($errno = xmlrpc_errno()) {
146 case SUBSCRIPTION_NOT_FOUND
:
147 case SUBSCRIPTION_EXPIRED
:
148 variable_del('acquia_subscription_data');
152 elseif (acquia_agent_valid_response($data)) {
153 $subscription += $data['result']['body'];
154 variable_set('acquia_subscription_data', $subscription);
157 watchdog('acquia agent', 'HMAC validation error: <pre>@data</pre>', array('@data' => print_r($data, TRUE
)), WATCHDOG_ERROR
);
161 module_invoke_all('acquia_subscription_status', acquia_agent_subscription_is_active());
163 return $subscription;
166 function acquia_agent_report_xmlrpc_error() {
167 drupal_set_message(t('Error: @message (@errno)', array('@message' => xmlrpc_error_msg(), '@errno' => xmlrpc_errno())), 'error');
170 * Implementation of hook_update_status_alter().
172 * This compares the array of computed information about projects that are
173 * missing available updates with the saved settings. If the settings specify
174 * that a particular project or release should be ignored, the status for that
175 * project is altered to indicate it is ignored because of settings.
178 * Reference to an array of information about available updates to each
179 * project installed on the system.
181 * @see update_calculate_project_data()
183 function acquia_agent_update_status_alter(&$projects) {
185 if (!$subscription = acquia_agent_has_update_service()) {
186 // Get subscription data or return if the service is not enabled.
190 foreach ($projects as
$project => $project_info) {
191 if ($project == 'drupal') {
192 if (isset($subscription['update'])) {
193 $projects[$project]['status'] = $subscription['update']['status'];
194 $projects[$project]['releases'] = $subscription['update']['releases'];
195 $projects[$project]['recommended'] = $subscription['update']['recommended'];
196 $projects[$project]['latest_version'] = $subscription['update']['latest_version'];
197 // Security updates are a separate piece of data. If we leave it, then core
198 // security warnings from druapl.org will also be displayed on the update page.
199 unset($projects[$project]['security updates']);
202 $projects[$project]['status'] = UPDATE_NOT_CHECKED
;
203 $projects[$project]['reason'] = t('No information available from the Acquia Network');
204 unset($projects[$project]['releases']);
205 unset($projects[$project]['recommended']);
207 $projects[$project]['link'] = 'http://acquia.com/products-services/acquia-drupal';
208 $projects[$project]['title'] = 'Acquia Drupal';
209 $projects[$project]['existing_version'] = ACQUIA_DRUPAL_VERSION
;
210 $projects[$project]['install_type'] = 'official';
211 unset($projects[$project]['extra']);
213 elseif ($project_info['datestamp'] == 'acquia drupal') {
214 $projects['drupal']['includes'][$project] = $project_info['title'];
215 unset($projects[$project]);
221 * Implementation of hook_system_info_alter()
223 function acquia_agent_system_info_alter(&$info) {
224 if (!$subscription = acquia_agent_has_update_service()) {
225 // Get subscription data or return if the service is not enabled.
228 if (isset($info['acquia'])) {
229 // Slight hack - the datestamp field is carried thourgh by update.module.
230 $info['datestamp'] = 'acquia drupal';
235 * Returns the stored subscription data if update service is enabled or FALSE otherwise.
237 function acquia_agent_has_update_service() {
238 // Include version number information.
239 acquia_agent_load_versions();
241 $subscription = acquia_agent_settings('acquia_subscription_data');
242 if (!IS_ACQUIA_DRUPAL
|| empty($subscription['active']) || (isset($subscription['update_service']) && empty($subscription['update_service']))) {
243 // We don't have update service if (1) this is not Acquia Drupal, (2) there
244 // is no subscription or (3) the update service was disabled on acquia.com.
245 // Requiring the update_service key and checking its value separately is
246 // important for backwards compatibility. Isset & empty tells us
247 // that the web service willingly told us to not do update notifications.
251 return $subscription;
255 * Implemetation of hook_menu_alter()
257 function acquia_agent_menu_alter(&$items) {
258 if (isset($items['admin/reports/updates/check'])) {
259 $items['admin/reports/updates/check']['page callback'] = 'acquia_agent_manual_status';
264 * Menu callback for 'admin/config/system/acquia-agent/refresh-status'.
266 function acquia_agent_refresh_status() {
267 // Refresh subscription information, so we are sure about our update status.
268 if (module_exists('acquia_spi')) {
269 acquia_spi_send_profile_info();
271 // We send a heartbeat here so that all of our status information gets
272 // updated locally via the return data.
273 acquia_agent_check_subscription();
274 // Return to the setting page (or destination)
275 drupal_goto('admin/config/system/acquia-agent');
279 * Substituted menu callback for 'admin/reports/updates/check'.
281 function acquia_agent_manual_status() {
282 // Refresh subscription information, so we are sure about our update status.
283 if (module_exists('acquia_spi')) {
284 acquia_spi_send_profile_info();
286 // We send a heartbeat here so that all of our status information gets
287 // updated locally via the return data.
288 acquia_agent_check_subscription();
289 // This callback will only ever be available if update module is active.
290 update_manual_status();
294 * Implementation of hook_cron().
296 function acquia_agent_cron() {
297 // Check subscription and send a heartbeat to Acquia Network via XML-RPC.
298 acquia_agent_check_subscription();
302 * Implementation of hook_watchdog().
304 function acquia_agent_watchdog($log_entry) {
305 // Make sure that even when cron failures prevent hook_cron() from being
306 // called, we still send out a heartbeat.
307 $cron_failure_messages = array(
308 'Cron has been running for more than an hour and is most likely stuck.',
309 'Attempting to re-run cron while it is already running.',
311 if (in_array($log_entry['message'], $cron_failure_messages, TRUE
)) {
312 acquia_agent_check_subscription();
317 * @defgroup acquia_admin_menu Alter or add to the administration menu.
319 * The admin_menu module is enabled by default - we alter it to add our icon and
320 * subscription information.
324 * Implementation of hook_admin_menu().
326 function acquia_agent_admin_menu() {
327 // Add link to show current subscription status
329 'title' => 'acquia_subscription_status',
330 'path' => 'http://acquia.com',
332 'parent_path' => '<root>',
333 'options' => array('extra class' => 'admin-menu-action acquia-subscription-status', 'html' => TRUE
),
340 * Render an icon to display in the Administration Menu.
342 function acquia_agent_menu_icon() {
343 return '<img class="admin-menu-icon" src="' .
base_path() .
drupal_get_path('module', 'acquia_agent') .
'/acquia.ico" height = "16" alt="" />';
348 * @} End of "acquia_admin_menu".
352 * Validate identifier/key pair via XML-RPC call to Acquia Network address.
354 * This is generaly only useful when actually entering the values in the form.
355 * Normally, use acquia_agent_check_subscription() since it also validates
358 function acquia_agent_valid_credentials($identifier, $key, $acquia_network_address = NULL
) {
359 $data = acquia_agent_call('acquia.agent.validate', array(), $identifier, $key, $acquia_network_address);
360 return (bool
)$data['result'];
364 * Prepare and send a XML-RPC request to Acquia Network with an authenticator.
367 function acquia_agent_call($method, $params, $identifier = NULL
, $key = NULL
, $acquia_network_address = NULL
) {
368 $acquia_network_address = acquia_agent_network_address($acquia_network_address);
369 $host = isset($_SERVER["SERVER_ADDR"]) ?
$_SERVER["SERVER_ADDR"] : '';
371 'authenticator' => _acquia_agent_authenticator($params, $identifier, $key),
375 $data['result'] = _acquia_agent_request($acquia_network_address, $method, $data);
380 * Returns an error message for the most recent (failed) attempt to connect
381 * to the Acquia Network during the current page request. If there were no
382 * failed attempts, returns FALSE.
384 * This function assumes that the most recent XML-RPC error came from the
385 * Acquia Network; otherwise, it will not work correctly.
387 function acquia_agent_connection_error_message() {
388 $errno = xmlrpc_errno();
391 case SUBSCRIPTION_NOT_FOUND
:
392 return t('The identifier you have provided does not exist in the Acquia Network or is expired. Please make sure you have used the correct value and try again.');
394 case SUBSCRIPTION_EXPIRED
:
395 return t('Your Acquia Network subscription has expired. Please renew your subscription so that you can resume using Acquia Network services.');
397 case SUBSCRIPTION_MESSAGE_FUTURE
:
398 return t('Your server is unable to communicate with the Acquia Network due to a problem with your clock settings. For security reasons, we reject messages that are more than @time ahead of the actual time recorded by our servers. Please fix the clock on your server and try again.', array('@time' => format_interval(SUBSCRIPTION_MESSAGE_LIFETIME
)));
400 case SUBSCRIPTION_MESSAGE_EXPIRED
:
401 return t('Your server is unable to communicate with the Acquia Network due to a problem with your clock settings. For security reasons, we reject messages that are more than @time older than the actual time recorded by our servers. Please fix the clock on your server and try again.', array('@time' => format_interval(SUBSCRIPTION_MESSAGE_LIFETIME
)));
403 case SUBSCRIPTION_VALIDATION_ERROR
:
404 return t('The identifier and key you have provided for the Acquia Network do not match. Please make sure you have used the correct values and try again.');
407 return t('There is an error communicating with the Acquia Network at this time. Please check your identifier and key and try again.');
415 * Helper function to build the xmlrpc target address.
417 function acquia_agent_network_address($acquia_network_address = NULL
) {
418 if (empty($acquia_network_address)) {
419 $acquia_network_address = acquia_agent_settings('acquia_network_address');
421 // Strip protocol (scheme) from Network address
422 $uri = parse_url($acquia_network_address);
423 $port = isset($uri['port']) ?
':' .
$uri['port'] : '';
424 $path = isset($uri['path']) ?
$uri['path'] : '';
425 $acquia_network_address = $uri['host'] .
$port .
$path;
426 // Add a scheme based on PHP's capacity.
427 if (in_array('ssl', stream_get_transports(), TRUE
) && !defined('ACQUIA_DEVELOPMENT_NOSSL')) {
428 // OpenSSL is available in PHP
429 $acquia_network_address = 'https://' .
$acquia_network_address;
432 $acquia_network_address = 'http://' .
$acquia_network_address;
434 $acquia_network_address .
= '/xmlrpc.php';
435 return $acquia_network_address;
439 * Helper function to check if an identifer and key exist.
441 function acquia_agent_has_credentials() {
442 return (bool
)(variable_get('acquia_identifier', FALSE
) && variable_get('acquia_key', FALSE
));
446 * Helper function to check if the site has an active subscription.
448 function acquia_agent_subscription_is_active() {
450 // Subscription cannot be active if we have no credentials.
451 if (acquia_agent_has_credentials()) {
452 $subscription = acquia_agent_settings('acquia_subscription_data');
453 // Make sure we have data at least once per day.
454 if (isset($subscription['timestamp']) && (time() - $subscription['timestamp'] > 60*60*24)) {
455 $subscription = acquia_agent_check_subscription(array('no_heartbeat' => 1));
457 $active = !empty($subscription['active']);
463 * Helper function so that we don't need to repeat defaults.
465 function acquia_agent_settings($variable_name) {
466 switch ($variable_name) {
467 case
'acquia_identifier':
468 return variable_get('acquia_identifier', '');
470 return variable_get('acquia_key', '');
471 case
'acquia_network_address':
472 return variable_get('acquia_network_address', 'https://rpc.acquia.com');
473 case
'acquia_subscription_data':
474 return variable_get('acquia_subscription_data', array('active' => FALSE
));
479 * API function used by others to ensure version information is loaded.
481 * Saves us some cycles to not load it each time, when it is actually
482 * not needed. We store this in a separate file, so that the Acquia
483 * build process only needs to alter that file instead of the main
486 function acquia_agent_load_versions() {
487 // Include version number information.
488 include_once
'acquia_agent_drupal_version.inc';
492 * Implementation of hook_form_[form_id]_alter()..
494 function acquia_agent_form_system_modules_alter(&$form, &$form_state) {
496 if (isset($form['description']['acquia_search'])) {
497 $subscription = acquia_agent_settings('acquia_subscription_data');
499 if (!module_exists('acquia_search') && empty($subscription['active'])) {
500 $form['status']['#disabled_modules'][] = 'acquia_search';
501 $text = 'Acquia Search requires an <a href="@network-url">Acquia Network subscription</a>';
502 $message = t($text, array('@network-url' => 'http://acquia.com/products-services/acquia-search'));
503 $form['description']['acquia_search']['#value'] = '<div style="padding-left:5px; margin:8px 0px" class="messages warning" id="acquia-agent-no-search">' .
$message .
'</div>' .
$form['description']['acquia_search']['#value'];
509 * Builds a stream context based on a url and local .pem file if available.
511 function acquia_agent_stream_context_create($url, $module = 'acquia_agent') {
513 $uri = parse_url($url);
514 if (isset($uri['scheme']) && ($uri['scheme'] == 'https') && variable_get('acquia_agent_verify_peer', 0)) {
515 // Look for a local certificate to validate the server identity.
516 $pem_file = drupal_get_path('module', $module) .
'/' .
$uri['host'] .
'.pem';
517 if (file_exists($pem_file)) {
518 $opts['ssl'] = array(
519 'verify_peer' => TRUE
,
520 'cafile' => $pem_file,
521 'allow_self_signed' => FALSE
, // doesn't mean anything in this case
522 'CN_match' => $uri['host']);
525 return stream_context_create($opts);
530 * Determine if a response from the Acquia Network is valid.
533 * The data array returned by acquia_agent_call().
537 function acquia_agent_valid_response($data) {
538 $authenticator = $data['authenticator'];
539 $result = $data['result'];
540 $result_auth = $result['authenticator'];
541 $valid = ($authenticator['nonce'] == $result_auth['nonce']);
542 $valid = $valid && ($authenticator['time'] < $result_auth['time']);
543 $key = acquia_agent_settings('acquia_key');
544 $hash = _acquia_agent_hmac($key, $result_auth['time'], $result_auth['nonce'], $result['body']);
545 return $valid && ($hash == $result_auth['hash']);
549 * Send a XML-RPC request.
551 * This function should never be called directly - use acquia_agent_call().
553 function _acquia_agent_request($url, $method, $data) {
554 $ctx = acquia_agent_stream_context_create($url);
556 // TODO: what's a meaningful fault code?
557 xmlrpc_error(-1, t('SSL is not supported or setup failed'));
561 $result = xmlrpc($url, array($method => array($data)), array('context' => $ctx));
563 if ($errno = xmlrpc_errno()) {
564 watchdog('acquia agent', '@message (@errno): %server - %method - <pre>@data</pre>', array('@message' => xmlrpc_error_msg(), '@errno' => xmlrpc_errno(), '%server' => $url, '%method' => $method, '@data' => print_r($data, TRUE
)), WATCHDOG_ERROR
);
570 * Creates an authenticator based on xmlrpc params and a HMAC-SHA1.
572 function _acquia_agent_authenticator($params = array(), $identifier = NULL
, $key = NULL
) {
573 if (empty($identifier)) {
574 $identifier = acquia_agent_settings('acquia_identifier');
577 $key = acquia_agent_settings('acquia_key');
579 $time = REQUEST_TIME
;
580 $nonce = base64_encode(hash('sha256', drupal_random_bytes(55), TRUE
));
581 $authenticator['identifier'] = $identifier;
582 $authenticator['time'] = $time;
583 $authenticator['hash'] = _acquia_agent_hmac($key, $time, $nonce, $params);
584 $authenticator['nonce'] = $nonce;
585 return $authenticator;
589 * Calculates a HMAC-SHA1 according to RFC2104 (http://www.ietf.org/rfc/rfc2104.txt).
590 * With addition of xmlrpc params.
592 function _acquia_agent_hmac($key, $time, $nonce, $params) {
593 $data = $time .
':' .
$nonce .
':' .
$key .
':' .
serialize($params);
594 return base64_encode(hash_hmac('sha1', $data, $key, TRUE
));