Amazon Proxy – Teil 2: Die AmaProx PHP Server-Komponente

Amazon Proxy

Im zweiten Teil dieser Artikelserie geht es um die Programmierung eines eigenen Amazon Proxy Dienstes. Das klingt hochtrabender als es eigentlich ist, denn der Proxy ist lediglich ein PHP-Script. Ich möchte an dieser Stelle nochmal auf den vorhergegangenen Artikel verweisen, in dem erklärt wird, wofür ein Amazon Proxy eigentlich gut ist, wie er funktioniert und wie wir die Amazon Product Advertising API ansprechen können. Im dritten Teil dieser Serie geht es dann um die Clientseite und wie wir den Amazon Proxy aus einer Webseite oder einem WordPress Artikel heraus ansprechen können.

Achtung: Seit Anfang 2020 gibt es die Amazon Product Advertising API (PA-API) in der Version 5.0, die vieles verändert. Und damit ist dieser Artikel weitestgehend überholt, denn er bezieht sich noch auf die PA-API Version 4. Den Update auf Version 5.0 gibts in Teil 4 dieser Artikelserie.

Nun bin ich Blogger und kein WordPress Plugin Programmierer, deshalb bitte ich nachzusehen, das ich hier kein fertiges WordPress Plugin abliefere, das man nur noch installieren bräuchte. Dieses Programm wird zwar funktionsfähig sein, aber keinesfalls perfekt und soll eher als Gerüst dienen, einen Proxy nach eigenen Vorstellungen zu realisieren. Dazu werde ich auch noch Anregungen liefern.

Was wird benötigt?

  • Das Vorwissen aus Teil 1
  • Ein Grundverständnis über HTML, JavaScript, Ajax und PHP
  • Natürlich einen eigenen WordPress Blog oder eine andere Website
  • Eine Clientkomponente zum Amazon Proxy – das kommt in Teil 3
  • Eine Amazon Partner-ID und Zugangsschlüssel zur Amazon Product Advertising API

Das AmaProx PHP Programm

Ich programmiere schon gerne objektorientiert – wo es sinnvoll ist. Aber diese Aufgabe ist so „straightforward“, dass ich es batch-mäßig einfach der Reihe nach herunter programmiere. Der Übersichtlichkeit halber teile ich das Programm aber in neun aufeinander folgende Schritte, die ich nun nachfolgen einzeln bespreche. Am Ende des Artikels kommt das ganze Programm zum Kopieren dann nochmal in einem Stück. Im Folgenden wird davon ausgegangen, dass das Amazon Proxy Programm den Namen amaprox.php trägt und in einem Verzeichnis amaprox unterhalb des Domänen-Stammverzeichnisses liegt. Das kann man auch bei WordPress so machen oder das Verzeichnis in den eigenen Theme-Ordner oder unter wp-content legen.

I. Definitionen

<?php

$access_key_id = "ABCDEFGHIJKLMNO";
$secret_key = "AbCdEfGhIjKlMnOpQrStUvWxYz";
$associate_tag = "wwwmeinesite-21";

$endpoint = "webservices.amazon.de";
$uri = "/onca/xml";
$response_group = "Large";

$cachefolder = "cache";
$amaprox_homeURL = "https://www.meine-site.de/amaprox";

Hier definieren wir die variablen Bestandteile des Scripts. Am Anfang stehen die beiden Schlüssel zur Amazon Product Advertising API und der $associate_tag ist nichts anderes als die Amazon Partner-ID. Hier müssen natürlich die eigenen Schlüssel eingetragen werden, anstelle meiner Platzhalter. $endpoint und $uri können für das deutsche Amazon so bleiben und die $response_group ist mit Large für den einstig gut belegt. Der $cachefolder ist das Verzeichnis unterhalb des AmaProx-Programmverzeichnisses, in dem temporär die Produktbilder ablegt werden. Und $amaprox_homeURL ist die URL zum AmaProx-Programmverzeichnis. Auch das muss natürlich den eigenen Gegebenheiten angepasst werden.

II. Parameter übernehmen

if (isset($_GET["template"]) && !empty($_GET["template"])) {
  $template = $_GET["template"];
} else {
  $template = "widebox";
}
if (isset($_GET["asin"]) && !empty($_GET["asin"])) {
  $asin = $_GET["asin"];
} else {
  return;
}

An dieser Stelle werden die übergebenen Parameter ausgewertet. Dabei muss man sich vorstellen, dass der Aufruf des Proxies per HTTP(S)-GET folgendermaßen aussieht:

https://www.meine-site.de/amaprox/amaprox.php?asin=B00XLVXILI&template=widebox

Es werden also zwei Parameter übergeben, asin und template. Letzteres ist optional und wird als „widebox“ angenommen, wenn die Angabe fehlt. Die asin ist dagegen verpflichtend, das Script beendet sich schweigend mit return, wenn die asin fehlt.

Hier kann man schon Verbesserungspotential an dem Script erkennen. Es erfolgt kein Fehlerhandling, das Programm beendet sich im Fehlerfall einfach still. Hier könnten beispielsweise auch Logfileeinträge geschrieben oder eine Fehlermeldung zurückgegeben werden.

III. Cache Verzeichnis bereinigen

$files = glob($cachefolder . '/*');
foreach($files as $file) {
  if(is_file($file) && (time() - filemtime($file) > 10)) {
    unlink($file);
  }
}

Als nächstes wird der Cachefolder gelöscht. Dazu wird mit glob eine Dateiliste geholt, vergleichbar mit dir, bzw ls bei Linux. Diese Liste wird dann durchgegangen und jede Datei, die älter als 10 Sekunden ist, wird mit unlink gelöscht. So ist das Cacheverzeichnis immer (ziemlich) sauber.

IV. Abfrage-Request erstellen

$params = array(
  "Service" => "AWSECommerceService",
  "Operation" => "ItemLookup",
  "AWSAccessKeyId" => $access_key_id,
  "AssociateTag" => $associate_tag,
  "ItemId" => $asin,
  "IdType" => "ASIN",
  "ResponseGroup" => $response_group
);
if (!isset($params["Timestamp"])) {
  $params["Timestamp"] = gmdate('Y-m-d\TH:i:s\Z');
}
ksort($params);
$pairs = array();
foreach ($params as $key => $value) {
  array_push($pairs, rawurlencode($key)."=".rawurlencode($value));
}
$canonical_query_string = join("&", $pairs);
$string_to_sign = "GET\n".$endpoint."\n".$uri."\n".$canonical_query_string;
$signature = base64_encode(hash_hmac("sha256", $string_to_sign,
                                     $secret_key, true));
$request_url = 'https://'.$endpoint.$uri.'?'.$canonical_query_string.
                          '&Signature='.rawurlencode($signature);

Nun geht es um die Anfrage an die Amazon Product Advertising API. Die Formulierung dieser Anfrage ist nicht trivial, da werden Parameter zusammengestellt, mit einem Timestamp versehen, sortiert, kodiert und signiert. Zum Glück gibt Amazon diesen Programmteil im Scratchpad unten bei Code snippets bereits komplett vor – ich habe das (nahezu) 1:1 übernommen.

V. Amazon abfragen und Antwort entgegennehmen

$response = @file_get_contents($request_url);

So aufwändig es war, die Amazon Abfrage zu formulieren, so einfach ist nun die Abfrage der API an sich. Das geht in einer Programmzeile mit file_get_contents.

VI. Antwort parsen

if ($response === FALSE) {
  return;
}
$amaxml = simplexml_load_string($response);
if ($amaxml === FALSE) {
  return;
}
if ($amaxml->Items->Request->IsValid != "True") {
  return;
}
if (!empty($amaxml->Items->Request->Errors)) {
  return;
}

$ama_id = $amaxml->OperationRequest->RequestId;
$ama_titel = $amaxml->Items->Item->ItemAttributes->Title;
$ama_medium = $amaxml->Items->Item->MediumImage;
if (empty($ama_medium)) {
  $ama_medium = $amaxml->Items->Item->ImageSets->ImageSet->MediumImage;
}
$ama_page_url = $amaxml ->Items->Item->DetailPageURL;
$ama_preis = $amaxml->Items->Item->OfferSummary->LowestNewPrice->FormattedPrice;
if (empty($ama_preis)) {
  $ama_preis = $amaxml->Items->Item->OfferSummary->LowestUsedPrice->FormattedPrice;
}
$ama_beschreibung = $amaxml->Items->Item->EditorialReviews->EditorialReview->Content;
$ama_feature = '<ul>';
foreach($amaxml ->Items->Item->ItemAttributes->Feature as $feature) {
  $ama_feature .= '<li>'.$feature.'</li>';
};
$ama_feature .= '</ul>';
if (empty($ama_beschreibung)) {
  $ama_beschreibung = $ama_feature;
} elseif (strlen($ama_beschreibung) < 100) {
  $ama_beschreibung .= '<br/>'.$ama_feature;
}

Nun kommt die eigentliche Arbeit – aus der Antwort der Amazon API (siehe Scratchpad), die sich in $response befindet, müssen wir die Daten herauslösen, die uns interessieren. Zuerst wird der Fehlerfall behandelt, dass gar keine Daten zurückkommen, dann wird das XML Datenkonstrukt per simplexml_load_string in ein Objekt gewandelt, mit dem sich einfach arbeiten lässt. Dazu muss man nur noch wissen, wo was steht. Und hier hilft wieder das Amazon Scratchpad. Schauen wir es uns am Beispiel des Preises einmal an, der steht unter:

$amaxml->Items->Item->OfferSummary->LowestNewPrice->FormattedPrice;

Sollte dort aber ausnahmsweise kein Preis verzeichnet sein, so gibt es eine zweite Lokation, wo das Programm nachsehen kann, nämlich:

$amaxml->Items->Item->OfferSummary->LowestUsedPrice->FormattedPrice;

Ähnlich läuft es auch mit der Produktbeschreibung, dafür gibt es eine vorgesehene Stelle, die aber auch nicht immer gefüllt ist. In diesem Fall verwendet das Programm die Feature-Zeilen, die zu einer HTML-Liste zusammengesetzt werden.

Ein Wort noch zu den Bildern. Das Programm nutzt das MediumImage, die Amazon API bietet aber auch andere Bildgrößen, die man bei Bedarf per Scratchpad finden kann, um dieses Programm individuell anzupassen. Wir bleiben bei den Bildern:

VII. Bilder abrufen

$mediumImg = $cachefolder."/Medium-".$ama_id.".jpg";
$ch = curl_init($ama_medium->URL);
$zieldatei = fopen($mediumImg, "w");
curl_setopt($ch, CURLOPT_FILE, $zieldatei);
curl_setopt($ch, CURLOPT_TIMEOUT, 600);
curl_exec($ch);
fclose($zieldatei);

Um ein Bild auf dem eigenen Server speichern zu können generieren wir erst mal einen eindeutigen Namen. Dazu nutzt das Programm eine Request-ID, die Amazon für jeden API-Abruf eindeutig vergibt und die wir oben bereits unter $ama_id aus den Daten heraus gelöst haben. Um das MediumImage über das Internet abzurufen, bedienen wir uns der Curl-Funktionen von PHP, die solche Fernzugriffe und auch gleich das Abspeichern elegant erledigen können. Danach befindet sich das Bild im Cacheordner und $mediumImg verweist auf die Datei.

Wir haben nun alle Daten und das Bild und können beides für die Rückgabe an den Client aufbereiten. Der Amazon Proxy baut dazu ein HTML-Codeschnipsel zusammen, das der Client dann direkt in die Webseite einschleusen kann.

VIII. HTML Code generieren

switch ($template) {
  case "widebox":
    $a=' <div style="border: 1px solid #aaa; padding: 5px; margin: 30px 0;">';
    $a.='  <div style="width: '.$ama_medium->Width.'px; float: left; margin-right: 16px;">';
    $a.='   <a href="'.$ama_page_url.'" target="_blank" rel="nofollow"><img src="'.$amaprox_homeURL.'/'.$mediumImg.'" style="border: 0;" width="'.$ama_medium->Width.'" height="'.$ama_medium->Height.'" border="0"></a>';
    $a.='  </div>';
    $a.='  <div>';
    $a.='   <p style="padding-bottom: 5px;">';
    $a.='    <a href="'.$ama_page_url.'" target="_blank rel="nofollow"">'.$ama_titel.'</a>';
    $a.='   </p>';
    $a.='   <p style="padding-bottom: 5px; margin-bottom: 0;"><strong>Preis: <span style="color: #990000;">'.$ama_preis.'</span></strong></p>';
    $a.='   <div style="font-size: 12px; line-height: 16px;">'.$ama_beschreibung.'</div>';
    $a.='  </div>';
    $a.='  <div style="clear: both;"></div>';
    $a.=' </div>';
    break;
  case "smallbox":

    break;
  case "link":
    $a=' <div style="border: 1px solid #ff9201; padding: 5px; margin: 6px 0 -10px 0;">';
    $a.=' <p><a href="'.$ama_page_url.'" target="_blank" rel="nofollow">'.$ama_titel.'</a></p>';
    $a.=' </div>';
    break;
  default:
    $a='';
}

Der Amazon Proxy kann nicht nur eine einzige Art von Codeschnipsel generieren, sondern viele verschiedene, die durch Templatenamen unterschieden werden. Deshalb kommt hier ein switch-case Konstrukt zum Einsatz, um mehrere Templates bedienen zu können. Folgende Templates gibt es bereits:

  • widebox: Das ist ein vollständiger Werbeblock mit Bild, Titel, Preis und Produktbeschreibung
  • smallbox: Das Template ist noch nicht angelegt
  • link: Eine einfache Titelzeile mit Verlinkung und Rahmen

Für Erweiterungen ist hier noch viel Raum.
Die Funktionsweise ist immer gleich, eine Variable $a wird mit HTML-Text befüllt unter Verwendung der Daten, die wir oben aus dem XML Konstrukt heraus gelöst haben. Und ein bisschen Styling ist auch dabei. Wichtig ist, dass die Verlinkung des Bildes ins Cacheverzeichnis des Proxies erfolgt und nicht zu Amazon.

IX. HTML absenden

header("Content-Type: text/html; charset=UTF-8");
echo $a;

?>

Wir haben ein HTML-Codeschnipsel anhand eines Templatenamens und mit den Daten von Amazon generiert. Das muss nur noch an den aufrufenden Client zurückgegeben werden. Das ist denkbar einfach, wir definieren einen geeigneten Header und geben den Codeschnipsel per echo aus.

Code komplett

Für diejenigen, die den AmaProx PHP-Code kopieren wollen, kommt er hier nochmal in einem Stück:

<?php

// I. Definitionen
$access_key_id = "ABCDEFGHIJKLMNO";
$secret_key = "AbCdEfGhIjKlMnOpQrStUvWxYz";
$associate_tag = "wwwmeinesite-21";

$endpoint = "webservices.amazon.de";
$uri = "/onca/xml";
$response_group = "Large";

$cachefolder = "cache";
$amaprox_homeURL = "https://www.meine-site.de/amaprox";

// II. Parameter übernehmen
if (isset($_GET["template"]) && !empty($_GET["template"])) {
  $template = $_GET["template"];
} else {
  $template = "widebox";
}
if (isset($_GET["asin"]) && !empty($_GET["asin"])) {
  $asin = $_GET["asin"];
} else {
  return;
}

// III. Cache Verzeichnis bereinigen
$files = glob($cachefolder . '/*');
foreach($files as $file) {
  if(is_file($file) && (time() - filemtime($file) > 10)) {
    unlink($file);
  }
}

// IV. Abfrage-Request erstellen
$params = array(
  "Service" => "AWSECommerceService",
  "Operation" => "ItemLookup",
  "AWSAccessKeyId" => $access_key_id,
  "AssociateTag" => $associate_tag,
  "ItemId" => $asin,
  "IdType" => "ASIN",
  "ResponseGroup" => $response_group
);
if (!isset($params["Timestamp"])) {
  $params["Timestamp"] = gmdate('Y-m-d\TH:i:s\Z');
}
ksort($params);
$pairs = array();
foreach ($params as $key => $value) {
  array_push($pairs, rawurlencode($key)."=".rawurlencode($value));
}
$canonical_query_string = join("&", $pairs);
$string_to_sign = "GET\n".$endpoint."\n".$uri."\n".$canonical_query_string;
$signature = base64_encode(hash_hmac("sha256", $string_to_sign,
                                     $secret_key, true));
$request_url = 'https://'.$endpoint.$uri.'?'.$canonical_query_string.
                          '&Signature='.rawurlencode($signature);

// V. Amazon abfragen und Antwort entgegennehmen
$response = @file_get_contents($request_url);

// VI. Antwort parsen
if ($response === FALSE) {
  return;
}
$amaxml = simplexml_load_string($response);
if ($amaxml === FALSE) {
  return;
}
if ($amaxml->Items->Request->IsValid != "True") {
  return;
}
if (!empty($amaxml->Items->Request->Errors)) {
  return;
}

$ama_id = $amaxml->OperationRequest->RequestId;
$ama_titel = $amaxml->Items->Item->ItemAttributes->Title;
$ama_medium = $amaxml->Items->Item->MediumImage;
if (empty($ama_medium)) {
  $ama_medium = $amaxml->Items->Item->ImageSets->ImageSet->MediumImage;
}
$ama_page_url = $amaxml ->Items->Item->DetailPageURL;
$ama_preis = $amaxml->Items->Item->OfferSummary->LowestNewPrice->FormattedPrice;
if (empty($ama_preis)) {
  $ama_preis = $amaxml->Items->Item->OfferSummary->LowestUsedPrice->FormattedPrice;
}
$ama_beschreibung = $amaxml->Items->Item->EditorialReviews->EditorialReview->Content;
$ama_feature = '<ul>';
foreach($amaxml ->Items->Item->ItemAttributes->Feature as $feature) {
  $ama_feature .= '<li>'.$feature.'</li>';
};
$ama_feature .= '</ul>';
if (empty($ama_beschreibung)) {
  $ama_beschreibung = $ama_feature;
} elseif (strlen($ama_beschreibung) < 100) {
  $ama_beschreibung .= '<br/>'.$ama_feature;
}

// VII. Bilder abrufen
$mediumImg = $cachefolder."/Medium-".$ama_id.".jpg";
$ch = curl_init($ama_medium->URL);
$zieldatei = fopen($mediumImg, "w");
curl_setopt($ch, CURLOPT_FILE, $zieldatei);
curl_setopt($ch, CURLOPT_TIMEOUT, 600);
curl_exec($ch);
fclose($zieldatei);


// VIII. HTML Code generieren
switch ($template) {
  case "widebox":
    $a=' <div style="border: 1px solid #aaa; padding: 5px; margin: 30px 0;">';
    $a.='  <div style="width: '.$ama_medium->Width.'px; float: left; margin-right: 16px;">';
    $a.='   <a href="'.$ama_page_url.'" target="_blank" rel="nofollow"><img src="'.$amaprox_homeURL.'/'.$mediumImg.'" style="border: 0;" width="'.$ama_medium->Width.'" height="'.$ama_medium->Height.'" border="0"></a>';
    $a.='  </div>';
    $a.='  <div>';
    $a.='   <p style="padding-bottom: 5px;">';
    $a.='    <a href="'.$ama_page_url.'" target="_blank rel="nofollow"">'.$ama_titel.'</a>';
    $a.='   </p>';
    $a.='   <p style="padding-bottom: 5px; margin-bottom: 0;"><strong>Preis: <span style="color: #990000;">'.$ama_preis.'</span></strong></p>';
    $a.='   <div style="font-size: 12px; line-height: 16px;">'.$ama_beschreibung.'</div>';
    $a.='  </div>';
    $a.='  <div style="clear: both;"></div>';
    $a.=' </div>';
    break;
  case "smallbox":

    break;
  case "link":
    $a=' <div style="border: 1px solid #ff9201; padding: 5px; margin: 6px 0 -10px 0;">';
    $a.=' <p><a href="'.$ama_page_url.'" target="_blank" rel="nofollow">'.$ama_titel.'</a></p>';
    $a.=' </div>';
    break;
  default:
    $a='';
}

// IX. HTML absenden
header("Content-Type: text/html; charset=UTF-8");
echo $a;

?>

Achtung: Diesen Programmcode bitte nicht mehr verwenden! Er bezieht sich noch auf die Amazon Product Advertising API (PA-API) in der Version 4. Den Update auf die neue PA-API 5.0 gibts in Teil 4 dieser Artikelserie.

Amazon Proxy testen

Den Proxy können wir direkt mit folgender URL aufrufen:

https://www.meine-site.de/amaprox/amaprox.php?asin=B00XLVXILI&template=widebox

Domäne und ggf. Verzeichnis müssen natürlich angepasst werden. Dann sieht das in etwa wie in diesem Screenshot aus.

Oder in Realität mit ein paar Verbesserungen (siehe unten) dann so:

Amazon Proxy verbessern

Wie bereits erwähnt, ist der Amazon Proxy eine recht rudimentäre Konstruktion, um ihn halbwegs übersichtlich zu halten. Hier ein paar Anregungen, wie er sich weiter ausbauen lässt:

  • Fehlerbehandlung: Auftretende Fehler könnten in eine Logdatei geschrieben werden
  • Mailbenachrichtigung, zum Beispiel, wenn ein Produkt oder ein Preis nicht mehr verfügbar ist
  • weitere Templates für andere Anzeigeformate
  • Amazon Werbung versehen mit einem Amazon Logo
  • Ergänzen eines ‚Kaufen-Buttons‘

Weitere Artikel in dieser Kategorie:

6 Kommentare

  1. Poolboy

    Vielen Dank für die Anleitung!

    Ich hätte eine Frage und zwar: ist damit auch möglich über https://www.meine-site.de/amaprox/amaprox.php?asin=XXXXXX alle Infos welche man von der PA API abrufen kann als JSON Response zurück kommt? Das reicht mir schon aus :) Ich brauche nur eine URL die ich aufrufen muss um anschließend ein JSON mit den Produktinfos zurück zu bekommen.

    Ein paar Tipps wären hier denke ich hilfreich. Vielen Dank an dieser Stelle!

    Antworten
    1. Helmut (Beitrag Autor)

      Du musst die PA-API 5 verwenden, siehe Amazon-proxy-teil-4-product-advertising-api-pa-api-5-0/.
      Und ja auf die Anfrage an Amazon kommt ein JSON-String zurück. Was da genau drin steht lässt sich am besten über das Amazon Scratchpad ermitteln: https://webservices.amazon.com/paapi5/scratchpad/index.html.

      Antworten
  2. Olli

    Darf man den Proxy frei verwenden und anpassen, bzw. ist der von dir selbst?

    Antworten
    1. Helmut (Beitrag Autor)

      Der dargestellte Programmcode ist in der Tat von mir und Du kannst ihn gerne nach Deinen Wünschen ändern und verwenden. Falls Du ein darauf aufbauendes Programm veröffentlichst, bin ich um eine Quellenangabe dankbar.

      Antworten
  3. Andreas

    Hallo Helmut,
    vielen Dank für die Anleitung.
    Leider bekomme ich es unter WordPress aber nicht zum laufen, da ich ständig eine Apache-Fehlermeldung bekomme, was den Zugriff auf den cache-Ordner angeht:
    AH01630: client denied by server configuration: /var/www/vhosts/meinedomain.de/html/wordpress/amaprox/cache/index.php, referer: https://www.meinedomain.de/amaprox/amaprox.php?asin=B06W9L66D7&template=widebox

    Daraufhin haben meine Recherchen ergeben, dass man unter Apache 2.4 in den Konfigurationen dieses hinzufügen soll:

    Require all granted

    Doch leider bleiben die Fehlermeldungen unverändert. Hast du eine Idee, warum der Zugriff auf das Verzeichnis nicht gestattet wird? Bei der Gelegenheit ein Hinweis. Der Text des Amazon-Artikels wird im Browser scheinbar richtig ausgegeben (!), jedoch erscheint das Bild nicht. Stattdessen kommt wie gesagt die Apache-Fehlermeldung in Plesk.
    Vielen Dank im voraus für deine Hilfe!
    Andreas

    Antworten
  4. Andreas

    Sorry, mein Kommentar wurde gekürzt, es sollte heißen (ohne spitze Klammern):
    Directory /var/www/vhosts/promi-geburtstage.de/html/wordpress/amaprox/cache/
    Require all granted
    /Directiory

    Antworten

Schreiben Sie einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert