Merge pull request #15666 from revati90/shared_address
[civicrm-core.git] / CRM / Extension / Container / Basic.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2020 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 * @package CRM
30 * @copyright CiviCRM LLC (c) 2004-2020
31 */
32
33 /**
34 * An extension container is a locally-accessible source tree which can be
35 * scanned for extensions.
36 */
37 class CRM_Extension_Container_Basic implements CRM_Extension_Container_Interface {
38
39 /**
40 * @var string
41 *
42 * Note: Treat as private. This is only public to facilitate debugging.
43 */
44 public $baseDir;
45
46 /**
47 * @var string
48 *
49 * Note: Treat as private. This is only public to facilitate debugging.
50 */
51 public $baseUrl;
52
53 /**
54 * @var CRM_Utils_Cache_Interface|NULL
55 *
56 * Note: Treat as private. This is only public to facilitate debugging.
57 */
58 public $cache;
59
60 /**
61 * @var string the cache key used for any data stored by this container
62 *
63 * Note: Treat as private. This is only public to facilitate debugging.
64 */
65 public $cacheKey;
66
67 /**
68 * @var array($key => $relPath)
69 *
70 * Note: Treat as private. This is only public to facilitate debugging.
71 */
72 public $relPaths = FALSE;
73
74 /**
75 * @var array($key => $relUrl)
76 *
77 * Derived from $relPaths. On Unix systems (where file-paths and
78 * URL-paths both use '/' separator), this isn't necessary. On Windows
79 * systems, this is derived from $relPaths.
80 *
81 * Note: Treat as private. This is only public to facilitate debugging.
82 */
83 public $relUrls = FALSE;
84
85 /**
86 * @var array
87 * Array(function(CRM_Extension_Info $info): bool)
88 * List of callables which determine whether an extension is visible.
89 * Each function returns TRUE if the extension should be visible.
90 */
91 protected $filters = [];
92
93 /**
94 * @param string $baseDir
95 * Local path to the container.
96 * @param string $baseUrl
97 * Public URL of the container.
98 * @param CRM_Utils_Cache_Interface $cache
99 * Cache in which to store extension metadata.
100 * @param string $cacheKey
101 * Unique name for this container.
102 */
103 public function __construct($baseDir, $baseUrl, CRM_Utils_Cache_Interface $cache = NULL, $cacheKey = NULL) {
104 $this->cache = $cache;
105 $this->cacheKey = $cacheKey;
106 $this->baseDir = rtrim($baseDir, '/');
107 $this->baseUrl = rtrim($baseUrl, '/');
108 }
109
110 /**
111 * @inheritDoc
112 *
113 * @return array
114 */
115 public function checkRequirements() {
116 $errors = [];
117
118 if (empty($this->baseDir) || !is_dir($this->baseDir)) {
119 $errors[] = [
120 'title' => ts('Invalid Base Directory'),
121 'message' => ts('An extension container has been defined with a blank directory.'),
122 ];
123 }
124 if (empty($this->baseUrl)) {
125 $errors[] = [
126 'title' => ts('Invalid Base URL'),
127 'message' => ts('An extension container has been defined with a blank URL.'),
128 ];
129 }
130
131 return $errors;
132 }
133
134 /**
135 * @inheritDoc
136 *
137 * @return array_keys
138 */
139 public function getKeys() {
140 return array_keys($this->getRelPaths());
141 }
142
143 /**
144 * @inheritDoc
145 */
146 public function getPath($key) {
147 return $this->baseDir . $this->getRelPath($key);
148 }
149
150 /**
151 * @inheritDoc
152 */
153 public function getResUrl($key) {
154 if (!$this->baseUrl) {
155 CRM_Core_Session::setStatus(
156 ts('Failed to determine URL for extension (%1). Please update <a href="%2">Resource URLs</a>.',
157 [
158 1 => $key,
159 2 => CRM_Utils_System::url('civicrm/admin/setting/url', 'reset=1'),
160 ]
161 )
162 );
163 }
164 return $this->baseUrl . $this->getRelUrl($key);
165 }
166
167 /**
168 * @inheritDoc
169 */
170 public function refresh() {
171 $this->relPaths = NULL;
172 if ($this->cache) {
173 $this->cache->delete($this->cacheKey);
174 }
175 }
176
177 /**
178 * @return string
179 */
180 public function getBaseDir() {
181 return $this->baseDir;
182 }
183
184 /**
185 * Determine the relative path of an extension directory.
186 *
187 * @param string $key
188 * Extension name.
189 *
190 * @throws CRM_Extension_Exception_MissingException
191 * @return string
192 */
193 protected function getRelPath($key) {
194 $keypaths = $this->getRelPaths();
195 if (!isset($keypaths[$key])) {
196 throw new CRM_Extension_Exception_MissingException("Failed to find extension: $key");
197 }
198 return $keypaths[$key];
199 }
200
201 /**
202 * Scan $basedir for a list of extension-keys
203 *
204 * @return array
205 * ($key => $relPath)
206 */
207 protected function getRelPaths() {
208 if (!is_array($this->relPaths)) {
209 if ($this->cache) {
210 $this->relPaths = $this->cache->get($this->cacheKey);
211 }
212 if (!is_array($this->relPaths)) {
213 $this->relPaths = [];
214 $infoPaths = CRM_Utils_File::findFiles($this->baseDir, 'info.xml');
215 foreach ($infoPaths as $infoPath) {
216 $relPath = CRM_Utils_File::relativize(dirname($infoPath), $this->baseDir);
217 try {
218 $info = CRM_Extension_Info::loadFromFile($infoPath);
219 }
220 catch (CRM_Extension_Exception_ParseException $e) {
221 CRM_Core_Session::setStatus(ts('Parse error in extension: %1', [
222 1 => $e->getMessage(),
223 ]), '', 'error');
224 CRM_Core_Error::debug_log_message("Parse error in extension: " . $e->getMessage());
225 continue;
226 }
227 $visible = TRUE;
228 foreach ($this->filters as $filter) {
229 if (!$filter($info)) {
230 $visible = FALSE;
231 break;
232 }
233 }
234 if ($visible) {
235 $this->relPaths[$info->key] = $relPath;
236 }
237 }
238 if ($this->cache) {
239 $this->cache->set($this->cacheKey, $this->relPaths);
240 }
241 }
242 }
243 return $this->relPaths;
244 }
245
246 /**
247 * Determine the relative path of an extension directory.
248 *
249 * @param string $key
250 * Extension name.
251 *
252 * @throws CRM_Extension_Exception_MissingException
253 * @return string
254 */
255 protected function getRelUrl($key) {
256 $relUrls = $this->getRelUrls();
257 if (!isset($relUrls[$key])) {
258 throw new CRM_Extension_Exception_MissingException("Failed to find extension: $key");
259 }
260 return $relUrls[$key];
261 }
262
263 /**
264 * Scan $basedir for a list of extension-keys
265 *
266 * @return array
267 * ($key => $relUrl)
268 */
269 protected function getRelUrls() {
270 if (DIRECTORY_SEPARATOR == '/') {
271 return $this->getRelPaths();
272 }
273 if (!is_array($this->relUrls)) {
274 $this->relUrls = self::convertPathsToUrls(DIRECTORY_SEPARATOR, $this->getRelPaths());
275 }
276 return $this->relUrls;
277 }
278
279 /**
280 * Register a filter which determine whether a copy of an extension
281 * appears as available.
282 *
283 * @param callable $callable
284 * function(CRM_Extension_Info $info): bool
285 * Each function returns TRUE if the extension should be visible.
286 * @return $this
287 */
288 public function addFilter($callable) {
289 $this->filters[] = $callable;
290 return $this;
291 }
292
293 /**
294 * Convert a list of relative paths to relative URLs.
295 *
296 * Note: Treat as private. This is only public to facilitate testing.
297 *
298 * @param string $dirSep
299 * Directory separator ("/" or "\").
300 * @param array $relPaths
301 * Array($key => $relPath).
302 * @return array
303 * Array($key => $relUrl).
304 */
305 public static function convertPathsToUrls($dirSep, $relPaths) {
306 $relUrls = [];
307 foreach ($relPaths as $key => $relPath) {
308 $relUrls[$key] = str_replace($dirSep, '/', $relPath);
309 }
310 return $relUrls;
311 }
312
313 }