//TODO remove the os-templates javascript if all the templates are rendered on the server (saves many Kb's in gadget size) class TemplateParser { /** * @var array */ private $dataContext; /** * @var TemplateLibrary */ private $templateLibrary; /** * dumps a node to stdout * * @param DOMnode $node * @param string $function */ public function dumpNode($node, $function) { $doc = new \DOMDocument(null, 'utf-8'); $doc->preserveWhiteSpace = true; $doc->formatOutput = false; $doc->strictErrorChecking = false; $doc->recover = false; $doc->resolveExternals = false; if (! $newNode = @$doc->importNode($node, false)) { echo "[Invalid node, dump failed]

"; return; } $doc->appendChild($newNode); echo "$function (" . get_class($node) . "):
" . htmlentities(str_replace('', '', $doc->saveXML()) . "\n") . "

"; } /** * Processes an os-template * * @param DOMnode $osTemplate * @param array $dataContext * @param TemplateLibrary $templateLibrary */ public function process(\DOMnode &$osTemplate, $dataContext, TemplateLibrary $templateLibrary) { $this->setDataContext($dataContext); $this->templateLibrary = $templateLibrary; if ($osTemplate instanceof \DOMElement) { if (($removeNode = $this->parseNode($osTemplate)) !== false) { $removeNode->parentNode->removeChild($removeNode); } } } /** * Sets and initializes the data context to use while processing the template * * @param array $dataContext */ private function setDataContext($dataContext) { $this->dataContext = array(); $this->dataContext['Top'] = $dataContext; $this->dataContext['Cur'] = array(); $this->dataContext['My'] = array(); $this->dataContext['Context'] = array('UniqueId' => uniqid()); } /** * returns the current datacontext, used mainly for testing purposes * * @return array */ public function getDataContext() { return $this->dataContext; } /** * @param DOMNode $node * @return boolean */ public function parseNode(\DOMNode &$node) { $removeNode = false; if ($node instanceof \DOMText) { if (! $node->isWhitespaceInElementContent() && ! empty($node->nodeValue)) { $this->parseNodeText($node); } } else { $tagName = isset($node->tagName) ? $node->tagName : ''; if (substr($tagName, 0, 3) == 'os:' || substr($tagName, 0, 4) == 'osx:') { $removeNode = $this->parseOsmlNode($node); } elseif ($this->templateLibrary->hasTemplate($tagName)) { // the tag name refers to an existing template (myapp:EmployeeCard type naming) // the extra check on the : character is to make sure this is a name spaced custom tag and not some one trying to override basic html tags (br, img, etc) $this->parseLibrary($tagName, $node); } else { $removeNode = $this->parseNodeAttributes($node); } } return is_object($removeNode) ? $removeNode : false; } /** * Misc function that maps the node's attributes to a key => value array * and results any expressions to actual values * * @param DOMElement $node * @return array */ private function nodeAttributesToScope(\DOMElement &$node) { $myContext = array(); if ($node->hasAttributes()) { foreach ($node->attributes as $attr) { if (strpos($attr->value, '${') !== false) { // attribute value contains an expression $expressions = array(); preg_match_all('/(\$\{)(.*)(\})/imsxU', $attr->value, $expressions); for ($i = 0; $i < count($expressions[0]); $i ++) { $expression = $expressions[2][$i]; $myContext[$attr->name] = ExpressionParser::evaluate($expression, $this->dataContext); } } else { // plain old string $myContext[$attr->name] = trim($attr->value); } } } return $myContext; } /** * Parses the specified template library * * @param string $tagName * @param DOMNode $node */ private function parseLibrary($tagName, \DOMNode &$node) { // Set the My context based on the node's attributes $myContext = $this->nodeAttributesToScope($node); // Template call has child nodes, those are params that can be used in a os:Render call, store them $oldNodeContext = isset($this->dataContext['_os_render_nodes']) ? $this->dataContext['_os_render_nodes'] : array(); $this->dataContext['_os_render_nodes'] = array(); if ($node->childNodes->length) { foreach ($node->childNodes as $childNode) { if (isset($childNode->tagName) && ! empty($childNode->tagName)) { $nodeParam = ($pos = strpos($childNode->tagName, ':')) ? trim(substr($childNode->tagName, $pos + 1)) : trim($childNode->tagName); $this->dataContext['_os_render_nodes'][$nodeParam] = $childNode; $myContext[$nodeParam] = $this->nodeAttributesToScope($childNode); } } } // Parse the template library (store the My scope since this could be a nested call) $previousMy = $this->dataContext['My']; $this->dataContext['My'] = $myContext; $ret = $this->templateLibrary->parseTemplate($tagName, $this); $this->dataContext['My'] = $previousMy; $this->dataContext['_os_render_nodes'] = $oldNodeContext; if ($ret) { // And replace the node with the parsed output $ownerDocument = $node->ownerDocument; foreach ($ret->childNodes as $childNode) { $importedNode = $ownerDocument->importNode($childNode, true); $importedNode = $node->parentNode->insertBefore($importedNode, $node); } } } /** * * @param DOMText $node */ private function parseNodeText(\DOMText &$node) { if (strpos($node->nodeValue, '${') !== false) { $expressions = array(); preg_match_all('/(\$\{)(.*)(\})/imsxU', $node->wholeText, $expressions); for ($i = 0; $i < count($expressions[0]); $i ++) { $toReplace = $expressions[0][$i]; $expression = $expressions[2][$i]; $expressionResult = ExpressionParser::evaluate($expression, $this->dataContext); $stringVal = htmlentities(ExpressionParser::stringValue($expressionResult), ENT_QUOTES, 'UTF-8'); $node->nodeValue = str_replace($toReplace, $stringVal, $node->nodeValue); } } } /** * * @param DOMNode $node * @return DOMNode or false */ private function parseNodeAttributes(\DOMNode &$node) { if ($node->hasAttributes()) { foreach ($node->attributes as $attr) { if (strpos($attr->value, '${') !== false) { $expressions = array(); preg_match_all('/(\$\{)(.*)(\})/imsxU', $attr->value, $expressions); for ($i = 0; $i < count($expressions[0]); $i ++) { $toReplace = $expressions[0][$i]; $expression = $expressions[2][$i]; $expressionResult = ExpressionParser::evaluate($expression, $this->dataContext); switch (strtolower($attr->name)) { case 'repeat': // Can only loop if the result of the expression was an array if (! is_array($expressionResult)) { throw new ExpressionException("Can't repeat on a singular var"); } // Make sure the repeat variable doesn't show up in the cloned nodes (otherwise it would infinit recurse on this->parseNode()) $node->removeAttribute('repeat'); // Is a named var requested? $variableName = $node->getAttribute('var') ? trim($node->getAttribute('var')) : false; // Store the current 'Cur', index and count state, we might be in a nested repeat loop $previousCount = isset($this->dataContext['Context']['Count']) ? $this->dataContext['Context']['Count'] : null; $previousIndex = isset($this->dataContext['Context']['Index']) ? $this->dataContext['Context']['Index'] : null; $previousCur = $this->dataContext['Cur']; // For information on the loop context, see http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Templating.xml#rfc.section.10.1 $this->dataContext['Context']['Count'] = count($expressionResult); foreach ($expressionResult as $index => $entry) { if ($variableName) { // this is cheating a little since we're not putting it on the top level scope, the variable resolver will check 'Cur' first though so myVar.Something will still resolve correctly $this->dataContext['Cur'][$variableName] = $entry; } $this->dataContext['Cur'] = $entry; $this->dataContext['Context']['Index'] = $index; // Clone this node and it's children $newNode = $node->cloneNode(true); // Append the parsed & expanded node to the parent $newNode = $node->parentNode->insertBefore($newNode, $node); // And parse it (using the global + loop context) $this->parseNode($newNode, true); } // Restore our previous data context state $this->dataContext['Cur'] = $previousCur; if ($previousCount) { $this->dataContext['Context']['Count'] = $previousCount; } else { unset($this->dataContext['Context']['Count']); } if ($previousIndex) { $this->dataContext['Context']['Index'] = $previousIndex; } else { unset($this->dataContext['Context']['Index']); } return $node; break; case 'if': if (! $expressionResult) { return $node; } else { $node->removeAttribute('if'); } break; // These special cases that only apply for certain tag types case 'selected': if ($node->tagName == 'option') { if ($expressionResult) { $node->setAttribute('selected', 'selected'); } else { $node->removeAttribute('selected'); } } else { throw new ExpressionException("Can only use selected on an option tag"); } break; case 'checked': if ($node->tagName == 'input') { if ($expressionResult) { $node->setAttribute('checked', 'checked'); } else { $node->removeAttribute('checked'); } } else { throw new ExpressionException("Can only use checked on an input tag"); } break; case 'disabled': $disabledTags = array('input', 'button', 'select', 'textarea'); if (in_array($node->tagName, $disabledTags)) { if ($expressionResult) { $node->setAttribute('disabled', 'disabled'); } else { $node->removeAttribute('disabled'); } } else { throw new ExpressionException("Can only use disabled on input, button, select and textarea tags"); } break; default: // On non os-template spec attributes, do a simple str_replace with the evaluated value $stringVal = htmlentities(ExpressionParser::stringValue($expressionResult), ENT_QUOTES, 'UTF-8'); $newAttrVal = str_replace($toReplace, $stringVal, $attr->value); $node->setAttribute($attr->name, $newAttrVal); break; } } } } } // if a repeat attribute was found, don't recurse on it's child nodes, the repeat handling already did that if (isset($node->childNodes) && $node->childNodes->length > 0) { $removeNodes = array(); // recursive loop to all this node's children foreach ($node->childNodes as $childNode) { if (($removeNode = $this->parseNode($childNode)) !== false) { $removeNodes[] = $removeNode; } } if (count($removeNodes)) { foreach ($removeNodes as $removeNode) { $removeNode->parentNode->removeChild($removeNode); } } } return false; } /** * Function that handles the os: and osx: tags * * @param DOMNode $node * @return DOMNode or false */ private function parseOsmlNode(\DOMNode &$node) { $tagName = strtolower($node->tagName); if (! $this->checkIf($node)) { // If the OSML tag contains an if attribute and the expression evaluates to false // flag it for removal and don't process it return $node; } switch ($tagName) { /****** Control statements ******/ case 'os:repeat': if (! $node->getAttribute('expression')) { throw new ExpressionException("Invalid os:Repeat tag, missing expression attribute"); } $expressions = array(); preg_match_all('/(\$\{)(.*)(\})/imsxU', $node->getAttribute('expression'), $expressions); $expression = $expressions[2][0]; $expressionResult = ExpressionParser::evaluate($expression, $this->dataContext); if (! is_array($expressionResult)) { throw new ExpressionException("Can't repeat on a singular var"); } // Store the current 'Cur', index and count state, we might be in a nested repeat loop $previousCount = isset($this->dataContext['Context']['Count']) ? $this->dataContext['Context']['Count'] : null; $previousIndex = isset($this->dataContext['Context']['Index']) ? $this->dataContext['Context']['Index'] : null; $previousCur = $this->dataContext['Cur']; // Is a named var requested? $variableName = $node->getAttribute('var') ? trim($node->getAttribute('var')) : false; // For information on the loop context, see http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Templating.xml#rfc.section.10.1 $this->dataContext['Context']['Count'] = count($expressionResult); foreach ($expressionResult as $index => $entry) { if ($variableName) { // this is cheating a little since we're not putting it on the top level scope, the variable resolver will check 'Cur' first though so myVar.Something will still resolve correctly $this->dataContext['Cur'][$variableName] = $entry; } $this->dataContext['Cur'] = $entry; $this->dataContext['Context']['Index'] = $index; foreach ($node->childNodes as $childNode) { $newNode = $childNode->cloneNode(true); $newNode = $node->parentNode->insertBefore($newNode, $node); $this->parseNode($newNode); } } // Restore our previous data context state $this->dataContext['Cur'] = $previousCur; if ($previousCount) { $this->dataContext['Context']['Count'] = $previousCount; } else { unset($this->dataContext['Context']['Count']); } if ($previousIndex) { $this->dataContext['Context']['Index'] = $previousIndex; } else { unset($this->dataContext['Context']['Index']); } return $node; break; case 'os:if': $expressions = array(); if (! $node->getAttribute('condition')) { throw new ExpressionException("Invalid os:If tag, missing condition attribute"); } preg_match_all('/(\$\{)(.*)(\})/imsxU', $node->getAttribute('condition'), $expressions); if (! count($expressions[2])) { throw new ExpressionException("Invalid os:If tag, missing condition expression"); } $expression = $expressions[2][0]; $expressionResult = ExpressionParser::evaluate($expression, $this->dataContext); if ($expressionResult) { foreach ($node->childNodes as $childNode) { $newNode = $childNode->cloneNode(true); $this->parseNode($newNode); $newNode = $node->parentNode->insertBefore($newNode, $node); } } return $node; break; /****** OSML tags (os: name space) ******/ case 'os:name': $this->parseLibrary('os:Name', $node); return $node; break; case 'os:badge': $this->parseLibrary('os:Badge', $node); return $node; break; case 'os:peopleselector': $this->parseLibrary('os:PeopleSelector', $node); return $node; break; case 'os:html': if (! $node->getAttribute('code')) { throw new ExpressionException("Invalid os:Html tag, missing code attribute"); } preg_match_all('/(\$\{)(.*)(\})/imsxU', $node->getAttribute('code'), $expressions); if (count($expressions[2])) { $expression = $expressions[2][0]; $code = ExpressionParser::evaluate($expression, $this->dataContext); } else { $code = $node->getAttribute('code'); } $node->parentNode->insertBefore($node->ownerDocument->createTextNode($code), $node); return $node; break; case 'os:render': if (! ($content = $node->getAttribute('content'))) { throw new ExpressionException("os:Render missing attribute: content"); } $content = $node->getAttribute('content'); if (! isset($this->dataContext['_os_render_nodes'][$content])) { throw new ExpressionException("os:Render, Unknown entry: " . htmlentities($content)); } $nodes = $this->dataContext['_os_render_nodes'][$content]; $ownerDocument = $node->ownerDocument; // Only parse the child nodes of the dom tree and not the (myapp:foo) top level element foreach ($nodes->childNodes as $childNode) { $importedNode = $ownerDocument->importNode($childNode, true); $importedNode = $node->parentNode->insertBefore($importedNode, $node); $this->parseNode($importedNode); } return $node; break; /****** Extension - Tags ******/ case 'os:flash': // handle expressions $this->parseNodeAttributes($node); // read swf config from attributes $swfConfig = array('width' => '100px', 'height' => '100px', 'play' => 'immediate'); foreach ($node->attributes as $attr) { $swfConfig[$attr->name] = $attr->value; } // attach security token in the flash var $st = 'st=' . BasicSecurityToken::getTokenStringFromRequest(); if (array_key_exists('flashvars', $swfConfig)) { $swfConfig['flashvars'] = $swfConfig['flashvars'] . '&' . $st; } else { $swfConfig['flashvars'] = $st; } // Restrict the content if sanitization is enabled $sanitizationEnabled = Config::get('sanitize_views'); if ($sanitizationEnabled) { $swfConfig['allowscriptaccess'] = 'never'; $swfConfig['swliveconnect'] = 'false'; $swfConfig['allownetworking'] = 'internal'; } // Generate unique id for this swf $ALT_CONTENT_PREFIX = 'os_Flash_alt_'; $altContentId = uniqid($ALT_CONTENT_PREFIX); // Create a div wrapper around the provided alternate content, and add the alternate content to the holder $altHolder = $node->ownerDocument->createElement('div'); $altHolder->setAttribute('id', $altContentId); foreach ($node->childNodes as $childNode) { $altHolder->appendChild($childNode); } $node->parentNode->insertBefore($altHolder, $node); // Create the call to swfobject in header $scriptCode = SwfConfig::buildSwfObjectCall($swfConfig, $altContentId); $scriptBlock = $node->ownerDocument->createElement('script'); $scriptBlock->setAttribute('type', 'text/javascript'); $node->parentNode->insertBefore($scriptBlock, $node); if ($swfConfig['play'] != 'immediate') { // Add onclick handler to trigger call to swfobject $scriptCode = "function {$altContentId}()\{{$scriptCode};\}"; $altHolder->setAttribute('onclick', "{$altContentId}()"); } $scriptCodeNode = $node->ownerDocument->createTextNode($scriptCode); $scriptBlock->appendChild($scriptCodeNode); return $node; break; case 'os:var': // handle expressions $this->parseNodeAttributes($node); if (! ($key = $node->getAttribute('key'))) { throw new ExpressionException("os:Var missing attribute: key"); } // either get value from attribute if (! ($value = $node->getAttribute('value'))) { $value = ''; } // or from inner text of node if (! $value && $node->textContent) { $value = $node->textContent; } // try to decode if the value is a valid json object $parsedValue = json_decode($value, true); if ($parsedValue) { $value = $parsedValue; } $this->dataContext['Top'][$key] = $value; return $node; break; case 'osx:navigatetoapp': break; case 'osx:navigatetoperson': break; } return false; } /** * Misc function that checks if the OSML tag $node has an if attribute, returns * true if the expression is true or no if attribute is set * * @param DOMElement $node * @return boolean */ private function checkIf(\DOMElement &$node) { if (($if = $node->getAttribute('if'))) { $expressions = array(); preg_match_all('/(\$\{)(.*)(\})/imsxU', $if, $expressions); if (! count($expressions[2])) { throw new ExpressionException("Invalid os:If tag, missing condition expression"); } $expression = $expressions[2][0]; $expressionResult = ExpressionParser::evaluate($expression, $this->dataContext); return $expressionResult ? true : false; } return true; } }