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>• ' . 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>• ' . 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>• ' . PINGBACK_SENT .'</div>'; 100 } else { 101 echo '<div>• ' . 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('&', '&', $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>• ' . 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>• ' . 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>• ' . TRACKBACK_SENT .'</div>'; 230 } else { 231 echo '<div>• ' . 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>• ' . TRACKBACK_NO_DATA . '</div>'; 275 return; 276 } 277 278 echo '<div>• '. 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>• ' . 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>• ' . 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(' ', ' ', $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