8 * This paths class translates path-expressions into local file paths and
9 * URLs. Path-expressions may take a few forms:
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]).
18 const DEFAULT_URL
= 'cms.root';
19 const DEFAULT_PATH
= 'civicrm.files';
23 * Array(string $name => array(url => $, path => $)).
25 private $variables = [];
27 private $variableFactory = [];
32 public function __construct() {
35 ->register('civicrm.root', function () {
36 return \CRM_Core_Config
::singleton()->userSystem
->getCiviSourceStorage();
38 ->register('civicrm.packages', function () {
40 'path' => \Civi
::paths()->getPath('[civicrm.root]/packages/'),
41 'url' => \Civi
::paths()->getUrl('[civicrm.root]/packages/'),
44 ->register('civicrm.vendor', function () {
46 'path' => \Civi
::paths()->getPath('[civicrm.root]/vendor/'),
47 'url' => \Civi
::paths()->getUrl('[civicrm.root]/vendor/'),
50 ->register('civicrm.bower', function () {
52 'path' => \Civi
::paths()->getPath('[civicrm.root]/bower_components/'),
53 'url' => \Civi
::paths()->getUrl('[civicrm.root]/bower_components/'),
56 ->register('civicrm.files', function () {
57 return \CRM_Core_Config
::singleton()->userSystem
->getDefaultFileStorage();
59 ->register('civicrm.private', function () {
61 // For backward compatibility with existing deployments, this
62 // effectively returns `dirname(CIVICRM_TEMPLATE_COMPILEDIR)`.
63 // That's confusing. Future installers should probably set `civicrm.private`
64 // explicitly instead of setting `CIVICRM_TEMPLATE_COMPILEDIR`.
65 'path' => \CRM_Utils_File
::baseFilePath(),
68 ->register('civicrm.log', function () {
70 'path' => \Civi
::paths()->getPath('[civicrm.private]/ConfigAndLog'),
73 ->register('civicrm.compile', function () {
75 // These two formulations are equivalent in typical deployments; however,
76 // for existing systems which previously customized CIVICRM_TEMPLATE_COMPILEDIR,
77 // using the constant should be more backward-compatibility.
78 'path' => defined('CIVICRM_TEMPLATE_COMPILEDIR') ? CIVICRM_TEMPLATE_COMPILEDIR
: \Civi
::paths()->getPath('[civicrm.private]/templates_c'),
81 ->register('civicrm.l10n', function () {
82 $dir = defined('CIVICRM_L10N_BASEDIR') ? CIVICRM_L10N_BASEDIR
: \Civi
::paths()->getPath('[civicrm.private]/l10n');
84 'path' => is_dir($dir) ?
$dir : \Civi
::paths()->getPath('[civicrm.root]/l10n'),
87 ->register('wp.frontend.base', function () {
88 return ['url' => rtrim(CIVICRM_UF_BASEURL
, '/') . '/'];
90 ->register('wp.frontend', function () use ($paths) {
91 $config = \CRM_Core_Config
::singleton();
92 $suffix = defined('CIVICRM_UF_WP_BASEPAGE') ? CIVICRM_UF_WP_BASEPAGE
: $config->wpBasePage
;
94 'url' => $paths->getVariable('wp.frontend.base', 'url') . $suffix,
97 ->register('wp.backend.base', function () {
98 return ['url' => rtrim(CIVICRM_UF_BASEURL
, '/') . '/wp-admin/'];
100 ->register('wp.backend', function () use ($paths) {
102 'url' => $paths->getVariable('wp.backend.base', 'url') . 'admin.php',
105 ->register('cms', function () {
107 'path' => \CRM_Core_Config
::singleton()->userSystem
->cmsRootPath(),
108 'url' => \CRM_Utils_System
::baseCMSURL(),
111 ->register('cms.root', function () {
113 'path' => \CRM_Core_Config
::singleton()->userSystem
->cmsRootPath(),
114 // Misleading: this *removes* the language part of the URL, producing a pristine base URL.
115 'url' => \CRM_Utils_System
::languageNegotiationURL(\CRM_Utils_System
::baseCMSURL(), FALSE, TRUE),
121 * Register a new URL/file path mapping.
123 * @param string $name
124 * The name of the variable.
125 * @param callable $factory
126 * Function which returns an array with keys:
131 public function register($name, $factory) {
132 $this->variableFactory
[$name] = $factory;
137 * @param string $name
138 * Ex: 'civicrm.root'.
139 * @param string $attr
143 public function getVariable($name, $attr) {
144 if (!isset($this->variables
[$name])) {
145 $this->variables
[$name] = call_user_func($this->variableFactory
[$name]);
146 if (isset($GLOBALS['civicrm_paths'][$name])) {
147 $this->variables
[$name] = array_merge($this->variables
[$name], $GLOBALS['civicrm_paths'][$name]);
149 if (isset($this->variables
[$name]['url'])) {
150 // Typical behavior is to return an absolute URL. If an admin has put an override that's site-relative, then convert.
151 $this->variables
[$name]['url'] = $this->toAbsoluteUrl($this->variables
[$name]['url'], $name);
154 if (!isset($this->variables
[$name][$attr])) {
155 throw new \
RuntimeException("Cannot resolve path using \"$name.$attr\"");
157 return $this->variables
[$name][$attr];
162 * Ex: 'https://example.com:8000/foobar' or '/foobar'
164 * Ex: 'civicrm.root' or 'civicrm.packages'
167 private function toAbsoluteUrl($url, $for) {
171 elseif ($url[0] === '/') {
172 // Relative URL interpretation
173 if ($for === 'cms.root') {
174 throw new \
RuntimeException('Invalid configuration: the [cms.root] path must be an absolute URL');
176 $cmsUrl = rtrim($this->getVariable('cms.root', 'url'), '/');
177 // The norms for relative URLs dictate:
178 // Single-slash: "/sub/dir" or "/" (domain-relative)
179 // Double-slash: "//example.com/sub/dir" (same-scheme)
180 $prefix = ($url === '/' ||
$url[1] !== '/')
182 : (parse_url($cmsUrl, PHP_URL_SCHEME
) . ':');
183 return $prefix . $url;
186 // Assume this is an absolute URL, as in the past ('_h_ttp://').
192 * Does the variable exist.
194 * @param string $name
198 public function hasVariable($name) {
199 return isset($this->variableFactory
[$name]);
203 * Determine the absolute path to a file, given that the file is most likely
204 * in a given particular variable.
206 * @param string $value
208 * Use "." to reference to default file root.
209 * Values may begin with a variable, e.g. "[civicrm.files]/upload".
210 * @return mixed|string
212 public function getPath($value) {
213 $defaultContainer = self
::DEFAULT_PATH
;
214 if ($value && $value{0} == '[' && preg_match(';^\[([a-zA-Z0-9\._]+)\]/(.*);', $value, $matches)) {
215 $defaultContainer = $matches[1];
216 $value = $matches[2];
221 if ($value === '.') {
224 return \CRM_Utils_File
::absoluteDirectory($value, $this->getVariable($defaultContainer, 'path'));
228 * Determine the URL to a file.
230 * @param string $value
231 * The file path. The path may begin with a variable, e.g. "[civicrm.files]/upload".
232 * @param string $preferFormat
233 * The preferred format ('absolute', 'relative').
234 * The result data may not meet the preference -- if the setting
235 * refers to an external domain, then the result will be
236 * absolute (regardless of preference).
237 * @param bool|NULL $ssl
238 * NULL to autodetect. TRUE to force to SSL.
239 * @return mixed|string
241 public function getUrl($value, $preferFormat = 'relative', $ssl = NULL) {
242 $defaultContainer = self
::DEFAULT_URL
;
243 if ($value && $value{0} == '[' && preg_match(';^\[([a-zA-Z0-9\._]+)\](/(.*))$;', $value, $matches)) {
244 $defaultContainer = $matches[1];
245 $value = empty($matches[3]) ?
'.' : $matches[3];
251 if ($value === '.') {
254 if (substr($value, 0, 4) == 'http') {
258 $value = rtrim($this->getVariable($defaultContainer, 'url'), '/') . '/' . $value;
260 if ($preferFormat === 'relative') {
261 $parsed = parse_url($value);
262 if (isset($_SERVER['HTTP_HOST']) && isset($parsed['host']) && $_SERVER['HTTP_HOST'] == $parsed['host']) {
263 $value = $parsed['path'];
267 if ($ssl ||
($ssl === NULL && \CRM_Utils_System
::isSSL())) {
268 $value = str_replace('http://', 'https://', $value);