Allow most values of $civicrm_paths['XXX']['url'] to be relative
authorTim Otten <totten@civicrm.org>
Thu, 16 Jan 2020 08:42:55 +0000 (00:42 -0800)
committerTim Otten <totten@civicrm.org>
Tue, 28 Jan 2020 22:00:13 +0000 (14:00 -0800)
Overview
--------

The `$civicrm_paths` variable allows a sysadmin to override various path and
URL computations.

```php
$civicrm_paths['civicrm.packages']['url'] = 'https://example.com/libraries/civicrm/packages';
```

The variable was originally tested with absolute URLs, and the subsequent
examples/docs use absolute URLs (https://docs.civicrm.org/dev/en/latest/framework/filesystem/).

These values are used to generate addresses, as in:

```php
$abs = Civi::paths()->getUrl('[civicrm.packages]/foo.js', 'absolute');
$rel = Civi::paths()->getUrl('[civicrm.packages]/foo.js', 'relative');
```

The patch allows more values in `$civicrm_paths` while ensuring that
`getUrl()` works as expected.

Before
------

The `getUrl()` requests only behave correctly if the override is an absolute URL - not if it's relative.

After
-----

The `getUrl()` requests behave correctly if the override is either an absolute URL or a relative URL.

```php
$civicrm_paths['civicrm.packages']['url'] = 'https://example.com/libraries/civicrm/packages';
$civicrm_paths['civicrm.packages']['url'] = '/libraries/civicrm/packages';
```

Comments
--------

* `toAbsoluteUrl()` needs a base to prepend. I initially used `HTTP_HOST`
  but switched to `cms.root`, but correctly inferring scheme and host and port
  and httpd prefixes would be more complex - esp for background/CLI jobs.
  Using `cms.root` as the base is simpler.
* It's tempting to allow recursive variables. But it's not actually needed for
  my purposes, and it would add complexity/maintenance. If it's really needed,
  one could update `toAbsoluteUrl()` to quickly check for variables
  (`$url[0] === '['`) and then evaluate them. But for now... I think the
  simpler format is fine.

Civi/Core/Paths.php

index a4ab40c220209185ca468a44518fdf8d313c80f6..e2e1ff62ea53b3d87cf1cc290ac0bfdade1109ca 100644 (file)
@@ -146,6 +146,10 @@ class Paths {
       if (isset($GLOBALS['civicrm_paths'][$name])) {
         $this->variables[$name] = array_merge($this->variables[$name], $GLOBALS['civicrm_paths'][$name]);
       }
+      if (isset($this->variables[$name]['url'])) {
+        // Typical behavior is to return an absolute URL. If an admin has put an override that's site-relative, then convert.
+        $this->variables[$name]['url'] = $this->toAbsoluteUrl($this->variables[$name]['url'], $name);
+      }
     }
     if (!isset($this->variables[$name][$attr])) {
       throw new \RuntimeException("Cannot resolve path using \"$name.$attr\"");
@@ -153,6 +157,37 @@ class Paths {
     return $this->variables[$name][$attr];
   }
 
+  /**
+   * @param string $url
+   *   Ex: 'https://example.com:8000/foobar' or '/foobar'
+   * @param string $for
+   *   Ex: 'civicrm.root' or 'civicrm.packages'
+   * @return string
+   */
+  private function toAbsoluteUrl($url, $for) {
+    if (!$url) {
+      return $url;
+    }
+    elseif ($url[0] === '/') {
+      // Relative URL interpretation
+      if ($for === 'cms.root') {
+        throw new \RuntimeException('Invalid configuration: the [cms.root] path must be an absolute URL');
+      }
+      $cmsUrl = rtrim($this->getVariable('cms.root', 'url'), '/');
+      // The norms for relative URLs dictate:
+      // Single-slash: "/sub/dir" or "/" (domain-relative)
+      // Double-slash: "//example.com/sub/dir" (same-scheme)
+      $prefix = ($url === '/' || $url[1] !== '/')
+        ? $cmsUrl
+        : (parse_url($cmsUrl, PHP_URL_SCHEME) . ':');
+      return $prefix . $url;
+    }
+    else {
+      // Assume this is an absolute URL, as in the past ('_h_ttp://').
+      return $url;
+    }
+  }
+
   /**
    * Does the variable exist.
    *