feedback = $feed; } else { $this->feedback[] = $feed; } } public 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 = (array) $data; foreach ($copy as &$value) { if (is_bool($value)) { $value = $value ? 'y' : 'n'; } } return $copy; } public static function getProfileKeyfor($domain, $profile) { if (strpos($domain, '://') === false) { if (\Tiki\Package\ExtensionManager::isExtensionEnabled($domain)) { $path = Tiki\Package\ExtensionManager::get($domain)->getPath(); $domain = "file://" . $path . '/profiles'; } elseif (is_dir($domain)) { $domain = "file://" . $domain; } else { $domain = "http://" . $domain; } } 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; if (strpos($object['domain'], '://') === false) { if (is_dir($object['domain'])) { $object['domain'] = "file://" . $object['domain']; } else { $object['domain'] = "http://" . $object['domain']; } } $shortdomain = substr($object['domain'], strpos($object['domain'], '://') + 3); $result = $tikilib->query( "SELECT value FROM tiki_profile_symbols WHERE (domain = ? || domain = ?) AND profile = ? AND object = ?", [ $object['domain'], $shortdomain, $object['profile'], $object['object'] ] ); if ($row = $result->fetchRow()) { return $row['value']; } return null; } public static function fromUrl($url) { $profile = new self(); $profile->transport = new Tiki_Profile_Transport_Repository($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 = trim(substr($content, $begin + 2)); if (empty($content)) { return false; } $profile->loadYaml($content); } return $profile; } public static function fromNames($domain, $profile) { if (strpos($domain, '://') === false) { if (is_dir($domain)) { $domain = "file://$domain"; } else { $domain = "http://$domain"; } } if ($domain == 'tiki://local') { return self::fromDb($profile); } elseif (strpos($domain, 'file://') === 0) { $path = substr($domain, strlen('file://')); return self::fromFile($path, $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) { $tikilib = TikiLib::lib('tiki'); $wikilib = TikiLib::lib('wiki'); $parserlib = TikiLib::lib('parser'); $profile = new self(); $profile->domain = 'tiki://local'; $profile->profile = $pageName; $profile->pageUrl = $wikilib->sefurl($pageName); $profile->transport = new Tiki_Profile_Transport_Local(); if ($info = $tikilib->get_page_info($pageName)) { $content = html_entity_decode($info['data']); $parserlib->parse_wiki_argvariable($content); $profile->loadYaml($content); return $profile; } return false; } public static function fromString($string, $name = '') { $profile = new self(); $profile->domain = 'tiki://local'; $profile->profile = $name; $profile->pageUrl = $name; $profile->transport = new Tiki_Profile_Transport_Local(); $content = html_entity_decode($string); $profile->loadYaml($content); return $profile; } public static function fromFile($path, $name) { $path = rtrim($path, '/'); $ymlPath = "$path/$name.yml"; // Make paths to the local install relative $tikiRoot = realpath(__DIR__ . '/../../../') . '/'; if (strpos($path, $tikiRoot) === 0) { $path = substr($path, strlen($tikiRoot)); } $profile = new self(); $profile->domain = "file://$path"; $profile->profile = $name; $profile->pageUrl = $name; $profile->transport = new Tiki_Profile_Transport_File($path, $name); if (file_exists($ymlPath)) { $profile->data = Yaml::parse(file_get_contents($ymlPath)); $profile->fetchExternals(); $profile->getObjects(); return $profile; } return false; } /** * Validates if the value supplied can be considered a valid reference * * @param string $value The value to test * @param bool $simpleCheck if you want to check only the name of the reference without the rest of the reference * * @return bool */ public static function isValidReference($value, $simpleCheck = false) { if ($simpleCheck) { $value = '$simple_test:' . $value; } if (preg_match(self::SHORT_PATTERN, $value, $parts)) { return true; } elseif (preg_match_all(self::LONG_PATTERN, $value, $parts, PREG_SET_ORDER)) { return true; } elseif (preg_match(self::SHORTER_PATTERN, $value, $parts)) { return true; } return false; } private function __construct() { } public 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 = []; $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']) && strtoupper($arguments['caption']) == 'YAML' ) || $match->getName() == 'profile' ) { $yaml = $match->getBody(); try { $data = Yaml::parse($yaml); } catch (Exception $e) { $this->data = [ 'error' => tr('Could not parse YAML in profile: "%0"', $e->getMessage()) ]; return; } 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(); } public function getData() { return $this->data; } public function setData($data) { $this->data = $data; } public 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); } elseif (0 === strpos($data, 'wikiparsed:')) { $pageName = substr($data, strlen('wikiparsed:')); $data = $this->getPageParsed($pageName); } } public function getPageContent($pageName) { $content = $this->transport->getPageContent($pageName); if (! $content) { $this->setFeedback(tra('Page cannot be found') . ' ' . $pageName); } return $content; } public function getProfilePath() { $path_tmp = $this->transport->getProfilePath(); if (! $path_tmp) { $this->setFeedback(tra('Profile Path cannot be found')); } return $path_tmp; } public function getPageParsed($pageName) { $content = $this->transport->getPageParsed($pageName); if (! $content) { $this->setFeedback(tra('Page cannot be found') . ' ' . $pageName); } return $content; } public 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(isset($old[$key]) ? $old[$key] : null, $value); } } return $old; } else { return $new; } } public function getNamedObjects() { if (! isset($this->data['objects'])) { return []; } $named = []; foreach ($this->data['objects'] as $object) { if (isset($object['ref'])) { $named[] = [ 'domain' => $this->domain, 'profile' => $this->profile, 'object' => trim($object['ref']) ]; } } return $named; } public function getReferences() { return $this->traverseForReferences($this->data); } public function getExternalReferences() { $out = []; foreach ($this->getReferences() as $ref) { if ($this->domain != $ref['domain'] || $this->profile != $ref['profile']) { $out[] = $ref; } } return $out; } private function traverseForReferences($value) { $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); } } elseif (preg_match(self::SHORTER_PATTERN, $value, $parts)) { $array[] = $this->convertReference($parts); } $array = array_unique($array, SORT_REGULAR); return $array; } public function containsReferences($value) // {{{ { $refs = $this->traverseForReferences($value); return count($refs) > 0; } /** * Convert references into an array key => value with domain, profile and object. * * It can use 5 parts (when referring to a profile itself) or 6 parts (when referring to an object in the profile) * * @param $parts * @return array */ public function convertReference($parts) { if (count($parts) == '5') { list($full, $null0, $null1, $domain, $profile) = $parts; $object = null; } else { list($full, $null0, $null1, $domain, $null2, $profile, $object) = $parts; } if (empty($domain)) { $domain = $this->domain; } if (empty($profile)) { $profile = $this->profile; } return [ 'domain' => $domain, 'profile' => $profile, 'object' => $object ]; } public function getRequiredInput() { return $this->traverseForRequiredInput($this->data); } public function traverseForRequiredInput($value) { $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; } public function getRequiredProfiles($recursive = false, $known = []) { $profiles = []; 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) { if (is_object($profile)) { $profiles = array_merge($profiles, $profile->getRequiredProfiles(true, $profiles)); } } } return $profiles; } public function replaceReferences(&$data, $suppliedUserData = false, $leaveUnknown = false) { if ($suppliedUserData === false) { $suppliedUserData = $this->getRequiredInput(); } if (is_array($data)) { foreach ($data as &$sub) { $this->replaceReferences($sub, $suppliedUserData, $leaveUnknown); } $toReplace = []; foreach (array_keys($data) as $key) { $newKey = $key; $this->replaceReferences($newKey, $suppliedUserData, $leaveUnknown); 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); $value = self::getObjectReference($object); if (! is_null($value) || ! $leaveUnknown) { $data = $value; } return; } $needles = []; $replacements = []; if (preg_match_all(self::LONG_PATTERN, $data, $parts, PREG_SET_ORDER)) { foreach ($parts as $row) { $object = $this->convertReference($row); $value = self::getObjectReference($object); if (! is_null($value) || ! $leaveUnknown) { $needles[] = $row[0]; $replacements[] = $value; } } } 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 = []; $replacements = []; // 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 (preg_match_all(self::PREFERENCE_PATTERN, $data, $parts, PREG_SET_ORDER)) { foreach ($parts as $row) { $preferenceName = $row[1]; $definition = TikiLib::lib('prefs')->getPreference($preferenceName); if (! empty($definition)) { $needles[] = $row[0]; $replacements[] = $definition['value']; } } } if (count($needles)) { $data = str_replace($needles, $replacements, $data); } } } public function getInstructionPage() { if (isset($this->data['instructions'])) { return $this->data['instructions']; } } public function getPreferences() { $prefs = []; if (array_key_exists('preferences', $this->data) && is_array($this->data['preferences'])) { $prefs = Tiki_Profile::convertLists($this->data['preferences'], ['enable' => 'y', 'disable' => 'n']); $prefs = Tiki_Profile::convertYesNo($prefs); } return $prefs; } /** * Return the list of packages in the profile * * @return array|mixed */ public function getPackages() { $packages = []; if (array_key_exists('package', $this->data) && is_array($this->data['package'])) { $packages = Tiki_Profile::convertLists($this->data['package'], ['enable' => 'y', 'disable' => 'n']); $packages = Tiki_Profile::convertYesNo($packages); } return $packages; } public function getGroupMap() { if (! isset($this->data['mappings'])) { return []; } return $this->data['mappings']; } public function getPermissions($groupMap = []) { if (! array_key_exists('permissions', $this->data)) { return []; } $groups = []; foreach ($this->data['permissions'] as $groupName => $data) { if (isset($groupMap[ $groupName ])) { $groupName = $groupMap[$groupName]; } $permissions = Tiki_Profile::convertLists($data, [ '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]); } } if (TikiLib::lib('user')->group_exists($groupName)) { $groupInfo = TikiLib::lib('user')->get_group_info($groupName); } else { $groupInfo = []; } $defaultInfo = [ 'description' => ! empty($groupInfo['groupDesc']) ? $groupInfo['groupDesc'] : '', 'home' => ! empty($groupInfo['groupHome']) ? $groupInfo['groupHome'] : '', 'user_tracker' => ! empty($groupInfo['usersTrackerId']) ? $groupInfo['usersTrackerId'] : 0, 'user_tracker_field' => ! empty($groupInfo['usersFieldId']) ? $groupInfo['usersFieldId'] : 0, 'group_tracker' => ! empty($groupInfo['groupTrackerId']) ? $groupInfo['groupTrackerId'] : 0, 'group_tracker_field' => ! empty($groupInfo['groupFieldId']) ? $groupInfo['groupFieldId'] : 0, 'user_signup' => ! empty($groupInfo['userChoice']) ? $groupInfo['userChoice'] : 'n', 'default_category' => ! empty($groupInfo['groupDefCat']) ? $groupInfo['groupDefCat'] : 0, 'theme' => ! empty($groupInfo['groupTheme']) ? $groupInfo['groupTheme'] : '', 'color' => ! empty($groupInfo['groupColor']) ? $groupInfo['groupColor'] : '', 'registration_fields' => ! empty($groupInfo['registrationUsersFieldIds']) ? explode(':', $groupInfo['registrationUsersFieldIds']) : [], 'is_external' => ! empty($groupInfo['isExternal']) ? $groupInfo['isExternal'] : 'n', 'expire_after' => ! empty($groupInfo['expireAfter']) ? $groupInfo['expireAfter'] : 0, 'email_pattern' => ! empty($groupInfo['emailPattern']) ? $groupInfo['emailPattern'] : '', 'anniversary' => ! empty($groupInfo['anniversary']) ? $groupInfo['anniversary'] : '', 'prorate_interval' => ! empty($groupInfo['prorateInterval']) ? $groupInfo['prorateInterval'] : '', 'include' => [], '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 = []; 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, [ '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] = [ 'permissions' => $permissions, 'objects' => $objects, 'general' => $defaultInfo, ]; } return $groups; } /** * Gets the objects that have already been loaded from the profile or have been installed, otherwise * it loads it from the profile itself. * @return array|null */ public function getLoadedObjects() { if (! is_null($this->objects)) { return $this->objects; } else { return $this->getObjects(); } } /** * Loads objects for the profile for the purpose of installation or the steps before the installation. * Should not be called after installation is complete as it will reload it from the profile causing things * like the reference IDs to be lost. * @return array|null */ public function getObjects() { // Note this function needs to be called each time the objects need to be refreshed after YAML replacements $objects = []; 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 = []; $names = []; // Order object creations to make sure all objects are created when needed // Circular dependencies get dicarded $counter = 0; $refs = []; 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; } public function removeSymbols() { global $tikilib; $tikilib->query( "DELETE FROM tiki_profile_symbols WHERE domain = ? AND profile = ?", [ $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]); } } } public function setSymbol($type, $name, $value, $named = 'y') { $symbols = TikiDb::get()->table('tiki_profile_symbols'); $symbols->insert( [ 'domain' => $this->domain, 'profile' => $this->withPrefix($this->profile), 'object' => $name, 'type' => $type, 'value' => $value, 'named' => $named, ] ); } public function getProfileKey($prefix = true) { if (! $prefix) { return self::getProfileKeyfor($this->domain, $this->profile); } else { return self::getProfileKeyfor($this->domain, $this->withPrefix($this->profile)); } } /** * Based on an objectType (eg: menu) and an objectId (eg: Id of a menu) query tiki_profile_symbols table and return domain, profile and object information * * @param string $objectType Object type * @param mixed $objectId Name or ID of object * @return array */ public static function getObjectSymbolDetails($objectType, $objectId) { $result = []; if ($objectType && $objectId) { $result = TikiDb::get()->table('tiki_profile_symbols')->fetchRow( ['domain', 'profile', 'object'], ['type' => $objectType, 'value' => $objectId] ); } return $result; } public function getPath() { $domain = $this->domain; $profile = $this->profile; if (strpos($domain, '://') === false) { if (is_dir($domain)) { $domain = "file://" . $domain; } else { $domain = "http://" . $domain; } } if (substr($domain, 0, 7) == "file://") { return TIKI_PATH . '/' . substr($domain, 7); } else { return $domain; } } /** * Validate that the values of the Named Objects that will be used as references are valid references * * @return bool */ public function validateNamedObjectsReferences() { $namedObjects = $this->getNamedObjects(); foreach ($namedObjects as $namedObject) { if (! self::isValidReference($namedObject['object'], true)) { return false; } } return true; } /** * Checks if a profile is compatible with given tiki version * * @param string $version Tiki Version * @return bool */ public function isCompatible($version) { $supported = $this->getTikiSupportedVersions(); if (empty($supported)) { return true; // Unable to determine required tiki version } return Semver::satisfies($version, $this->data['profile']['tiki']); } /** * Get tiki version supported constraint * * @return string|null */ public function getTikiSupportedVersions() { if (! isset($this->data['profile']['tiki'])) { return null; } return $this->data['profile']['tiki']; } }