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 '…</span>'; 389 }else{ 390 print htmlspecialchars($string); 391 } 392 } 393 394} 395 396//Setup VIM: ex: et ts=4 enc=utf-8 : 397