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