xref: /plugin/amazonlight/syntax.php (revision 38539ccd682f85599dbcb8a235827aba0053a3a6)
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