Commit | Line | Data |
---|---|---|
e3d28c74 TO |
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 | */ | |
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 | 202 | $defaultContainer = self::DEFAULT_PATH; |
543c1a2d | 203 | if ($value && $value[0] == '[' && preg_match(';^\[([a-zA-Z0-9\._]+)\]/(.*);', $value, $matches)) { |
e3d28c74 TO |
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; |
20fd9f37 | 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 | } |