<?php
// (c) Copyright 2002-2011 by authors of the Tiki Wiki CMS Groupware Project
// 
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id: ointegratelib.php 37840 2011-10-01 10:23:51Z changi67 $

class OIntegrate
{
	private $schemaVersion = array();
	private $acceptTemplates = array();

	public static function getEngine( $name, $engineOutput ) // {{{
	{
		switch( $name )
		{
		case 'javascript':
			return new OIntegrate_Engine_JavaScript;
		case 'smarty':
			return new OIntegrate_Engine_Smarty( $engineOutput == 'tikiwiki' );
		}
	} // }}}

	public static function getConverter( $from, $to ) // {{{
	{
		switch( $from )
		{
		case 'html':
			if ( $to == 'tikiwiki' )
				return new OIntegrate_Converter_HtmlToTiki;
			elseif ( $to == 'html' )
				return new OIntegrate_Converter_Direct;
			break;	
		case 'tikiwiki':
			if ( $to == 'html' )
				return new OIntegrate_Converter_TikiToHtml;
			elseif ( $to == 'tikiwiki' )
				return new OIntegrate_Converter_EncodeHtml;
		}
	} // }}}

	function performRequest( $url, $postBody = null ) // {{{
	{
		$cachelib = TikiLib::lib('cache');
		$tikilib = TikiLib::lib('tiki');

		if ( $cache = $cachelib->getSerialized( $url.$postBody )) {
			if ( time() < $cache['expires'] )
				return $cache['data'];

			$cachelib->invalidate( $url.$postBody );
		}

		$client = $tikilib->get_http_client( $url );
		$method = null;

		if ( empty($postBody) ) {
			$method = 'GET';
			$client->setHeaders(array(
				'Accept' => 'application/json,text/x-yaml',
				'OIntegrate-Version' => '1.0',
			));
		} else {
			$client->setHeaders(array(
				'Accept' => 'application/json,text/x-yaml',
				'OIntegrate-Version' => '1.0',
			));
			$client->setRawData( $postBody, 'application/x-www-form-urlencoded' );
		}

		if ( count( $this->schemaVersion ) ) {
			$client->setHeaders( 'OIntegrate-SchemaVersion', implode( ', ', $this->schemaVersion ) );
		}
		if ( count( $this->acceptTemplates ) ) {
			$client->setHeaders( 'OIntegrate-AcceptTemplate', implode( ', ', $this->acceptTemplates ) );
		}

		$httpResponse = $client->request($method);
		$content = $httpResponse->getBody();

		$contentType = $httpResponse->getHeader('Content-Type');
		$cacheControl = $httpResponse->getHeader('Cache-Control');

		$response = new OIntegrate_Response;
		$response->contentType = $contentType;
		$response->cacheControl = $cacheControl;
		$response->data = $this->unserialize( $contentType, $content );

		$filter = new DeclFilter;
		$filter->addCatchAllFilter( 'xss' );

		$response->data = $filter->filter( $response->data );
		$response->version = $httpResponse->getHeader('OIntegrate-Version');
		$response->schemaVersion = $httpResponse->getHeader('OIntegrate-SchemaVersion');
		if ( ! $response->schemaVersion && isset( $response->data->_version ) )
			$response->schemaVersion = $response->data->_version;
		$response->schemaDocumentation = $httpResponse->getHeader('OIntegrate-SchemaDocumentation');

		global $prefs;
		// Respect cache duration asked for
		if ( preg_match( '/max-age=(\d+)/', $cacheControl, $parts ) ) {
			$expiry = time() + $parts[1];

			$cachelib->cacheItem( $url, serialize( array(
				'expires' => $expiry,
				'data' => $response,
			) ) );
		// Unless service specifies not to cache result, apply a default cache
		} elseif ( false !== strpos( $cacheControl, 'no-cache' ) && $prefs['webservice_consume_defaultcache'] > 0 ) {
			$expiry = time() + $prefs['webservice_consume_defaultcache'];

			$cachelib->cacheItem( $url, serialize( array(
				'expires' => $expiry,
				'data' => $response,
			) ) );
		}

		return $response;
	} // }}}

	function unserialize( $type, $data ) // {{{
	{
		$parts = explode( ';', $type );
		$type = trim($parts[0]);

		if ( empty($data) ) {
			return null;
		}

		switch( $type )
		{
		case 'application/json':
		case 'text/javascript':
			if ( $out = json_decode( $data, true ) )
				return $out;

			// Handle invalid JSON too...
			$fixed = preg_replace( '/(\w+):/', '"$1":', $data );
			$out = json_decode( $fixed, true );
			return $out;
		case 'text/x-yaml':
			require_once 'Horde/Yaml.php';
			require_once 'Horde/Yaml/Loader.php';
			require_once 'Horde/Yaml/Node.php';
			return Horde_Yaml::load( $data );
		default:
			// Attempt anything...
			if ( $out = $this->unserialize( 'application/json', $data ) )
				return $out;
			if ( $out = $this->unserialize( 'text/x-yaml', $data ) )
				return $out;
		}
	} // }}}

	function addSchemaVersion( $version ) // {{{
	{
		$this->schemaVersion[] = $version;
	} // }}}

	function addAcceptTemplate( $engine, $output ) // {{{
	{
		$this->acceptTemplate[] = "$engine/$output";
	} // }}}
}

class OIntegrate_Response
{
	public $version = null;
	public $schemaVersion = null;
	public $schemaDocumentation = null;
	public $contentType = null;
	public $cacheControl = null;
	public $data;

	private $errors = array();

	public static function create( $data, $schemaVersion, $cacheLength = 300 ) // {{{
	{
		$response = new self;
		$response->version = '1.0';
		$response->data = $data;
		$response->schemaVersion = $schemaVersion;

		if ( $cacheLength > 0 )
			$response->cacheControl = "max-age=$cacheLength";
		else
			$response->cacheControl = "no-cache";

		return $response;
	} // }}}

	function addTemplate( $engine, $output, $templateLocation ) // {{{
	{
		if ( ! array_key_exists( '_template', $this->data ) )
			$this->data['_template'] = array();
		if ( ! array_key_exists( $engine, $this->data['_template'] ) )
			$this->data['_template'][$engine] = array();
		if ( ! array_key_exists( $output, $this->data['_template'][$engine] ) )
			$this->data['_template'][$engine][$output] = array();

		if ( 0 !== strpos( $templateLocation, 'http' ) ) {
			$host = $_SERVER['HTTP_HOST'];
			$proto = 'http';
			$path = dirname( $_SERVER['SCRIPT_NAME'] );
			$templateLocation = ltrim( $templateLocation, '/' );

			$templateLocation = "$proto://$host$path/$templateLocation";
		}

		$this->data['_template'][$engine][$output][] = $templateLocation;
	} // }}}

	function send() // {{{
	{
		header( 'OIntegrate-Version: 1.0' );
		header( 'OIntegrate-SchemaVersion: ' . $this->schemaVersion );
		if ( $this->schemaDocumentation )
			header( 'OIntegrate-SchemaDocumentation: ' . $this->schemaDocumentation );
		header( 'Cache-Control: ' . $this->cacheControl );

		$data = $this->data;
		$data['_version'] = $this->schemaVersion;

		global $access;
		$access->output_serialized( $data );
		exit;
	} // }}}

	function render( $engine, $engineOutput, $outputContext, $templateFile ) // {{{
	{
		$engine = OIntegrate::getEngine( $engine, $engineOutput );
		if ( ! $output = OIntegrate::getConverter( $engineOutput, $outputContext ) ) {
			$this->errors = array( 1001, 'Output converter not found.' );
			return;
		}

		if ( ! $engine ) {
			$this->errors = array( 1000, 'Engine not found' );
			return;
		}

		$raw = $engine->process( $this->data, $templateFile );
		return $output->convert( $raw );
	} // }}}

	function getTemplates( $supportedPairs = null ) // {{{
	{
		if ( !is_array( $this->data ) || ! isset( $this->data['_template'] ) || ! is_array( $this->data['_template'] ) )
			return array();

		$templates = array();

		foreach( $this->data['_template'] as $engine => $outputs ) {
			foreach( $outputs as $output => $files ) {
				if ( is_array( $supportedPairs ) && ! in_array( "$engine/$output", $supportedPairs ) )
					continue;

				$files = (array) $files;

				foreach( $files as $file ) {
					$content = TikiLib::lib('tiki')->httprequest( $file );

					$templates[] = array(
						'engine' => $engine,
						'output' => $output,
						'content' => $content,
					);
				}
			}
		}

		return $templates;
	} // }}}
}

interface OIntegrate_Converter
{
	function convert( $content );
}

interface OIntegrate_Engine
{
	function process( $data, $templateFile );
}

class OIntegrate_Engine_JavaScript implements OIntegrate_Engine // {{{
{
	function process( $data, $templateFile )
	{
		$json = json_encode( $data );

		return <<<EOC
<script type="text/javascript">
var response = $json;
</script>
EOC
		. file_get_contents( $templateFile );
	}
} // }}}

class OIntegrate_Engine_Smarty implements OIntegrate_Engine // {{{
{
	private $changeDelimiters;

	function __construct( $changeDelimiters = false )
	{
		$this->changeDelimiters = $changeDelimiters;
	}

	function process( $data, $templateFile )
	{
		$smarty = new Smarty;
		$smarty->security = true;
		$smarty->template_dir = dirname($templateFile);
		$smarty->plugins_dir = array();

		if ( $this->changeDelimiters ) {
			$smarty->left_delimiter = '{{';
			$smarty->right_delimiter = '}}';
		}

		$smarty->assign( 'response', $data );
		return $smarty->fetch( $templateFile );
	}
} // }}}

class OIntegrate_Converter_Direct implements OIntegrate_Converter // {{{
{
	function convert( $content )
	{
		return $content;
	}
} // }}}

class OIntegrate_Converter_EncodeHtml implements OIntegrate_Converter // {{{
{
	function convert( $content )
	{
		return htmlentities( $content, ENT_QUOTES, 'UTF-8' );
	}
} // }}}

class OIntegrate_Converter_HtmlToTiki implements OIntegrate_Converter // {{{
{
	function convert( $content )
	{
		return '~np~' . $content . '~/np~';
	}
} // }}}

class OIntegrate_Converter_TikiToHtml implements OIntegrate_Converter // {{{
{
	function convert( $content )
	{
		global $tikilib;
		return $tikilib->parse_data( htmlentities( $content, ENT_QUOTES, 'UTF-8' ) );
	}
} // }}}
