You are here

amazon.module in Amazon Product Advertisement API 7

Same filename and directory in other branches
  1. 8.2 amazon.module
  2. 6 amazon.module
  3. 7.2 amazon.module

File

amazon.module
View source
<?php

/**
 * Amazon Integration
 *
 * Provides a Drupal wrapper and caching mechanism for the Amazon
 * Ecommerce APIs. This module provides no user-visible functionality
 * save configuration and setup.
 */
define('AMAZON_ECS_SCHEMA', '2011-08-01');
define('AMAZON_PARTICIPANT_TYPES', 'Author,Artist,Actor,Director,Creator');

// Other common sizes include SwatchImage, TinyImage, and ThumbnailImage.
define('AMAZON_IMAGE_SIZES', 'SmallImage,MediumImage,LargeImage');

/**
 * Implementation of hook_menu. Adds the url path for the Amazon
 * settings page.
 */
function amazon_menu() {
  $items = array();
  $items['admin/config/services/amazon'] = array(
    'title' => 'Amazon API',
    'description' => 'Global settings for the Amazon Ecommerce API.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'amazon_settings_form',
    ),
    'file' => 'amazon.admin.inc',
    'access callback' => 'user_access',
    'access arguments' => array(
      'administer amazon',
    ),
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/config/services/amazon/storage'] = array(
    'title' => 'Storage',
    'description' => 'Local data storage settings for Amazon products.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'amazon_storage_settings_form',
    ),
    'file' => 'amazon.admin.inc',
    'access callback' => 'user_access',
    'access arguments' => array(
      'administer amazon',
    ),
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/config/services/amazon/test'] = array(
    'title' => 'Test',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'amazon_test_form',
    ),
    'file' => 'amazon.admin.inc',
    'access callback' => 'user_access',
    'access arguments' => array(
      'administer amazon',
    ),
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/config/services/amazon/api'] = array(
    'title' => 'Settings',
    'weight' => -10,
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  return $items;
}

/**
 * Implementation of hook_permission
 */
function amazon_permission() {
  return array(
    'administer amazon' => array(
      'title' => t('Administer Amazon module'),
      'description' => t('Configure and test Amazon module.'),
    ),
  );
}

/**
 * Implementation of hook_theme().
 */
function amazon_theme() {
  $templates = array(
    'amazon_item' => array(
      'variables' => array(
        'item' => array(),
        'style' => NULL,
      ),
      'template' => 'amazon-item',
      'path' => drupal_get_path('module', 'amazon') . '/templates',
    ),
    'amazon_inline_item' => array(
      'variables' => array(
        'item' => array(),
      ),
      'template' => 'amazon-item--inline',
      'preprocess functions' => array(
        'template_preprocess_amazon_item',
      ),
      'path' => drupal_get_path('module', 'amazon') . '/templates',
    ),
    'amazon_detail' => array(
      'variables' => array(
        'item' => array(),
        'detail' => NULL,
      ),
      'template' => 'amazon-detail',
      'preprocess functions' => array(
        'template_preprocess_amazon_item',
      ),
      'path' => drupal_get_path('module', 'amazon') . '/templates',
    ),
    // All templates provided at the module level have to be explicitly listed.
    'amazon_item__details' => array(
      'variables' => array(
        'item' => array(),
      ),
      'template' => 'amazon-item--details',
      'preprocess functions' => array(
        'template_preprocess_amazon_item',
      ),
      'path' => drupal_get_path('module', 'amazon') . '/templates',
    ),
    'amazon_item__default' => array(
      'variables' => array(
        'item' => array(),
      ),
      'template' => 'amazon-item',
      'preprocess functions' => array(
        'template_preprocess_amazon_item',
      ),
      'path' => drupal_get_path('module', 'amazon') . '/templates',
    ),
    'amazon_item__thumbnail' => array(
      'variables' => array(
        'item' => array(),
      ),
      'template' => 'amazon-item--thumbnail',
      'preprocess functions' => array(
        'template_preprocess_amazon_item',
      ),
      'path' => drupal_get_path('module', 'amazon') . '/templates',
    ),
    'amazon_item__medium' => array(
      'variables' => array(
        'item' => array(),
      ),
      'template' => 'amazon-item--medium',
      'preprocess functions' => array(
        'template_preprocess_amazon_item',
      ),
      'path' => drupal_get_path('module', 'amazon') . '/templates',
    ),
    'amazon_item__large' => array(
      'variables' => array(
        'item' => array(),
      ),
      'template' => 'amazon-item--large',
      'preprocess functions' => array(
        'template_preprocess_amazon_item',
      ),
      'path' => drupal_get_path('module', 'amazon') . '/templates',
    ),
  );
  return $templates;
}

/**
 * template_preprocess: amazon_item.
 */
function template_preprocess_amazon_item(&$variables, $theme_type) {
  $item = $variables['item'];

  // Do a quick cycle through the simple keys on the item, filter_xss() them,
  // and stick them in the variables collection.
  foreach ($item as $key => $value) {
    if (is_string($value)) {
      $variables[$key] = filter_xss($value);
    }
  }
  $variables['type'] = _amazon_clean_type($item['producttypename']);
  $variables['detailpageurl'] = check_url($item['detailpageurl']);
  $variables['editorialreview'] = !empty($item['editorialreviews']) ? filter_xss_admin($item['editorialreviews'][0]['content']) : '';
  $variables['customerreviews_iframe'] = !empty($item['customerreviews_iframe']) ? check_url($item['customerreviews_iframe']) : '';
  $variables['invalid_asin'] = !empty($item['invalid_asin']) ? 1 : 0;
  if (!empty($variables['customerreviews'])) {
    foreach ($variables['customerreviews'] as $key => $review) {
      $variables['customerreviews'][$key] = filter_xss($variables['customerreviews'][$key]);
    }
  }
  $variables['invalid_asin'] = !empty($variables['invalid_asin']) ? 1 : 0;
  if (!empty($variables['publicationdate'])) {
    $date = explode('-', $variables['publicationdate']);
    $variables['publicationyear'] = check_plain($date[0]);
  }
  else {
    $variables['publicationyear'] = '';
  }

  // Handle participants and types.
  if (isset($item['participants'])) {
    $variables['participants'] = filter_xss(filter_xss(implode(', ', $item['participants'])));
    $participant_types = preg_split('/,/', AMAZON_PARTICIPANT_TYPES);
    foreach ($participant_types as $participant_type) {
      $participant_type = strtolower($participant_type);
      if (!empty($item[$participant_type])) {
        if (is_string($item[$participant_type])) {
          $variables[$participant_type] = filter_xss($item[$participant_type]);
        }
        else {
          $variables[$participant_type] = filter_xss(implode(', ', $item[$participant_type]));
        }
      }
    }
  }
  $variables += array(
    'participants' => '',
    'director' => '',
    'actor' => '',
    'artist' => '',
    'author' => '',
  );

  // Handle supported image resolutions.
  if (isset($item['imagesets'])) {
    foreach ($item['imagesets'] as $key => $image) {
      $variables[$key] = theme('image', array(
        'path' => $image['url'],
        'alt' => t('Image of') . ' ' . strip_tags($item['title']),
        'title' => strip_tags($item['title']),
        'attributes' => array(
          'height' => $image['height'],
          'width' => $image['width'],
        ),
        'getsize' => FALSE,
      ));
      $variables["{$key}url"] = check_url($image['url']);
      $variables["{$key}height"] = check_plain($image['height']);
      $variables["{$key}width"] = check_plain($image['width']);
    }
  }
  $variables['image'] = !empty($variables['mediumimage']) ? $variables['mediumimage'] : '';
  $variables['classes_array'] = array();
  $variables['attributes_array'] = array();
  $variables['title_attributes_array'] = array();
  $variables['content_attributes_array'] = array();
  if (!empty($variables['detail']) && $theme_type == 'amazon_detail') {
    $variables['classes_array'] = array(
      "amazon-item",
      "amazon-item-detail-{$variables['detail']}",
    );
  }
  if (!empty($variables['style']) && $variables['style'] == 'inline' && !empty($variables['producttypename'])) {
    $variables['theme_hook_suggestions'][] = 'amazon_inline_item__' . _amazon_clean_type($item['producttypename']);
  }
  if (!empty($variables['style'])) {
    $variables['classes'] = _amazon_item_classes($item) . ' amazon-item-' . check_plain($variables['style']);

    // A set of more specific templates to use when displaying items.
    $variables['theme_hook_suggestions'][] = 'amazon_item__' . $variables['style'];
    $variables['theme_hook_suggestions'][] = 'amazon_item__' . strtolower($variables['type']);
    $variables['theme_hook_suggestions'][] = 'amazon_item__' . strtolower($variables['type']) . '__' . $variables['style'];
    if (!empty($item['view']) && !empty($item['view']->name)) {
      $variables['theme_hook_suggestions'][] = 'amazon_item_view__' . $item['view']->name;
      $variables['theme_hook_suggestions'][] = 'amazon_item__' . strtolower($variables['type']) . '__view__' . $item['view']->name;
    }
  }
}

/**
 * Convert a type like ABIS_whatever into just whatever.
 * @param unknown_type $type
 */
function _amazon_clean_type($type) {
  return check_plain(strtolower(str_replace(array(
    'ABIS_',
    'CONSOLE_',
    'VIDEO_',
    ' ',
  ), array(
    '',
    '',
    '',
    '_',
  ), $type)));
}

/**
 * Create name of the extra css class for an item.
 *
 * @param $item
 *   The amazon_item.
 */
function _amazon_item_classes($item) {
  return 'amazon-item amazon-item-' . str_replace('_', '-', _amazon_clean_type($item['producttypename']));
}

/**
 * Create an issue an HTTP request to the Amazon API.
 *
 * Most of this is determined by the Amazon Product Advertising API.
 * @see http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html?CHAP_response_elements.html
 *
 *
 * @param $operation
 *   Must be 'AWSECommerceService'
 * @param $parameters
 *   An associative array with the parameters for the API call.
 * @param $locale
 *   The (optional) locale, a 2-character Amazon locale indicator. This has
 *   nothing to do with an actual locale - it's really shorthand for what
 *   Amazon site to use.
 */
function amazon_http_request($operation, $parameters = array(), $locale = NULL) {
  if (!isset($locale)) {
    $locale = variable_get('amazon_default_locale', 'US');
  }
  $metadata = amazon_data_cache();
  $locale_data = $metadata['locales'][$locale];

  // Populate the params with default data.
  $parameters += array(
    'Service' => 'AWSECommerceService',
    'Version' => AMAZON_ECS_SCHEMA,
    'AWSAccessKeyId' => variable_get('amazon_aws_access_key', ''),
    'Operation' => $operation,
  );
  if ($associate_id = amazon_get_associate_id($locale)) {
    $parameters += array(
      'AssociateTag' => $associate_id,
    );
  }
  $parameters_after_hook = module_invoke_all('amazon_request', $parameters);
  if (!empty($parameters_after_hook)) {
    $parameters = $parameters_after_hook;
  }
  $parameters += array(
    'Timestamp' => gmdate("Y-m-d\\TH:i:s") . 'Z',
  );
  uksort($parameters, 'strnatcmp');
  $params = array();
  foreach ($parameters as $key => $value) {
    if (is_array($value)) {
      $value = implode(',', $value);
    }
    $param = str_replace("%7E", "~", rawurlencode($key));
    $value = str_replace("%7E", "~", rawurlencode($value));
    $params[] = $param . '=' . $value;
  }
  $secret_access_key = variable_get('amazon_aws_secret_access_key', "");
  if ($secret_access_key == "") {
    watchdog('amazon', "No Secret Access Key configured. You must configure one at Admin->Settings->Amazon API", NULL, WATCHDOG_ERROR);
    drupal_set_message(t("Amazon Module: No Secret Access Key is configured. Please contact your site administrator"));
    return FALSE;
  }

  // Thanks for signature creation code from http://mierendo.com/software/aws_signed_query/
  $query_string = implode('&', $params);
  $parsed_url = parse_url($locale_data['url']);
  $host = strtolower($parsed_url['host']);
  $string_to_sign = "GET\n{$host}\n{$parsed_url['path']}\n{$query_string}";
  $signature = base64_encode(hash_hmac('sha256', $string_to_sign, $secret_access_key, TRUE));
  $signature = str_replace("%7E", "~", rawurlencode($signature));
  $query_string .= "&Signature={$signature}";
  $url = $locale_data['url'] . '?' . $query_string;

  // Make the request and return a SimpleXML object.
  $results = drupal_http_request($url, array(
    'method' => 'GET',
  ));
  if ($results->code == 200) {
    $xml = new SimpleXMLElement($results->data);
    return $xml;
  }
  if ($results->code >= 400 && $results->code < 500) {
    try {
      $xml = new SimpleXMLElement($results->data);
    } catch (Exception $e) {
      watchdog('amazon', "Error handling results: http_code=%http_code, data=%data.", array(
        '%http_code' => $results->code,
        '%data' => (string) $results->data,
      ));
      return FALSE;
    }
    watchdog('amazon', "HTTP code %http_code accessing Amazon's AWS service: %code, %message", array(
      '%http_code' => $results->code,
      '%code' => (string) $xml->Error->Code,
      '%message' => (string) $xml->Error->Message,
    ));
    return FALSE;
  }
  watchdog('amazon', "Error accessing Amazon AWS web service with query '%url'. HTTP result code=%code, error=%error", array(
    '%code' => $results->code,
    '%error' => $results->error,
    '%url' => $url,
  ));
  return FALSE;
}

/**
 * Look up an item using database or web.
 * The default is to look in the database for existing data, and then to do the
 * web search if that fails. $force_lookup==TRUE forces going to Amazon's
 * API.
 * @param $item_ids
 *   An array of ASIN strings or a single ASIN as a string.
 * @param $force_lookup
 *   If TRUE, skip the database lookup and just go to the Amazon API lookup.
 * @return array
 *   Array of "cleaned" XML item descriptions, keyed on ASIN.
 */
function amazon_item_lookup($item_ids = array(), $force_lookup = FALSE, $locale = NULL) {
  if (empty($item_ids)) {
    return array();
  }
  if (is_string($item_ids)) {
    $item_ids = array(
      $item_ids,
    );
  }
  $items = array();
  if (!$force_lookup) {
    $items = amazon_item_lookup_from_db($item_ids);
  }
  $items_to_fetch = array();
  foreach ($item_ids as $item_id) {
    if (!isset($items[$item_id])) {
      $items_to_fetch[] = $item_id;
    }
  }
  $items_from_web = amazon_item_lookup_from_web($items_to_fetch, $locale);
  $full_set = $items + $items_from_web;
  return $full_set;
}

/**
 * Use Amazon API to look up an array of ASINs.
 * @param $item_ids
 *   Array of ASIN strings to look up.
 * @return array
 *   Array of cleaned XML structures keyed by ASIN.
 */
function amazon_item_lookup_from_web($item_ids = array(), $locale = NULL) {
  $amazon_limit = 10;

  // Amazon will accept no more than 10 items
  $asins = array();
  $results = array();
  $item_ids = array_filter($item_ids);

  // Remove any empty items.
  foreach ($item_ids as $asin) {
    if (!empty($asin)) {
      $asins[] = $asin;
      if (count($asins) >= $amazon_limit || count($asins) == count($item_ids)) {
        $results += _amazon_item_batch_lookup_from_web($asins, $locale);
        $asins = array();
      }
    }
  }
  return $results;
}

/**
 * Get 10 or less items from the AWS web service.
 * AWS allows ONLY 10 items,
 * See http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html?ItemLookup.html.
 * @param $item_ids
 *   Array of ASINs to be looked up.
 * @return
 *   Array of ASIN data structures keyed by ASIN.
 */
function _amazon_item_batch_lookup_from_web($item_ids = array(), $locale = NULL) {
  if (!empty($item_ids)) {
    $params = array(
      'ItemId' => implode(',', $item_ids),
      'ResponseGroup' => amazon_get_response_groups(),
    );
    $results = amazon_http_request('ItemLookup', $params, $locale);
    if (!empty($results->Items->Request->Errors)) {
      _amazon_item_batch_lookup_from_web_errors($results->Items->Request->Errors);
    }
    $items = array();
    if (!empty($results->Items->Item)) {
      foreach ($results->Items->Item as $xml) {
        $item = amazon_item_clean_xml($xml);
        amazon_item_insert($item);
        $items["{$item['asin']}"] = $item;
      }
    }
    return $items;
  }
  return array();
}
function _amazon_item_batch_lookup_from_web_errors($errors) {
  foreach ($errors->Error as $error) {
    $code = (string) $error->Code;
    $message = (string) $error->Message;
    $matches = array();

    // Find and extract the failing ASIN, so we can mark it in the db.
    if (preg_match('/^([^ ]+) is not a valid value for ItemId/', $message, $matches)) {
      $error_asin = $matches[1];
      $update_fields = array(
        'invalid_asin' => TRUE,
      );
      try {
        $result = db_update('amazon_item')
          ->fields($update_fields)
          ->condition('asin', $error_asin)
          ->execute();
      } catch (Exception $e) {
        amazon_db_error_watchdog('Failed to update invalid_asin=TRUE on amazon_item.', $e);
      }
    }
    watchdog('amazon', 'Error retrieving Amazon item %code, message: %message.', array(
      '%code' => $code,
      '%message' => $message,
    ), WATCHDOG_WARNING);
  }
}

/**
 * Look up ASINs in database and return arrays of information keyed by ASIN.
 * @param $item_ids
 *   An array of string ASINs.
 * @return array
 *   Array of Amazon 'cleaned' data structures keyed by ASIN.
 */
function amazon_item_lookup_from_db($item_ids = array()) {
  if (!empty($item_ids)) {
    $timestamp = REQUEST_TIME - variable_get('amazon_refresh_schedule', 86400);
    $result = db_query('SELECT * from {amazon_item} WHERE asin IN (:asins) AND timestamp > :timestamp', array(
      ':asins' => $item_ids,
      ':timestamp' => $timestamp,
    ), array(
      'fetch' => PDO::FETCH_ASSOC,
    ));
    $items = array();
    foreach ($result as $item) {
      _amazon_load_child_data($item);
      $item += module_invoke_all('amazon_item_load', $item);
      $items["{$item['asin']}"] = $item;
    }
    return $items;
  }
  return array();
}

/**
 * Load participant, image, editorial_review data into database.
 * @param $item
 *   Amazon data structure.
 */
function _amazon_load_child_data(&$item) {
  $result = db_query('SELECT type, participant FROM {amazon_item_participant} WHERE asin = :asin', array(
    ':asin' => $item['asin'],
  ), array(
    'fetch' => PDO::FETCH_ASSOC,
  ));
  foreach ($result as $participant) {

    // unset($participant['asin']);
    $item[$participant['type']][] = $participant['participant'];
    $item['participants'][] = $participant['participant'];
  }
  $result = db_query('SELECT * FROM {amazon_item_image} WHERE asin = :asin', array(
    ':asin' => $item['asin'],
  ), array(
    'fetch' => PDO::FETCH_ASSOC,
  ));
  foreach ($result as $image) {
    unset($image['asin']);
    $item['imagesets'][$image['size']] = $image;
  }
  $result = db_query('SELECT * FROM {amazon_item_editorial_review} WHERE asin = :asin', array(
    ':asin' => $item['asin'],
  ), array(
    'fetch' => PDO::FETCH_ASSOC,
  ));
  foreach ($result as $review) {
    unset($review['asin']);
    $item['editorialreviews'][] = $review;
  }
}

/**
 * Take the Amazon XML item and turn it into our own private 'cleaned'
 * data structure.
 * @param $xml
 *   XML structure as returned from Amazon API call.
 * @return
 *   'Cleaned' XML structure for local use.
 */
function amazon_item_clean_xml($xml) {
  $metadata = amazon_data_cache();
  $item = array();

  // Pull the absolute basic information Amazon keeps at the top level
  // of the XML tree, cast to string, and move on.
  $item['asin'] = (string) $xml->ASIN;
  if (!empty($xml->ItemAttributes->ISBN)) {
    $item['isbn'] = (string) $xml->ItemAttributes->ISBN;
  }
  if (!empty($xml->ItemAttributes->EAN)) {
    $item['ean'] = (string) $xml->ItemAttributes->EAN;
  }
  $item['salesrank'] = intval($xml->SalesRank);
  $item['detailpageurl'] = (string) $xml->DetailPageURL;
  if (!empty($xml->ItemAttributes->ListPrice)) {
    $item['listpriceamount'] = intval($xml->ItemAttributes->ListPrice->Amount);
    $item['listpricecurrencycode'] = (string) $xml->ItemAttributes->ListPrice->CurrencyCode;
    $item['listpriceformattedprice'] = (string) $xml->ItemAttributes->ListPrice->FormattedPrice;
  }
  if (!empty($xml->OfferSummary->LowestNewPrice)) {

    // Lowest may be "too low to display" so we need to make it an integer
    $item['lowestpriceamount'] = intval($xml->OfferSummary->LowestNewPrice->Amount);
    $item['lowestpricecurrencycode'] = (string) $xml->OfferSummary->LowestNewPrice->CurrencyCode;
    $item['lowestpriceformattedprice'] = (string) $xml->OfferSummary->LowestNewPrice->FormattedPrice;
  }

  // Note that this one assumes we've searched with Merchant = Amazon.
  // Otherwise we can do an xpath search looking for the actual amazon listing.
  if (!empty($xml->Offers->Offer[0]->OfferListing->Price)) {
    $item['amazonpriceamount'] = intval($xml->Offers->Offer[0]->OfferListing->Price->Amount);
    $item['amazonpricecurrencycode'] = (string) $xml->Offers->Offer[0]->OfferListing->Price->CurrencyCode;
    $item['amazonpriceformattedprice'] = (string) $xml->Offers->Offer[0]->OfferListing->Price->FormattedPrice;
  }
  $participant_types = preg_split('/,/', AMAZON_PARTICIPANT_TYPES);

  // Pull in the basics of the ItemAttributes collection.
  foreach ((array) $xml->ItemAttributes as $key => $value) {
    if (is_string($value) && !in_array($key, $participant_types)) {
      $key = strtolower($key);
      $item[$key] = $value;
    }
  }

  // Handle the Authors/Artists/Etc.
  foreach ($participant_types as $key) {
    if (isset($xml->ItemAttributes->{$key})) {
      foreach ($xml->ItemAttributes->{$key} as $value) {
        $item[strtolower($key)][] = (string) $value;
        $item['participants'][] = (string) $value;
      }
    }
  }

  // Handle the product images. In theory, there could be a million different
  // product image types. We're only going to check for the most common ones
  // and ignore the rest for now.
  $supported_sizes = preg_split('/,/', AMAZON_IMAGE_SIZES);
  if (isset($xml->ImageSets->ImageSet)) {
    foreach ((array) $xml->ImageSets->ImageSet as $key => $data) {
      if (in_array($key, $supported_sizes)) {
        $item['imagesets'][strtolower($key)] = array(
          'url' => (string) $data->URL,
          'height' => intval($data->Height),
          'width' => intval($data->Width),
        );
      }
    }
  }

  // Handle the editorial reviews.
  if (isset($xml->EditorialReviews)) {
    foreach ($xml->EditorialReviews->EditorialReview as $data) {
      $item['editorialreviews'][] = array(
        'source' => (string) $data->Source,
        'content' => (string) $data->Content,
      );
    }
  }

  // And the customer reviews.
  if (isset($xml->CustomerReviews)) {
    $item['customerreviews_iframe'] = (string) $xml->CustomerReviews->IFrameURL;
  }

  // Give other modules an opportunity to pull out other bits of Amazon data
  // that would otherwise be ignored. We can't use module_invoke_all, as it
  // would lose the reference.
  foreach (module_implements('amazon_item_clean_xml') as $module) {
    $function = $module . '_amazon_item_clean_xml';
    $function($item, $xml);
  }
  return $item;
}

/**
 * Insert 'cleaned' amazon item into database.
 * @param $item
 *   'Cleaned' amazon structure.
 * @return
 *   No return value.
 */
function amazon_item_insert($item) {
  static $item_keys = NULL;
  if (empty($item_keys)) {
    require_once 'amazon.install';
    $schema = amazon_schema();
    $item_keys = $schema['amazon_item']['fields'];
  }

  // We have boatloads of data to insert in here, so we're going to
  // cheat and blow away the old entries first.
  amazon_item_delete($item['asin']);
  $metadata = amazon_data_cache();
  $item['timestamp'] = REQUEST_TIME;

  // The db_insert needs fields that match exactly to the database,
  // so we'll intersect with what the actual schema says.
  $db_item = array_intersect_key($item, $item_keys);

  // Remove urlencoding for select characters before storing item in the database
  // This is purely for the sake of prettying up the resulting URLs, they work
  // either way.
  $replacements = array(
    '%7E' => '~',
    '%3D' => '=',
    '%3A' => ':',
    '%2C' => ',',
    '%2F' => '/',
    '%26' => '&',
    '%3F' => '?',
  );
  if (isset($db_item['detailpageurl'])) {
    $db_item['detailpageurl'] = str_replace(array_keys($replacements), $replacements, $db_item['detailpageurl']);
  }
  if (isset($db_item['customerreviews_iframe'])) {
    $db_item['customerreviews_iframe'] = str_replace(array_keys($replacements), $replacements, $db_item['customerreviews_iframe']);
  }
  try {
    db_insert('amazon_item')
      ->fields($db_item)
      ->execute();
  } catch (Exception $e) {
    amazon_db_error_watchdog("Failed to insert item into amazon_item table", $e, $db_item);
  }

  // Handle the various credits for a product, including Artist, Author,
  // Actor, etc. We map these to a separate table.
  if (in_array('creators', variable_get('amazon_core_data', array(
    'creators',
    'images',
  )))) {
    $participant_types = preg_split('/,/', AMAZON_PARTICIPANT_TYPES);
    foreach ($participant_types as $type) {
      if (isset($item[strtolower($type)])) {
        foreach ((array) $item[strtolower($type)] as $participant) {
          $item_participant = array(
            'asin' => $item['asin'],
            'type' => strtolower($type),
            'participant' => $participant,
          );
          try {
            db_insert('amazon_item_participant')
              ->fields($item_participant)
              ->execute();
          } catch (Exception $e) {
            amazon_db_error_watchdog("Failed to insert item into amazon_item_participant table", $e, $item_participant);
          }
        }
      }
    }
  }

  // Save the product images if they exist.
  if (in_array('images', variable_get('amazon_core_data', array(
    'creators',
    'images',
  )))) {
    if (isset($item['imagesets'])) {
      foreach ($item['imagesets'] as $size => $data) {
        $image = array(
          'asin' => $item['asin'],
          'size' => $size,
          'height' => $data['height'],
          'width' => $data['width'],
          'url' => $data['url'],
        );
        try {
          db_insert('amazon_item_image')
            ->fields($image)
            ->execute();
        } catch (Exception $e) {
          amazon_db_error_watchdog("Failed to insert item into amazon_item_image table", $e, $image);
        }
      }
    }
  }

  // Save the editorial reviews if they exist.
  if (in_array('editorial_reviews', variable_get('amazon_core_data', array(
    'creators',
    'images',
    'editorial_reviews',
  )))) {
    if (isset($item['editorialreviews'])) {
      foreach ($item['editorialreviews'] as $data) {
        $review = array(
          'asin' => $item['asin'],
          'source' => $data['source'],
          'content' => $data['content'],
        );
        try {
          db_insert('amazon_item_editorial_review')
            ->fields($review)
            ->execute();
        } catch (Exception $e) {
          amazon_db_error_watchdog("Failed to insert item into amazon_item_editorial_review table", $e, $review);
        }
      }
    }
  }
  module_invoke_all('amazon_item_insert', $item);
}

/**
 * Delete all vestiges of Amazon item.
 * @param $asin
 *   ASIN to be deleted.
 * @return No return.
 */
function amazon_item_delete($asin) {
  module_invoke_all('amazon_item_delete', $asin);
  db_delete('amazon_item')
    ->condition('asin', $asin)
    ->execute();
  db_delete('amazon_item_participant')
    ->condition('asin', $asin)
    ->execute();
  db_delete('amazon_item_image')
    ->condition('asin', $asin)
    ->execute();
  db_delete('amazon_item_editorial_review')
    ->condition('asin', $asin)
    ->execute();
}

/**
 * Try to turn a non-asin into an ASIN where possible.
 *
 * If the received input appears to be an EAN (ISBN-13) or an Amazon.com/de/uk
 * link, then this tries to convert it into an ASIN.
 * @param $input
 * @return
 *   An ASIN if possible. Otherwise whatever was passed in,
 *   after removing dashes.
 */
function amazon_convert_to_asin($input) {
  $input = preg_replace('/-/', '', $input);

  // Remove dashes.
  if (preg_match('/^https?:/', $input)) {
    $parts = preg_split('/\\//', $input);
    $asin = $parts[5];

    // 6th section of split, right after /dp/
    return $asin;
  }

  // Attempt conversion of 13-digit ASIN by doing an Amazon lookup.
  if (strlen($input) == 13 && is_numeric($input)) {
    $asin = amazon_ean_to_asin($input);
    return $asin;
  }
  return $input;
}

/**
 * Given an EAN (ISBN-13), try to get Amazon to give it to us.
 * @param $ean
 *   The EAN, ISBN-13 value
 * @return
 *   The asin, or NULL if unsuccessful.
 * @see https://affiliate-program.amazon.com/gp/associates/help/t5/a16?ie=UTF8&pf_rd_t=501&pf_rd_m=ATVPDKIKX0DER&pf_rd_p=&pf_rd_s=assoc-center-2&pf_rd_r=&pf_rd_i=assoc_glossary
 */
function amazon_ean_to_asin($ean, $locale = NULL) {
  $asin = NULL;
  $params = array(
    'ItemId' => $ean,
    'IdType' => 'EAN',
    'SearchIndex' => 'Books',
  );
  $results = amazon_http_request('ItemLookup', $params, $locale);
  if (!empty($results->Items->Item->ASIN)) {
    $asin = (string) $results->Items->Item->ASIN;
  }
  return $asin;
}

/**
 * Misc. helper functions for managing the wide array of Amazon
 * data bitsies.
 */
function amazon_data_cache($reset = FALSE) {
  static $data;
  if (!isset($data) || $reset) {
    if (!$reset && ($cache = cache_get('amazon:metadata')) && !empty($cache->data)) {
      $data = $cache->data;
    }
    else {
      $data = array();
      $data['locales'] = _amazon_default_locales();
      drupal_alter('amazon_metadata', $data);
      cache_set('amazon:metadata', $data);
    }
  }
  return $data;
}
function _amazon_default_locales() {
  $locales = array();

  // Load default locales from includes/amazon.locales.inc
  require_once drupal_get_path('module', 'amazon') . '/includes/amazon.locales.inc';
  $locales = _amazon_load_locales();

  // Fire hook_amazon_locales_alter() to allow modules to manipulate default locales.
  drupal_alter('amazon_locales', $locales);
  return $locales;
}
function amazon_get_associate_id($locale = NULL) {
  if (!$locale) {
    $locale = variable_get('amazon_default_locale', 'US');
  }
  $cache = amazon_data_cache();
  return variable_get('amazon_locale_' . $locale . '_associate_id', $cache['locales'][$locale]['da_associate_id']);
}
function amazon_cron() {

  // Here, we're going to chug through all the existing ASINs and update them.
  // We'll grab 50 at a time to avoid thrashing things.
  $per_cron_limit = 50;
  $needs_update_time = REQUEST_TIME - variable_get('amazon_refresh_schedule', 86400);
  $result = db_select('amazon_item', NULL, array(
    'fetch' => PDO::FETCH_ASSOC,
  ))
    ->fields('amazon_item', array(
    'asin',
  ))
    ->condition('timestamp', $needs_update_time, '<')
    ->range(0, $per_cron_limit)
    ->execute();
  $asins = $result
    ->FetchCol();
  if (!empty($asins)) {
    if ($items = amazon_item_lookup_from_web($asins)) {
      foreach ($items as $item) {
        amazon_item_insert($item);
      }
      watchdog('amazon', 'Amazon items were updated.');
    }
    else {
      watchdog('amazon', 'Amazon items could not be updated.');
    }
  }
}

/**
 * Implementation of hook_views_api.
 */
function amazon_views_api() {
  return array(
    'api' => 2,
    'path' => drupal_get_path('module', 'amazon') . '/includes',
  );
}

/**
 * Template helper for theme_amazon_views_view_row_item
 */
function template_preprocess_amazon_views_view_row_item(&$vars) {
  $options = $vars['options'];
  $vars['amazon_item'] = '';

  // make sure var is defined.
  $asin = $vars['row']->asin;
  $items = amazon_item_lookup($asin);
  if (empty($items[$asin])) {
    return;
  }
  else {
    $item = $items[$asin];
    $item['view'] = $vars['view'];
    $vars['amazon_item'] = $item;
    if ($options['display_format'] == 'inline') {
      $vars['content'] = theme('amazon_item_inline', array(
        'item' => $item,
        'style' => $options['display_format'],
      ));
    }
    else {
      $vars['content'] = theme('amazon_item', array(
        'item' => $item,
        'style' => $options['display_format'],
      ));
    }
  }
}
function amazon_init() {
  drupal_add_css(drupal_get_path('module', 'amazon') . '/amazon.css');
}

/**
 * Have watchdog emit complete errors about a database exception.
 *
 * @param $msg
 *   The message explaining the error.
 *   The message should not be localized, as it will be passed to watchdog().
 * @param $e
 *   The exception which was caught
 * @param $extra
 *   Array: The fields being updated, or other information.
 *
 */
function amazon_db_error_watchdog($msg, $e, $extra = array()) {
  watchdog('amazon', "{$msg}: Message = %message, query= %query fields=<pre>%fields</pre>", array(
    '%message' => $e
      ->getMessage(),
    '%query' => $e->query_string,
    '%fields' => print_r($extra, TRUE),
  ));
}

/**
 * Allow other modules to alter the list of response groups used when sending an http request. Invoke using the alter
 * hook modulename_amazon_response_groups_alter.
 *
 * @return string
 *   Imploded array of response groups, delimetered by commas.
 *
 */
function amazon_get_response_groups() {
  $response_groups = array(
    'Large',
  );

  // Fire hook_amazon_response_groups_alter().
  drupal_alter('amazon_response_groups', $response_groups);
  return implode(',', $response_groups);
}

/**
 * Allows expiration of items in the amazon_item table. Items can be
 * expired by productgroup type, specifically by asin, or all items.
 * If no parameters are specified, all items will be expired.
 *
 * @param $asins
 *   array of ASINs to expire
 * @param $types
 *   array of productgroup types to expire
 */
function amazon_expire_items(array $asins = array(), array $types = array()) {
  $query = db_update('amazon_item')
    ->fields(array(
    'timestamp' => 0,
  ));
  if (!empty($asins)) {
    $query
      ->condition('asin', $asins, 'IN');
  }
  if (!empty($types)) {
    foreach ($types as $key => $value) {
      $types[$key] = strtolower($value);
    }
    $query
      ->condition('lower(productgroup)', $types, 'IN');
  }
  $updated = $query
    ->execute();
  $update_msg = format_plural($updated, '1 Amazon Item has been expired', '@count Amazon Items have been expired');
  watchdog('amazon', $update_msg, NULL, WATCHDOG_INFO, NULL);
}

/**
 * Implements hook_flush_caches().
 */
function amazon_flush_caches() {

  // Determine what function is actually causing this hook to fire
  $trace = debug_backtrace(FALSE);
  while (in_array($trace[0]['function'], array(
    'amazon_flush_caches',
    'call_user_func_array',
    'module_invoke_all',
    'drupal_flush_all_caches',
  ))) {
    $foo = array_shift($trace);
  }

  // If this hook was not fired by a cron run expire items.
  if ($trace[0]['function'] != 'system_cron') {
    amazon_expire_items();
  }
  return array();
}

Functions

Namesort descending Description
amazon_convert_to_asin Try to turn a non-asin into an ASIN where possible.
amazon_cron
amazon_data_cache Misc. helper functions for managing the wide array of Amazon data bitsies.
amazon_db_error_watchdog Have watchdog emit complete errors about a database exception.
amazon_ean_to_asin Given an EAN (ISBN-13), try to get Amazon to give it to us.
amazon_expire_items Allows expiration of items in the amazon_item table. Items can be expired by productgroup type, specifically by asin, or all items. If no parameters are specified, all items will be expired.
amazon_flush_caches Implements hook_flush_caches().
amazon_get_associate_id
amazon_get_response_groups Allow other modules to alter the list of response groups used when sending an http request. Invoke using the alter hook modulename_amazon_response_groups_alter.
amazon_http_request Create an issue an HTTP request to the Amazon API.
amazon_init
amazon_item_clean_xml Take the Amazon XML item and turn it into our own private 'cleaned' data structure.
amazon_item_delete Delete all vestiges of Amazon item.
amazon_item_insert Insert 'cleaned' amazon item into database.
amazon_item_lookup Look up an item using database or web. The default is to look in the database for existing data, and then to do the web search if that fails. $force_lookup==TRUE forces going to Amazon's API.
amazon_item_lookup_from_db Look up ASINs in database and return arrays of information keyed by ASIN.
amazon_item_lookup_from_web Use Amazon API to look up an array of ASINs.
amazon_menu Implementation of hook_menu. Adds the url path for the Amazon settings page.
amazon_permission Implementation of hook_permission
amazon_theme Implementation of hook_theme().
amazon_views_api Implementation of hook_views_api.
template_preprocess_amazon_item template_preprocess: amazon_item.
template_preprocess_amazon_views_view_row_item Template helper for theme_amazon_views_view_row_item
_amazon_clean_type Convert a type like ABIS_whatever into just whatever.
_amazon_default_locales
_amazon_item_batch_lookup_from_web Get 10 or less items from the AWS web service. AWS allows ONLY 10 items, See http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.ht....
_amazon_item_batch_lookup_from_web_errors
_amazon_item_classes Create name of the extra css class for an item.
_amazon_load_child_data Load participant, image, editorial_review data into database.

Constants