1<?php 2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project 3// 4// All Rights Reserved. See copyright.txt for details and a complete list of authors. 5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. 6// $Id$ 7 8use Symfony\Component\Yaml\Yaml; 9 10/** 11 * 12 */ 13interface OIntegrate_Converter 14{ 15 /** 16 * @param $content 17 * @return mixed 18 */ 19 function convert($content); 20} 21 22/** 23 * 24 */ 25interface OIntegrate_Engine 26{ 27 /** 28 * @param $data 29 * @param $templateFile 30 * @return mixed 31 */ 32 function process($data, $templateFile); 33} 34 35/** 36 * 37 */ 38class OIntegrate 39{ 40 private $schemaVersion = []; 41 private $acceptTemplates = []; 42 43 /** 44 * @param $name 45 * @param $engineOutput 46 * @return OIntegrate_Engine_JavaScript|OIntegrate_Engine_Smarty|OIntegrate_Engine_Index 47 */ 48 public static function getEngine($name, $engineOutput) // {{{ 49 { 50 switch ($name) { 51 case 'javascript': 52 return new OIntegrate_Engine_JavaScript; 53 case 'smarty': 54 return new OIntegrate_Engine_Smarty($engineOutput == 'tikiwiki'); 55 case 'index': 56 return new OIntegrate_Engine_Index; 57 } 58 } // }}} 59 60 /** 61 * @param $from 62 * @param $to 63 * @return OIntegrate_Converter_Direct|OIntegrate_Converter_EncodeHtml|OIntegrate_Converter_HtmlToTiki|OIntegrate_Converter_TikiToHtml|OIntegrate_Converter_Indexer 64 */ 65 public static function getConverter($from, $to) // {{{ 66 { 67 switch ($from) { 68 case 'html': 69 if ($to == 'tikiwiki') { 70 return new OIntegrate_Converter_HtmlToTiki; 71 } elseif ($to == 'html') { 72 return new OIntegrate_Converter_Direct; 73 } 74 break; 75 case 'tikiwiki': 76 if ($to == 'html') { 77 return new OIntegrate_Converter_TikiToHtml; 78 } elseif ($to == 'tikiwiki') { 79 return new OIntegrate_Converter_EncodeHtml; 80 } 81 break; 82 case 'index': 83 case 'mindex': 84 if ($to == 'index') { 85 return new OIntegrate_Converter_Indexer; 86 } elseif ($to == 'html') { 87 return new OIntegrate_Converter_Indexer('html'); 88 } elseif ($to == 'tikiwiki') { 89 return new OIntegrate_Converter_Indexer('tikiwiki'); 90 } 91 break; 92 } 93 } // }}} 94 95 /** 96 * @param string $url 97 * @param string $postBody url or json encoded post parameters 98 * @param bool $clearCache 99 * @return OIntegrate_Response 100 */ 101 function performRequest($url, $postBody = null, $clearCache = false) // {{{ 102 { 103 $cachelib = TikiLib::lib('cache'); 104 $tikilib = TikiLib::lib('tiki'); 105 106 $cacheKey = $url . $postBody; 107 108 if ($cache = $cachelib->getSerialized($cacheKey)) { 109 if (time() < $cache['expires'] && ! $clearCache) { 110 return $cache['data']; 111 } 112 113 $cachelib->invalidate($cacheKey); 114 } 115 116 $client = $tikilib->get_http_client($url); 117 $method = null; 118 119 if (empty($postBody)) { 120 $method = 'GET'; 121 $http_headers = [ 122 'Accept' => 'application/json,text/x-yaml', 123 'OIntegrate-Version' => '1.0', 124 ]; 125 } else { 126 $method = 'POST'; 127 if (json_decode($postBody)) { // autodetect if the content type should be json 128 $requestContentType = 'application/json'; 129 } else { 130 $requestContentType = 'application/x-www-form-urlencoded'; 131 } 132 $http_headers = [ 133 'Accept' => 'application/json,text/x-yaml', 134 'OIntegrate-Version' => '1.0', 135 'Content-Type' => $requestContentType, 136 ]; 137 $client->setRawBody($postBody); 138 } 139 140 if (count($this->schemaVersion)) { 141 $http_headers['OIntegrate-SchemaVersion'] = implode(', ', $this->schemaVersion); 142 } 143 if (count($this->acceptTemplates)) { 144 $http_headers['OIntegrate-AcceptTemplate'] = implode(', ', $this->acceptTemplates); 145 } 146 147 // merge with existing headers 148 $headers = $client->getRequest()->getHeaders(); 149 $http_headers = array_merge($headers->toArray(), $http_headers); 150 151 $client->setHeaders($http_headers); 152 153 $client->setMethod($method); 154 $httpResponse = $client->send(); 155 $content = $httpResponse->getBody(); 156 157 $requestContentType = $httpResponse->getHeaders()->get('Content-Type'); 158 $cacheControl = $httpResponse->getHeaders()->get('Cache-Control'); 159 160 $response = new OIntegrate_Response; 161 $response->contentType = $requestContentType; 162 $response->cacheControl = $cacheControl; 163 if ($requestContentType) { 164 $mediaType = $requestContentType->getMediaType(); 165 } else { 166 $mediaType = ''; 167 } 168 $response->data = $this->unserialize($mediaType, $content); 169 170 $filter = new DeclFilter; 171 $filter->addCatchAllFilter('xss'); 172 173 $response->data = $filter->filter($response->data); 174 $response->version = $httpResponse->getHeaders()->get('OIntegrate-Version'); 175 $response->schemaVersion = $httpResponse->getHeaders()->get('OIntegrate-SchemaVersion'); 176 if (! $response->schemaVersion && isset($response->data->_version)) { 177 $response->schemaVersion = $response->data->_version; 178 } 179 $response->schemaDocumentation = $httpResponse->getHeaders()->get('OIntegrate-SchemaDocumentation'); 180 181 global $prefs; 182 if (empty($cacheControl)) { 183 $maxage = 0; 184 $nocache = false; 185 } else { 186 // Respect cache duration and no-cache asked for 187 $maxage = $cacheControl->getDirective('max-age'); 188 $nocache = $cacheControl->getDirective('no-cache'); 189 } 190 if ($maxage) { 191 $expiry = time() + $maxage; 192 193 $cachelib->cacheItem( 194 $cacheKey, 195 serialize(['expires' => $expiry, 'data' => $response]) 196 ); 197 // Unless service specifies not to cache result, apply a default cache 198 } elseif (empty($nocache) && $prefs['webservice_consume_defaultcache'] > 0) { 199 $expiry = time() + $prefs['webservice_consume_defaultcache']; 200 201 $cachelib->cacheItem($cacheKey, serialize(['expires' => $expiry, 'data' => $response])); 202 } 203 204 return $response; 205 } // }}} 206 207 /** 208 * @param string $type 209 * @param string $data 210 * @return array|mixed|null 211 */ 212 function unserialize($type, $data) // {{{ 213 { 214 215 if (empty($data)) { 216 return null; 217 } 218 219 switch ($type) { 220 case 'application/json': 221 case 'text/javascript': 222 if ($out = json_decode($data, true)) { 223 return $out; 224 } 225 226 // Handle invalid JSON too... 227 $fixed = preg_replace('/(\w+):/', '"$1":', $data); 228 $out = json_decode($fixed, true); 229 return $out; 230 case 'text/x-yaml': 231 return Yaml::parse($data); 232 default: 233 // Attempt anything... 234 if ($out = $this->unserialize('application/json', $data)) { 235 return $out; 236 } 237 if ($out = $this->unserialize('text/x-yaml', $data)) { 238 return $out; 239 } 240 } 241 } // }}} 242 243 /** 244 * @param $version 245 */ 246 function addSchemaVersion($version) // {{{ 247 { 248 $this->schemaVersion[] = $version; 249 } // }}} 250 251 /** 252 * @param $engine 253 * @param $output 254 */ 255 function addAcceptTemplate($engine, $output) // {{{ 256 { 257 $this->acceptTemplate[] = "$engine/$output"; 258 } // }}} 259} 260 261/** 262 * 263 */ 264class OIntegrate_Response 265{ 266 public $version = null; 267 public $schemaVersion = null; 268 public $schemaDocumentation = null; 269 public $contentType = null; 270 public $cacheControl = null; 271 public $data; 272 public $errors = []; 273 274 /** 275 * @param $data 276 * @param $schemaVersion 277 * @param int $cacheLength 278 * @return OIntegrate_Response 279 */ 280 public static function create($data, $schemaVersion, $cacheLength = 300) // {{{ 281 { 282 $response = new self; 283 $response->version = '1.0'; 284 $response->data = $data; 285 $response->schemaVersion = $schemaVersion; 286 287 if ($cacheLength > 0) { 288 $response->cacheControl = "max-age=$cacheLength"; 289 } else { 290 $response->cacheControl = "no-cache"; 291 } 292 293 return $response; 294 } // }}} 295 296 /** 297 * @param $engine 298 * @param $output 299 * @param $templateLocation 300 */ 301 function addTemplate($engine, $output, $templateLocation) // {{{ 302 { 303 if (! array_key_exists('_template', $this->data)) { 304 $this->data['_template'] = []; 305 } 306 if (! array_key_exists($engine, $this->data['_template'])) { 307 $this->data['_template'][$engine] = []; 308 } 309 if (! array_key_exists($output, $this->data['_template'][$engine])) { 310 $this->data['_template'][$engine][$output] = []; 311 } 312 313 if (0 !== strpos($templateLocation, 'http')) { 314 $host = $_SERVER['HTTP_HOST']; 315 $proto = 'http'; 316 $path = dirname($_SERVER['SCRIPT_NAME']); 317 $templateLocation = ltrim($templateLocation, '/'); 318 319 $templateLocation = "$proto://$host$path/$templateLocation"; 320 } 321 322 $this->data['_template'][$engine][$output][] = $templateLocation; 323 } // }}} 324 325 function send() // {{{ 326 { 327 header('OIntegrate-Version: 1.0'); 328 header('OIntegrate-SchemaVersion: ' . $this->schemaVersion); 329 if ($this->schemaDocumentation) { 330 header('OIntegrate-SchemaDocumentation: ' . $this->schemaDocumentation); 331 } 332 header('Cache-Control: ' . $this->cacheControl); 333 334 $data = $this->data; 335 $data['_version'] = $this->schemaVersion; 336 337 $access = TikiLib::lib('access'); 338 $access->output_serialized($data); 339 exit; 340 } // }}} 341 342 /** 343 * @param $engine 344 * @param $engineOutput 345 * @param $outputContext 346 * @param $templateFile 347 * @return mixed|string 348 */ 349 function render($engine, $engineOutput, $outputContext, $templateFile) // {{{ 350 { 351 $engine = OIntegrate::getEngine($engine, $engineOutput); 352 if (! $engine) { 353 $this->errors = [ 1000, tr('Engine "%0" not found.', $engineOutput) ]; 354 return false; 355 } 356 357 if (! $output = OIntegrate::getConverter($engineOutput, $outputContext)) { 358 $this->errors = [ 1001, tr('Output converter "%0" not found.', $outputContext) ]; 359 return false; 360 } 361 362 $raw = $engine->process($this->data, $templateFile); 363 return $output->convert($raw); 364 } // }}} 365 366 /** 367 * @param null $supportedPairs 368 * @return array 369 */ 370 function getTemplates($supportedPairs = null) // {{{ 371 { 372 if (! is_array($this->data) || ! isset($this->data['_template']) || ! is_array($this->data['_template'])) { 373 return []; 374 } 375 376 $templates = []; 377 378 foreach ($this->data['_template'] as $engine => $outputs) { 379 foreach ($outputs as $output => $files) { 380 if (is_array($supportedPairs) && ! in_array("$engine/$output", $supportedPairs)) { 381 continue; 382 } 383 384 $files = (array) $files; 385 386 foreach ($files as $file) { 387 $content = TikiLib::lib('tiki')->httprequest($file); 388 389 $templates[] = [ 390 'engine' => $engine, 391 'output' => $output, 392 'content' => $content, 393 ]; 394 } 395 } 396 } 397 398 return $templates; 399 } // }}} 400} 401 402/** 403 * 404 */ 405class OIntegrate_Engine_JavaScript implements OIntegrate_Engine // {{{ 406{ 407 /** 408 * @param $data 409 * @param $templateFile 410 * @return string 411 */ 412 function process($data, $templateFile) 413 { 414 $json = json_encode($data); 415 416 return <<<EOC 417<script type="text/javascript"> 418var response = $json; 419</script> 420EOC 421 . file_get_contents($templateFile); 422 } 423} // }}} 424 425/** 426 * 427 */ 428class OIntegrate_Engine_Smarty implements OIntegrate_Engine // {{{ 429{ 430 private $changeDelimiters; 431 432 /** 433 * @param bool $changeDelimiters 434 */ 435 function __construct($changeDelimiters = false) 436 { 437 $this->changeDelimiters = $changeDelimiters; 438 } 439 440 /** 441 * @param $data 442 * @param $templateFile 443 * @return mixed 444 */ 445 function process($data, $templateFile) 446 { 447 /** @var Smarty_Tiki $smarty */ 448 $smarty = new Smarty_Tiki; 449 $smarty->setTemplateDir(dirname($templateFile)); 450 451 if ($this->changeDelimiters) { 452 $smarty->left_delimiter = '{{'; 453 $smarty->right_delimiter = '}}'; 454 } 455 456 $smarty->assign('response', $data); 457 return $smarty->fetch($templateFile); 458 } 459} // }}} 460 461/** 462 * Engine to pass on raw data and mapping info from the template 463 */ 464class OIntegrate_Engine_Index implements OIntegrate_Engine 465{ 466 /** 467 * @param array $data 468 * @param string $templateFile 469 * @return array 470 */ 471 function process($data, $templateFile) 472 { 473 $mappingString = file_get_contents($templateFile); 474 $mapping = json_decode($mappingString, true); 475 476 return [ 477 'data' => $data, 478 'mapping' => $mapping, 479 ]; 480 } 481} 482 483 484/** 485 * 486 */ 487class OIntegrate_Converter_Direct implements OIntegrate_Converter // {{{ 488{ 489 /** 490 * @param $content 491 * @return mixed 492 */ 493 function convert($content) 494 { 495 return $content; 496 } 497} // }}} 498 499/** 500 * 501 */ 502class OIntegrate_Converter_EncodeHtml implements OIntegrate_Converter // {{{ 503{ 504 /** 505 * @param $content 506 * @return string 507 */ 508 function convert($content) 509 { 510 return htmlentities($content, ENT_QUOTES, 'UTF-8'); 511 } 512} // }}} 513 514/** 515 * 516 */ 517class OIntegrate_Converter_HtmlToTiki implements OIntegrate_Converter // {{{ 518{ 519 /** 520 * @param $content 521 * @return string 522 */ 523 function convert($content) 524 { 525 return '~np~' . $content . '~/np~'; 526 } 527} // }}} 528 529/** 530 * 531 */ 532class OIntegrate_Converter_TikiToHtml implements OIntegrate_Converter // {{{ 533{ 534 /** 535 * @param $content 536 * @return mixed|string 537 */ 538 function convert($content) 539 { 540 return TikiLib::lib('parser')->parse_data(htmlentities($content, ENT_QUOTES, 'UTF-8')); 541 } 542} // }}} 543 544/** 545 * Attempt to index the result from the request 546 */ 547class OIntegrate_Converter_Indexer implements OIntegrate_Converter 548{ 549 private $format; 550 551 function __construct($format = 'none') 552 { 553 $this->format = $format; 554 } 555 556 /** 557 * @param $content 558 * @return mixed|string 559 */ 560 function convert($content) 561 { 562 if ($this->format === 'html' || $this->format === 'tikiwiki') { 563 if (! empty($_REQUEST['nt_name'])) { // preview from admin/webservice page 564 $source = new Search_ContentSource_WebserviceSource(); 565 $factory = new Search_Type_Factory_Direct(); 566 567 if ($_REQUEST['nt_output'] === 'mindex') { 568 $documents = $source->getDocuments(); 569 $data = []; 570 $count = 0; 571 foreach ($documents as $document) { 572 if (strpos($document, $_REQUEST['nt_name']) === 0) { 573 $data[$document] = $source->getDocument($document, $factory); 574 $count++; 575 if ($count > 100) { // enough for a preview? 576 break; 577 } 578 } 579 } 580 } else { 581 $data = $source->getDocument($_REQUEST['nt_name'], $factory); 582 } 583 584 $output = '<h3>' . tr('Parsed Data') . '</h3>'; 585 $output .= '<pre style="max-height: 40em; overflow: auto; white-space: pre-wrap">'; 586 $output .= htmlentities( 587 print_r($data, true), 588 ENT_QUOTES, 589 'UTF-8' 590 ); 591 } else { 592 $output = '<h3>' . tr('Data') . '</h3>'; 593 $output .= '<pre style="max-height: 20em; overflow: auto; white-space: pre-wrap">'; 594 $output .= htmlentities( 595 json_encode($content['data'], JSON_PRETTY_PRINT), 596 ENT_QUOTES, 597 'UTF-8' 598 ); 599 $output .= '</pre>'; 600 601 if ($this->format === 'html') { 602 $output .= '<h3>' . tr('Mapping') . '</h3>'; 603 $output .= '<pre style="max-height: 20em; overflow: auto; white-space: pre-wrap">'; 604 $output .= htmlentities( 605 json_encode($content['mapping'], JSON_PRETTY_PRINT), 606 ENT_QUOTES, 607 'UTF-8' 608 ); 609 $output .= '</pre>'; 610 } else { // wiki mode from plugin 611 $output = "~np~{$output}~/np~"; 612 } 613 } 614 615 return $output; 616 } else { 617 return $content; 618 } 619 } 620} 621