1<?php 2 3use dokuwiki\HTTP\DokuHTTPClient; 4use DOMWrap\Document; 5 6/** 7 * DokuWiki Plugin amazonlight (Syntax Component) 8 * 9 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 10 * @author Andreas Gohr <andi@splitbrain.org> 11 */ 12class syntax_plugin_amazonlight extends DokuWiki_Syntax_Plugin 13{ 14 15 /** @var array what regions to use for the different countries */ 16 const REGIONS = [ 17 'us' => 'www.amazon.com', 18 'ca' => 'www.amazon.ca', 19 'de' => 'www.amazon.de', 20 'gb' => 'www.amazon.co.uk', 21 'fr' => 'www.amazon.fr', 22 'jp' => 'www.amazon.co.jp', 23 ]; 24 25 /** @inheritDoc */ 26 public function getType() 27 { 28 return 'substition'; 29 } 30 31 /** @inheritDoc */ 32 public function getPType() 33 { 34 return 'block'; 35 } 36 37 /** @inheritDoc */ 38 public function getSort() 39 { 40 return 160; 41 } 42 43 /** 44 * Connect lookup pattern to lexer. 45 * 46 * @param string $mode Parser mode 47 */ 48 public function connectTo($mode) 49 { 50 $this->Lexer->addSpecialPattern('\{\{amazon>[\w:\\- =]+\}\}', $mode, 'plugin_amazonlight'); 51 } 52 53 /** @inheritDoc */ 54 public function handle($match, $state, $pos, Doku_Handler $handler) 55 { 56 $match = substr($match, 9, -2); 57 list($ctry, $asin) = explode(':', $match, 2); 58 59 // no country given? 60 if (empty($asin)) { 61 $asin = $ctry; 62 $ctry = 'us'; 63 } 64 65 // default parameters... 66 $params = array( 67 'imgw' => $this->getConf('imgw'), 68 'imgh' => $this->getConf('imgh'), 69 'price' => $this->getConf('showprice'), 70 ); 71 // ...can be overridden 72 list($asin, $more) = explode(' ', $asin, 2); 73 $params['asin'] = $asin; 74 75 if (preg_match('/(\d+)x(\d+)/i', $more, $match)) { 76 $params['imgw'] = $match[1]; 77 $params['imgh'] = $match[2]; 78 } 79 if (preg_match('/noprice/i', $more, $match)) { 80 $params['price'] = false; 81 } elseif (preg_match('/(show)?price/i', $more, $match)) { 82 $params['price'] = true; 83 } 84 85 // correct country given? 86 if ($ctry === 'uk') $ctry = 'gb'; 87 if (!preg_match('/^(us|gb|jp|de|fr|ca)$/', $ctry)) { 88 $ctry = 'us'; 89 } 90 $params['country'] = $ctry; 91 92 return $params; 93 } 94 95 /** @inheritDoc */ 96 public function render($mode, Doku_Renderer $renderer, $data) 97 { 98 if ($mode !== 'xhtml') { 99 return false; 100 } 101 102 $html = $this->output($data); 103 if (!$html) { 104 if ($data['country'] == 'de') { 105 $renderer->interwikilink('Amazon', 'Amazon.de', 'amazon.de', $data['asin']); 106 } else { 107 $renderer->interwikilink('Amazon', 'Amazon', 'amazon', $data['asin']); 108 } 109 } 110 111 $renderer->doc .= $html; 112 113 return true; 114 } 115 116 /** 117 * @param array $param 118 * @return string 119 */ 120 protected function output($param) 121 { 122 global $conf; 123 124 try { 125 $data = $this->fetchData($param['asin'], $param['country']); 126 } catch (Exception $e) { 127 msg(hsc($e->getMessage()), -1); 128 return false; 129 } 130 131 $img = ml($data['img'], array('w' => $param['imgw'], 'h' => $param['imgh'])); 132 133 ob_start(); 134 echo '<div class="amazon">'; 135 echo '<a href="' . $data['url'] . '"'; 136 if ($conf['target']['extern']) echo ' target="' . $conf['target']['extern'] . '"'; 137 echo '>'; 138 echo '<img src="' . $img . '" width="' . $param['imgw'] . '" height="' . $param['imgh'] . '" alt="" />'; 139 echo '</a>'; 140 141 echo '<div class="amazon_title">'; 142 echo '<a href="' . $data['url'] . '"'; 143 if ($conf['target']['extern']) echo ' target="' . $conf['target']['extern'] . '"'; 144 echo '>'; 145 echo hsc($data['title']); 146 echo '</a>'; 147 echo '</div>'; 148 149 echo '<div class="amazon_author">'; 150 echo hsc($data['author']); 151 echo '</div>'; 152 153 echo '<div class="amazon_isbn">'; 154 echo hsc($data['isbn']); 155 echo '</div>'; 156 157 if ($param['price'] && $data['price']) { 158 echo '<div class="amazon_price">' . hsc($data['price']) . '</div>'; 159 } 160 echo '</div>'; 161 162 return ob_get_clean(); 163 } 164 165 /** 166 * Fetch the meta data 167 * 168 * @param string $asin 169 * @param string $country 170 * @return array 171 * @throws Exception 172 */ 173 protected function fetchData($asin, $country) 174 { 175 $partner = $this->getConf('partner_' . $country); 176 if (!$partner) $partner = 'none'; 177 $region = self::REGIONS[$country]; 178 179 $url = 'https://' . $region . '/dp/' . $asin; 180 181 $http = new DokuHTTPClient(); 182 $http->headers['User-Agent'] = 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'; 183 $html = $http->get($url); 184 if (!$html) { 185 throw new Exception('Failed to fetch data. Status ' . $http->status); 186 } 187 188 189 $doc = new Document(); 190 $doc->html($html); 191 192 $result = [ 193 'title' => $this->extract($doc, '#productTitle'), 194 'author' => $this->extract($doc, '#bylineInfo a'), 195 'rating' => $this->extract($doc, '#averageCustomerReviews span.a-declarative a > span'), 196 'price' => $this->extract($doc, '.priceToPay'), 197 'isbn' => $this->extract($doc, '#rpi-attribute-book_details-isbn10 .rpi-attribute-value'), 198 'img' => $this->extract($doc, '#imgTagWrapperId img', 'src'), 199 'url' => $url . '?tag=' . $partner, 200 ]; 201 202 if (!$result['title']) { 203 $result['title'] = $this->extract($doc, 'title'); 204 } 205 if (!$result['title']) { 206 throw new Exception('Could not find title in data'); 207 } 208 209 return $result; 210 } 211 212 /** 213 * Extract text or attribute from a selector 214 * 215 * @param Document $doc 216 * @param string $selector 217 * @param string|null $attr attribute to extract, omit for text 218 * @return string 219 */ 220 protected function extract(Document $doc, string $selector, $attr = null): string 221 { 222 $element = $doc->find($selector)->first(); 223 if($element === null) { 224 return ''; 225 } 226 if ($attr) { 227 return $element->attr($attr); 228 } else { 229 return $element->text(); 230 } 231 } 232} 233 234