Merge pull request #19554 from colemanw/searchFields
[civicrm-core.git] / CRM / Utils / JS.php
index bcd80dd79967e5223b3518c738dc511554206d31..66c03e0f483d6175b3a8337723eb98d332cae5ed 100644 (file)
@@ -54,13 +54,15 @@ class CRM_Utils_JS {
    * Note that you can only dedupe closures if they are directly adjacent and
    * have exactly the same parameters.
    *
+   * Also dedupes the "use strict" directive as it is only meaningful at the beginning of a closure.
+   *
    * @param array $scripts
    *   Javascript source.
    * @param array $localVars
    *   Ordered list of JS vars to identify the start of a closure.
    * @param array $inputVals
    *   Ordered list of input values passed into the closure.
-   * @return string
+   * @return string[]
    *   Javascript source.
    */
   public static function dedupeClosures($scripts, $localVars, $inputVals) {
@@ -70,7 +72,7 @@ class CRM_Utils_JS {
       return preg_quote($v, '/');
     }, $localVars));
     $opening .= '\)\s*\{';
-    $opening = '/^' . $opening . '/';
+    $opening = '/^' . $opening . '\s*(?:"use strict";\s|\'use strict\';\s)?/';
 
     // Example closing: })(angular, CRM.$, CRM._);
     $closing = '\}\s*\)\s*\(\s*';
@@ -107,7 +109,7 @@ class CRM_Utils_JS {
    * @return string
    */
   public static function stripComments($script) {
-    return preg_replace(":^\\s*//[^\n]+$:m", "", $script);
+    return preg_replace("#^\\s*//[^\n]*$(?:\r\n|\n)?#m", "", $script);
   }
 
   /**
@@ -123,25 +125,52 @@ class CRM_Utils_JS {
    * ]
    *
    * @param string $js
+   * @param bool $throwException
    * @return mixed
+   * @throws CRM_Core_Exception
    */
-  public static function decode($js) {
+  public static function decode($js, $throwException = FALSE) {
     $js = trim($js);
     $first = substr($js, 0, 1);
     $last = substr($js, -1);
-    if ($last === $first && ($first === "'" || $first === '"')) {
-      // Use a temp placeholder for escaped backslashes
-      $backslash = chr(0) . 'backslash' . chr(0);
-      return str_replace(['\\\\', "\\'", '\\"', '\\&', '\\/', $backslash], [$backslash, "'", '"', '&', '/', '\\'], substr($js, 1, -1));
+    if ($first === "'" && $last === "'") {
+      $js = self::convertSingleQuoteString($js, $throwException);
     }
-    if (($first === '{' && $last === '}') || ($first === '[' && $last === ']')) {
+    elseif (($first === '{' && $last === '}') || ($first === '[' && $last === ']')) {
       $obj = self::getRawProps($js);
       foreach ($obj as $idx => $item) {
-        $obj[$idx] = self::decode($item);
+        $obj[$idx] = self::decode($item, $throwException);
       }
       return $obj;
     }
-    return json_decode($js);
+    $result = json_decode($js);
+    if ($throwException && $result === NULL && $js !== 'null') {
+      throw new CRM_Core_Exception(json_last_error_msg());
+    }
+    return $result;
+  }
+
+  /**
+   * @param string $str
+   * @param bool $throwException
+   * @return string|null
+   * @throws CRM_Core_Exception
+   */
+  public static function convertSingleQuoteString(string $str, $throwException) {
+    // json_decode can only handle double quotes around strings, so convert single-quoted strings
+    $backslash = chr(0) . 'backslash' . chr(0);
+    $str = str_replace(['\\\\', '\\"', '"', '\\&', '\\/', $backslash], [$backslash, '"', '\\"', '&', '/', '\\'], substr($str, 1, -1));
+    // Ensure the string doesn't terminate early by checking that all single quotes are escaped
+    $pos = -1;
+    while (($pos = strpos($str, "'", $pos + 1)) !== FALSE) {
+      if (($pos - strlen(rtrim(substr($str, 0, $pos)))) % 2) {
+        if ($throwException) {
+          throw new CRM_Core_Exception('Invalid string passed to CRM_Utils_JS::decode');
+        }
+        return NULL;
+      }
+    }
+    return '"' . $str . '"';
   }
 
   /**
@@ -187,7 +216,7 @@ class CRM_Utils_JS {
    *
    * @param $js
    * @return array
-   * @throws \Exception
+   * @throws Exception
    */
   public static function getRawProps($js) {
     $js = trim($js);