1<?php
2# Copyright (c) 2003-2005, Jannis Hermanns (on behalf the Serendipity Developer Team)
3# All rights reserved.  See LICENSE file for licensing details
4
5if (IN_serendipity !== true) {
6    die ("Don't hack!");
7}
8
9if (defined('S9Y_FRAMEWORK_TRACKBACKS')) {
10    return;
11}
12@define('S9Y_FRAMEWORK_TRACKBACKS', true);
13
14/**
15 * Check a HTTP response if it is a valid XML trackback response
16 *
17 * @access public
18 * @param   string  HTTP Response string
19 * @return  mixed   Boolean or error message
20 */
21function serendipity_trackback_is_success($resp) {
22    if (preg_match('@<error>(\d+)</error>@', $resp, $matches)) {
23        if ((int) $matches[1] === 0) {
24            return true;
25        } else {
26            if (preg_match('@<message>([^<]+)</message>@', $resp, $matches)) {
27                return $matches[1];
28            }
29            else {
30                return 'unknown error';
31            }
32        }
33    }
34    return true;
35}
36
37/**
38 * Check a HTTP response if it is a valid XML pingback response
39 *
40 * @access public
41 * @param   string  HTTP Response string
42 * @return  mixed   Boolean or error message
43 */
44function serendipity_pingback_is_success($resp) {
45    // This is very rudimentary, but the fault is printed later, so what..
46    if (preg_match('@<fault>@', $resp, $matches)) {
47        return false;
48    }
49    return true;
50}
51
52/**
53 * Perform a HTTP query for autodiscovering a pingback URL
54 *
55 * @access public
56 * @param   string  The URL to try autodiscovery
57 * @param   string  The HTML of the source URL
58 * @param   string  The URL of our blog article
59 * @return
60 */
61function serendipity_pingback_autodiscover($loc, $body, $url=null) {
62    global $serendipity;
63
64    // This is the old way, sending pingbacks, for downward compatibility.
65    // But this is wrong, as it does link from the main blog URL instead of the article URL
66    if (!isset($url)) {
67        $url = $serendipity['baseURL'];
68    }
69
70    if (!empty($_SERVER['X-PINGBACK'])) {
71        $pingback = $_SERVER['X-PINGBACK'];
72    } elseif (preg_match('@<link rel="pingback" href="([^"]+)" ?/?>@i', $body, $matches)) {
73        $pingback = $matches[1];
74    } else {
75        echo '<div>&#8226; ' . sprintf(PINGBACK_FAILED, PINGBACK_NOT_FOUND) . '</div>';
76        return false;
77    }
78
79    // xml-rpc pingback call
80    $query = "<?xml version=\"1.0\"?>
81<methodCall>
82  <methodName>pingback.ping</methodName>
83  <params>
84    <param>
85      <value><string>$url</string></value>
86    </param>
87    <param>
88      <value><string>$loc</string></value>
89    </param>
90  </params>
91</methodCall>";
92
93    echo '<div>&#8226; ' . sprintf(PINGBACK_SENDING, serendipity_specialchars($pingback)) . '</div>';
94    flush();
95
96    $response =  _serendipity_send($pingback, $query, 'text/html');
97    $success  =   serendipity_pingback_is_success($response);
98    if ($success == true) {
99        echo '<div>&#8226; ' . PINGBACK_SENT .'</div>';
100    } else {
101        echo '<div>&#8226; ' . sprintf(PINGBACK_FAILED, $response) . '</div>';
102    }
103    return $success;
104}
105
106/**
107 * Send a track/pingback ping
108 *
109 * @access public
110 * @param   string  The URL to send a trackback to
111 * @param   string  The XML data with the trackback contents
112 * @return  string  Response
113 */
114function _serendipity_send($loc, $data, $contenttype = null) {
115    global $serendipity;
116
117    $target = parse_url($loc);
118    if ($target['query'] != '') {
119        $target['query'] = '?' . str_replace('&amp;', '&', $target['query']);
120    }
121
122    if ($target['scheme'] == 'https' && empty($target['port'])) {
123       $uri = $target['scheme'] . '://' . $target['host'] . $target['path'] . $target['query'];
124    } elseif (!is_numeric($target['port'])) {
125       $target['port'] = 80;
126       $uri = $target['scheme'] . '://' . $target['host'] . ':' . $target['port'] . $target['path'] . $target['query'];
127    }
128
129    require_once S9Y_PEAR_PATH . 'HTTP/Request2.php';
130    $options = array('follow_redirects' => true, 'max_redirects' => 5);
131    serendipity_plugin_api::hook_event('backend_http_request', $options, 'trackback_send');
132    serendipity_request_start();
133    if (version_compare(PHP_VERSION, '5.6.0', '<')) {
134        // On earlier PHP versions, the certificate validation fails. We deactivate it on them to restore the functionality we had with HTTP/Request1
135        $options['ssl_verify_peer'] = false;
136    }
137
138    $req = new HTTP_Request2($uri, HTTP_Request2::METHOD_POST, $options);
139    if (isset($contenttype)){
140       $req->setHeader('Content-Type', $contenttype);
141    }
142
143    $req->setBody($data);
144    try {
145        $res = $req->send();
146    } catch (HTTP_Request2_Exception $e) {
147        serendipity_request_end();
148        return false;
149    }
150
151
152    $fContent = $res->getBody();
153    serendipity_request_end();
154    return $fContent;
155}
156
157/**
158 * Autodiscover a trackback location URL
159 *
160 * @access public
161 * @param   string  The HTML of the source URL
162 * @param   string  The source URL
163 * @param   string  The URL of our blog
164 * @param   string  The author of our entry
165 * @param   string  The title of our entry
166 * @param   string  The text of our entry
167 * @param   string  A comparison URL
168
169 * @return string   Response
170 */
171function serendipity_trackback_autodiscover($res, $loc, $url, $author, $title, $text, $loc2 = '') {
172    $is_wp    = false;
173    $wp_check = false;
174
175    // the new detection method via rel=trackback should have priority
176    if (preg_match('@link\s*rel=["\']trackback["\'].*href=["\'](https?:[^"\']+)["\']@i', $res, $matches)) {
177        $trackURI = trim($matches[1]);
178    } else {
179        if (preg_match('@((' . preg_quote($loc, '@') . '|' . preg_quote($loc2, '@') . ')/?trackback/)@i', $res, $wp_loc)) {
180            // We found a WP-blog that may not advertise RDF-Tags!
181            $is_wp = true;
182        }
183
184        if (!preg_match('@trackback:ping(\s*rdf:resource)?\s*=\s*["\'](https?:[^"\']+)["\']@i', $res, $matches)) {
185            $matches = array();
186            serendipity_plugin_api::hook_event('backend_trackback_check', $matches, $loc);
187
188            // Plugins may say that a URI is valid, in situations where a blog has no valid RDF metadata
189            if (empty($matches[2])) {
190                if ($is_wp) {
191                    $wp_check = true;
192                } else {
193                    echo '<div>&#8226; ' . sprintf(TRACKBACK_FAILED, TRACKBACK_NOT_FOUND) . '</div>';
194                    return false;
195                }
196            }
197        }
198
199        $trackURI = trim($matches[2]);
200
201        if (preg_match('@dc:identifier\s*=\s*["\'](https?:[^\'"]+)["\']@i', $res, $test)) {
202            if ($loc != $test[1] && $loc2 != $test[1]) {
203                if ($is_wp) {
204                    $wp_check = true;
205                } else {
206                    echo '<div>&#8226; ' . sprintf(TRACKBACK_FAILED, TRACKBACK_URI_MISMATCH) . '</div>';
207                    return false;
208                }
209            }
210        }
211
212        // If $wp_check is set it means no RDF metadata was found, and we simply try the /trackback/ url.
213        if ($wp_check) {
214            $trackURI = $wp_loc[0];
215        }
216    }
217
218    $data = 'url='        . rawurlencode($url)
219          . '&title='     . rawurlencode($title)
220          . '&blog_name=' . rawurlencode($author)
221          . '&excerpt='   . rawurlencode(strip_tags($text));
222
223    printf(TRACKBACK_SENDING, serendipity_specialchars($trackURI));
224    flush();
225
226    $response = serendipity_trackback_is_success(_serendipity_send($trackURI, $data));
227
228    if ($response === true) {
229        echo '<div>&#8226; ' . TRACKBACK_SENT .'</div>';
230    } else {
231        echo '<div>&#8226; ' . sprintf(TRACKBACK_FAILED, $response) . '</div>';
232    }
233
234    return $response;
235}
236
237/**
238 * Open a URL and autodetect contained ping/trackback locations
239 *
240 * @access public
241 * @param   string  The URL to autodetect/try
242 * @param   string  The URL to our blog
243 * @param   string  The author of our entry
244 * @param   string  The title of our entry
245 * @param   string  The body of our entry
246 * @return null
247 */
248function serendipity_reference_autodiscover($loc, $url, $author, $title, $text) {
249    global $serendipity;
250    $timeout   = 30;
251
252    $u = parse_url($loc);
253
254    if ($u['scheme'] != 'http' && $u['scheme'] != 'https') {
255        return;
256    } elseif ($u['scheme'] == 'https' && !extension_loaded('openssl')) {
257        return; // Trackbacks to HTTPS URLs can only be performed with openssl activated
258    }
259
260    if (empty($u['port'])) {
261        $u['port'] = 80;
262        $port      = '';
263    } else {
264        $port      = ':' . $u['port'];
265    }
266
267    if (!empty($u['query'])) {
268        $u['path'] .= '?' . $u['query'];
269    }
270
271    $parsed_loc = $u['scheme'] . '://' . $u['host'] . $port . $u['path'];
272
273    if (preg_match('@\.(jpe?g|aiff?|gif|png|pdf|doc|rtf|wave?|mp2|mp4|mpe?g3|mpe?g4|divx|xvid|bz2|mpe?g|avi|mp3|xl?|ppt|pps|xslt?|xsd|zip|tar|t?gz|swf|rm|ram?|exe|mov|qt|midi?|qcp|emf|wmf|snd|pmg|w?bmp|gcd|mms|ogg|ogm|rv|wmv|wma|jad|3g?|jar)$@i', $u['path'])) {
274        // echo '<div>&#8226; ' . TRACKBACK_NO_DATA . '</div>';
275        return;
276    }
277
278    echo '<div>&#8226; '. sprintf(TRACKBACK_CHECKING, $loc) .'</div>';
279    flush();
280
281    require_once S9Y_PEAR_PATH . 'HTTP/Request2.php';
282    $options = array('follow_redirects' => true, 'max_redirects' => 5);
283    serendipity_plugin_api::hook_event('backend_http_request', $options, 'trackback_detect');
284    serendipity_request_start();
285    if (version_compare(PHP_VERSION, '5.6.0', '<')) {
286        // On earlier PHP versions, the certificate validation fails. We deactivate it on them to restore the functionality we had with HTTP/Request1
287        $options['ssl_verify_peer'] = false;
288    }
289    $req = new HTTP_Request2($parsed_loc, HTTP_Request2::METHOD_GET, $options);
290
291    try {
292        $res = $req->send();
293    } catch (HTTP_Request2_Exception $e) {
294        echo '<div>&#8226; ' . sprintf(TRACKBACK_COULD_NOT_CONNECT, $u['host'], $u['port']) .'</div>';
295        serendipity_request_end();
296        return;
297    }
298
299    $fContent = $res->getBody();
300    serendipity_request_end();
301
302    if (strlen($fContent) != 0) {
303        $trackback_result = serendipity_trackback_autodiscover($fContent, $parsed_loc, $url, $author, $title, $text, $loc);
304        if ($trackback_result == false) {
305            serendipity_pingback_autodiscover($parsed_loc, $fContent, $url);
306        }
307    } else {
308        echo '<div>&#8226; ' . TRACKBACK_NO_DATA . '</div>';
309    }
310    echo '<hr noshade="noshade" />';
311}
312
313/**
314 * Receive a trackback
315 *
316 * @access public
317 * @param   int     The ID of our entry
318 * @param   string  The title of the foreign blog
319 * @param   string  The URL of the foreign blog
320 * @param   string  The name of the foreign blog
321 * @param   string  The excerpt text of the foreign blog
322 * @return true
323 */
324function add_trackback($id, $title, $url, $name, $excerpt) {
325    global $serendipity;
326
327    if ($GLOBALS['tb_logging']) {
328        $fp = fopen('trackback2.log', 'a');
329        fwrite($fp, '[' . date('d.m.Y H:i') . '] add_trackback:' . print_r(func_get_args(), true) . "\n");
330        fclose($fp);
331    }
332
333    // We can't accept a trackback if we don't get any URL
334    // This is a protocol rule.
335    if (empty($url)) {
336        if ($GLOBALS['tb_logging']) {
337            $fp = fopen('trackback2.log', 'a');
338            fwrite($fp, '[' . date('d.m.Y H:i') . '] Empty URL.' . "\n");
339            fclose($fp);
340        }
341
342        return 0;
343    }
344
345    // If title is not provided, the value for url will be set as the title
346    // This is a protocol rule.
347    if (empty($title)) {
348        $title = $url;
349    }
350
351    // Decode HTML Entities
352    $excerpt = trackback_body_strip($excerpt);
353
354    if ($tb_logging) {
355        $fp = fopen('trackback2.log', 'a');
356        fwrite($fp, '[' . date('d.m.Y H:i') . '] Trackback body:' . $excerpt . "\n");
357        fclose($fp);
358    }
359
360    $comment = array(
361        'title'   => $title,
362        'url'     => $url,
363        'name'    => $name,
364        'comment' => $excerpt
365    );
366
367    $is_utf8 = strtolower(LANG_CHARSET) == 'utf-8';
368
369    if ($GLOBALS['tb_logging']) {
370        $fp = fopen('trackback2.log', 'a');
371        fwrite($fp, '[' . date('d.m.Y H:i') . '] TRACKBACK TRANSCODING CHECK' . "\n");
372    }
373
374    foreach($comment AS $idx => $field) {
375        if (is_utf8($field)) {
376            // Trackback is in UTF-8. Check if our blog also is UTF-8.
377            if (!$is_utf8) {
378                if ($GLOBALS['tb_logging']) {
379                    fwrite($fp, '[' . date('d.m.Y H:i') . '] Transcoding ' . $idx . ' from UTF-8 to ISO' . "\n");
380                }
381                $comment[$idx] = utf8_decode($field);
382            }
383        } else {
384            // Trackback is in some native format. We assume ISO-8859-1. Check if our blog is also ISO.
385            if ($is_utf8) {
386                if ($GLOBALS['tb_logging']) {
387                    fwrite($fp, '[' . date('d.m.Y H:i') . '] Transcoding ' . $idx . ' from ISO to UTF-8' . "\n");
388                }
389                $comment[$idx] = utf8_encode($field);
390            }
391        }
392    }
393
394    if ($GLOBALS['tb_logging']) {
395        fwrite($fp, '[' . date('d.m.Y H:i') . '] TRACKBACK DATA: ' . print_r($comment, true) . '...' . "\n");
396        fwrite($fp, '[' . date('d.m.Y H:i') . '] TRACKBACK STORING...' . "\n");
397        fclose($fp);
398    }
399
400    if ($id>0) {
401        // first check, if we already have this pingback
402        $comments = serendipity_fetchComments($id,1,'co.id',true,'TRACKBACK'," AND co.url='" . serendipity_db_escape_string($url) . "'");
403        if (is_array($comments) && sizeof($comments) == 1) {
404            log_pingback("We already have that TRACKBACK!");
405            return 0; // We already have it!
406        }
407        // We don't have it, so save the pingback
408        serendipity_saveComment($id, $comment, 'TRACKBACK');
409        return 1;
410    } else {
411        return 0;
412    }
413}
414
415/**
416 * Receive a pingback
417 *
418 * @access public
419 * @param   int     The entryid to receive a pingback for
420 * @param   string  The foreign postdata to add
421 * @return boolean
422 */
423function add_pingback($id, $postdata) {
424    global $serendipity;
425    log_pingback("Reached add_pingback. ID:[$id]");
426
427    // XML-RPC Method call without named parameter. This seems to be the default way using XML-RPC
428    if(preg_match('@<methodCall>\s*<methodName>\s*pingback.ping\s*</methodName>\s*<params>\s*<param>\s*<value>\s*<string>([^<]*)</string>\s*</value>\s*</param>\s*<param>\s*<value>\s*<string>([^<]*)</string>\s*</value>\s*</param>\s*</params>\s*</methodCall>@is', $postdata, $matches)) {
429        log_pingback("Pingback wp structure.");
430        $remote             = $matches[1];
431        $local              = $matches[2];
432        log_pingback("remote=$remote, local=$local");
433        $path = parse_url($remote);
434        $comment['title']   = 'PingBack';
435        $comment['url']     = $remote;
436        $comment['comment'] = '';
437        $comment['name']    = $path['host'];
438        fetchPingbackData($comment);
439
440        // if no ID parameter was given, try to get one from targetURI
441        if (!isset($id) || $id==0) {
442            log_pingback("ID not found");
443            $id = evaluateIdByLocalUrl($local);
444            log_pingback("ID set to $id");
445        }
446
447        if ($id>0) {
448            // first check, if we already have this pingback
449            $comments = serendipity_fetchComments($id,1,'co.id',true,'PINGBACK'," AND co.url='" . serendipity_db_escape_string($remote) . "'");
450            if (is_array($comments) && sizeof($comments) == 1) {
451                log_pingback("We already have that PINGBACK!");
452                return 0; // We already have it!
453            }
454            // We don't have it, so save the pingback
455            serendipity_saveComment($id, $comment, 'PINGBACK');
456            return 1;
457        } else {
458            return 0;
459        }
460    }
461
462    // XML-RPC Method call with named parameter. I'm not sure, if XML-RPC supports this, but just to be sure
463    $sourceURI = getPingbackParam('sourceURI', $postdata);
464    $targetURI = getPingbackParam('targetURI', $postdata);
465    if (isset($sourceURI) && isset($targetURI)) {
466        log_pingback("Pingback spec structure.");
467        $path = parse_url($sourceURI);
468        $local              = $targetURI;
469        $comment['title']   = 'PingBack';
470        $comment['url']     = $sourceURI;
471        $comment['comment'] = '';
472        $comment['name']    = $path['host'];
473        fetchPingbackData($comment);
474
475        // if no ID parameter was given, try to get one from targetURI
476        if (!isset($id) || $id==0) {
477            log_pingback("ID not found");
478            $id = evaluateIdByLocalUrl($local);
479            log_pingback("ID set to $id");
480        }
481        if ($id>0) {
482            serendipity_saveComment($id, $comment, 'PINGBACK');
483            return 1;
484        } else {
485            return 0;
486        }
487    }
488
489    return 0;
490}
491
492function evaluateIdByLocalUrl($localUrl) {
493    global $serendipity;
494
495    // Build an ID searchpattern in configured permaling structure:
496    $permalink_article = $serendipity['permalinkStructure'];
497    log_pingback("perma: $permalink_article");
498    $permalink_article = str_replace('.','\.',$permalink_article);
499    $permalink_article = str_replace('+','\+',$permalink_article);
500    $permalink_article = str_replace('?','\?',$permalink_article);
501    $permalink_article = str_replace('%id%','(\d+)',$permalink_article);
502    $permalink_article = str_replace('%title%','[^/]*',$permalink_article);
503    $permalink_article_regex = '@' . $permalink_article . '$@';
504    log_pingback("regex: $permalink_article_regex");
505
506    if (preg_match($permalink_article_regex, $localUrl, $matches)) {
507        return (int)$matches[1];
508    } else {
509        return 0;
510    }
511}
512
513/**
514 * Gets a XML-RPC pingback.ping value by given parameter name
515 * @access private
516 * @param string Name of the paramameter
517 * @param string Buffer containing the pingback XML
518 */
519function getPingbackParam($paramName, $data) {
520    $pattern = "<methodCall>.*?<methodName>\s*pingback.ping\s*</methodName>.*?<params>.*?<param>\s*((<name>\s*$paramName\s*</name>\s*<value>\s*<string>([^<]*)</string>\s*</value>)|(<value>\s*<string>([^<]*)</string>\s*</value>\s*<name>\s*$paramName\s*</name>))\s*</param>.*?</params>.*?</methodCall>";
521    if (preg_match('@' . $pattern .'@is',$data, $matches)) {
522        return $matches[3];
523    } else {
524        return null;
525    }
526}
527
528/**
529 * Fetches additional comment data from the page that sent the pingback
530 * @access private
531 * @param array comment array to be filled
532 */
533function fetchPingbackData(&$comment) {
534    global $serendipity;
535
536    // Don't fetch remote page, if not explicitly allowed in serendipity_config_local.php:
537    if (empty($serendipity['pingbackFetchPage'])) {
538        return;
539    }
540
541    // If we don't have a comment or a commentors url, stop it.
542    if (!isset($comment) || !is_array($comment) || !isset($comment['url'])) {
543        return;
544    }
545
546    // Max amount of characters fetched from the page doing a pingback:
547    $fetchPageMaxLength = 200;
548    if (isset($serendipity['pingbackFetchPageMaxLength'])){
549        $fetchPageMaxLength = $serendipity['pingbackFetchPageMaxLength'];
550    }
551    require_once S9Y_PEAR_PATH . 'HTTP/Request2.php';
552    $url = $comment['url'];
553
554    if (function_exists('serendipity_request_start')) serendipity_request_start();
555
556    // Request the page
557    $options = array('follow_redirects' => true, 'max_redirects' => 5, 'timeout' => 20);
558    if (version_compare(PHP_VERSION, '5.6.0', '<')) {
559        // On earlier PHP versions, the certificate validation fails. We deactivate it on them to restore the functionality we had with HTTP/Request1
560        $options['ssl_verify_peer'] = false;
561    }
562    $req = new HTTP_Request2($url, HTTP_Request2::METHOD_GET, $options);
563
564    // code 200: OK, code 30x: REDIRECTION
565    $responses = "/(200)|(30[0-9])/"; // |(30[0-9] Moved)
566    try {
567        $response = $req->send();
568        if (preg_match($responses, $response->getStatus())) {
569
570        }
571        $fContent = $response->getBody();
572
573        // Get a title
574        if (preg_match('@<head[^>]*>.*?<title[^>]*>(.*?)</title>.*?</head>@is',$fContent,$matches)) {
575            $comment['title'] = serendipity_entity_decode(strip_tags($matches[1]), ENT_COMPAT, LANG_CHARSET);
576        }
577
578        // Try to get content from first <p> tag on:
579        if (preg_match('@<p[^>]*>(.*?)</body>@is',$fContent,$matches)) {
580            $body = $matches[1];
581        }
582        if (empty($body) && preg_match('@<body[^>]*>(.*?)</body>@is',$fContent,$matches)){
583            $body = $matches[1];
584        }
585        // Get a part of the article
586        if (!empty($body)) {
587            $body = trackback_body_strip($body);
588
589            // truncate the text to 200 chars
590            $arr = str_split($body, $fetchPageMaxLength);
591            $body = $arr[0];
592
593            $comment['comment'] = $body . '[..]';
594        }
595    } catch (HTTP_Request2_Exception $e) {
596
597    }
598
599    if (function_exists('serendipity_request_end')) serendipity_request_end();
600
601}
602
603/**
604 * Strips any unneeded code from trackback / pingback bodies returning pure (UTF8) text.
605 */
606function trackback_body_strip($body){
607    // replace non breakable space with normal space:
608    $body = str_replace('&nbsp;', ' ', $body);
609
610    // strip html entities and tags.
611    $body = serendipity_entity_decode(strip_tags($body), ENT_COMPAT, LANG_CHARSET);
612
613    // replace whitespace with single space
614    $body = preg_replace('@\s+@s', ' ', $body);
615
616    return $body;
617}
618
619/**
620 * Create an excerpt for a trackback to send
621 *
622 * @access public
623 * @param   string  Input text
624 * @return  string  Output text
625 */
626function serendipity_trackback_excerpt($text) {
627    return serendipity_mb('substr', strip_tags($text), 0, 255);
628}
629
630/**
631 * Report success of a trackback
632 *
633 * @access public
634 */
635function report_trackback_success () {
636print '<?xml version="1.0" encoding="iso-8859-1"?>' . "\n";
637print <<<SUCCESS
638<response>
639    <error>0</error>
640</response>
641SUCCESS;
642}
643
644/**
645 * Report failure of a trackback
646 *
647 * @access public
648 */
649function report_trackback_failure () {
650print '<?xml version="1.0" encoding="iso-8859-1"?>' . "\n";
651print <<<FAILURE
652<response>
653    <error>1</error>
654    <message>Danger Will Robinson, trackback failed.</message>
655</response>
656FAILURE;
657}
658
659/**
660 * Return success of a pingback
661 *
662 * @access public
663 */
664function report_pingback_success () {
665print '<?xml version="1.0"?>' . "\n";
666print <<<SUCCESS
667<methodResponse>
668   <params>
669      <param>
670         <value><string>success</string></value>
671         </param>
672      </params>
673   </methodResponse>
674SUCCESS;
675}
676
677/**
678 * Return failure of a pingback
679 *
680 * @access public
681 */
682function report_pingback_failure () {
683print '<?xml version="1.0"?>' . "\n";
684print <<<FAILURE
685<methodResponse>
686    <fault>
687    <value><i4>0</i4></value>
688    </fault>
689</methodResponse>
690FAILURE;
691}
692
693/**
694 * Search through link body, and automagically send a trackback ping.
695 *
696 * This is the trackback starter function that searches your text and sees if any
697 * trackback URLs are in there
698 *
699 * @access public
700 * @param   int     The ID of our entry
701 * @param   string  The author of our entry
702 * @param   string  The title of our entry
703 * @param   string  The text of our entry
704 * @param   boolean Dry-Run, without performing trackbacks?
705 * @return
706 */
707function serendipity_handle_references($id, $author, $title, $text, $dry_run = false) {
708    global $serendipity;
709    static $old_references = array();
710    static $saved_references = array();
711    static $saved_urls = array();
712    if (is_object($serendipity['logger'])) $serendipity['logger']->debug("serendipity_handle_references");
713
714    if ($dry_run) {
715        // Store the current list of references. We might need to restore them for later user.
716        $old_references = serendipity_db_query("SELECT * FROM {$serendipity['dbPrefix']}references WHERE (type = '' OR type IS NULL) AND entry_id = " . (int)$id, false, 'assoc');
717
718        if (is_string($old_references)) {
719            if (is_object($serendipity['logger'])) $serendipity['logger']->debug($old_references);
720        }
721
722        if (is_array($old_references) && count($old_references) > 0) {
723            $current_references = array();
724            foreach($old_references AS $idx => $old_reference) {
725                // We need the current reference ID to restore it later.
726                $saved_references[$old_reference['link'] . $old_reference['name']] = $current_references[$old_reference['link'] . $old_reference['name']] = $old_reference;
727                $saved_urls[$old_reference['link']] = true;
728            }
729        }
730        if (is_object($serendipity['logger'])) $serendipity['logger']->debug("Got references in dry run: " . print_r($current_references, true));
731    } else {
732        // A dry-run was called previously and restorable references are found. Restore them now.
733        $del = serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}references WHERE (type = '' OR type IS NULL) AND entry_id = " . (int)$id);
734        if (is_string($del)) {
735            if (is_object($serendipity['logger'])) $serendipity['logger']->debug($del);
736        }
737        if (is_object($serendipity['logger'])) $serendipity['logger']->debug("Deleted references");
738
739        if (is_array($old_references) && count($old_references) > 0) {
740            $current_references = array();
741            foreach($old_references AS $idx => $old_reference) {
742                // We need the current reference ID to restore it later.
743                $current_references[$old_reference['link'] . $old_reference['name']] = $old_reference;
744                $q = serendipity_db_insert('references', $old_reference, 'show');
745                $cr = serendipity_db_query($q);
746                if (is_string($cr)) {
747                    if (is_object($serendipity['logger'])) $serendipity['logger']->debug($cr);
748                }
749            }
750        }
751
752        if (is_object($serendipity['logger'])) $serendipity['logger']->debug("Got references in final run:" . print_r($current_references, true));
753    }
754
755    if (!preg_match_all('@<a[^>]+?href\s*=\s*["\']?([^\'" >]+?)[ \'"][^>]*>(.+?)</a>@i', $text, $matches)) {
756        $matches = array(0 => array(), 1 => array());
757    } else {
758        // remove full matches
759        array_shift($matches);
760    }
761
762    // Make trackback URL
763    $url = serendipity_archiveURL($id, $title, 'baseURL');
764    // Make sure that the trackback-URL does not point to https
765    $url = str_replace('https://', 'http://', $url);
766
767    // Add URL references
768    $locations = $matches[0];
769    $names     = $matches[1];
770
771    $checked_locations = array();
772    serendipity_plugin_api::hook_event('backend_trackbacks', $locations);
773    for ($i = 0, $j = count($locations); $i < $j; ++$i) {
774        if (is_object($serendipity['logger'])) $serendipity['logger']->debug("Checking {$locations[$i]}...");
775        if ($locations[$i][0] == '/') {
776            $locations[$i] = 'http' . (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off' ? 's' : '') . '://' . $_SERVER['HTTP_HOST'] . $locations[$i];
777        }
778
779        if (isset($checked_locations[$locations[$i]])) {
780            if (is_object($serendipity['logger'])) $serendipity['logger']->debug("Already checked");
781            continue;
782        }
783
784        if (preg_match_all('@<img[^>]+?alt=["\']?([^\'">]+?)[\'"][^>]+?>@i', $names[$i], $img_alt)) {
785            if (is_array($img_alt) && is_array($img_alt[0])) {
786                foreach($img_alt[0] as $alt_idx => $alt_img) {
787                    // Replace all <img>s within a link with their respective ALT tag, so that references
788                    // can be stored with a title.
789                    $names[$i] = str_replace($alt_img, $img_alt[1][$alt_idx], $names[$i]);
790                }
791            }
792        }
793
794        $query = "SELECT COUNT(id) FROM {$serendipity['dbPrefix']}references
795                                  WHERE entry_id = ". (int)$id ."
796                                    AND link = '" . serendipity_db_escape_string($locations[$i]) . "'
797                                    AND (type = '' OR type IS NULL)";
798
799        $row = serendipity_db_query($query, true, 'num');
800        if (is_string($row)) {
801            if (is_object($serendipity['logger'])) $serendipity['logger']->debug($row);
802        }
803
804        $names[$i] = strip_tags($names[$i]);
805        if (empty($names[$i])) {
806            if (is_object($serendipity['logger'])) $serendipity['logger']->debug("Found reference $locations[$i] w/o name. Adding location as name");
807            $names[$i] = $locations[$i];
808        }
809
810        if ($row[0] > 0 && isset($saved_references[$locations[$i] . $names[$i]])) {
811            if (is_object($serendipity['logger'])) $serendipity['logger']->debug("Found references for $id, skipping rest");
812            continue;
813        }
814
815        if (!isset($serendipity['noautodiscovery']) || !$serendipity['noautodiscovery']) {
816            if (!$dry_run) {
817                if (!isset($saved_urls[$locations[$i]])){
818                    if (is_object($serendipity['logger'])) $serendipity['logger']->debug("Enabling autodiscovery");
819                    serendipity_reference_autodiscover($locations[$i], $url, $author, $title, serendipity_trackback_excerpt($text));
820                } else {
821                    if (is_object($serendipity['logger'])) $serendipity['logger']->debug("This reference was already used before in $id and therefore will not be trackbacked again");
822                }
823            } else {
824                if (is_object($serendipity['logger'])) $serendipity['logger']->debug("Dry run: Skipping autodiscovery");
825            }
826            $checked_locations[$locations[$i]] = true; // Store trackbacked link so that no further trackbacks will be sent to the same link
827        } else {
828            if (is_object($serendipity['logger'])) $serendipity['logger']->debug("Skipping full autodiscovery");
829        }
830    }
831    $del = serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}references WHERE entry_id=" . (int)$id . " AND (type = '' OR type IS NULL)");
832    if (is_string($del)) {
833        if (is_object($serendipity['logger'])) $serendipity['logger']->debug($del);
834    }
835    if (is_object($serendipity['logger'])) $serendipity['logger']->debug("Deleted references again");
836
837    if (!is_array($old_references)) {
838        $old_references = array();
839    }
840
841    $duplicate_check = array();
842    for ($i = 0; $i < $j; ++$i) {
843        $i_link     = serendipity_db_escape_string(strip_tags($names[$i]));
844        $i_location = serendipity_db_escape_string($locations[$i]);
845
846        // No link with same description AND same text should be inserted.
847        if (isset($duplicate_check[$i_location . $i_link])) {
848            continue;
849        }
850
851        if (isset($current_references[$locations[$i] . $names[$i]])) {
852            $query = "INSERT INTO {$serendipity['dbPrefix']}references (id, entry_id, name, link) VALUES(";
853            $query .= (int)$current_references[$locations[$i] . $names[$i]]['id'] . ", " . (int)$id . ", '" . $i_link . "', '" . $i_location . "')";
854            $ins = serendipity_db_query($query);
855            if (is_string($ins)) {
856                if (is_object($serendipity['logger'])) $serendipity['logger']->debug($ins);
857            }
858            $duplicate_check[$locations[$i] . $names[$i]] = true;
859        } else {
860            $query = "INSERT INTO {$serendipity['dbPrefix']}references (entry_id, name, link) VALUES(";
861            $query .= (int)$id . ", '" . $i_link . "', '" . $i_location . "')";
862            $ins = serendipity_db_query($query);
863            if (is_string($ins)) {
864                if (is_object($serendipity['logger'])) $serendipity['logger']->debug($ins);
865            }
866
867            $old_references[] = array(
868                'id'       => serendipity_db_insert_id('references', 'id'),
869                'name'     => $i_link,
870                'link'     => $i_location,
871                'entry_id' => (int)$id
872            );
873            $duplicate_check[$i_location . $i_link] = true;
874        }
875
876        if (is_object($serendipity['logger'])) $serendipity['logger']->debug("Current lookup for {$locations[$i]}{$names[$i]} is" . print_r($current_references[$locations[$i] . $names[$i]], true));
877        if (is_object($serendipity['logger'])) $serendipity['logger']->debug($query);
878    }
879
880    if (is_object($serendipity['logger'])) $serendipity['logger']->debug(print_r($old_references, true));
881
882    // Add citations
883    preg_match_all('@<cite[^>]*>([^<]+)</cite>@i', $text, $matches);
884
885    foreach ($matches[1] as $citation) {
886        $query = "INSERT INTO {$serendipity['dbPrefix']}references (entry_id, name) VALUES(";
887        $query .= (int)$id . ", '" . serendipity_db_escape_string($citation) . "')";
888
889        $cite = serendipity_db_query($query);
890        if (is_string($cite)) {
891            if (is_object($serendipity['logger'])) $serendipity['logger']->debug($cite);
892        }
893    }
894}
895
896/**
897 * Check if a string is in UTF-8 format.
898 *
899 * @access public
900 * @param   string  The string to check
901 * @return  bool
902 */
903function is_utf8($string) {
904   // From http://w3.org/International/questions/qa-forms-utf-8.html
905   return preg_match('%^(?:'
906         . '[\x09\x0A\x0D\x20-\x7E]'             # ASCII
907         . '|[\xC2-\xDF][\x80-\xBF]'             # non-overlong 2-byte
908         . '|\xE0[\xA0-\xBF][\x80-\xBF]'         # excluding overlongs
909         . '|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}'  # straight 3-byte
910         . '|\xED[\x80-\x9F][\x80-\xBF]'         # excluding surrogates
911         . '|\xF0[\x90-\xBF][\x80-\xBF]{2}'      # planes 1-3
912         . '|[\xF1-\xF3][\x80-\xBF]{3}'          # planes 4-15
913         . '|\xF4[\x80-\x8F][\x80-\xBF]{2}'      # plane 16
914         . ')*$%xs', $string);
915}
916