<?php
// (c) Copyright 2002-2012 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: profilelib.php 39469 2012-01-12 21:13:48Z changi67 $

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 $feedback = array();

	private $objects = null;

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

	function setFeedback( $feed ) // {{{
	{
		if (is_array($feed)) {
			$this->feedback = $feed;
		} else {
			$this->feedback[] = $feed;
		}
	} // }}}
	function getFeedback( $index = null ) // {{{
	{
		if (! is_null($index) && $index < count($this->feedback) ) {
			return $this->feedback[ $index ];
		} else {
			return $this->feedback;
		}
	} // }}}

	public static function enableDeveloperMode() // {{{
	{
		self::$developerMode = true;
	} // }}}

	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 = TikiLib::lib('tiki')->httprequest($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 {
			if (self::$developerMode) {
				$url = "$domain/tiki-export_wiki_pages.php?latest=1&page=" . urlencode($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';
		$parserlib = TikiLib::lib('parser');
		
		$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']);
		$parserlib->parse_wiki_argvariable($content);
		$profile->loadYaml($content);

		return $profile;
	} // }}}

	public static function fromString( $string, $name = '' ) // {{{
	{
		$profile = new self;
		$profile->domain = 'tiki://local';
		$profile->profile = $name;
		$profile->pageUrl = $name;
		$profile->url = 'tiki://local/' . $name;

		$content = html_entity_decode($string);
		$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;
	} // }}}
	
	public function refreshYaml()
	{
		$this->objects = null;
		$this->loadYaml($this->pageContent);
	}

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

		$pos = 0;

		$this->data = array();

		$matches = WikiParser_PluginMatcher::match($content);
		$parser = new WikiParser_PluginArgumentParser;

		foreach ($matches as $match) {
			$arguments = $parser->parse($match->getArguments());
			if ( ($match->getName() == 'code' && isset($arguments['caption']) && $arguments['caption'] == 'YAML')
				|| $match->getName() == 'profile' ) {
				$yaml = $match->getBody();

				$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 findNextPluginStart($content, $pos)
	{
		preg_match('/\{CODE\(\s*caption\s*=[>]?\s*[\'"]?YAML/', substr($content, $pos), $matches);
		if (count($matches) > 0) {
			$pattern = $matches[0];
		} else {
			$pattern = '{CODE(caption=>YAML';
		}
		return strpos($content, $pattern, $pos);
	}

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

	public function getPageContent( $pageName ) // {{{
	{
		if ($this->domain == 'tiki://local') {
			global $tikilib;
			$info = $tikilib->get_page_info($pageName);
			if (empty($info)) {
				$this->setFeedback(tra('Page cannot be found').' '.$pageName);
				return null;
			}
			return $info['data'];
		}
		$exportUrl = dirname($this->url) . '/tiki-export_wiki_pages.php?'
			. http_build_query(array( 'page' => $pageName ));

		$content = TikiLib::lib('tiki')->httprequest($exportUrl);
		$content = str_replace("\r", '', $content);
		$begin = strpos($content, "\n\n");

		if ( $begin !== false ) {
			$content = substr($content, $begin + 2);
			
			// This allows compatibility with Tiki 8 and below, which export page content HTML-escaped. This should not be done for Tiki 9 and above and should be removed once only these are supported (after Tiki 6 reaches EOL).
			$content = htmlspecialchars_decode($content);
			 
			return $content;
		} else {
			return null;
		}
	} // }}}

	public function getPageParsed( $pageName ) // {{{
	{
		if ($this->domain == 'tiki://local' || strpos($this->domain, 'localhost') === 0) {
			global $tikilib;
			$info = $tikilib->get_page_info($pageName, true, true);
			if (empty($info)) {
				$this->setFeedback(tra('Page cannot be found').' '.$pageName);
				return null;
			}
			return $tikilib->parse_data($info['data']);
		}
		$pageUrl = dirname($this->url) . '/tiki-index_raw.php?'
			. http_build_query(array( 'page' => $pageName ));

		$content = TikiLib::lib('tiki')->httprequest($pageUrl);
		// index_raw replaces index.php with itself, so undo that here
		$content = str_replace('tiki-index_raw.php', 'tiki-index.php', $content);

		return $content;
	} // }}}

	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 getGroupMap() // {{{
	{
		if ( ! isset($this->data['mappings']) ) {
			return array();
		}

		return $this->data['mappings'];
	} // }}}

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

		$groups = array();
		foreach ( $this->data['permissions'] as $groupName => $data ) {
			if ( isset($groupMap[ $groupName ]) ) {
				$groupName = $groupMap[$groupName];
			}

			$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(),
				'autojoin' => 'n',
			);
			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']) ) {
						$this->setFeedback(tra('Syntax error: ').tra("Permissions' object must have a field 'type' and '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;
				} else {
					$str = '';
					foreach ($entry as $k => $v) {
						$str .= empty($str) ? '' : ', ';
						$str .= "$k: $v";
					}
					$this->setFeedback(tra('Syntax error: ').$str."\n".tra("Needs a 'type' and 'data' field"));
				}
			}

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

		// Order object creations to make sure all objects are created when needed
		// Circular dependencies get dicarded
		$counter = 0;
		$refs = array();
		while ( ! empty($objects) ) {
			// Circular dependency found... give what we have
			if ( $counter++ > count($objects) * 2 ) {
				$this->setFeedback(tra('Circular reference') . ': ' . implode(', ', array_unique($refs)));
				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 getDescription()
	{
		$str = '';
		if ($this->isWellStructured()) {
			$str .= $this->getType().' ';
			$str .= '"'.isset($this->data['data']['name']) ? $this->data['data']['name'] : tra('No name').'"';
		} else {
			$str .= tra('Bad object');
		}
		return $str;
	}
	
	function isWellStructured() // {{{
	{
		$is =  isset($this->data['type'], $this->data['data']);
		return $is;
	} // }}}

	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);
	} // }}}

	public function refreshExternals() // {{{
	{
		$this->profile->refreshYaml();
	} // }}}

	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];
	} // }}}
}
