Merge pull request #17480 from tunbola/email-template-perms
[civicrm-core.git] / Civi / Core / Paths.php
CommitLineData
e3d28c74
TO
1<?php
2namespace Civi\Core;
3
4/**
5 * Class Paths
6 * @package Civi\Core
7 *
8 * This paths class translates path-expressions into local file paths and
9 * URLs. Path-expressions may take a few forms:
10 *
11 * - Paths and URLs may use a variable prefix. For example, '[civicrm.files]/upload'
12 * - Paths and URLS may be absolute.
13 * - Paths may be relative (base dir: [civicrm.files]).
14 * - URLs may be relative (base dir: [cms.root]).
15 */
16class Paths {
17
18 const DEFAULT_URL = 'cms.root';
19 const DEFAULT_PATH = 'civicrm.files';
20
21 /**
22 * @var array
23 * Array(string $name => array(url => $, path => $)).
24 */
c64f69d9 25 private $variables = [];
e3d28c74 26
c64f69d9 27 private $variableFactory = [];
e3d28c74 28
e8e8f3ad 29 /**
30 * Class constructor.
31 */
e3d28c74 32 public function __construct() {
716bdc94
TO
33 // Below is a *default* set of functions to calculate paths/URLs.
34 // Some variables may be overridden as follow:
35 // - The global `$civicrm_paths` may be preset before Civi boots. (Ex: via `civicrm.settings.php`, `settings.php`, or `vendor/autoload.php`)
36 // - Variables may be re-registered. (Ex: via `CRM_Utils_System_WordPress`)
e3d28c74 37 $this
e3d28c74
TO
38 ->register('civicrm.root', function () {
39 return \CRM_Core_Config::singleton()->userSystem->getCiviSourceStorage();
40 })
2f4b426c 41 ->register('civicrm.packages', function () {
c64f69d9 42 return [
2f4b426c 43 'path' => \Civi::paths()->getPath('[civicrm.root]/packages/'),
d0cda11f 44 'url' => \Civi::paths()->getUrl('[civicrm.root]/packages/', 'absolute'),
c64f69d9 45 ];
2f4b426c
TO
46 })
47 ->register('civicrm.vendor', function () {
c64f69d9 48 return [
2f4b426c 49 'path' => \Civi::paths()->getPath('[civicrm.root]/vendor/'),
d0cda11f 50 'url' => \Civi::paths()->getUrl('[civicrm.root]/vendor/', 'absolute'),
c64f69d9 51 ];
2f4b426c
TO
52 })
53 ->register('civicrm.bower', function () {
c64f69d9 54 return [
2f4b426c 55 'path' => \Civi::paths()->getPath('[civicrm.root]/bower_components/'),
d0cda11f 56 'url' => \Civi::paths()->getUrl('[civicrm.root]/bower_components/', 'absolute'),
c64f69d9 57 ];
2f4b426c 58 })
e3d28c74
TO
59 ->register('civicrm.files', function () {
60 return \CRM_Core_Config::singleton()->userSystem->getDefaultFileStorage();
61 })
86a36a73 62 ->register('civicrm.private', function () {
0bd3f65c
TO
63 return [
64 // For backward compatibility with existing deployments, this
65 // effectively returns `dirname(CIVICRM_TEMPLATE_COMPILEDIR)`.
66 // That's confusing. Future installers should probably set `civicrm.private`
67 // explicitly instead of setting `CIVICRM_TEMPLATE_COMPILEDIR`.
68 'path' => \CRM_Utils_File::baseFilePath(),
69 ];
70 })
71 ->register('civicrm.log', function () {
72 return [
73 'path' => \Civi::paths()->getPath('[civicrm.private]/ConfigAndLog'),
74 ];
75 })
76 ->register('civicrm.compile', function () {
77 return [
78 // These two formulations are equivalent in typical deployments; however,
79 // for existing systems which previously customized CIVICRM_TEMPLATE_COMPILEDIR,
80 // using the constant should be more backward-compatibility.
81 'path' => defined('CIVICRM_TEMPLATE_COMPILEDIR') ? CIVICRM_TEMPLATE_COMPILEDIR : \Civi::paths()->getPath('[civicrm.private]/templates_c'),
82 ];
86a36a73 83 })
1b94154b
EE
84 ->register('civicrm.l10n', function () {
85 $dir = defined('CIVICRM_L10N_BASEDIR') ? CIVICRM_L10N_BASEDIR : \Civi::paths()->getPath('[civicrm.private]/l10n');
86 return [
87 'path' => is_dir($dir) ? $dir : \Civi::paths()->getPath('[civicrm.root]/l10n'),
88 ];
89 })
e3d28c74 90 ->register('cms', function () {
c64f69d9 91 return [
e3d28c74
TO
92 'path' => \CRM_Core_Config::singleton()->userSystem->cmsRootPath(),
93 'url' => \CRM_Utils_System::baseCMSURL(),
c64f69d9 94 ];
e3d28c74
TO
95 })
96 ->register('cms.root', function () {
c64f69d9 97 return [
e3d28c74
TO
98 'path' => \CRM_Core_Config::singleton()->userSystem->cmsRootPath(),
99 // Misleading: this *removes* the language part of the URL, producing a pristine base URL.
100 'url' => \CRM_Utils_System::languageNegotiationURL(\CRM_Utils_System::baseCMSURL(), FALSE, TRUE),
c64f69d9 101 ];
e3d28c74
TO
102 });
103 }
104
105 /**
106 * Register a new URL/file path mapping.
107 *
108 * @param string $name
b698e2d5 109 * The name of the variable.
e3d28c74
TO
110 * @param callable $factory
111 * Function which returns an array with keys:
112 * - path: string.
113 * - url: string.
4b350175 114 * @return Paths
e3d28c74
TO
115 */
116 public function register($name, $factory) {
b698e2d5 117 $this->variableFactory[$name] = $factory;
e3d28c74
TO
118 return $this;
119 }
120
b698e2d5
TO
121 /**
122 * @param string $name
123 * Ex: 'civicrm.root'.
124 * @param string $attr
125 * Ex: 'url', 'path'.
126 * @return mixed
127 */
128 public function getVariable($name, $attr) {
129 if (!isset($this->variables[$name])) {
130 $this->variables[$name] = call_user_func($this->variableFactory[$name]);
313a57a0
TO
131 if (isset($GLOBALS['civicrm_paths'][$name])) {
132 $this->variables[$name] = array_merge($this->variables[$name], $GLOBALS['civicrm_paths'][$name]);
133 }
d1532c9d
TO
134 if (isset($this->variables[$name]['url'])) {
135 // Typical behavior is to return an absolute URL. If an admin has put an override that's site-relative, then convert.
136 $this->variables[$name]['url'] = $this->toAbsoluteUrl($this->variables[$name]['url'], $name);
137 }
e3d28c74 138 }
b698e2d5 139 if (!isset($this->variables[$name][$attr])) {
e3d28c74
TO
140 throw new \RuntimeException("Cannot resolve path using \"$name.$attr\"");
141 }
b698e2d5
TO
142 return $this->variables[$name][$attr];
143 }
144
d1532c9d
TO
145 /**
146 * @param string $url
147 * Ex: 'https://example.com:8000/foobar' or '/foobar'
148 * @param string $for
149 * Ex: 'civicrm.root' or 'civicrm.packages'
150 * @return string
151 */
152 private function toAbsoluteUrl($url, $for) {
153 if (!$url) {
154 return $url;
155 }
156 elseif ($url[0] === '/') {
157 // Relative URL interpretation
158 if ($for === 'cms.root') {
159 throw new \RuntimeException('Invalid configuration: the [cms.root] path must be an absolute URL');
160 }
161 $cmsUrl = rtrim($this->getVariable('cms.root', 'url'), '/');
162 // The norms for relative URLs dictate:
163 // Single-slash: "/sub/dir" or "/" (domain-relative)
164 // Double-slash: "//example.com/sub/dir" (same-scheme)
165 $prefix = ($url === '/' || $url[1] !== '/')
166 ? $cmsUrl
167 : (parse_url($cmsUrl, PHP_URL_SCHEME) . ':');
168 return $prefix . $url;
169 }
170 else {
171 // Assume this is an absolute URL, as in the past ('_h_ttp://').
172 return $url;
173 }
174 }
175
e8e8f3ad 176 /**
177 * Does the variable exist.
178 *
179 * @param string $name
180 *
181 * @return bool
182 */
b698e2d5
TO
183 public function hasVariable($name) {
184 return isset($this->variableFactory[$name]);
e3d28c74
TO
185 }
186
187 /**
188 * Determine the absolute path to a file, given that the file is most likely
b698e2d5 189 * in a given particular variable.
e3d28c74
TO
190 *
191 * @param string $value
b698e2d5
TO
192 * The file path.
193 * Use "." to reference to default file root.
194 * Values may begin with a variable, e.g. "[civicrm.files]/upload".
e3d28c74
TO
195 * @return mixed|string
196 */
197 public function getPath($value) {
b66d559e
TO
198 if ($value === NULL || $value === FALSE || $value === '') {
199 return FALSE;
200 }
201
e3d28c74
TO
202 $defaultContainer = self::DEFAULT_PATH;
203 if ($value && $value{0} == '[' && preg_match(';^\[([a-zA-Z0-9\._]+)\]/(.*);', $value, $matches)) {
204 $defaultContainer = $matches[1];
205 $value = $matches[2];
206 }
b66d559e
TO
207
208 $isDot = $value === '.';
209 if ($isDot) {
e3d28c74
TO
210 $value = '';
211 }
b66d559e
TO
212
213 $result = \CRM_Utils_File::absoluteDirectory($value, $this->getVariable($defaultContainer, 'path'));
214 return $isDot ? rtrim($result, '/' . DIRECTORY_SEPARATOR) : $result;
e3d28c74
TO
215 }
216
217 /**
b698e2d5 218 * Determine the URL to a file.
e3d28c74
TO
219 *
220 * @param string $value
b698e2d5 221 * The file path. The path may begin with a variable, e.g. "[civicrm.files]/upload".
b66d559e
TO
222 *
223 * This function was designed for locating files under a given tree, and the
224 * the result for a straight variable expressions ("[foo.bar]") was not
225 * originally defined. You may wish to use one of these:
226 *
227 * - getVariable('foo.bar', 'url') => Lookup variable by itself
228 * - getUrl('[foo.bar]/') => Get the variable (normalized with a trailing "/").
229 * - getUrl('[foo.bar]/.') => Get the variable (normalized without a trailing "/").
e3d28c74
TO
230 * @param string $preferFormat
231 * The preferred format ('absolute', 'relative').
232 * The result data may not meet the preference -- if the setting
233 * refers to an external domain, then the result will be
234 * absolute (regardless of preference).
235 * @param bool|NULL $ssl
236 * NULL to autodetect. TRUE to force to SSL.
b66d559e
TO
237 * @return FALSE|string
238 * The URL for $value (string), or FALSE if the $value is not specified.
e3d28c74
TO
239 */
240 public function getUrl($value, $preferFormat = 'relative', $ssl = NULL) {
b66d559e
TO
241 if ($value === NULL || $value === FALSE || $value === '') {
242 return FALSE;
243 }
244
e3d28c74 245 $defaultContainer = self::DEFAULT_URL;
ac47f7ca 246 if ($value && $value{0} == '[' && preg_match(';^\[([a-zA-Z0-9\._]+)\](/(.*))$;', $value, $matches)) {
e3d28c74 247 $defaultContainer = $matches[1];
b66d559e 248 $value = $matches[3];
e3d28c74
TO
249 }
250
b66d559e
TO
251 $isDot = $value === '.';
252 if (substr($value, 0, 5) === 'http:' || substr($value, 0, 6) === 'https:') {
e3d28c74
TO
253 return $value;
254 }
255
b66d559e 256 $value = rtrim($this->getVariable($defaultContainer, 'url'), '/') . ($isDot ? '' : "/$value");
e3d28c74
TO
257
258 if ($preferFormat === 'relative') {
259 $parsed = parse_url($value);
260 if (isset($_SERVER['HTTP_HOST']) && isset($parsed['host']) && $_SERVER['HTTP_HOST'] == $parsed['host']) {
261 $value = $parsed['path'];
262 }
263 }
264
265 if ($ssl || ($ssl === NULL && \CRM_Utils_System::isSSL())) {
266 $value = str_replace('http://', 'https://', $value);
267 }
268
269 return $value;
270 }
271
272}