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