1<?php
2/**
3 * Amazon Plugin: pulls Bookinfo from amazon.com
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Andreas Gohr <andi@splitbrain.org>
7 */
8
9if(!defined('AMAZON_APIKEY')) define('AMAZON_APIKEY','0R9FK149P6SYHXZZDZ82');
10
11/**
12 * All DokuWiki plugins to extend the parser/rendering mechanism
13 * need to inherit from this class
14 */
15class syntax_plugin_amazon extends DokuWiki_Syntax_Plugin {
16
17    /**
18     * What kind of syntax are we?
19     */
20    function getType(){
21        return 'substition';
22    }
23
24    function getPType(){
25        return 'block';
26    }
27
28    /**
29     * Where to sort in?
30     */
31    function getSort(){
32        return 160;
33    }
34
35    /**
36     * Connect pattern to lexer
37     */
38    function connectTo($mode) {
39        $this->Lexer->addSpecialPattern('\{\{amazon>[\w:\\- =]+\}\}',$mode,'plugin_amazon');
40        $this->Lexer->addSpecialPattern('\{\{wishlist>[\w:\\- =]+\}\}',$mode,'plugin_amazon');
41        $this->Lexer->addSpecialPattern('\{\{amazonlist>[\w:\\- =]+\}\}',$mode,'plugin_amazon');
42    }
43
44    /**
45     * Do all the API work, fetch the data, parse it and return it for the renderer
46     */
47    function handle($match, $state, $pos, Doku_Handler $handler){
48        // check type and remove markup
49        if(substr($match,2,8) == 'wishlist'){
50            $match = substr($match,11,-2);
51            $type = 'wishlist';
52        }elseif(substr($match,2,10) == 'amazonlist'){
53            $match = substr($match,13,-2);
54            $type = 'amazonlist';
55        }else{
56            $match = substr($match,9,-2);
57            $type = 'product';
58        }
59        list($ctry,$asin) = explode(':',$match,2);
60
61        // default parameters...
62        $params = array(
63            'type'      => $type,
64            'imgw'      => $this->getConf('imgw'),
65            'imgh'      => $this->getConf('imgh'),
66            'maxlen'    => $this->getConf('maxlen'),
67            'price'     => $this->getConf('showprice'),
68            'purchased' => $this->getConf('showpurchased'),
69            'sort'      => $this->getConf('sort'),
70        );
71        // ...can be overridden
72        list($asin,$more) = explode(' ',$asin,2);
73        if(preg_match('/(\d+)x(\d+)/i',$more,$match)){
74            $params['imgw'] = $match[1];
75            $params['imgh'] = $match[2];
76        }
77        if(preg_match('/=(\d+)/',$more,$match)){
78            $params['maxlen'] = $match[1];
79        }
80        if(preg_match('/noprice/i',$more,$match)){
81            $params['price'] = false;
82        }elseif(preg_match('/(show)?price/i',$more,$match)){
83            $params['price'] = true;
84        }
85        if(preg_match('/nopurchased/i',$more,$match)){
86            $params['purchased'] = false;
87        }elseif(preg_match('/(show)?purchased/i',$more,$match)){
88            $params['purchased'] = true;
89        }
90        if(preg_match('/sortprice/i',$more,$match)){
91            $params['sort'] = 'Price';
92        }elseif(preg_match('/sortpriority/i',$more,$match)){
93            $params['sort'] = 'Priority';
94        }elseif(preg_match('/sortadded/i',$more,$match)){
95            $params['sort'] = 'DateAdded';
96        }
97
98        // no country given?
99        if(empty($asin)){
100            $asin = $ctry;
101            $ctry = 'us';
102        }
103
104        // correct country given?
105        if(!preg_match('/^(us|uk|jp|de|fr|ca)$/',$ctry)){
106            $ctry = 'us';
107        }
108
109        // get partner id
110        $partner = $this->getConf('partner_'.$ctry);
111
112        // correct domains
113        if($ctry == 'us') $ctry = 'com';
114        if($ctry == 'uk') $ctry = 'co.uk';
115
116        // basic API parameters
117        $opts = array();
118        $opts['Service']        = 'AWSECommerceService';
119        $opts['AWSAccessKeyId'] = AMAZON_APIKEY;
120        $opts['AssociateTag']   = $partner;
121        if($type == 'product'){
122            // parameters for querying a single product
123            $opts['Operation']      = 'ItemLookup';
124            $opts['ResponseGroup']  = 'Medium,OfferSummary';
125            if(strlen($asin)<13){
126                $opts['IdType'] = 'ASIN';
127                $opts['ItemId'] = $asin;
128            }else{
129                $opts['SearchIndex'] = 'Books';
130                $opts['IdType']      = 'ISBN';
131                $opts['ItemId']      = $asin;
132            }
133        }else{
134            // parameters to query a wishlist
135            $opts['Operation']            = 'ListLookup';
136            $opts['ResponseGroup']        = 'ListItems,Medium,OfferSummary';
137            $opts['ListId']               = $asin;
138            $opts['Sort']                 = $params['sort'];
139            $opts['IsIncludeUniversal']   = 'True';
140            $opts['IsOmitPurchasedItems'] = ($params['purchased'] ? 'False' : 'True');
141            if($type == 'wishlist'){
142                $opts['ListType']   = 'WishList';
143            }else{
144                $opts['ListType']   = 'Listmania';
145            }
146        }
147
148        // support paged results
149        $result = array();
150        $pages = 1;
151        for($page=1; $page <= $pages; $page++){
152            $opts['ProductPage'] = $page;
153
154            // fetch it
155            $http = new DokuHTTPClient();
156            $url = $this->_signedRequestURI($ctry,$opts,$this->getConf('publickey'),$this->getConf('privatekey'));
157            $xml  = $http->get($url);
158            if(empty($xml)){
159                if($http->error) return $http->error;
160                if($http->status == 403) return 'Signature check failed, did you set your Access Keys in config?';
161                return 'unkown error';
162            }
163
164            // parse it
165            require_once(dirname(__FILE__).'/XMLParser.php');
166            $xmlp = new XMLParser($xml);
167            $data = $xmlp->getTree();
168
169            //dbg($data);
170
171            // check for errors and return the item(s)
172            if($type == 'product'){
173                // error?
174                if($data['ITEMLOOKUPRESPONSE'][0]['ITEMS'][0]['REQUEST'][0]['ERRORS']){
175                    return $data['ITEMLOOKUPRESPONSE'][0]['ITEMS'][0]['REQUEST'][0]
176                                ['ERRORS'][0]['ERROR'][0]['MESSAGE'][0]['VALUE'];
177                }
178                // return item
179                $result = array_merge($result, (array)
180                              $data['ITEMLOOKUPRESPONSE'][0]['ITEMS'][0]['ITEM']);
181            }else{
182                // error?
183                if($data['LISTLOOKUPRESPONSE'][0]['LISTS'][0]['REQUEST'][0]['ERRORS']){
184                    return $data['LISTLOOKUPRESPONSE'][0]['LISTS'][0]['REQUEST'][0]
185                                ['ERRORS'][0]['ERROR'][0]['MESSAGE'][0]['VALUE'];
186                }
187                // multiple pages?
188                $pages = (int) $data['LISTLOOKUPRESPONSE'][0]['LISTS'][0]['LIST'][0]
189                                        ['TOTALPAGES'][0]['VALUE'];
190
191                // return items
192                $result = array_merge($result, (array)
193                              $data['LISTLOOKUPRESPONSE'][0]['LISTS'][0]['LIST'][0]['LISTITEM']);
194            }
195        }
196
197        return array($result,$params);
198    }
199
200    /**
201     * Create output
202     */
203    function render($mode, Doku_Renderer $renderer, $data) {
204        if($mode != 'xhtml') return false;
205        if(is_array($data)){
206            foreach($data[0] as $item){
207                $renderer->doc .= $this->_format($item,$data[1]);
208            }
209        }else{
210            $renderer->doc .= '<p>failed to fetch data: <code>'.hsc($data).'</code></p>';
211        }
212        return true;
213    }
214
215    /**
216     * Create a signed Request URI
217     *
218     * Original copyright notice:
219     *
220     *   Copyright (c) 2009 Ulrich Mierendorff
221     *
222     *   Permission is hereby granted, free of charge, to any person obtaining a
223     *   copy of this software and associated documentation files (the "Software"),
224     *   to deal in the Software without restriction, including without limitation
225     *   the rights to use, copy, modify, merge, publish, distribute, sublicense,
226     *   and/or sell copies of the Software, and to permit persons to whom the
227     *   Software is furnished to do so, subject to the following conditions:
228     *
229     *   The above copyright notice and this permission notice shall be included in
230     *   all copies or substantial portions of the Software.
231     *
232     * @author Ulrich Mierendorff <ulrich.mierendorff@gmx.net>
233     * @link   http://mierendo.com/software/aws_signed_query/
234     */
235    function _signedRequestURI($region, $params, $public_key, $private_key){
236        $method = "GET";
237        $host = "ecs.amazonaws.".$region;
238        $uri = "/onca/xml";
239
240        // additional parameters
241        $params["Service"] = "AWSECommerceService";
242        $params["AWSAccessKeyId"] = $public_key;
243        // GMT timestamp
244        $params["Timestamp"] = gmdate("Y-m-d\TH:i:s\Z");
245        // API version
246        $params["Version"] = "2009-11-01";
247
248        // sort the parameters
249        ksort($params);
250
251        // create the canonicalized query
252        $canonicalized_query = array();
253        foreach ($params as $param=>$value)
254        {
255            $param = str_replace("%7E", "~", rawurlencode($param));
256            $value = str_replace("%7E", "~", rawurlencode($value));
257            $canonicalized_query[] = $param."=".$value;
258        }
259        $canonicalized_query = implode("&", $canonicalized_query);
260
261        // create the string to sign
262        $string_to_sign = $method."\n".$host."\n".$uri."\n".$canonicalized_query;
263
264        // calculate HMAC with SHA256 and base64-encoding
265        if(function_exists('hash_hmac')){
266            $signature = base64_encode(hash_hmac("sha256", $string_to_sign, $private_key, true));
267        }elseif(function_exists('mhash')){
268            $signature = base64_encode(mhash(MHASH_SHA256, $string_to_sign, $private_key));
269        }else{
270            msg('missing crypto function, can\'t sign request',-1);
271        }
272
273        // encode the signature for the request
274        $signature = str_replace("%7E", "~", rawurlencode($signature));
275
276        // create request
277        return "http://".$host.$uri."?".$canonicalized_query."&Signature=".$signature;
278    }
279
280    /**
281     * Output a single item
282     */
283    function _format($item,$param){
284        if(isset($item['ITEM'])) $item = $item['ITEM'][0]; // sub item?
285        $attr = $item['ITEMATTRIBUTES'][0];
286        if(!$attr) $attr = $item['UNIVERSALLISTITEM'][0];
287        if(!$attr) return ''; // happens on list items no longer in catalogue
288
289//        dbg($item);
290//        dbg($attr);
291
292        $img = '';
293        if(!$img) $img = $item['UNIVERSALLISTITEM'][0]['IMAGEURL'][0]['VALUE'];
294        if(!$img) $img = $item['MEDIUMIMAGE'][0]['URL'][0]['VALUE'];
295        if(!$img) $img = $item['IMAGESETS'][0]['IMAGESET'][0]['MEDIUMIMAGE'][0]['URL'][0]['VALUE'];
296        if(!$img) $img = $item['LARGEIMAGE'][0]['URL'][0]['VALUE'];
297        if(!$img) $img = $item['IMAGESETS'][0]['IMAGESET'][0]['LARGEIMAGE'][0]['URL'][0]['VALUE'];
298        if(!$img) $img = $item['SMALLIMAGE'][0]['URL'][0]['VALUE'];
299        if(!$img) $img = $item['IMAGESETS'][0]['IMAGESET'][0]['SMALLIMAGE'][0]['URL'][0]['VALUE'];
300        if(!$img) $img = 'http://images.amazon.com/images/P/01.MZZZZZZZ.gif'; // transparent pixel
301
302        $img = ml($img,array('w'=>$param['imgw'],'h'=>$param['imgh']));
303
304        $link = $item['DETAILPAGEURL'][0]['VALUE'];
305        if(!$link) $link = $item['UNIVERSALLISTITEM'][0]['PRODUCTURL'][0]['VALUE'];
306
307        ob_start();
308        print '<div class="amazon">';
309        print '<a href="'.$link.'"';
310        if($conf['target']['extern']) print ' target="'.$conf['target']['extern'].'"';
311        print '>';
312        print '<img src="'.$img.'" width="'.$param['imgw'].'" height="'.$param['imgh'].'" alt="" />';
313        print '</a>';
314
315
316        print '<div class="amazon_author">';
317        if($attr['AUTHOR']){
318            $this->display($attr['AUTHOR'],$param['maxlen']);
319        }elseif($attr['DIRECTOR']){
320            $this->display($attr['DIRECTOR'],$param['maxlen']);
321        }elseif($attr['ARTIST']){
322            $this->display($attr['ARTIST'],$param['maxlen']);
323        }elseif($attr['STUDIO']){
324            $this->display($attr['STUDIO'],$param['maxlen']);
325        }elseif($attr['LABEL']){
326            $this->display($attr['LABEL'],$param['maxlen']);
327        }elseif($attr['BRAND']){
328            $this->display($attr['BRAND'],$param['maxlen']);
329        }elseif($attr['SOLDBY'][0]['VALUE']){
330            $this->display($attr['SOLDBY'][0]['VALUE'],$param['maxlen']);
331        }
332        print '</div>';
333
334        print '<div class="amazon_title">';
335        print '<a href="'.$link.'"';
336        if($conf['target']['extern']) print ' target="'.$conf['target']['extern'].'"';
337        print '>';
338        $this->display($attr['TITLE'][0]['VALUE'],$param['maxlen']);
339        print '</a>';
340        print '</div>';
341
342
343
344        print '<div class="amazon_isbn">';
345        if($attr['ISBN']){
346            print 'ISBN ';
347            $this->display($attr['ISBN'][0]['VALUE'],$param['maxlen']);
348        }elseif($attr['RUNNINGTIME']){
349            $this->display($attr['RUNNINGTIME'][0]['VALUE'].' ',$param['maxlen']);
350            $this->display($attr['RUNNINGTIME'][0]['ATTRIBUTES']['UNITS'],$param['maxlen']);
351        }elseif($attr['PLATFORM']){
352            $this->display($attr['PLATFORM'][0]['VALUE'],$param['maxlen']);
353        }
354        print '</div>';
355
356        if($param['price']){
357            $price = $item['OFFERSUMMARY'][0]['LOWESTNEWPRICE'][0]['FORMATTEDPRICE'][0]['VALUE'];
358            if(!$price) $price = $item['OFFERSUMMARY'][0]['LOWESTUSEDPRICE'][0]['FORMATTEDPRICE'][0]['VALUE'];
359            if(!$price) $price = $attr['SAVEDPRICE'][0]['FORMATTEDPRICE'][0]['VALUE'];
360            if($price){
361                print '<div class="amazon_price">'.hsc($price).'</div>';
362            }
363        }
364        print '</div>';
365        $out = ob_get_contents();
366        ob_end_clean();
367
368        return $out;
369    }
370
371    function display($input,$maxlen){
372        $string = '';
373        if(is_array($input)){
374            foreach($input as $opt){
375                if(is_array($opt) && $opt['VALUE']){
376                    $string .= $opt['VALUE'].', ';
377                }
378            }
379            $string = rtrim($string,', ');
380        }else{
381            $string = $input;
382        }
383
384        if($maxlen && utf8_strlen($string) > $maxlen){
385            print '<span title="'.htmlspecialchars($string).'">';
386            $string = utf8_substr($string,0,$maxlen - 3);
387            print htmlspecialchars($string);
388            print '&hellip;</span>';
389        }else{
390            print htmlspecialchars($string);
391        }
392    }
393
394}
395
396//Setup VIM: ex: et ts=4 enc=utf-8 :
397