1<?php
2/**
3 * DokuWiki Plugin yql (Syntax Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Michael Hamann <michael@content-space.de>
7 */
8
9// must be run within Dokuwiki
10if (!defined('DOKU_INC')) die();
11
12if (!defined('DOKU_LF')) define('DOKU_LF', "\n");
13if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t");
14if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
15
16/**
17 * The YQL syntax plugin
18 */
19class syntax_plugin_yql extends DokuWiki_Syntax_Plugin {
20    /**
21     * Syntax Type
22     * @return string The type
23     */
24    public function getType() {
25        return 'substition';
26    }
27
28    /**
29     * Paragraph Type
30     *
31     * Defines how this syntax is handled regarding paragraphs:
32     * 'block'  - Open paragraphs need to be closed before plugin output
33     * @return string The paragraph type
34     * @see Doku_Handler_Block
35     */
36    public function getPType() {
37        return 'block';
38    }
39
40    /**
41     * @return int The sort order
42     */
43    public function getSort() {
44        return 120;
45    }
46
47
48    /**
49     * Connect the plugin to the parser modes
50     *
51     * @param string $mode The current mode
52     */
53    public function connectTo($mode) {
54        $this->Lexer->addSpecialPattern('<YQL.*?>.*?<\/YQL>',$mode,'plugin_yql');
55    }
56
57    /**
58     * Handler to prepare matched data for the rendering process
59     *
60     * @param   string       $match   The text matched by the patterns
61     * @param   int          $state   The lexer state for the match
62     * @param   int          $pos     The character position of the matched text
63     * @param   Doku_Handler $handler Reference to the Doku_Handler object
64     * @return  array The data that shall be passed to render()
65     */
66    public function handle($match, $state, $pos, Doku_Handler $handler){
67        $data = array();
68        preg_match('/<YQL ?(.*)>(.*)<\/YQL>/ms', $match, $components);
69
70        if ($components[1]) { // parse parameters
71            preg_match_all('/\s*(\S+)="([^"]*)"\s*/', $components[1], $params, PREG_SET_ORDER);
72            foreach ($params as $param) {
73                array_shift($param);
74                list($key, $value) = $param;
75                switch ($key) {
76                case 'refresh':
77                    $data['refresh'] = (int)$value;
78                    break;
79                case 'format':
80                    $parts = explode('%%', $value);
81                    foreach ($parts as $pos => $part) {
82                        if ($pos % 2 == 0) { // the start and every second part is pure character data
83                            $data['format'][] = $part;
84                        } else { // this is the stuff inside %% %%
85                            if (strpos($part, '|') !== FALSE) { // is this a link?
86                                list($link, $title) = explode('|', $part, 2);
87                                $data['format'][] = array($link => $title);
88                            } else { // if not just store the name, we'll recognize that again because of the position
89                                $data['format'][] = $part;
90                            }
91                        }
92                    }
93                    break;
94                case 'item_name':
95                    $data['item_name'] = $value;
96                    break;
97                }
98            }
99        }
100
101        $data['query'] = $components[2];
102        // set default values
103        if (!isset($data['refresh'])) $data['refresh'] = 14400;
104        if (!isset($data['format'])) $data['format'] = array('', array('link' => 'title'), '');
105        if (!isset($data['item_name'])) $data['item_name'] = 'item';
106
107        return $data;
108    }
109
110    /**
111     * Handles the actual output creation.
112     *
113     * @param   $mode     string        output format being rendered
114     * @param   $renderer Doku_Renderer reference to the current renderer object
115     * @param   $data     array         data created by handler()
116     * @return  boolean                 rendered correctly?
117     */
118    public function render($mode, Doku_Renderer $renderer, $data) {
119        $refresh = $data['refresh'];
120        $format  = $data['format'];
121        $item_name = $data['item_name'];
122        $query   = $data['query'];
123
124        // Don't fetch the data for rendering metadata
125        // But still do it for all other modes in order to support different renderers
126        if ($mode == 'metadata') {
127            /** @var $renderer Doku_Renderer_metadata */
128            $renderer->meta['date']['valid']['age'] =
129                isset($renderer->meta['date']['valid']['age']) ?
130                    min($renderer->meta['date']['valid']['age'],$refresh) :
131                    $refresh;
132            return true;
133        }
134
135        // execute the YQL query
136
137        $yql_base_url = "http://query.yahooapis.com/v1/public/yql";
138        $yql_query_url = $yql_base_url . "?q=" . urlencode($query);
139        $yql_query_url .= "&format=json";
140        $client = new DokuHTTPClient();
141        $result = $client->sendRequest($yql_query_url);
142
143        if ($result === false) {
144            $this->render_error($renderer, 'YQL: Error: the request to the server failed: '.$client->error);
145            return true;
146        }
147
148        $json_parser = new JSON();
149        $json_result = $json_parser->decode($client->resp_body);
150
151        // catch YQL errors
152        if (isset($json_result->error)) {
153            $this->render_error($renderer, 'YQL: YQL Error: '.$json_result->error->description);
154            return true;
155        }
156
157        if (is_null($json_result->query->results)) {
158            $this->render_error($renderer, 'YQL: Unknown error: there is neither an error nor results in the YQL result.');
159            return true;
160        }
161
162        if (!isset($json_result->query->results->$item_name)) {
163            $this->render_error($renderer, 'YQL: Error: The item name '.$item_name.' doesn\'t exist in the results');
164            return true;
165        }
166
167        $renderer->listu_open();
168        foreach ($json_result->query->results->$item_name as $item) {
169            $renderer->listitem_open(1);
170            $renderer->listcontent_open();
171            foreach ($format as $pos => $val) {
172                if ($pos % 2 == 0) { // outside %% %%, just character data
173                    $renderer->cdata($val);
174                } else { // inside %% %%, either links or other fields
175                    if (is_array($val)) { // arrays are links
176                        foreach ($val as $link => $title) {
177                            // check if there is a link at all and if the title isn't an instance of stdClass (can't be casted to string)
178                            if (!isset($item->$link)) {
179                                $this->render_error($renderer, 'YQL: Error: The given attribute '.$link.' doesn\'t exist');
180                                continue;
181                            }
182
183                            if (!isset($item->$title)) {
184                                $this->render_error($renderer, 'YQL: Error: The given attribute '.$title.' doesn\'t exist');
185                                continue;
186                            }
187
188                            if ($item->$title instanceof stdClass) {
189                                $this->render_error($renderer, 'YQL: Error: The given attribute '.$title.' is not a simple string but an object');
190                                continue;
191                            }
192
193                            // links can be objects, then they should have an attribute "href" which contains the actual url
194                            if ($item->$link instanceof stdClass && !isset($item->$link->href)) {
195                                $this->render_error($renderer, 'YQL: Error: The given attribute '.$link.' is not a simple string but also doesn\'t have a href attribute as link objects have.');
196                                continue;
197                            }
198
199                            if ($item->$link instanceof stdClass) {
200                                $renderer->externallink($item->$link->href, (string)$item->$title);
201                            } else {
202                                $renderer->externallink($item->$link, (string)$item->$title);
203                            }
204                        }
205                    } else { // just a field
206                        // test if the value really exists and if isn't a stdClass (can't be casted to string)
207                        if (!isset($item->$val)) {
208                            $this->render_error($renderer, 'YQL: Error: The given attribute '.$val.' doesn\'t exist');
209                            continue;
210                        }
211
212                        if ($item->$val instanceof stdClass) {
213                            $this->render_error($renderer, 'YQL: Error: The given attribute '.$val.' is not a simple string but an object');
214                            continue;
215                        }
216
217                        $renderer->cdata((string)$item->$val);
218                    }
219                }
220            }
221            $renderer->listcontent_close();
222            $renderer->listitem_close();
223        }
224        $renderer->listu_close();
225
226        return true;
227    }
228
229    /**
230     * Helper function for displaying error messages. Currently just adds a paragraph with emphasis and the error message in it
231     */
232    private function render_error(Doku_Renderer $renderer, $error) {
233        $renderer->p_open();
234        $renderer->emphasis_open();
235        $renderer->cdata($error);
236        $renderer->emphasis_close();
237        $renderer->p_close();
238    }
239}
240
241// vim:ts=4:sw=4:et:
242