Smarty - Add support for `{url}frontend://civicrm/profile{/url}` (etal)
authorTim Otten <totten@civicrm.org>
Tue, 25 Jul 2023 05:01:38 +0000 (22:01 -0700)
committerTim Otten <totten@civicrm.org>
Tue, 25 Jul 2023 05:59:30 +0000 (22:59 -0700)
CRM/Core/Smarty/plugins/block.url.php [new file with mode: 0644]
tests/phpunit/CRM/Core/Smarty/plugins/UrlTest.php [new file with mode: 0644]

diff --git a/CRM/Core/Smarty/plugins/block.url.php b/CRM/Core/Smarty/plugins/block.url.php
new file mode 100644 (file)
index 0000000..71814af
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * Generate a URL. This is thin wrapper for the Civi::url() helper.
+ *
+ * @see Civi::url()
+ *
+ * Ex: Generate a backend URL.
+ *     {url}backend://civicrm/admin?reset=1{/url}
+ *
+ * Ex: Generate a backend URL. Assign it to a Smarty variable.
+ *     {url assign=tmpVar}backend://civicrm/admin?reset=1{/url}
+ *
+ * Ex: Generate a backend URL. Set optional flags: (t)ext, (s)sl, (a)bsolute.
+ *     {url flags=tsa}backend://civicrm/admin?reset=1{/url}
+ *
+ * Ex: Generate a URL in the current (active) routing scheme. Add named variables. (Values are escaped).
+ *     {url verb="Eat" target="Apples and bananas"}//civicrm/fruit?method=[verb]&data=[target]{/url}
+ *
+ * Ex: As above, but use numerical variables.
+ *     {url 1="Eat" 2="Apples and bananas"}//civicrm/fruit?method=[1]&data=[2]{/url}
+ *
+ * Ex: Generate a URL. Add some pre-escaped variables using Smarty {$foo}.
+ *     {assign var=myEscapedAction value="Eat"}
+ *     {assign var=myEscapedData value="Apples+and+bananas"}
+ *     {url}//civicrm/fruit?method={$myEscapedAction}&data={$myEscapedData}{/url}
+ *
+ * @param array $params
+ *   The following parameters have specific meanings:
+ *   - "assign" (string) - Assign output to a Smarty variable
+ *   - "flags" (string) - List of options, as per `Civi::url(...$flags)`
+ *   All other parameters will be passed-through as variables for the URL.
+ * @param string $text
+ *   Contents of block.
+ * @param CRM_Core_Smarty $smarty
+ *   The Smarty object.
+ * @return string
+ */
+function smarty_block_url($params, $text, &$smarty) {
+  if ($text === NULL) {
+    return NULL;
+  }
+
+  $flags = 'h' . ($params['flags'] ?? '');
+  $assign = $params['assign'] ?? NULL;
+  unset($params['flags'], $params['assign']);
+
+  $url = (string) Civi::url($text, $flags)->addVars($params);
+
+  // This could be neat, but see discussion in CRM_Core_Smarty_plugins_UrlTest for why it's currently off.
+  // $url->setVarsCallback([$smarty, 'get_template_vars']);
+
+  if ($assign !== NULL) {
+    $smarty->assign([$assign => $url]);
+    return '';
+  }
+  else {
+    return $url;
+  }
+}
diff --git a/tests/phpunit/CRM/Core/Smarty/plugins/UrlTest.php b/tests/phpunit/CRM/Core/Smarty/plugins/UrlTest.php
new file mode 100644 (file)
index 0000000..b9a2141
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * Class CRM_Core_Smarty_plugins_UrlTest
+ * @group headless
+ */
+class CRM_Core_Smarty_plugins_UrlTest extends CiviUnitTestCase {
+
+  public function setUp(): void {
+    parent::setUp();
+    require_once 'CRM/Core/Smarty.php';
+
+    // Templates should normally be file names, but for unit-testing it's handy to use "string:" notation
+    require_once 'CRM/Core/Smarty/resources/String.php';
+    civicrm_smarty_register_string_resource();
+
+    $this->useTransaction();
+  }
+
+  /**
+   * @return array
+   */
+  public function urlCases() {
+    $literal = function(string $s) {
+      return '!' . preg_quote($s, '!') . '!';
+    };
+
+    $cases = [];
+    $cases[] = [
+      // Generate an ordinary, HTML-style URL.
+      $literal('q=civicrm/profile/view&amp;id=123&amp;gid=456'),
+      '{url}//civicrm/profile/view?id=123&gid=456{/url}',
+    ];
+    $cases[] = [
+      // Here, we assign the plain-text variable and then use it for JS expression
+      '!window.location = ".*q=civicrm/profile/view&id=123&gid=456"!',
+      '{url assign=myUrl flags=t}//civicrm/profile/view?id=123&gid=456{/url}' .
+      'window.location = "{$myUrl}";',
+    ];
+    $cases[] = [
+      $literal('q=civicrm/profile/view&amp;id=999&amp;message=hello+world'),
+      '{url 1="999" 2="hello world"}//civicrm/profile/view?id=[1]&message=[2]{/url}',
+    ];
+    $cases[] = [
+      $literal('q=civicrm/profile/view&amp;id=123&amp;message=hello+world'),
+      '{url msg="hello world"}//civicrm/profile/view?id=123&message=[msg]{/url}',
+    ];
+    $cases[] = [
+      // Define a temporary variable for use in the URL.
+      $literal('q=civicrm/profile/view&amp;id=123&amp;message=this+%26+that'),
+      '{url msg="this & that"}//civicrm/profile/view?id=123&message=[msg]{/url}',
+    ];
+    $cases[] = [
+      // We have a Smarty variable which already included escaped data. Smarty should do substitution.
+      $literal('q=civicrm/profile/view&amp;id=123&amp;message=this+%2B+that'),
+      '{assign var=msg value="this+%2B+that"}' .
+      '{url flags=%}//civicrm/profile/view?id=123&message={$msg}{/url}',
+    ];
+    $cases[] = [
+      // Generate client-side route (with Angular path and params)
+      $literal('q=civicrm/a/#/mailing/100?angularDebug=1'),
+      '{url id=100}backend://civicrm/a/#/mailing/[id]?angularDebug=1{/url}',
+    ];
+
+    // This example is neat - you just replace `{$msg}` with `[msg]`, and then you get encoded URL data.
+    // But... it's pretty shallow. You can't use Smarty expressions or modifiers. Additionally,
+    // enabling this mode increases the risk of accidental collisions between Smarty variables
+    // and deep-form-params. So I've left it disabled for now.
+    //
+    // $cases[] = [
+    //   // We have a Smarty variable with canonical (unescaped) data. Use it as URL variable.
+    //   $literal('q=civicrm/profile/view&amp;id=123&amp;message=this+%2B+that'),
+    //   '{assign var=msg value="this + that"}' .
+    //   '{url}//civicrm/profile/view?id=123&message=[msg]{/url}',
+    // ];
+
+    // return CRM_Utils_Array::subset($cases, [2]);
+    return $cases;
+  }
+
+  /**
+   * @dataProvider urlCases
+   * @param string $expected
+   * @param string $input
+   */
+  public function testUrl($expected, $input) {
+    $smarty = CRM_Core_Smarty::singleton();
+    $actual = $smarty->fetch('string:' . $input);
+    $this->assertRegExp($expected, $actual, "Process input=[$input]");
+  }
+
+}