1<?php
2
3/**
4 * Send component of the DokuWiki Linkback action plugin.
5 *
6 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author     Gina Haeussge <osd@foosel.net>
8 * @link       http://wiki.foosel.net/snippets/dokuwiki/linkback
9 */
10
11use dokuwiki\Form\Form;
12use IXR\Client\Client;
13
14require_once (DOKU_PLUGIN . 'linkback/http.php');
15
16class action_plugin_linkback_send extends DokuWiki_Action_Plugin {
17
18    var $preact;
19
20    /**
21     * Register the eventhandlers.
22     */
23    function register(Doku_Event_Handler $controller) {
24        $controller->register_hook('HTML_EDITFORM_OUTPUT', 'BEFORE', $this, 'handle_editform_output', array ());
25        $controller->register_hook('FORM_EDIT_OUTPUT', 'BEFORE', $this, 'handle_editform_output', array ());
26        $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_action_act_preprocess_before', array());
27        $controller->register_hook('ACTION_ACT_PREPROCESS', 'AFTER', $this, 'handle_action_act_preprocess_after', array());
28    }
29
30    /**
31     * Handler for the ACTION_ACT_PREPROCESS event and BEFORE advise.
32     *
33     * Saves current action.
34     */
35    function handle_action_act_preprocess_before(Doku_Event $event, $params) {
36        if (is_array($event->data))
37            list($this->preact) = array_keys($event->data);
38        else
39            $this->preact = $event->data;
40    }
41
42    /**
43     * Handler for the ACTION_ACT_PREPROCESS event and AFTER advise.
44     *
45     * Sends linkback if previous action was 'save' and new one is show.
46     */
47    function handle_action_act_preprocess_after(Doku_Event $event, $params) {
48        global $ID;
49        global $conf;
50
51        // only perform linkbacks on save of a wikipage
52        if ($this->preact != 'save' || $event->data != 'show')
53            return;
54
55        // if guests are not allowed to perform linkbacks, return
56        if (!$this->getConf('allow_guests') && !$_SERVER['REMOTE_USER'])
57            return;
58
59        // get linkback meta file name
60        $file = metaFN($ID, '.linkbacks');
61        $data = array (
62            'send' => false,
63            'receive' => false,
64            'display' => false,
65            'sentpings' => array (),
66            'receivedpings' => array (),
67            'number' => 0,
68
69        );
70        if (@ file_exists($file)) {
71            $data = unserialize(io_readFile($file, false));
72        }
73        $data['send'] = (bool)$_REQUEST['plugin__linkback_toggle'];
74
75        if (!$data['send'])
76            return;
77
78        $meta = p_get_metadata($ID);
79
80        // prepare linkback info
81        $linkback_info = array ();
82        $linkback_info['title'] = tpl_pagetitle($ID, true);
83        $linkback_info['url'] = wl($ID, '', true);
84        $linkback_info['blog_name'] = $conf['title'];
85        $linkback_info['excerpt'] = $meta['description']['abstract'];
86
87        // get links
88        $ilist = p_cached_instructions(wikiFN($ID),false,$ID);
89        if (!is_array($ilist))
90            return;
91        $pages = $this->_parse_instructionlist($ilist);
92
93        $sentpings = array ();
94        foreach ($pages as $page) {
95            if (!$data['sentpings'][$page]) {
96                // try to ping pages not already pinged
97                $this->_ping_page($page, $linkback_info);
98            }
99            $sentpings[$page] = true;
100        }
101        $data['sentpings'] = $sentpings;
102
103        // save sent ping info
104        io_saveFile($file, serialize($data));
105    }
106
107    /**
108     * Parses a given instruction list and extracts external and -- if configured
109     * that way -- internal links.
110     *
111     * @param array $list    instruction list as generated by the DokuWiki parser
112     */
113    function _parse_instructionlist($list) {
114        $pages = array ();
115
116        foreach ($list as $item) {
117            if ($item[0] == 'externallink') {
118                $pages[] = $item[1][0];
119            } else
120                if ($item[0] == 'internallink' && $this->getConf('ping_internal')) {
121                    $pages[] = wl($item[1][0], '', true);
122                }
123        }
124
125        return $pages;
126    }
127
128    /**
129     * Handles HTML_EDITFORM_OUTPUT event.
130     */
131    function handle_editform_output(Doku_Event $event, $params) {
132        global $ID;
133        global $ACT;
134        global $INFO;
135
136        // Not in edit mode? Quit
137        if ($ACT != 'edit' && $ACT != 'preview')
138            return;
139
140        // page not writable? Quit
141        if (!$INFO['writable'])
142            return;
143
144        // if guests are not allowed to perform linkbacks, return
145        if (!$this->getConf('allow_guests') && !$_SERVER['REMOTE_USER'])
146            return;
147
148        // get linkback meta file name
149        $file = metaFN($ID, '.linkbacks');
150        $data = array (
151            'send' => false,
152            'receive' => false,
153            'display' => false,
154            'sentpings' => array (),
155            'receivedpings' => array (),
156            'number' => 0,
157
158        );
159        if (@ file_exists($file)) {
160            $data = unserialize(io_readFile($file, false));
161        } else {
162            $namespace_conf = $this->getConf('enabled_namespaces');
163            if ($namespace_conf == '*') {
164                $data['send'] = true;
165            } else {
166                $namespaces = explode(',', $namespace_conf);
167                $ns = getNS($ID);
168                foreach($namespaces as $namespace) {
169                    if ($namespace == '') {
170                        continue;
171                    } else if ($namespace == '*') {
172                        $data['send'] = true;
173                        break;
174                    } else if (strstr($ns, $namespace) === $ns) {
175                        $data['send'] = true;
176                        break;
177                    }
178                }
179            }
180        }
181
182        /** @var Form|Doku_Form $form */
183        $form = $event->data;
184        if(is_a($form, Form::class)) {
185            /** @var Form $pos */
186            $pos = $form->findPositionByAttribute('id','wiki__editbar');
187
188            $form->addTagOpen('div', $pos);
189
190            //value of an unchecked checkbox is not submitted, set an extra hidden different value to fix prefilling
191            $form->setHiddenField('plugin__linkback_toggle','');
192            $checkbox = $form->addCheckbox('plugin__linkback_toggle', $this->getLang('linkback_enabledisable'), $pos + 1)
193                ->id('plugin__linkback_toggle')
194                ->addClass('edit')
195                ->val('1');
196            if($data['send']) {
197                $checkbox->attr('checked', 'checked');
198            }
199
200            $form->addTagClose('div', $pos + 2);
201
202        } else {
203            $pos = $form->findElementById('wiki__editbar');
204            $form->insertElement($pos, form_makeOpenTag('div', array('id'=>'plugin__linkback_wrapper')));
205            $form->insertElement($pos + 1, form_makeCheckboxField('plugin__linkback_toggle', '1', $this->getLang('linkback_enabledisable'), 'plugin__linkback_toggle', 'edit', (($data['send']) ? array('checked' => 'checked') : array())));
206            $form->insertElement($pos + 2, form_makeCloseTag('div'));
207        }
208    }
209
210    /**
211     * Pings a given page with the given info.
212     */
213    function _ping_page($page, $linkback_info) {
214        $range = $this->getConf('range') * 1024;
215
216        $http_client = new LinkbackHTTPClient();
217        $http_client->headers['Range'] = 'bytes=0-' . $range;
218        $http_client->max_bodysize = $range;
219        $http_client->max_bodysize_limit = true;
220
221        $data = $http_client->get($page, true);
222        if (!$data)
223            return false;
224
225        $order = explode(',', $this->getConf('order'));
226        foreach ($order as $type) {
227            if ($this->_ping_page_linkback(trim($type), $page, $http_client->resp_headers, $data, $linkback_info))
228                return true;
229        }
230
231        return false;
232    }
233
234    /**
235     * Discovers and executes the actual linkback of given type
236     *
237     * @param string $type type of linkback to send, can be "pingback" or "trackback"
238     * @param string $page URL of the page to ping
239     * @param array $headers headers received from page
240     * @param string $body first range bytes of the pages body
241     * @param array $linkback_info linkback info
242     */
243    function _ping_page_linkback($type, $page, $headers, $body, $linkback_info) {
244        if (!$this->getConf('enable_' . $type)) {
245            return false;
246        }
247
248        switch ($type) {
249            case 'trackback' :
250                {
251                    $pingurl = $this->_autodiscover_trackback($page, $body);
252                    if (!$pingurl) {
253                        return false;
254                    }
255                    return (bool) $this->_ping_page_trackback($pingurl, $linkback_info);
256                }
257            case 'pingback' :
258                {
259                    $xmlrpc_server = $this->_autodiscover_pingback($headers, $body);
260                    if (!$xmlrpc_server) {
261                        return false;
262                    }
263                    return (bool) $this->_ping_page_pingback($xmlrpc_server, $linkback_info['url'], $page);
264                }
265        }
266        return false;
267    }
268
269    /**
270     * Sends a Pingback to the given url, using the supplied data.
271     *
272     * @param string $xmlrpc_server URL of remote XML-RPC server
273     * @param string $source_url URL from which to ping
274     * @param string $target_url URL to ping
275     */
276    function _ping_page_pingback($xmlrpc_server, $source_url, $target_url) {
277        $client = new Client($xmlrpc_server);
278        return $client->query('pingback.ping', $source_url, $target_url);
279    }
280
281    /**
282     * Sends a Trackback to the given url, using the supplied data.
283     *
284     * @param string $pingurl URL to ping
285     * @param array $linkback_info Hash containing title, url and blog_name of linking post
286     * @return bool
287     */
288    function _ping_page_trackback($pingurl, $linkback_info) {
289        $http_client = new \dokuwiki\HTTP\DokuHTTPClient();
290        $success = $http_client->post($pingurl, $linkback_info);
291        return ($success !== false);
292    }
293
294    /**
295     * Autodiscovers a pingback URL in the given HTTP headers and body.
296     *
297     * @param array $headers the headers received from to be pinged page.
298     * @param string $data the body received from the pinged page.
299     * @return false|string
300     */
301    function _autodiscover_pingback(array $headers, string $data) {
302        if (isset ($headers['X-Pingback'])) {
303            return $headers['X-Pingback'];
304        }
305        $regex = '!<link rel="pingback" href="([^"]+)" ?/?>!';
306        if (!preg_match($regex, $data, $match)) {
307            return false;
308        }
309        return $match[1];
310    }
311
312    /**
313     * Autodiscovers a trackback URL for the given page URL and site body.
314     *
315     * @param string $page the url of the page to be pinged.
316     * @param string $data the body received from the page to be pinged.
317     */
318    function _autodiscover_trackback($page, $data) {
319        $page_anchorless = substr($page, 0, strrpos($page, '#'));
320
321        $regex = '!<rdf:RDF.*?</rdf:RDF>!is';
322        if (preg_match_all($regex, $data, $matches)) {
323            foreach ($matches[0] as $rdf) {
324                if (!preg_match('!dc:identifier="([^"]+)"!is', $rdf, $match_id))
325                    continue;
326                $perm_link = $match_id[1];
327                if (!($perm_link == $page || $perm_link == $page_anchorless))
328                    continue;
329                if (!(preg_match('!trackback:ping="([^"]+)"!is', $rdf, $match_plink) || preg_match('!about="([^"]+)"!is', $rdf, $match_plink)))
330                    continue;
331                return $match_plink[1];
332            }
333        } else {
334            // fix for wordpress
335            $regex = '!<a href="([^"]*?)" rel="trackback">!is';
336            if (preg_match($regex, $data, $match))
337                return $match[1];
338        }
339        return false;
340    }
341
342}
343