This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * @author Todd Burry <[email protected]> |
||
4 | * @copyright 2009-2014 Vanilla Forums Inc. |
||
5 | * @license MIT |
||
6 | */ |
||
7 | |||
8 | namespace Garden; |
||
9 | |||
10 | /** |
||
11 | * Contains functionality that allows addons to enhance or change an application's functionality. |
||
12 | * |
||
13 | * An addon can do the following. |
||
14 | * |
||
15 | * 1. Any classes that the addon defines in its root, /controllers, /library, /models, and /modules |
||
16 | * directories are made available. |
||
17 | * 2. The addon can contain a bootstrap.php which will be included at the app startup. |
||
18 | * 3. If the addon declares any classes ending in *Plugin then those plugins will automatically |
||
19 | * bind their event handlers. (also *Hooks) |
||
20 | */ |
||
21 | class Addons { |
||
22 | /// Constants /// |
||
23 | const K_BOOTSTRAP = 'bootstrap'; // bootstrap path key |
||
24 | const K_CLASSES = 'classes'; |
||
25 | const K_DIR = 'dir'; |
||
26 | const K_INFO = 'info'; // addon info key |
||
27 | |||
28 | /// Properties /// |
||
29 | |||
30 | /** |
||
31 | * @var array An array that maps addon keys to full addon information. |
||
32 | */ |
||
33 | protected static $all; |
||
34 | |||
35 | /** |
||
36 | * @var string The base directory where all of the addons are found. |
||
37 | */ |
||
38 | protected static $baseDir; |
||
39 | |||
40 | /** |
||
41 | * @var array An array that maps class names to their fully namespaced class names. |
||
42 | */ |
||
43 | // protected static $basenameMap; |
||
44 | |||
45 | /** |
||
46 | * @var array|null An array that maps class names to file paths. |
||
47 | */ |
||
48 | protected static $classMap; |
||
49 | |||
50 | /** |
||
51 | * @var array An array that maps addon keys to full addon information for enabled addons. |
||
52 | */ |
||
53 | protected static $enabled; |
||
54 | |||
55 | /** |
||
56 | * @var array An array of enabled addon keys. |
||
57 | */ |
||
58 | protected static $enabledKeys; |
||
59 | |||
60 | /** |
||
61 | * @var bool Signals that the addon framework is in a shared environment and shouldn't use the enabled cache. |
||
62 | */ |
||
63 | public static $sharedEnvironment; |
||
64 | |||
65 | /// Methods /// |
||
66 | |||
67 | /** |
||
68 | * Get all of the available addons or a single addon from the available list. |
||
69 | * |
||
70 | * @param string $addon_key If you supply an addon key then only that addon will be returned. |
||
71 | * @param string $key Supply one of the Addons::K_* constants to get a specific key from the addon. |
||
72 | * @return array Returns the addon with the given key or all available addons if no key is passed. |
||
73 | */ |
||
74 | public static function all($addon_key = null, $key = null) { |
||
75 | if (self::$all === null) { |
||
76 | self::$all = static::cacheGet('addons-all', array(get_class(), 'scanAddons')); |
||
77 | } |
||
78 | |||
79 | // The array should be built now return the addon. |
||
80 | View Code Duplication | if ($addon_key === null) { |
|
81 | return self::$all; |
||
82 | } else { |
||
83 | $addon = val(strtolower($addon_key), self::$all); |
||
84 | if ($addon && $key) { |
||
85 | return val($key, $addon); |
||
86 | } elseif ($addon) { |
||
87 | return $addon; |
||
88 | } else { |
||
89 | return null; |
||
90 | } |
||
91 | } |
||
92 | } |
||
93 | |||
94 | /** |
||
95 | * An autoloader that will autoload a class based on which addons are enabled. |
||
96 | * |
||
97 | * @param string $classname The name of the class to load. |
||
98 | */ |
||
99 | 6 | public static function autoload($classname) { |
|
100 | 6 | list($fullClass, $path) = static::classMap($classname); |
|
101 | 6 | if ($path) { |
|
102 | 4 | require $path; |
|
103 | 4 | } |
|
104 | 6 | } |
|
105 | |||
106 | /** |
||
107 | * Gets/sets the base directory for addons. |
||
108 | * |
||
109 | * @param string $value Pass a value to set the new base directory. |
||
110 | * @return string Returns the base directory for addons. |
||
111 | */ |
||
112 | 34 | public static function baseDir($value = null) { |
|
113 | 34 | if ($value !== null) { |
|
114 | 34 | self::$baseDir = rtrim($value, '/'); |
|
115 | 34 | } elseif (self::$baseDir === null) { |
|
116 | self::$baseDir = PATH_ROOT.'/addons'; |
||
117 | } |
||
118 | 34 | } |
|
119 | |||
120 | |||
121 | /** |
||
122 | * Start up the addon framework. |
||
123 | * |
||
124 | * @param array $enabled_addons An array of enabled addons. |
||
125 | */ |
||
126 | 34 | public static function bootstrap($enabled_addons = null) { |
|
127 | // Load the addons from the config if they aren't passed in. |
||
128 | 34 | if (!is_array($enabled_addons)) { |
|
129 | $enabled_addons = config('addons', array()); |
||
130 | } |
||
131 | // Reformat the enabled array into the form: array('addon_key' => 'addon_key') |
||
132 | 34 | $enabled_keys = array_keys(array_change_key_case(array_filter($enabled_addons))); |
|
133 | 34 | $enabled_keys = array_combine($enabled_keys, $enabled_keys); |
|
134 | 34 | self::$enabledKeys = $enabled_keys; |
|
135 | 34 | self::$classMap = null; // invalidate so it will rebuild |
|
136 | |||
137 | // Enable the addon autoloader. |
||
138 | 34 | spl_autoload_register(array(get_class(), 'autoload'), true, true); |
|
139 | |||
140 | // Bind all of the addon plugin events now. |
||
141 | 34 | foreach (self::enabled() as $addon) { |
|
142 | 34 | if (!isset($addon[self::K_CLASSES])) { |
|
143 | continue; |
||
144 | } |
||
145 | |||
146 | 34 | foreach ($addon[self::K_CLASSES] as $class_name => $class_path) { |
|
147 | 34 | if (str_ends($class_name, 'plugin')) { |
|
148 | Event::bindClass($class_name); |
||
149 | 34 | } elseif (str_ends($class_name, 'hooks')) { |
|
150 | // Vanilla 2 used hooks files for themes and applications. |
||
151 | $basename = ucfirst(rtrim_substr($class_name, 'hooks')); |
||
152 | deprecated($basename.'Hooks', $basename.'Plugin'); |
||
153 | Event::bindClass($class_name); |
||
154 | } |
||
155 | 34 | } |
|
156 | 34 | } |
|
157 | |||
158 | Event::bind('bootstrap', function () { |
||
159 | // Start each of the enabled addons. |
||
160 | 34 | foreach (self::enabled() as $key => $value) { |
|
161 | 34 | static::startAddon($key); |
|
162 | 34 | } |
|
163 | 34 | }); |
|
164 | 34 | } |
|
165 | |||
166 | /** |
||
167 | * Get the cached file or hydrate the cache with a callback. |
||
168 | * |
||
169 | * @param string $key The cache key to get. |
||
170 | * @param callable $cache_cb The function to run when hydrating the cache. |
||
171 | * @return array Returns the cached array. |
||
172 | */ |
||
173 | 1 | protected static function cacheGet($key, callable $cache_cb) { |
|
174 | // Salt the cache with the root path so that it will invalidate if the app is moved. |
||
175 | 1 | $salt = substr(md5(static::baseDir()), 0, 10); |
|
176 | |||
177 | 1 | $cache_path = PATH_ROOT."/cache/$key-$salt.json.php"; |
|
178 | 1 | if (file_exists($cache_path)) { |
|
179 | $result = array_load($cache_path); |
||
180 | return $result; |
||
181 | } else { |
||
182 | 1 | $result = $cache_cb(); |
|
183 | 1 | array_save($result, $cache_path); |
|
184 | } |
||
185 | 1 | return $result; |
|
186 | } |
||
187 | |||
188 | /** |
||
189 | * A an array that maps class names to physical paths. |
||
190 | * |
||
191 | * @param string $classname An optional class name to get the path of. |
||
192 | * @return array Returns an array in the form `[fullClassname, classPath]`. |
||
193 | * If no {@link $classname} is passed then the entire class map is returned. |
||
194 | * @throws \Exception Throws an exception if the class map is corrupt. |
||
195 | */ |
||
196 | 36 | public static function classMap($classname = null) { |
|
197 | 36 | if (self::$classMap === null) { |
|
198 | // Loop through the enabled addons and grab their classes. |
||
199 | 34 | $class_map = array(); |
|
200 | 34 | foreach (static::enabled() as $addon) { |
|
201 | 34 | if (isset($addon[self::K_CLASSES])) { |
|
202 | 34 | $class_map = array_replace($class_map, $addon[self::K_CLASSES]); |
|
203 | 34 | } |
|
204 | 34 | } |
|
205 | 34 | self::$classMap = $class_map; |
|
206 | 34 | } |
|
207 | |||
208 | // Now that the class map has been built return the result. |
||
209 | 36 | if ($classname !== null) { |
|
210 | 36 | if (strpos($classname, '\\') === false) { |
|
211 | 34 | $basename = strtolower($classname); |
|
212 | 34 | } else { |
|
213 | 3 | $basename = strtolower(trim(strrchr($classname, '\\'), '\\')); |
|
214 | } |
||
215 | |||
216 | 36 | $row = val($basename, self::$classMap); |
|
217 | |||
218 | 36 | if ($row === null) { |
|
219 | 3 | return ['', '']; |
|
220 | 34 | } elseif (is_string($row)) { |
|
221 | return [$classname, $row]; |
||
222 | 34 | } elseif (is_array($row)) { |
|
223 | 34 | return $row; |
|
224 | } else { |
||
225 | return ['', '']; |
||
226 | } |
||
227 | } else { |
||
228 | return self::$classMap; |
||
229 | } |
||
230 | } |
||
231 | |||
232 | /** |
||
233 | * Get all of the enabled addons or a single addon from the enabled list. |
||
234 | * |
||
235 | * @param string $addon_key If you supply an addon key then only that addon will be returned. |
||
236 | * @param string $key Supply one of the Addons::K_* constants to get a specific key from the addon. |
||
237 | * @return array Returns the addon with the given key or all enabled addons if no key is passed. |
||
238 | * @throws \Exception Throws an exception if {@link Addons::bootstrap()} hasn't been called yet. |
||
239 | */ |
||
240 | 34 | public static function enabled($addon_key = null, $key = null) { |
|
241 | // Lazy build the enabled array. |
||
242 | 34 | if (self::$enabled === null) { |
|
243 | // Make sure the enabled addons have been added first. |
||
244 | 1 | if (self::$enabledKeys === null) { |
|
245 | throw new \Exception("Addons::boostrap() must be called before Addons::enabled() can be called.", 500); |
||
246 | } |
||
247 | |||
248 | 1 | if (self::$all !== null || self::$sharedEnvironment) { |
|
249 | // Build the enabled array by filtering the all array. |
||
250 | self::$enabled = array(); |
||
251 | foreach (self::all() as $key => $row) { |
||
252 | if (isset($key, self::$enabledKeys)) { |
||
253 | self::$enabled[$key] = $row; |
||
254 | } |
||
255 | } |
||
256 | } else { |
||
257 | // Build the enabled array by walking the addons. |
||
258 | 1 | self::$enabled = static::cacheGet('addons-enabled', function () { |
|
259 | 1 | return static::scanAddons(null, self::$enabledKeys); |
|
260 | 1 | }); |
|
261 | } |
||
262 | 1 | } |
|
263 | |||
264 | // The array should be built now return the addon. |
||
265 | 34 | View Code Duplication | if ($addon_key === null) { |
266 | 34 | return self::$enabled; |
|
267 | } else { |
||
268 | 34 | $addon = val(strtolower($addon_key), self::$enabled); |
|
269 | 34 | if ($addon && $key) { |
|
270 | return val($key, $addon); |
||
271 | 34 | } elseif ($addon) { |
|
272 | 34 | return $addon; |
|
273 | } else { |
||
274 | return null; |
||
275 | } |
||
276 | } |
||
277 | } |
||
278 | |||
279 | /** |
||
280 | * Return the info array for an addon. |
||
281 | * |
||
282 | * @param string $addon_key The addon key. |
||
283 | * @return array|null Returns the addon's info array or null if the addon wasn't found. |
||
284 | */ |
||
285 | public static function info($addon_key) { |
||
286 | $addon_key = strtolower($addon_key); |
||
287 | |||
288 | // Check the enabled array first so that we don't load all addons if we don't have to. |
||
289 | if (isset(self::$enabledKeys[$addon_key])) { |
||
290 | return static::enabled($addon_key, self::K_INFO); |
||
291 | } else { |
||
292 | return static::all($addon_key, self::K_INFO); |
||
293 | } |
||
294 | } |
||
295 | |||
296 | /** |
||
297 | * Scan an addon directory for information. |
||
298 | * |
||
299 | * @param string $dir The addon directory to scan. |
||
300 | * @param array &$addons The addons array. |
||
301 | * @param array $enabled An array of enabled addons or null to scan all addons. |
||
302 | * @return array Returns an array in the form [addonKey, addonInfo]. |
||
303 | */ |
||
304 | 1 | protected static function scanAddonRecursive($dir, &$addons, $enabled = null) { |
|
305 | 1 | $dir = rtrim($dir, '/'); |
|
306 | 1 | $addonKey = strtolower(basename($dir)); |
|
307 | |||
308 | // Scan the addon if it is enabled. |
||
309 | 1 | if ($enabled === null || in_array($addonKey, $enabled)) { |
|
310 | 1 | list($addonKey, $addon) = static::scanAddon($dir); |
|
311 | 1 | } else { |
|
312 | $addon = null; |
||
313 | } |
||
314 | |||
315 | // Add the addon to the collection array if one was supplied. |
||
316 | 1 | if ($addon !== null) { |
|
317 | 1 | $addons[$addonKey] = $addon; |
|
318 | 1 | } |
|
319 | |||
320 | // Recurse. |
||
321 | 1 | $addon_subdirs = array('/addons'); |
|
322 | 1 | foreach ($addon_subdirs as $addon_subdir) { |
|
323 | 1 | if (is_dir($dir.$addon_subdir)) { |
|
324 | static::scanAddons($dir.$addon_subdir, $enabled, $addons); |
||
325 | } |
||
326 | 1 | } |
|
327 | |||
328 | 1 | return array($addonKey, $addon); |
|
329 | } |
||
330 | |||
331 | /** |
||
332 | * Scan an individual addon directory and return the information about that addon. |
||
333 | * |
||
334 | * @param string $dir The path to the addon. |
||
335 | * @return array An array in the form of `[$addon_key, $addon_row]` or `[$addon_key, null]` if the directory doesn't |
||
336 | * represent an addon. |
||
337 | */ |
||
338 | 1 | protected static function scanAddon($dir) { |
|
339 | 1 | $dir = rtrim($dir, '/'); |
|
340 | 1 | $addon_key = strtolower(basename($dir)); |
|
341 | |||
342 | // Look for the addon info array. |
||
343 | 1 | $info_path = $dir.'/addon.json'; |
|
344 | 1 | $info = false; |
|
345 | 1 | if (file_exists($info_path)) { |
|
346 | 1 | $info = json_decode(file_get_contents($info_path), true); |
|
347 | 1 | } |
|
348 | 1 | if (!$info) { |
|
349 | $info = array(); |
||
350 | } |
||
351 | 1 | array_touch('name', $info, $addon_key); |
|
352 | 1 | array_touch('version', $info, '0.0'); |
|
353 | |||
354 | // Look for the bootstrap. |
||
355 | 1 | $bootstrap = $dir.'/bootstrap.php'; |
|
356 | 1 | if (!file_exists($dir.'/bootstrap.php')) { |
|
357 | 1 | $bootstrap = null; |
|
358 | 1 | } |
|
359 | |||
360 | // Scan the appropriate subdirectories for classes. |
||
361 | 1 | $subdirs = array('', '/library', '/controllers', '/models', '/modules', '/settings'); |
|
362 | 1 | $classes = array(); |
|
363 | 1 | foreach ($subdirs as $subdir) { |
|
364 | // Get all of the php files in the subdirectory. |
||
365 | 1 | $paths = glob($dir.$subdir.'/*.php'); |
|
366 | 1 | foreach ($paths as $path) { |
|
367 | 1 | $decls = static::scanFile($path); |
|
368 | 1 | foreach ($decls as $namespace_row) { |
|
369 | 1 | if (isset($namespace_row['namespace']) && $namespace_row) { |
|
370 | $namespace = rtrim($namespace_row['namespace'], '\\').'\\'; |
||
371 | $namespace_classes = $namespace_row['classes']; |
||
372 | } else { |
||
373 | 1 | $namespace = ''; |
|
374 | 1 | $namespace_classes = $namespace_row; |
|
375 | } |
||
376 | |||
377 | 1 | foreach ($namespace_classes as $class_row) { |
|
378 | 1 | $classes[strtolower($class_row['name'])] = [$namespace.$class_row['name'], $path]; |
|
379 | 1 | } |
|
380 | 1 | } |
|
381 | 1 | } |
|
382 | 1 | } |
|
383 | |||
384 | $addon = array( |
||
385 | 1 | self::K_BOOTSTRAP => $bootstrap, |
|
386 | 1 | self::K_CLASSES => $classes, |
|
387 | 1 | self::K_DIR => $dir, |
|
388 | 1 | self::K_INFO => $info |
|
389 | 1 | ); |
|
390 | |||
391 | 1 | return array($addon_key, $addon); |
|
392 | } |
||
393 | |||
394 | /** |
||
395 | * Scan a directory for addons. |
||
396 | * |
||
397 | * @param string $dir The directory to scan. |
||
398 | * @param array $enabled An array of enabled addons in the form `[addonKey => enabled, ...]`. |
||
399 | * @param array &$addons The addons will fill this array. |
||
400 | * @return array Returns all of the addons. |
||
401 | */ |
||
402 | 1 | protected static function scanAddons($dir = null, $enabled = null, &$addons = null) { |
|
403 | 1 | if (!$dir) { |
|
404 | 1 | $dir = static::$baseDir; |
|
405 | 1 | } |
|
406 | 1 | if ($addons === null) { |
|
407 | 1 | $addons = array(); |
|
408 | 1 | } |
|
409 | |||
410 | /* @var \DirectoryIterator */ |
||
411 | 1 | foreach (new \DirectoryIterator($dir) as $subdir) { |
|
412 | 1 | if ($subdir->isDir() && !$subdir->isDot()) { |
|
413 | // echo $subdir->getPathname().$subdir->isDir().$subdir->isDot().'<br />'; |
||
414 | 1 | static::scanAddonRecursive($subdir->getPathname(), $addons, $enabled); |
|
415 | 1 | } |
|
416 | 1 | } |
|
417 | 1 | return $addons; |
|
418 | } |
||
419 | |||
420 | /** |
||
421 | * Looks what classes and namespaces are defined in a file and returns the first found. |
||
422 | * |
||
423 | * @param string $file Path to file. |
||
424 | * @return array Returns an empty array if no classes are found or an array with namespaces and |
||
425 | * classes found in the file. |
||
426 | * @see http://stackoverflow.com/a/11114724/1984219 |
||
427 | */ |
||
428 | 1 | protected static function scanFile($file) { |
|
429 | 1 | $classes = $nsPos = $final = array(); |
|
430 | 1 | $foundNamespace = false; |
|
431 | 1 | $ii = 0; |
|
432 | |||
433 | 1 | if (!file_exists($file)) { |
|
434 | return array(); |
||
435 | } |
||
436 | |||
437 | 1 | $er = error_reporting(); |
|
438 | 1 | error_reporting(E_ALL ^ E_NOTICE); |
|
439 | |||
440 | 1 | $php_code = file_get_contents($file); |
|
441 | 1 | $tokens = token_get_all($php_code); |
|
442 | 1 | $count = count($tokens); |
|
443 | |||
444 | 1 | for ($i = 0; $i < $count; $i++) { |
|
445 | 1 | if (!$foundNamespace && $tokens[$i][0] == T_NAMESPACE) { |
|
446 | $nsPos[$ii]['start'] = $i; |
||
447 | $foundNamespace = true; |
||
448 | 1 | } elseif ($foundNamespace && ($tokens[$i] == ';' || $tokens[$i] == '{')) { |
|
449 | $nsPos[$ii]['end'] = $i; |
||
450 | $ii++; |
||
451 | $foundNamespace = false; |
||
452 | 1 | } elseif ($i - 2 >= 0 && $tokens[$i - 2][0] == T_CLASS && $tokens[$i - 1][0] == T_WHITESPACE && $tokens[$i][0] == T_STRING) { |
|
453 | 1 | if ($i - 4 >= 0 && $tokens[$i - 4][0] == T_ABSTRACT) { |
|
454 | $classes[$ii][] = array('name' => $tokens[$i][1], 'type' => 'ABSTRACT CLASS'); |
||
455 | } else { |
||
456 | 1 | $classes[$ii][] = array('name' => $tokens[$i][1], 'type' => 'CLASS'); |
|
457 | } |
||
458 | 1 | } elseif ($i - 2 >= 0 && $tokens[$i - 2][0] == T_INTERFACE && $tokens[$i - 1][0] == T_WHITESPACE && $tokens[$i][0] == T_STRING) { |
|
459 | $classes[$ii][] = array('name' => $tokens[$i][1], 'type' => 'INTERFACE'); |
||
460 | } |
||
461 | 1 | } |
|
462 | 1 | error_reporting($er); |
|
463 | 1 | if (empty($classes)) { |
|
464 | return []; |
||
465 | } |
||
466 | |||
467 | 1 | if (!empty($nsPos)) { |
|
468 | foreach ($nsPos as $k => $p) { |
||
469 | $ns = ''; |
||
470 | for ($i = $p['start'] + 1; $i < $p['end']; $i++) { |
||
471 | $ns .= $tokens[$i][1]; |
||
472 | } |
||
473 | |||
474 | $ns = trim($ns); |
||
475 | $final[$k] = array('namespace' => $ns, 'classes' => $classes[$k + 1]); |
||
476 | } |
||
477 | $classes = $final; |
||
478 | } |
||
479 | 1 | return $classes; |
|
480 | } |
||
481 | |||
482 | /** |
||
483 | * Start an addon. |
||
484 | * |
||
485 | * This function does the following: |
||
486 | * |
||
487 | * 1. Make the addon available in the autoloader. |
||
488 | * 2. Run the addon's bootstrap.php if it exists. |
||
489 | * |
||
490 | * @param string $addon_key The key of the addon to enable. |
||
491 | * @return bool Returns true if the addon was enabled. False otherwise. |
||
492 | */ |
||
493 | 34 | public static function startAddon($addon_key) { |
|
494 | 34 | $addon = static::enabled($addon_key); |
|
495 | 34 | if (!$addon) { |
|
0 ignored issues
–
show
|
|||
496 | return false; |
||
497 | } |
||
498 | |||
499 | // Run the class' bootstrap. |
||
500 | 34 | if ($bootstrap_path = val(self::K_BOOTSTRAP, $addon)) { |
|
501 | include_once $bootstrap_path; |
||
502 | } |
||
503 | 34 | return true; |
|
504 | } |
||
505 | } |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.