PageRenderTime 48ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/vendor/Shindig/src/gadgets/templates/TemplateParser.php

https://github.com/kawahara/opOpenSocialPlugin
PHP | 556 lines | 421 code | 45 blank | 90 comment | 76 complexity | 62da8c1357b6a1ef36f7bfe8c7ac17c2 MD5 | raw file
Possible License(s): Apache-2.0
  1. <?php
  2. /*
  3. * Licensed to the Apache Software Foundation (ASF) under one
  4. * or more contributor license agreements. See the NOTICE file
  5. * distributed with this work for additional information
  6. * regarding copyright ownership. The ASF licenses this file
  7. * to you under the Apache License, Version 2.0 (the
  8. * "License"); you may not use this file except in compliance
  9. * with the License. You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing,
  14. * software distributed under the License is distributed on an
  15. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  16. * KIND, either express or implied. See the License for the
  17. * specific language governing permissions and limitations
  18. * under the License.
  19. */
  20. //TODO support repeat tags on OSML tags, ie this should work: <os:Html repeat="${Bar}" />
  21. //TODO remove the os-templates javascript if all the templates are rendered on the server (saves many Kb's in gadget size)
  22. require_once 'ExpressionParser.php';
  23. class TemplateParser {
  24. private $dataContext;
  25. private $templateLibrary;
  26. public function dumpNode($node, $function) {
  27. $doc = new DOMDocument(null, 'utf-8');
  28. $doc->preserveWhiteSpace = true;
  29. $doc->formatOutput = false;
  30. $doc->strictErrorChecking = false;
  31. $doc->recover = false;
  32. $doc->resolveExternals = false;
  33. if (! $newNode = @$doc->importNode($node, false)) {
  34. echo "[Invalid node, dump failed]<br><br>";
  35. return;
  36. }
  37. $doc->appendChild($newNode);
  38. echo "<b>$function (" . get_class($node) . "):</b><br>" . htmlentities(str_replace('<?xml version="" encoding="utf-8"?>', '', $doc->saveXML()) . "\n") . "<br><br>";
  39. }
  40. /**
  41. * Processes an os-template
  42. *
  43. * @param string $template
  44. */
  45. public function process(DOMnode &$osTemplate, $dataContext, $templateLibrary) {
  46. $this->setDataContext($dataContext);
  47. $this->templateLibrary = $templateLibrary;
  48. if ($osTemplate instanceof DOMElement) {
  49. if (($removeNode = $this->parseNode($osTemplate)) !== false) {
  50. $removeNode->parentNode->removeChild($removeNode);
  51. }
  52. }
  53. }
  54. /**
  55. * Sets and initializes the data context to use while processing the template
  56. *
  57. * @param array $dataContext
  58. */
  59. private function setDataContext($dataContext) {
  60. $this->dataContext = array();
  61. $this->dataContext['Top'] = $dataContext;
  62. $this->dataContext['Cur'] = array();
  63. $this->dataContext['My'] = array();
  64. $this->dataContext['Context'] = array('UniqueId' => uniqid());
  65. }
  66. public function parseNode(DOMNode &$node) {
  67. $removeNode = false;
  68. if ($node instanceof DOMText) {
  69. if (! $node->isWhitespaceInElementContent() && ! empty($node->nodeValue)) {
  70. $this->parseNodeText($node);
  71. }
  72. } else {
  73. $tagName = isset($node->tagName) ? $node->tagName : '';
  74. if (substr($tagName, 0, 3) == 'os:' || substr($tagName, 0, 4) == 'osx:') {
  75. $removeNode = $this->parseOsmlNode($node);
  76. } elseif ($this->templateLibrary->hasTemplate($tagName)) {
  77. // the tag name refers to an existing template (myapp:EmployeeCard type naming)
  78. // 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)
  79. $this->parseLibrary($tagName, $node);
  80. } else {
  81. $removeNode = $this->parseNodeAttributes($node);
  82. }
  83. }
  84. return is_object($removeNode) ? $removeNode : false;
  85. }
  86. /**
  87. * Misc function that maps the node's attributes to a key => value array
  88. * and results any expressions to actual values
  89. *
  90. * @param DOMElement $node
  91. * @return array
  92. */
  93. private function nodeAttributesToScope(DOMElement &$node) {
  94. $myContext = array();
  95. if ($node->hasAttributes()) {
  96. foreach ($node->attributes as $attr) {
  97. if (strpos($attr->value, '${') !== false) {
  98. // attribute value contains an expression
  99. $expressions = array();
  100. preg_match_all('/(\$\{)(.*)(\})/imsxU', $attr->value, $expressions);
  101. for ($i = 0; $i < count($expressions[0]); $i ++) {
  102. $expression = $expressions[2][$i];
  103. $myContext[$attr->name] = ExpressionParser::evaluate($expression, $this->dataContext);
  104. }
  105. } else {
  106. // plain old string
  107. $myContext[$attr->name] = trim($attr->value);
  108. }
  109. }
  110. }
  111. return $myContext;
  112. }
  113. /**
  114. * Parses the specified template library
  115. *
  116. * @param string $tagName
  117. * @param DOMNode $node
  118. */
  119. private function parseLibrary($tagName, DOMNode &$node) {
  120. // Set the My context based on the node's attributes
  121. $myContext = $this->nodeAttributesToScope($node);
  122. // Template call has child nodes, those are params that can be used in a os:Render call, store them
  123. $oldNodeContext = isset($this->dataContext['_os_render_nodes']) ? $this->dataContext['_os_render_nodes'] : array();
  124. $this->dataContext['_os_render_nodes'] = array();
  125. if ($node->childNodes->length) {
  126. foreach ($node->childNodes as $childNode) {
  127. if (isset($childNode->tagName) && ! empty($childNode->tagName)) {
  128. $nodeParam = ($pos = strpos($childNode->tagName, ':')) ? trim(substr($childNode->tagName, $pos + 1)) : trim($childNode->tagName);
  129. $this->dataContext['_os_render_nodes'][$nodeParam] = $childNode;
  130. $myContext[$nodeParam] = $this->nodeAttributesToScope($childNode);
  131. }
  132. }
  133. }
  134. // Parse the template library (store the My scope since this could be a nested call)
  135. $previousMy = $this->dataContext['My'];
  136. $this->dataContext['My'] = $myContext;
  137. $ret = $this->templateLibrary->parseTemplate($tagName, $this);
  138. $this->dataContext['My'] = $previousMy;
  139. $this->dataContext['_os_render_nodes'] = $oldNodeContext;
  140. if ($ret) {
  141. // And replace the node with the parsed output
  142. $ownerDocument = $node->ownerDocument;
  143. foreach ($ret->childNodes as $childNode) {
  144. $importedNode = $ownerDocument->importNode($childNode, true);
  145. $importedNode = $node->parentNode->insertBefore($importedNode, $node);
  146. }
  147. }
  148. }
  149. private function parseNodeText(DOMText &$node) {
  150. if (strpos($node->nodeValue, '${') !== false) {
  151. $expressions = array();
  152. preg_match_all('/(\$\{)(.*)(\})/imsxU', $node->wholeText, $expressions);
  153. for ($i = 0; $i < count($expressions[0]); $i ++) {
  154. $toReplace = $expressions[0][$i];
  155. $expression = $expressions[2][$i];
  156. $expressionResult = ExpressionParser::evaluate($expression, $this->dataContext);
  157. $stringVal = htmlentities(ExpressionParser::stringValue($expressionResult), ENT_QUOTES, 'UTF-8');
  158. $node->nodeValue = str_replace($toReplace, $stringVal, $node->nodeValue);
  159. }
  160. }
  161. }
  162. private function parseNodeAttributes(DOMNode &$node) {
  163. if ($node->hasAttributes()) {
  164. foreach ($node->attributes as $attr) {
  165. if (strpos($attr->value, '${') !== false) {
  166. $expressions = array();
  167. preg_match_all('/(\$\{)(.*)(\})/imsxU', $attr->value, $expressions);
  168. for ($i = 0; $i < count($expressions[0]); $i ++) {
  169. $toReplace = $expressions[0][$i];
  170. $expression = $expressions[2][$i];
  171. $expressionResult = ExpressionParser::evaluate($expression, $this->dataContext);
  172. switch (strtolower($attr->name)) {
  173. case 'repeat':
  174. // Can only loop if the result of the expression was an array
  175. if (! is_array($expressionResult)) {
  176. throw new ExpressionException("Can't repeat on a singular var");
  177. }
  178. // Make sure the repeat variable doesn't show up in the cloned nodes (otherwise it would infinit recurse on this->parseNode())
  179. $node->removeAttribute('repeat');
  180. // Is a named var requested?
  181. $variableName = $node->getAttribute('var') ? trim($node->getAttribute('var')) : false;
  182. // Store the current 'Cur', index and count state, we might be in a nested repeat loop
  183. $previousCount = isset($this->dataContext['Context']['Count']) ? $this->dataContext['Context']['Count'] : null;
  184. $previousIndex = isset($this->dataContext['Context']['Index']) ? $this->dataContext['Context']['Index'] : null;
  185. $previousCur = $this->dataContext['Cur'];
  186. // For information on the loop context, see http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Templating.xml#rfc.section.10.1
  187. $this->dataContext['Context']['Count'] = count($expressionResult);
  188. foreach ($expressionResult as $index => $entry) {
  189. if ($variableName) {
  190. // 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
  191. $this->dataContext['Cur'][$variableName] = $entry;
  192. }
  193. $this->dataContext['Cur'] = $entry;
  194. $this->dataContext['Context']['Index'] = $index;
  195. // Clone this node and it's children
  196. $newNode = $node->cloneNode(true);
  197. // Append the parsed & expanded node to the parent
  198. $newNode = $node->parentNode->insertBefore($newNode, $node);
  199. // And parse it (using the global + loop context)
  200. $this->parseNode($newNode, true);
  201. }
  202. // Restore our previous data context state
  203. $this->dataContext['Cur'] = $previousCur;
  204. if ($previousCount) {
  205. $this->dataContext['Context']['Count'] = $previousCount;
  206. } else {
  207. unset($this->dataContext['Context']['Count']);
  208. }
  209. if ($previousIndex) {
  210. $this->dataContext['Context']['Index'] = $previousIndex;
  211. } else {
  212. unset($this->dataContext['Context']['Index']);
  213. }
  214. return $node;
  215. break;
  216. case 'if':
  217. if (! $expressionResult) {
  218. return $node;
  219. } else {
  220. $node->removeAttribute('if');
  221. }
  222. break;
  223. // These special cases that only apply for certain tag types
  224. case 'selected':
  225. if ($node->tagName == 'option') {
  226. if ($expressionResult) {
  227. $node->setAttribute('selected', 'selected');
  228. } else {
  229. $node->removeAttribute('selected');
  230. }
  231. } else {
  232. throw new ExpressionException("Can only use selected on an option tag");
  233. }
  234. break;
  235. case 'checked':
  236. if ($node->tagName == 'input') {
  237. if ($expressionResult) {
  238. $node->setAttribute('checked', 'checked');
  239. } else {
  240. $node->removeAttribute('checked');
  241. }
  242. } else {
  243. throw new ExpressionException("Can only use checked on an input tag");
  244. }
  245. break;
  246. case 'disabled':
  247. $disabledTags = array('input', 'button',
  248. 'select', 'textarea');
  249. if (in_array($node->tagName, $disabledTags)) {
  250. if ($expressionResult) {
  251. $node->setAttribute('disabled', 'disabled');
  252. } else {
  253. $node->removeAttribute('disabled');
  254. }
  255. } else {
  256. throw new ExpressionException("Can only use disabled on input, button, select and textarea tags");
  257. }
  258. break;
  259. default:
  260. // On non os-template spec attributes, do a simple str_replace with the evaluated value
  261. $stringVal = htmlentities(ExpressionParser::stringValue($expressionResult), ENT_QUOTES, 'UTF-8');
  262. $newAttrVal = str_replace($toReplace, $stringVal, $attr->value);
  263. $node->setAttribute($attr->name, $newAttrVal);
  264. break;
  265. }
  266. }
  267. }
  268. }
  269. }
  270. // if a repeat attribute was found, don't recurse on it's child nodes, the repeat handling already did that
  271. if (isset($node->childNodes) && $node->childNodes->length > 0) {
  272. $removeNodes = array();
  273. // recursive loop to all this node's children
  274. foreach ($node->childNodes as $childNode) {
  275. if (($removeNode = $this->parseNode($childNode)) !== false) {
  276. $removeNodes[] = $removeNode;
  277. }
  278. }
  279. if (count($removeNodes)) {
  280. foreach ($removeNodes as $removeNode) {
  281. $removeNode->parentNode->removeChild($removeNode);
  282. }
  283. }
  284. }
  285. return false;
  286. }
  287. /**
  288. * Function that handles the os: and osx: tags
  289. *
  290. * @param DOMNode $node
  291. */
  292. private function parseOsmlNode(DOMNode &$node) {
  293. $tagName = strtolower($node->tagName);
  294. if (! $this->checkIf($node)) {
  295. // If the OSML tag contains an if attribute and the expression evaluates to false
  296. // flag it for removal and don't process it
  297. return $node;
  298. }
  299. switch ($tagName) {
  300. /****** Control statements ******/
  301. case 'os:repeat':
  302. if (! $node->getAttribute('expression')) {
  303. throw new ExpressionException("Invalid os:Repeat tag, missing expression attribute");
  304. }
  305. $expressions = array();
  306. preg_match_all('/(\$\{)(.*)(\})/imsxU', $node->getAttribute('expression'), $expressions);
  307. $expression = $expressions[2][0];
  308. $expressionResult = ExpressionParser::evaluate($expression, $this->dataContext);
  309. if (! is_array($expressionResult)) {
  310. throw new ExpressionException("Can't repeat on a singular var");
  311. }
  312. // Store the current 'Cur', index and count state, we might be in a nested repeat loop
  313. $previousCount = isset($this->dataContext['Context']['Count']) ? $this->dataContext['Context']['Count'] : null;
  314. $previousIndex = isset($this->dataContext['Context']['Index']) ? $this->dataContext['Context']['Index'] : null;
  315. $previousCur = $this->dataContext['Cur'];
  316. // Is a named var requested?
  317. $variableName = $node->getAttribute('var') ? trim($node->getAttribute('var')) : false;
  318. // For information on the loop context, see http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Templating.xml#rfc.section.10.1
  319. $this->dataContext['Context']['Count'] = count($expressionResult);
  320. foreach ($expressionResult as $index => $entry) {
  321. if ($variableName) {
  322. // 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
  323. $this->dataContext['Cur'][$variableName] = $entry;
  324. }
  325. $this->dataContext['Cur'] = $entry;
  326. $this->dataContext['Context']['Index'] = $index;
  327. foreach ($node->childNodes as $childNode) {
  328. $newNode = $childNode->cloneNode(true);
  329. $newNode = $node->parentNode->insertBefore($newNode, $node);
  330. $this->parseNode($newNode);
  331. }
  332. }
  333. // Restore our previous data context state
  334. $this->dataContext['Cur'] = $previousCur;
  335. if ($previousCount) {
  336. $this->dataContext['Context']['Count'] = $previousCount;
  337. } else {
  338. unset($this->dataContext['Context']['Count']);
  339. }
  340. if ($previousIndex) {
  341. $this->dataContext['Context']['Index'] = $previousIndex;
  342. } else {
  343. unset($this->dataContext['Context']['Index']);
  344. }
  345. return $node;
  346. break;
  347. case 'os:if':
  348. $expressions = array();
  349. if (! $node->getAttribute('condition')) {
  350. throw new ExpressionException("Invalid os:If tag, missing condition attribute");
  351. }
  352. preg_match_all('/(\$\{)(.*)(\})/imsxU', $node->getAttribute('condition'), $expressions);
  353. if (! count($expressions[2])) {
  354. throw new ExpressionException("Invalid os:If tag, missing condition expression");
  355. }
  356. $expression = $expressions[2][0];
  357. $expressionResult = ExpressionParser::evaluate($expression, $this->dataContext);
  358. if ($expressionResult) {
  359. foreach ($node->childNodes as $childNode) {
  360. $newNode = $childNode->cloneNode(true);
  361. $this->parseNode($newNode);
  362. $newNode = $node->parentNode->insertBefore($newNode, $node);
  363. }
  364. }
  365. return $node;
  366. break;
  367. /****** OSML tags (os: name space) ******/
  368. case 'os:name':
  369. $this->parseLibrary('os:Name', $node);
  370. return $node;
  371. break;
  372. case 'os:badge':
  373. $this->parseLibrary('os:Badge', $node);
  374. return $node;
  375. break;
  376. case 'os:peopleselector':
  377. $this->parseLibrary('os:PeopleSelector', $node);
  378. return $node;
  379. break;
  380. case 'os:html':
  381. if (! $node->getAttribute('code')) {
  382. throw new ExpressionException("Invalid os:Html tag, missing code attribute");
  383. }
  384. preg_match_all('/(\$\{)(.*)(\})/imsxU', $node->getAttribute('code'), $expressions);
  385. if (count($expressions[2])) {
  386. $expression = $expressions[2][0];
  387. $code = ExpressionParser::evaluate($expression, $this->dataContext);
  388. } else {
  389. $code = $node->getAttribute('code');
  390. }
  391. $node->parentNode->insertBefore($node->ownerDocument->createTextNode($code), $node);
  392. return $node;
  393. break;
  394. case 'os:render':
  395. if (! ($content = $node->getAttribute('content'))) {
  396. throw new ExpressionException("os:Render missing attribute: content");
  397. }
  398. $content = $node->getAttribute('content');
  399. if (! isset($this->dataContext['_os_render_nodes'][$content])) {
  400. throw new ExpressionException("os:Render, Unknown entry: " . htmlentities($content));
  401. }
  402. $nodes = $this->dataContext['_os_render_nodes'][$content];
  403. $ownerDocument = $node->ownerDocument;
  404. // Only parse the child nodes of the dom tree and not the (myapp:foo) top level element
  405. foreach ($nodes->childNodes as $childNode) {
  406. $importedNode = $ownerDocument->importNode($childNode, true);
  407. $importedNode = $node->parentNode->insertBefore($importedNode, $node);
  408. $this->parseNode($importedNode);
  409. }
  410. return $node;
  411. break;
  412. /****** Extension - Tags ******/
  413. case 'os:flash':
  414. // handle expressions
  415. $this->parseNodeAttributes($node);
  416. // read swf config from attributes
  417. $swfConfig = array('width' => '100px',
  418. 'height' => '100px', 'play' => 'immediate');
  419. foreach ($node->attributes as $attr) {
  420. $swfConfig[$attr->name] = $attr->value;
  421. }
  422. // attach security token in the flash var
  423. $st = 'st=' . $_GET['st'];
  424. if (array_key_exists('flashvars', $swfConfig)) {
  425. $swfConfig['flashvars'] = $swfConfig['flashvars'] . '&' . $st;
  426. } else {
  427. $swfConfig['flashvars'] = $st;
  428. }
  429. // Restrict the content if sanitization is enabled
  430. $sanitizationEnabled = Shindig_Config::get('sanitize_views');
  431. if ($sanitizationEnabled) {
  432. $swfConfig['allowscriptaccess'] = 'never';
  433. $swfConfig['swliveconnect'] = 'false';
  434. $swfConfig['allownetworking'] = 'internal';
  435. }
  436. // Generate unique id for this swf
  437. $ALT_CONTENT_PREFIX = 'os_Flash_alt_';
  438. $altContentId = uniqid($ALT_CONTENT_PREFIX);
  439. // Create a div wrapper around the provided alternate content, and add the alternate content to the holder
  440. $altHolder = $node->ownerDocument->createElement('div');
  441. $altHolder->setAttribute('id', $altContentId);
  442. foreach ($node->childNodes as $childNode) {
  443. $altHolder->appendChild($childNode);
  444. }
  445. $node->parentNode->insertBefore($altHolder, $node);
  446. // Create the call to swfobject in header
  447. $scriptCode = SwfConfig::buildSwfObjectCall($swfConfig, $altContentId);
  448. $scriptBlock = $node->ownerDocument->createElement('script');
  449. $scriptBlock->setAttribute('type', 'text/javascript');
  450. $node->parentNode->insertBefore($scriptBlock, $node);
  451. if ($swfConfig['play'] != 'immediate') {
  452. // Add onclick handler to trigger call to swfobject
  453. $scriptCode = "function {$altContentId}()\{{$scriptCode};\}";
  454. $altHolder->setAttribute('onclick', "{$altContentId}()");
  455. }
  456. $scriptCodeNode = $node->ownerDocument->createTextNode($scriptCode);
  457. $scriptBlock->appendChild($scriptCodeNode);
  458. return $node;
  459. break;
  460. case 'osx:navigatetoapp':
  461. break;
  462. case 'osx:navigatetoperson':
  463. break;
  464. }
  465. return false;
  466. }
  467. /**
  468. * Misc function that checks if the OSML tag $node has an if attribute, returns
  469. * true if the expression is true or no if attribute is set
  470. *
  471. * @param DOMElement $node
  472. */
  473. private function checkIf(DOMElement &$node) {
  474. if (($if = $node->getAttribute('if'))) {
  475. $expressions = array();
  476. preg_match_all('/(\$\{)(.*)(\})/imsxU', $if, $expressions);
  477. if (! count($expressions[2])) {
  478. throw new ExpressionException("Invalid os:If tag, missing condition expression");
  479. }
  480. $expression = $expressions[2][0];
  481. $expressionResult = ExpressionParser::evaluate($expression, $this->dataContext);
  482. return $expressionResult ? true : false;
  483. }
  484. return true;
  485. }
  486. }
  487. class SwfConfig {
  488. public static $FLASH_VER = '9.0.115';
  489. public static $PARAMS = array('loop', 'menu', 'quality', 'scale', 'salign', 'wmode', 'bgcolor',
  490. 'swliveconnect', 'flashvars', 'devicefont', 'allowscriptaccess', 'seamlesstabbing',
  491. 'allowfullscreen', 'allownetworking');
  492. public static $ATTRS = array('id', 'name', 'styleclass', 'align');
  493. public static function buildSwfObjectCall($swfConfig, $altContentId, $flashVars = 'null') {
  494. $params = SwfConfig::buildJsObj($swfConfig, SwfConfig::$PARAMS);
  495. $attrs = SwfConfig::buildJsObj($swfConfig, SwfConfig::$ATTRS);
  496. $flashVersion = SwfConfig::$FLASH_VER;
  497. $swfObject = "swfobject.embedSWF(\"{$swfConfig['swf']}\", \"{$altContentId}\", \"{$swfConfig['width']}\", \"{$swfConfig['height']}\", \"{$flashVersion}\", null, {$flashVars}, {$params}, {$attrs});";
  498. return $swfObject;
  499. }
  500. private static function buildJsObj($swfConfig, $keymap) {
  501. $arr = array();
  502. foreach ($swfConfig as $key => $value) {
  503. if (in_array($key, $keymap)) {
  504. $arr[] = "{$key}:\"{$value}\"";
  505. }
  506. }
  507. $output = implode(",", $arr);
  508. $output = '{' . $output . '}';
  509. return $output;
  510. }
  511. }