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