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