<?php
require_once( 'lib/Horde/Yaml.php' );
require_once( 'lib/Horde/Yaml/Loader.php' );
require_once( 'lib/Horde/Yaml/Node.php' );
require_once( 'lib/Horde/Yaml/Exception.php' );


class Tiki_Profile
{
	const SHORT_PATTERN = '/^\$((([\w\.-]+):)?((\w+):))?(\w+)$/';
	const LONG_PATTERN = '/\$profileobject:((([\w\.-]+):)?((\w+):))?(\w+)\$/';
	const INFO_REQUEST = '/\$profilerequest:([^\$\|]+)(\|(\w+))?\$([^\$]+)\$/';

	private $url;
	private $pageUrl;
	private $domain;
	private $profile;

	public $pageContent = null;
	private $data = array();

	private $objects = null;

	private static $known = array();
	private static $resolvePrefix = null;

	public static function convertLists( $data, $conversion, $prependKey = false ) // {{{
	{
		foreach( $conversion as $key => $endValue )
		{
			if( ! isset( $data[$key] ) )
				continue;

			$data[$key] = (array) $data[$key];

			foreach( $data[$key] as $item )
			{
				if( $prependKey === true )
					$item = "{$key}_{$item}";
				elseif( ! empty( $prependKey ) )
					$item = $prependKey . $item;

				if( !isset( $data[$item] ) )
					$data[$item] = $endValue;
			}

			unset( $data[$key] );
		}

		return $data;
	} // }}}

	public static function convertYesNo( $data ) // {{{
	{
		$copy = $data;
		foreach( $copy as &$value )
			if( is_bool( $value ) )
				$value = $value ? 'y' : 'n';

		return $copy;
	} // }}}

	public static function getProfileKeyFor( $domain, $profile ) // {{{
	{
		return $domain . '/' . $profile;
	} // }}}

	public static function useUnicityPrefix( $prefix ) // {{{
	{
		self::$resolvePrefix = $prefix;
	} // }}}

	public static function withPrefix( $profile ) // {{{
	{
		if( self::$resolvePrefix )
			return self::$resolvePrefix . ':' . $profile;
		else
			return $profile;
	} // }}}

	private static function getObjectReference( $object, $full = true ) // {{{
	{
		// If a prefix was set, attempt to isolate the lookup to the prefix first
		if( $full ) {
			$withPrefix = $object;
			$withPrefix['profile'] = self::withPrefix( $withPrefix['profile'] );

			if( ! is_null( $ref = self::getObjectReference( $withPrefix, false ) ) )
				return $ref;
		}

		$serialized = Tiki_Profile_Object::serializeNamedObject( $object );

		if( ! isset( self::$known[$serialized] ) )
			self::$known[$serialized] = self::findObjectReference( $object );

		return self::$known[$serialized];
	} // }}}

	private static function findObjectReference( $object ) // {{{
	{
		global $tikilib;

		$result = $tikilib->query( "SELECT value FROM tiki_profile_symbols WHERE domain = ? AND profile = ? AND object = ?",
			array( $object['domain'], $object['profile'], $object['object'] ) );

		if( $row = $result->fetchRow() )
			return $row['value'];

		return null;
	} // }}}

	public static function fromUrl( $url ) // {{{
	{
		$profile = new self;
		$profile->url = $url;

		if( $profile->analyseMeta( $url ) ) {

			// Obtain the page export
			$content = tiki_get_remote_file( $url );
			$content = html_entity_decode( $content );
			$content = str_replace( "\r", '', $content );

			// Find content start (strip headers)
			$begin = strpos( $content, "\n\n" );
			if( ! $begin )
				return false;

			$content = substr( $content, $begin + 2 );

			$profile->loadYaml( $content );
		}

		return $profile;
	} // }}}

	public static function fromNames( $domain, $profile ) // {{{
	{
		if( strpos( $domain, '://' ) === false )
			$domain = "http://$domain";

		if( $domain == 'tiki://local' ) {
			return self::fromDb( $profile );
		} else {
			$url = "$domain/tiki-export_wiki_pages.php?page=" . urlencode( $profile );
			return self::fromUrl( $url );
		}
	} // }}}
	
	public static function fromDb( $pageName ) // {{{
	{
		global $tikilib, $wikilib;
		require_once 'lib/wiki/wikilib.php';

		$profile = new self;
		$profile->domain = 'tiki://local';
		$profile->profile = $pageName;
		$profile->pageUrl = $wikilib->sefurl($pageName);
		$profile->url = 'tiki://local/' . urlencode($pageName);

		$info = $tikilib->get_page_info( $pageName );
		$content = html_entity_decode( $info['data'] );
		$profile->loadYaml( $content );

		return $profile;
	} // }}}

	private function __construct() // {{{
	{
	} // }}}

	function __get( $name ) // {{{
	{
		switch( $name )
		{
		case 'domain':
		case 'profile':
		case 'url':
		case 'pageUrl':
			return $this->$name;
		}
	} // }}}

	private function analyseMeta( $url ) // {{{
	{
		$parts = parse_url( $url );

		if( ! isset( $parts['query'], $parts['host'], $parts['path'] ) )
			return false;

		parse_str( $parts['query'], $args );

		if( ! isset( $args['page'] ) )
			return false;

		$dir = dirname( $parts['path'] );
		$this->domain = $parts['host'] . rtrim( $dir, '/' );
		$this->profile = $args['page'];

		$this->pageUrl = dirname( $url ) . '/' . urlencode($this->profile);

		return true;
	} // }}}

	private function loadYaml( $content ) // {{{
	{
		$this->pageContent = $content;

		$pos = 0;

		$this->data = array();

		while( false !== $base = strpos( $content, '{CODE(caption=>YAML', $pos ) )
		{
			$begin = strpos( $content, ')}', $base ) + 2;
			$end = strpos( $content, '{CODE}', $base );
			$pos = $end;

			if( false === $base || false === $begin || false === $end )
				return false;

			$yaml = substr( $content, $begin, $end - $begin );

			$data = Horde_Yaml::load( $yaml );

			foreach( $data as $key => $value )
			{
				if( array_key_exists( $key, $this->data ) )
					$this->data[$key] = $this->mergeData( $this->data[$key], $value );
				else
					$this->data[$key] = $value;
			}
		}

		$this->fetchExternals();
		$this->getObjects();
	} // }}}

	private function fetchExternals() // {{{
	{
		$this->traverseForExternals( $this->data );
	} // }}}
	
	private function traverseForExternals( &$data ) // {{{
	{
		if( is_array( $data ) )
			foreach( $data as &$value )
				$this->traverseForExternals( $value );
		elseif( 0 === strpos( $data, 'wikicontent:' ) )
		{
			$pageName = substr( $data, strlen('wikicontent:') );
			$data = $this->getPageContent( $pageName );
		}
	} // }}}

	private function getPageContent( $pageName ) // {{{
	{
		$exportUrl = dirname( $this->url ) . '/tiki-export_wiki_pages.php?'
			. http_build_query( array( 'page' => $pageName ) );

		$content = tiki_get_remote_file( $exportUrl );
		$content = str_replace( "\r", '', $content );
		$begin = strpos( $content, "\n\n" );

		if( $begin !== false )
			return substr( $content, $begin + 2 );
		else
			return null;
	} // }}}

	function mergeData( $old, $new ) // {{{
	{
		if( is_array( $old ) && is_array( $new ) )
		{
			foreach( $new as $key => $value )
			{
				if( is_numeric( $key ) )
					$old[] = $value;
				else
					$old[$key] = $this->mergeData( $old[$key], $value );
			}

			return $old;
		}
		else
			return $new;
	} // }}}

	function getNamedObjects() // {{{
	{
		if( ! isset( $this->data['objects'] ) )
			return array();

		$named = array();

		foreach( $this->data['objects'] as $object )
			if( isset( $object['ref'] ) )
				$named[] = array( 'domain' => $this->domain, 'profile' => $this->profile, 'object' => $object['ref'] );

		return $named;
	} // }}}

	function getReferences() // {{{
	{
		return $this->traverseForReferences( $this->data );
	} // }}}

	function getExternalReferences() // {{{
	{
		$out = array();

		foreach( $this->getReferences() as $ref )
			if( $this->domain != $ref['domain'] || $this->profile != $ref['profile'] )
				$out[] = $ref;

		return $out;
	} // }}}

	private function traverseForReferences( $value ) // {{{
	{
		$array = array();
		if( is_array( $value ) )
			foreach( $value as $v )
				$array = array_merge( $array, $this->traverseForReferences( $v ) );
		elseif( preg_match( self::SHORT_PATTERN, $value, $parts ) )
			$array[] = $this->convertReference( $parts );
		elseif( preg_match_all( self::LONG_PATTERN, $value, $parts, PREG_SET_ORDER ) )
			foreach( $parts as $row )
				$array[] = $this->convertReference( $row );

		return $array;
	} // }}}

	function convertReference( $parts ) // {{{
	{
		list( $full, $null0, $null1, $domain, $null2, $profile, $object ) = $parts;

		if( empty( $domain ) )
			$domain = $this->domain;
		if( empty( $profile ) )
			$profile = $this->profile;

		return array( 'domain' => $domain, 'profile' => $profile, 'object' => $object );
	} // }}}

	function getRequiredInput() // {{{
	{
		return $this->traverseForRequiredInput( $this->data );
	} // }}}

	function traverseForRequiredInput( $value ) // {{{
	{
		$array = array();
		if( is_array( $value ) )
			foreach( $value as $v )
				$array = array_merge( $array, $this->traverseForRequiredInput( $v ) );
		elseif( preg_match( self::INFO_REQUEST, $value, $parts ) )
			$array[$parts[1]] = $parts[4];

		return $array;
	} // }}}

	function getRequiredProfiles( $recursive = false, $known = array() ) // {{{
	{
		$profiles = array();

		foreach( $this->getExternalReferences() as $ext )
		{
			$key = Tiki_Profile::getProfileKeyFor( $ext['domain'], $ext['profile'] );
			if( array_key_exists( $key, $known ) || array_key_exists( $key, $profiles ) )
				continue;

			$profiles[$key] = self::fromNames( $ext['domain'], $ext['profile'] );
		}

		if( $recursive )
			foreach( $profiles as $profile )
				$profiles = array_merge( $profiles, $profile->getRequiredProfiles( true, $profiles ) );

		return $profiles;
	} // }}}

	public function replaceReferences( &$data, $suppliedUserData = false ) // {{{
	{
		if( $suppliedUserData === false )
			$suppliedUserData = $this->getRequiredInput();

		if( is_array( $data ) ) {
			foreach( $data as &$sub )
				$this->replaceReferences( $sub, $suppliedUserData );

			$toReplace = array();
			foreach( array_keys( $data ) as $key ) {
				$newKey = $key;
				$this->replaceReferences( $newKey, $suppliedUserData );
				if( $newKey != $key )
					$toReplace[$key] = $newKey;
			}

			foreach( $toReplace as $old => $new ) {
				$data[$new] = $data[$old];
				unset( $data[$old] );
			}
		}
		else
		{
			if( preg_match( self::SHORT_PATTERN, $data, $parts ) )
			{
				$object = $this->convertReference( $parts );
				$data = self::getObjectReference( $object );
				return;
			}

			$needles = array();
			$replacements = array();

			if( preg_match_all( self::LONG_PATTERN, $data, $parts, PREG_SET_ORDER ) )
				foreach( $parts as $row )
				{
					$object = $this->convertReference( $row );

					$needles[] = $row[0];
					$replacements[] = self::getObjectReference( $object );
				}

			if( preg_match_all( self::INFO_REQUEST, $data, $parts, PREG_SET_ORDER ) )
				foreach( $parts as $row )
				{
					list( $full, $label, $junk, $filter, $default ) = $row;

					if( ! array_key_exists( $label, $suppliedUserData ) )
						$value = $default;
					else
						$value = $suppliedUserData[$label];

					if( $filter )
						$value = TikiFilter::get($filter)->filter($value);
					else
						$value = TikiFilter::get('xss')->filter($value);

					if( empty($value) )
						$value = $default;

					$needles[] = $full;
					$replacements[] = $value;
				}
			
			if( count( $needles ) )
				$data = str_replace( $needles, $replacements, $data );

			$needles = array();
			$replacements = array();

			// Replace date formats D(...) to unix timestamps
			if( preg_match_all( "/D\\(([^\\)]+)\\)/", $data, $parts, PREG_SET_ORDER ) )
				foreach( $parts as $row )
				{
					list( $full, $date ) = $row;

					if( false !== $conv = strtotime( $date ) )
					{
						$needles[] = $full;
						$replacements = $conv;
					}
				}

			if( count( $needles ) )
				$data = str_replace( $needles, $replacements, $data );
		}
	} // }}}

	function getInstructionPage() // {{{
	{
		if( isset( $this->data['instructions'] ) ) {
			return $this->data['instructions'];
		}
	} // }}}

	function getPreferences() // {{{
	{
		$prefs = array();

		if( array_key_exists( 'preferences', $this->data ) )
		{
			$prefs = Tiki_Profile::convertLists( $this->data['preferences'], array(
				'enable' => 'y', 
				'disable' => 'n'
			) );

			$prefs = Tiki_Profile::convertYesNo( $prefs );
		}

		return $prefs;
	} // }}}

	function getPermissions() // {{{
	{
		if( ! array_key_exists( 'permissions', $this->data ) )
			return array();

		$groups = array();
		foreach( $this->data['permissions'] as $groupName => $data )
		{
			$permissions = Tiki_Profile::convertLists( $data, array( 'allow' => 'y', 'deny' => 'n' ), 'tiki_p_' );
			$permissions = Tiki_Profile::convertYesNo( $permissions );
			foreach( array_keys( $permissions ) as $key )
				if( strpos( $key, 'tiki_p_' ) !== 0 )
					unset( $permissions[$key] );

			$defaultInfo = array(
				'description' => '',
				'home' => '',
				'user_tracker' => 0,
				'user_tracker_field' => 0,
				'group_tracker' => 0,
				'group_tracker_field' => 0,
				'user_signup' => 'n',
				'default_category' => 0,
				'theme' => '',
				'registration_fields' => array(),
				'include' => array(),
			);
			foreach( $defaultInfo as $key => $value )
				if( array_key_exists( $key, $data ) )
				{
					if( is_array( $value ) )
						$defaultInfo[$key] = (array) $data[$key];
					else
						$defaultInfo[$key] = $data[$key];
				}

			$objects = array();
			if( isset( $data['objects'] ) )
				foreach( $data['objects'] as $o )
				{
					if( !isset($o['type'], $o['id']) )
						continue;

					$perms = Tiki_Profile::convertLists( $o, array( 'allow' => 'y', 'deny' => 'n' ), 'tiki_p_' );
					$perms = Tiki_Profile::convertYesNo( $perms );

					foreach( array_keys( $perms ) as $key )
						if( strpos( $key, 'tiki_p_' ) !== 0 )
							unset( $perms[$key] );

					$o['permissions'] = $perms;
					$objects[] = $o;
				}

			$groups[$groupName] = array(
				'permissions' => $permissions,
				'objects' => $objects,
				'general' => $defaultInfo,
			);
		}

		return $groups;
	} // }}}

	function getObjects() // {{{
	{
		if( !is_null( $this->objects ) )
			return $this->objects;

		$objects = array();

		if( array_key_exists( 'objects', $this->data ) )
			foreach( $this->data['objects'] as &$entry )
			{
				$o = new Tiki_Profile_Object( $entry, $this );
				if( $o->isWellStructured() )
					$objects[] = $o;
			}

		$classified = array();
		$names = array();

		// Order object creations to make sure all objects are created when needed
		// Circular dependencies get dicarded
		$counter = 0;
		while( ! empty( $objects ) )
		{
			// Circular dependency found... give what we have
			if( $counter++ > count($objects) * 2 )
				break;

			$object = array_shift( $objects );
			$refs = $object->getInternalReferences();
			$refs = array_diff( $refs, $names );
			if( empty( $refs ) )
			{
				$counter = 0;
				$classified[] = $object;
				if( $object->getRef() )
					$names[] = $object->getRef();
			}
			else
				$objects[] = $object;
		}

		$this->objects = $classified;
		return $this->objects;
	} // }}}

	function removeSymbols() // {{{
	{
		global $tikilib;
		$tikilib->query( "DELETE FROM tiki_profile_symbols WHERE domain = ? AND profile = ?",
			array( $this->domain, self::withPrefix($this->profile) ) );

		$key = self::getProfileKeyFor( $this->domain, self::withPrefix($this->profile) );
		foreach( array_keys(self::$known) as $obj )
			if( strpos( $obj, $key ) === 0 )
				unset(self::$known[$obj]);
	} // }}}

	function getProfileKey() // {{{
	{
		return self::getProfileKeyFor( $this->domain, $this->withPrefix( $this->profile ) );
	} // }}}
}

class Tiki_Profile_Object
{
	private $data;
	private $profile;
	private $id = false;

	private $references = null;

	public static function serializeNamedObject( $object ) // {{{
	{
		return sprintf( "%s#%s", Tiki_Profile::getProfileKeyFor($object['domain'], $object['profile']), $object['object'] );
	} // }}}

	public static function getNamedObjects() // {{{
	{
		global $tikilib;
	
		$objects = array();

		$result = $tikilib->query( "SELECT domain, profile, object FROM tiki_profile_symbols WHERE named = 'y'" );
		while( $row = $result->fetchRow() )
			$objects[] = $row;

		return $objects;
	} // }}}
	
	function __construct( &$data, Tiki_Profile $profile ) // {{{
	{
		$this->data = &$data;
		$this->profile = $profile;
	} // }}}

	function isWellStructured() // {{{
	{
		return isset( $this->data['type'], $this->data['data'] );
	} // }}}

	function getType() // {{{
	{
		return $this->data['type'];
	} // }}}

	function getRef() // {{{
	{
		if( array_key_exists( 'ref', $this->data ) )
			return $this->data['ref'];
	} // }}}

	function getValue() // {{{
	{
		return $this->id;
	} // }}}

	function setValue( $value ) // {{{
	{
		global $tikilib;
		$this->id = $value;

		$named = 'y';
		if( ! $name = $this->getRef() )
		{
			$name = uniqid();
			$named = 'n';
		}

		$tikilib->query( "INSERT INTO tiki_profile_symbols (domain, profile, object, type, value, named) VALUES(?, ?, ?, ?, ?, ?)", 
			array( $this->profile->domain, $this->profile->withPrefix($this->profile->profile), $name, $this->getType(), $this->id, $named ) );
	} // }}}

	function getInternalReferences() // {{{
	{
		if( !is_null( $this->references ) )
			return $this->references;

		$this->references = $this->traverseForReferences( $this->data );
		return $this->references;
	} // }}}

	function getData() // {{{
	{
		if( array_key_exists( 'data', $this->data ) )
			return $this->data['data'];

		return array();
	} // }}}

	public function replaceReferences( &$data, $suppliedUserData = false ) // {{{
	{
		$this->profile->replaceReferences( $data, $suppliedUserData );
	} // }}}

	private function traverseForReferences( $value ) // {{{
	{
		$array = array();
		if( is_array( $value ) )
			foreach( $value as $v )
				$array = array_merge( $array, $this->traverseForReferences( $v ) );
		elseif( preg_match( Tiki_Profile::SHORT_PATTERN, $value, $parts ) )
		{
			$ref = $this->profile->convertReference( $parts );
			if( $this->profile->domain == $ref['domain']
				&& $this->profile->profile == $ref['profile'] )
				$array[] = $ref['object'];
		}
		elseif( preg_match_all( Tiki_Profile::LONG_PATTERN, $value, $parts, PREG_SET_ORDER ) )
		{
			foreach( $parts as $row )
			{
				$ref = $this->profile->convertReference( $row );
				if( $this->profile->domain == $ref['domain']
					&& $this->profile->profile == $ref['profile'] )
					$array[] = $ref['object'];
			}
		}

		return $array;
	} // }}}

	function getProfile() // {{{
	{
		return $this->profile;
	} // }}}

	function __get( $name ) // {{{
	{
		if( array_key_exists( $name, $this->data['data'] ) )
			return $this->data['data'][$name];
	} // }}}
}

?>
