From 3203414a31f78441dc6f5ff1b71174dfb0d83865 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Tue, 18 Jun 2019 20:37:37 -0400 Subject: [PATCH] Utils_JS - add fn to get props of js object without parsing them --- CRM/Utils/JS.php | 83 ++++++++++++++++++++++++++++++ tests/phpunit/CRM/Utils/JSTest.php | 34 ++++++++++++ 2 files changed, 117 insertions(+) diff --git a/CRM/Utils/JS.php b/CRM/Utils/JS.php index 98624dd291..dd69a2d017 100644 --- a/CRM/Utils/JS.php +++ b/CRM/Utils/JS.php @@ -144,4 +144,87 @@ class CRM_Utils_JS { return json_decode(json_encode($result), TRUE); } + /** + * Gets the properties of a javascript object WITHOUT decoding them. + * + * Useful when the object might contain js functions, expressions, etc. which cannot be decoded. + * Returns an array with keys as property names and values as raw strings of js. + * + * Ex: {foo: getFoo(arg), bar: function() {return "bar"}} + * Returns [ + * 'foo' => 'getFoo(arg)', + * 'bar' => 'function() {return "bar"}', + * ] + * + * @param $js + * @return array + * @throws \Exception + */ + public static function getObjectProps($js) { + $js = trim($js); + if (!is_string($js) || !($js[0] === '{')) { + throw new Exception("Invalid js object string passed to CRM_Utils_JS::getObjectProps"); + } + $chars = str_split(substr($js, 1)); + $prev = NULL; + $key = NULL; + $quote = NULL; + $item = ''; + $length = strlen($js) - 2; + $quotes = ['"', "'"]; + $enclosures = [ + '{' => 0, + '(' => 0, + '[' => 0, + ]; + $closers = [ + '}' => '{', + ')' => '(', + ']' => '[', + ]; + $result = []; + foreach ($chars as $index => $char) { + // Open quotes - we'll ignore everything inside + if (in_array($char, $quotes) && $prev != '\\' && !$quote) { + $quote = $char; + } + // Close quotes + elseif (in_array($char, $quotes) && $prev != '\\' && $char === $quote) { + $quote = NULL; + } + if (!$quote) { + // Skip opening whitespace between properties + if ($char === ' ' && !strlen($item)) { + $prev = $char; + continue; + } + // Delineates property key + if ($char == ':' && !array_filter($enclosures)) { + $key = $item; + $item = ''; + $prev = $char; + continue; + } + // Delineates property value + if (($char == ',' || ($char == '}' && $index == $length)) && !array_filter($enclosures) && isset($key)) { + $result[trim(trim($key), '"\'')] = $item; + $item = ''; + $prev = $char; + continue; + } + // Open brackets - we'll ignore delineators inside + if (isset($enclosures[$char])) { + $enclosures[$char]++; + } + // Close brackets + if (isset($closers[$char]) && $enclosures[$closers[$char]]) { + $enclosures[$closers[$char]]--; + } + } + $item .= $char; + $prev = $char; + } + return $result; + } + } diff --git a/tests/phpunit/CRM/Utils/JSTest.php b/tests/phpunit/CRM/Utils/JSTest.php index 8cc3b062da..128d1e188e 100644 --- a/tests/phpunit/CRM/Utils/JSTest.php +++ b/tests/phpunit/CRM/Utils/JSTest.php @@ -221,4 +221,38 @@ class CRM_Utils_JSTest extends CiviUnitTestCase { $this->assertEquals($expectedOutput, CRM_Utils_JS::decode($input)); } + public static function objectExamples() { + return [ + [ + '{a: \'Apple\', \'b\': "Banana", "c ": [1,2,3]}', + ['a' => "'Apple'", 'b' => '"Banana"', 'c ' => '[1,2,3]'], + ], + [ + "{}", + [], + ], + [ + " {'fn' : function (foo, bar, baz) { return \"One, two, three\"; }, number: 55 } ", + ['fn' => "function (foo, bar, baz) { return \"One, two, three\"; }", 'number' => '55 '], + ], + [ + "{ string : 'this, has(some : weird, \\'stuff [{}!' , expr: callMeAl(1, 2, 3), ' notes ' : [Do, re mi] }", + ['string' => "'this, has(some : weird, \\'stuff [{}!' ", 'expr' => 'callMeAl(1, 2, 3)', ' notes ' => "[Do, re mi] "], + ], + [ + '{foo: getFoo("Some \"quoted\" thing"), bar: function() {return "bar"}}', + ['foo' => 'getFoo("Some \"quoted\" thing")', 'bar' => 'function() {return "bar"}'], + ], + ]; + } + + /** + * @param string $input + * @param string $expectedOutput + * @dataProvider objectExamples + */ + public function testgetObjectProps($input, $expectedOutput) { + $this->assertEquals($expectedOutput, CRM_Utils_JS::getObjectProps($input)); + } + } -- 2.25.1