Merge pull request #14876 from eileenmcnaughton/is_front
[civicrm-core.git] / CRM / Utils / JS.php
index f6d6710e979867f73da0600181ede1c8943d6034..06331b5a2c343b32f68cf200b58ee02b2a4da0e6 100644 (file)
@@ -32,6 +32,7 @@
  * @copyright CiviCRM LLC (c) 2004-2019
  */
 class CRM_Utils_JS {
+
   /**
    * Parse a javascript file for translatable strings.
    *
@@ -125,4 +126,151 @@ class CRM_Utils_JS {
     return preg_replace(":^\\s*//[^\n]+$:m", "", $script);
   }
 
+  /**
+   * Decodes a js variable (not necessarily strict json but valid js) into a php variable.
+   *
+   * This is similar to using json_decode($js, TRUE) but more forgiving about syntax.
+   *
+   * ex. {a: 'Apple', 'b': "Banana", c: [1, 2, 3]}
+   * Returns: [
+   *   'a' => 'Apple',
+   *   'b' => 'Banana',
+   *   'c' => [1, 2, 3],
+   * ]
+   *
+   * @param string $js
+   * @return mixed
+   */
+  public static function decode($js) {
+    $js = trim($js);
+    if ($js[0] === "'" || $js[0] === '"') {
+      // Use a temp placeholder for escaped backslashes
+      return str_replace(['\\\\', "\\'", '\\"', '\\&', '\\/', '**backslash**'], ['**backslash**', "'", '"', '&', '/', '\\'], substr($js, 1, -1));
+    }
+    if ($js[0] === '{' || $js[0] === '[') {
+      $obj = self::getRawProps($js);
+      foreach ($obj as $idx => $item) {
+        $obj[$idx] = self::decode($item);
+      }
+      return $obj;
+    }
+    return json_decode($js);
+  }
+
+  /**
+   * Gets the properties of a javascript object/array 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 Input: {foo: getFoo(arg), 'bar': function() {return "bar";}}
+   * Returns: [
+   *   'foo' => 'getFoo(arg)',
+   *   'bar' => 'function() {return "bar";}',
+   * ]
+   *
+   * @param $js
+   * @return array
+   * @throws \Exception
+   */
+  public static function getRawProps($js) {
+    $js = trim($js);
+    if (!is_string($js) || $js === '' || !($js[0] === '{' || $js[0] === '[')) {
+      throw new Exception("Invalid js object string passed to CRM_Utils_JS::getRawProps");
+    }
+    $chars = str_split(substr($js, 1));
+    $isEscaped = $quote = NULL;
+    $type = $js[0] === '{' ? 'object' : 'array';
+    $key = $type == 'array' ? 0 : NULL;
+    $item = '';
+    $end = strlen($js) - 2;
+    $quotes = ['"', "'", '/'];
+    $brackets = [
+      '}' => '{',
+      ')' => '(',
+      ']' => '[',
+      ':' => '?',
+    ];
+    $enclosures = array_fill_keys($brackets, 0);
+    $result = [];
+    foreach ($chars as $index => $char) {
+      if (!$isEscaped && in_array($char, $quotes, TRUE)) {
+        // Open quotes, taking care not to mistake the division symbol for opening a regex
+        if (!$quote && !($char == '/' && preg_match('{[\w)]\s*$}', $item))) {
+          $quote = $char;
+        }
+        // Close quotes
+        elseif ($char === $quote) {
+          $quote = NULL;
+        }
+      }
+      if (!$quote) {
+        // Delineates property key
+        if ($char == ':' && !array_filter($enclosures) && !$key) {
+          $key = $item;
+          $item = '';
+          continue;
+        }
+        // Delineates property value
+        if (($char == ',' || $index == $end) && !array_filter($enclosures) && isset($key) && trim($item) !== '') {
+          // Trim, unquote, and unescape characters in key
+          if ($type == 'object') {
+            $key = trim($key);
+            $key = in_array($key[0], $quotes) ? self::decode($key) : $key;
+          }
+          $result[$key] = trim($item);
+          $key = $type == 'array' ? $key + 1 : NULL;
+          $item = '';
+          continue;
+        }
+        // Open brackets - we'll ignore delineators inside
+        if (isset($enclosures[$char])) {
+          $enclosures[$char]++;
+        }
+        // Close brackets
+        if (isset($brackets[$char]) && $enclosures[$brackets[$char]]) {
+          $enclosures[$brackets[$char]]--;
+        }
+      }
+      $item .= $char;
+      // We are escaping the next char if this is a backslash not preceded by an odd number of backslashes
+      $isEscaped = $char === '\\' && ((strlen($item) - strlen(rtrim($item, '\\'))) % 2);
+    }
+    return $result;
+  }
+
+  /**
+   * Converts a php array to javascript object/array notation (not strict JSON).
+   *
+   * Does not encode keys unless they contain special characters.
+   * Does not encode values by default, so either specify $encodeValues = TRUE,
+   * or pass strings of valid js/json as values (per output from getRawProps).
+   * @see CRM_Utils_JS::getRawProps
+   *
+   * @param array $obj
+   * @param bool $encodeValues
+   * @return string
+   */
+  public static function writeObject($obj, $encodeValues = FALSE) {
+    $js = [];
+    $brackets = isset($obj[0]) && array_keys($obj) === range(0, count($obj) - 1) ? ['[', ']'] : ['{', '}'];
+    foreach ($obj as $key => $val) {
+      if ($encodeValues) {
+        $val = json_encode($val, JSON_UNESCAPED_SLASHES);
+      }
+      if ($brackets[0] == '{') {
+        // Enclose the key in quotes unless it is purely alphanumeric
+        if (preg_match('/\W/', $key)) {
+          // Prefer single quotes
+          $key = preg_match('/^[\w "]+$/', $key) ? "'" . $key . "'" : json_encode($key, JSON_UNESCAPED_SLASHES);
+        }
+        $js[] = "$key: $val";
+      }
+      else {
+        $js[] = $val;
+      }
+    }
+    return $brackets[0] . implode(', ', $js) . $brackets[1];
+  }
+
 }