Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

engine/classes/Elgg/Database/Plugins.php (29 issues)

1
<?php
2
3
namespace Elgg\Database;
4
5
use DatabaseException;
6
use ElggCache;
7
use Elgg\Database;
8
use Elgg\Profilable;
9
use ElggPlugin;
10
use ElggUser;
11
use Exception;
12
13
/**
14
 * Persistent, installation-wide key-value storage.
15
 *
16
 * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
17
 *
18
 * @access private
19
 *
20
 * @since  1.10.0
21
 */
22
class Plugins {
23
24
	use Profilable;
25
26
	/**
27
	 * @var ElggPlugin[]
28
	 */
29
	protected $boot_plugins;
30
31
	/**
32
	 * @var array|null
33
	 */
34
	protected $provides_cache;
35
36
	/**
37
	 * @var string[] Active plugins, with plugin ID => GUID. Missing keys imply inactive plugins.
38
	 */
39
	protected $active_guids = [];
40
41
	/**
42
	 * @var bool Has $active_guids been populated?
43
	 */
44
	protected $active_guids_known = false;
45
46
	/**
47
	 * @var ElggCache
48
	 */
49
	protected $cache;
50
51
	/**
52
	 * @var Database
53
	 */
54
	protected $db;
55
56
	/**
57
	 * Constructor
58
	 *
59
	 * @param ElggCache $cache Cache for referencing plugins by ID
60
	 * @param Database  $db    Database
61
	 */
62 4417
	public function __construct(ElggCache $cache, Database $db) {
63 4417
		$this->cache = $cache;
64 4417
		$this->db = $db;
65 4417
	}
66
67
	/**
68
	 * Set the list of active plugins according to the boot data cache
69
	 *
70
	 * @param ElggPlugin[]|null $plugins Set of active plugins
71
	 *
72
	 * @return void
73
	 */
74 4779
	public function setBootPlugins($plugins) {
75 4779
		$this->boot_plugins = $plugins;
76 4779
		if (is_array($plugins)) {
77 4777
			foreach ($plugins as $plugin) {
78 13
				if (!$plugin instanceof ElggPlugin || !$plugin->getID()) {
79
					continue;
80
				}
81 13
				$this->cache->save($plugin->getID(), $plugin);
82
			}
83
		}
84 4779
	}
85
86
	/**
87
	 * Clear plugin caches
88
	 * @return void
89
	 */
90 1
	public function clear() {
91 1
		$this->cache->clear();
92 1
		$this->invalidateProvidesCache();
93 1
		$this->invalidateIsActiveCache();
94 1
	}
95
96
	/**
97
	 * Returns a list of plugin directory names from a base directory.
98
	 *
99
	 * @param string $dir A dir to scan for plugins. Defaults to config's plugins_path.
100
	 *                    Must have a trailing slash.
101
	 *
102
	 * @return array Array of directory names (not full paths)
103
	 * @access private
104
	 */
105 2
	public function getDirsInDir($dir = null) {
106 2
		if (!$dir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dir of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
107
			$dir = elgg_get_plugins_path();
108
		}
109
110 2
		$plugin_dirs = [];
111 2
		$handle = opendir($dir);
112
113 2
		if ($handle) {
114 2
			while ($plugin_dir = readdir($handle)) {
115
				// must be directory and not begin with a .
116 2
				if (substr($plugin_dir, 0, 1) !== '.' && is_dir($dir . $plugin_dir)) {
117 2
					$plugin_dirs[] = $plugin_dir;
118
				}
119
			}
120
		}
121
122 2
		sort($plugin_dirs);
123
124 2
		return $plugin_dirs;
125
	}
126
127
	/**
128
	 * Discovers plugins in the plugins_path setting and creates \ElggPlugin
129
	 * entities for them if they don't exist.  If there are plugins with entities
130
	 * but not actual files, will disable the \ElggPlugin entities and mark as inactive.
131
	 * The \ElggPlugin object holds config data, so don't delete.
132
	 *
133
	 * @return bool
134
	 * @throws DatabaseException
135
	 * @throws \PluginException
136
	 * @access private
137
	 */
138 2
	public function generateEntities() {
139
140 2
		$mod_dir = elgg_get_plugins_path();
141
142
		// ignore access in case this is called with no admin logged in - needed for creating plugins perhaps?
143 2
		$old_ia = elgg_set_ignore_access(true);
144
145
		// show hidden entities so that we can enable them if appropriate
146 2
		$old_access = access_show_hidden_entities(true);
147
148 2
		$known_plugins = $this->find('all');
149
		/* @var \ElggPlugin[] $known_plugins */
150
151 2
		if (!$known_plugins) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $known_plugins of type ElggPlugin[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
152 2
			$known_plugins = [];
153
		}
154
155
		// map paths to indexes
156 2
		$id_map = [];
157 2
		foreach ($known_plugins as $i => $plugin) {
158
			// if the ID is wrong, delete the plugin because we can never load it.
159
			$id = $plugin->getID();
160
			if (!$id) {
161
				$plugin->delete();
162
				unset($known_plugins[$i]);
163
				continue;
164
			}
165
			$id_map[$plugin->getID()] = $i;
166
			$plugin->cache();
167
		}
168
169 2
		$physical_plugins = $this->getDirsInDir($mod_dir);
170 2
		if (!$physical_plugins) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $physical_plugins of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
171
			elgg_set_ignore_access($old_ia);
172
173
			return false;
174
		}
175
176
		// check real plugins against known ones
177 2
		foreach ($physical_plugins as $plugin_id) {
178
			// is this already in the db?
179 2
			if (array_key_exists($plugin_id, $id_map)) {
180
				$index = $id_map[$plugin_id];
181
				$plugin = $known_plugins[$index];
182
				// was this plugin deleted and its entity disabled?
183
				if (!$plugin->isEnabled()) {
184
					$plugin->enable();
185
					$plugin->deactivate();
186
					$plugin->setPriority('last');
187
				}
188
189
				// remove from the list of plugins to disable
190
				unset($known_plugins[$index]);
191
			} else {
192
				// create new plugin
193
				// priority is forced to last in save() if not set.
194 2
				$plugin = ElggPlugin::fromId($plugin_id);
195 2
				$plugin->cache();
196
			}
197
		}
198
199
		// everything remaining in $known_plugins needs to be disabled
200
		// because they are entities, but their dirs were removed.
201
		// don't delete the entities because they hold settings.
202 2
		foreach ($known_plugins as $plugin) {
203
			if ($plugin->isActive()) {
204
				$plugin->deactivate();
205
			}
206
			// remove the priority.
207
			$name = $this->namespacePrivateSetting('internal', 'priority');
208
			remove_private_setting($plugin->guid, $name);
209
			if ($plugin->isEnabled()) {
210
				$plugin->disable();
211
			}
212
		}
213
214 2
		access_show_hidden_entities($old_access);
215 2
		elgg_set_ignore_access($old_ia);
216
217 2
		return true;
218
	}
219
220
	/**
221
	 * Cache a reference to this plugin by its ID
222
	 *
223
	 * @param ElggPlugin $plugin the plugin to cache
224
	 *
225
	 * @return void
226
	 *
227
	 * @access private
228
	 */
229 308
	public function cache(ElggPlugin $plugin) {
230 308
		if (!$plugin->getID()) {
231 288
			return;
232
		}
233 32
		$this->cache->save($plugin->getID(), $plugin);
234 32
	}
235
236
	/**
237
	 * Remove plugin from cache
238
	 *
239
	 * @param string $plugin_id Plugin ID
240
	 *
241
	 * @return void
242
	 */
243 289
	public function invalidateCache($plugin_id) {
244
		try {
245 289
			$this->cache->delete($plugin_id);
246 289
			$this->invalidateProvidesCache();
247
		} catch (\InvalidArgumentException $ex) {
248
			// A plugin must have been deactivated due to missing folder
249
			// without proper cleanup
250
			elgg_flush_caches();
251
		}
252 289
	}
253
254
	/**
255
	 * Returns an \ElggPlugin object with the path $path.
256
	 *
257
	 * @param string $plugin_id The id (dir name) of the plugin. NOT the guid.
258
	 *
259
	 * @return ElggPlugin|null
260
	 */
261 347
	public function get($plugin_id) {
262 347
		if (!$plugin_id) {
263
			return;
264
		}
265
266 347
		$fallback = function () use ($plugin_id) {
267 301
			$plugins = elgg_get_entities([
268 301
				'type' => 'object',
269 301
				'subtype' => 'plugin',
270
				'metadata_name_value_pairs' => [
271 301
					'name' => 'title',
272 301
					'value' => $plugin_id,
273
				],
274 301
				'limit' => 1,
275
				'distinct' => false,
276
			]);
277
278 301
			if ($plugins) {
279 16
				return $plugins[0];
280
			}
281
282 288
			return null;
283 347
		};
284
285 347
		$plugin = $this->cache->load($plugin_id);
286 347
		if (!isset($plugin)) {
287 301
			$plugin = $fallback();
288 301
			if ($plugin instanceof ElggPlugin) {
289 16
				$plugin->cache();
290
			}
291
		}
292
293 347
		return $plugin;
294
	}
295
296
	/**
297
	 * Returns if a plugin exists in the system.
298
	 *
299
	 * @warning This checks only plugins that are registered in the system!
300
	 * If the plugin cache is outdated, be sure to regenerate it with
301
	 * {@link _elgg_generate_plugin_objects()} first.
302
	 *
303
	 * @param string $id The plugin ID.
304
	 *
305
	 * @return bool
306
	 */
307
	function exists($id) {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
308
		return (bool) $this->get($id);
309
	}
310
311
	/**
312
	 * Returns the highest priority of the plugins
313
	 *
314
	 * @return int
315
	 * @access private
316
	 * @throws DatabaseException
317
	 */
318 7
	public function getMaxPriority() {
319 7
		$priority = $this->namespacePrivateSetting('internal', 'priority');
320
321 7
		$qb = Select::fromTable('entities', 'e');
322 7
		$qb->select('MAX(CAST(ps.value AS unsigned)) as max')
323 7
			->join('e', 'private_settings', 'ps', 'e.guid = ps.entity_guid')
324 7
			->where($qb->compare('ps.name', '=', $priority, ELGG_VALUE_STRING))
325 7
			->andWhere($qb->compare('e.type', '=', 'object', ELGG_VALUE_STRING))
326 7
			->andWhere($qb->compare('e.subtype', '=', 'plugin', ELGG_VALUE_STRING));
327
328 7
		$data = _elgg_services()->db->getDataRow($qb);
329
330 7
		$max = 1;
331 7
		if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
332 7
			$max = (int) $data->max;
333
		}
334
335 7
		return max(1, $max);
336
	}
337
338
	/**
339
	 * Returns if a plugin is active for a current site.
340
	 *
341
	 * @param string $plugin_id The plugin ID
342
	 *
343
	 * @return bool
344
	 */
345 5
	function isActive($plugin_id) {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
346 5
		if ($this->active_guids_known) {
347 1
			return isset($this->active_guids[$plugin_id]);
348
		}
349
350 4
		$site = elgg_get_site_entity();
351
352 4
		if (!($site instanceof \ElggSite)) {
353
			return false;
354
		}
355
356 4
		$plugin = $this->get($plugin_id);
357
358 4
		if (!$plugin) {
359
			return false;
360
		}
361
362 4
		return $plugin->isActive();
363
	}
364
365
	/**
366
	 * Loads all active plugins in the order specified in the tool admin panel.
367
	 *
368
	 * @note   This is called on every page load. If a plugin is active and problematic, it
369
	 * will be disabled and a visible error emitted. This does not check the deps system because
370
	 * that was too slow.
371
	 *
372
	 * @return bool
373
	 * @access private
374
	 * @throws \PluginException
375
	 */
376 18
	function load() {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
377 18
		if ($this->timer) {
378
			$this->timer->begin([__METHOD__]);
379
		}
380
381 18
		$plugins_path = elgg_get_plugins_path();
382 18
		$start_flags = ELGG_PLUGIN_INCLUDE_START |
383 18
						ELGG_PLUGIN_REGISTER_VIEWS |
384 18
						ELGG_PLUGIN_REGISTER_ACTIONS |
385 18
						ELGG_PLUGIN_REGISTER_ROUTES |
386 18
						ELGG_PLUGIN_REGISTER_LANGUAGES |
387 18
						ELGG_PLUGIN_REGISTER_WIDGETS |
388 18
						ELGG_PLUGIN_REGISTER_CLASSES;
389
	
390 18
		if (!$plugins_path) {
391
			return false;
392
		}
393
394
		// temporary disable all plugins if there is a file called 'disabled' in the plugin dir
395 18
		if (file_exists("$plugins_path/disabled")) {
396
			if (elgg_is_admin_logged_in() && elgg_in_context('admin')) {
397
				system_message(_elgg_services()->translator->translate('plugins:disabled'));
398
			}
399
400
			return false;
401
		}
402
403 18
		$config = _elgg_config();
404
405 18
		if ($config->system_cache_loaded) {
406 12
			$start_flags = $start_flags & ~ELGG_PLUGIN_REGISTER_VIEWS;
407
		}
408
409 18
		if (_elgg_services()->translator->wasLoadedFromCache()) {
410 12
			$start_flags = $start_flags & ~ELGG_PLUGIN_REGISTER_LANGUAGES;
411
		}
412
413 18
		$plugins = $this->boot_plugins;
414 18
		if (!$plugins) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $plugins of type ElggPlugin[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
415 5
			$this->active_guids_known = true;
416
417 5
			return true;
418
		}
419
420 13
		$return = true;
421 13
		foreach ($plugins as $plugin) {
422 13
			$id = $plugin->getID();
423
			try {
424 13
				$plugin->start($start_flags);
425 13
				$this->active_guids[$id] = $plugin->guid;
426
			} catch (Exception $e) {
427
				$disable_plugins = _elgg_config()->auto_disable_plugins;
428
				if ($disable_plugins === null) {
429
					$disable_plugins = true;
430
				}
431
				if ($disable_plugins) {
432
					$plugin->deactivate();
433
434
					$msg = _elgg_services()->translator->translate('PluginException:CannotStart',
435
						[$id, $plugin->guid, $e->getMessage()]);
436
					elgg_add_admin_notice("cannot_start $id", $msg);
437 13
					$return = false;
438
				}
439
			}
440
		}
441
442 13
		$this->active_guids_known = true;
443
444 13
		if ($this->timer) {
445
			$this->timer->end([__METHOD__]);
446
		}
447
448 13
		return $return;
449
	}
450
451
	/**
452
	 * Returns an ordered list of plugins
453
	 *
454
	 * @param string $status The status of the plugins. active, inactive, or all.
455
	 *
456
	 * @return ElggPlugin[]
457
	 */
458 15
	public function find($status = 'active') {
459 15
		if (!_elgg_services()->db) {
460
			return [];
461
		}
462
463 15
		if ($status === 'active' && isset($this->boot_plugins)) {
464 3
			$plugins = $this->boot_plugins;
465
		} else {
466 14
			$priority = $this->namespacePrivateSetting('internal', 'priority');
467 14
			$site_guid = 1;
468
469
			// grab plugins
470
			$options = [
471 14
				'type' => 'object',
472 14
				'subtype' => 'plugin',
473 14
				'limit' => 0,
474
				'selects' => ['ps.value'],
475 14
				'private_setting_names' => [$priority],
476
				// ORDER BY CAST(ps.value) is super slow. We usort() below.
477
				'order_by' => false,
478
			];
479
480 14
			switch ($status) {
481
				case 'active':
482 14
					$options['relationship'] = 'active_plugin';
483 14
					$options['relationship_guid'] = $site_guid;
484 14
					$options['inverse_relationship'] = true;
485 14
					break;
486
487
				case 'inactive':
488
					$options['wheres'][] = function (QueryBuilder $qb) use ($site_guid) {
489
						$subquery = $qb->subquery('entity_relationships', 'active_er');
490
						$subquery->select('*')
491
							->where($qb->compare('active_er.guid_one', '=', 'e.guid'))
492
							->andWhere($qb->compare('active_er.relationship', '=', 'active_plugin', ELGG_VALUE_STRING))
493
							->andWhere($qb->compare('active_er.guid_two', '=', 1));
494
495
						return "NOT EXISTS ({$subquery->getSQL()})";
496
					};
497
					break;
498
499
				case 'all':
500
				default:
501
					break;
502
			}
503
504 14
			$old_ia = elgg_set_ignore_access(true);
505 14
			$plugins = elgg_get_entities($options) ? : [];
506 14
			elgg_set_ignore_access($old_ia);
507
		}
508
509 15
		usort($plugins, function (ElggPlugin $a, ElggPlugin $b) {
510 15
			$a_value = $a->getVolatileData('select:value');
511 15
			$b_value = $b->getVolatileData('select:value');
512
513 15
			if ($b_value !== $a_value) {
514 15
				return $a_value - $b_value;
515
			} else {
516 15
				return $a->guid - $b->guid;
517
			}
518 15
		});
519
520 15
		return $plugins;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $plugins returns the type integer which is incompatible with the documented return type ElggPlugin[].
Loading history...
521
	}
522
523
	/**
524
	 * Reorder plugins to an order specified by the array.
525
	 * Plugins not included in this array will be appended to the end.
526
	 *
527
	 * @note   This doesn't use the \ElggPlugin->setPriority() method because
528
	 *       all plugins are being changed and we don't want it to automatically
529
	 *       reorder plugins.
530
	 * @todo   Can this be done in a single sql command?
531
	 *
532
	 * @param array $order An array of plugin ids in the order to set them
533
	 *
534
	 * @return bool
535
	 * @access private
536
	 */
537
	function setPriorities(array $order) {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
538
		$name = $this->namespacePrivateSetting('internal', 'priority');
539
540
		$plugins = $this->find('any');
541
		if (!$plugins) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $plugins of type ElggPlugin[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
542
			return false;
543
		}
544
545
		// reindex to get standard counting. no need to increment by 10.
546
		// though we do start with 1
547
		$order = array_values($order);
548
549
		$missing_plugins = [];
550
		/* @var ElggPlugin[] $missing_plugins */
551
552
		$priority = 0;
553
		foreach ($plugins as $plugin) {
554
			$plugin_id = $plugin->getID();
555
556
			if (!in_array($plugin_id, $order)) {
557
				$missing_plugins[] = $plugin;
558
				continue;
559
			}
560
561
			$priority = array_search($plugin_id, $order) + 1;
562
563
			if (!$plugin->setPrivateSetting($name, $priority)) {
564
				return false;
565
			}
566
		}
567
568
		// set the missing plugins' priorities
569
		if (empty($missing_plugins)) {
570
			return true;
571
		}
572
573
		foreach ($missing_plugins as $plugin) {
574
			$priority++;
575
			if (!$plugin->setPrivateSetting($name, $priority)) {
576
				return false;
577
			}
578
		}
579
580
		return true;
581
	}
582
583
	/**
584
	 * Reindexes all plugin priorities starting at 1.
585
	 *
586
	 * @return bool
587
	 * @access private
588
	 */
589
	function reindexPriorities() {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
590
		return $this->setPriorities([]);
591
	}
592
593
	/**
594
	 * Namespaces a string to be used as a private setting name for a plugin.
595
	 *
596
	 * For user_settings, two namespaces are added: a user setting namespace and the
597
	 * plugin id.
598
	 *
599
	 * For internal (plugin priority), there is a single internal namespace added.
600
	 *
601
	 * @param string $type The type of setting: user_setting or internal.
602
	 * @param string $name The name to namespace.
603
	 * @param string $id   The plugin's ID to namespace with.  Required for user_setting.
604
	 *
605
	 * @return string
606
	 * @access private
607
	 */
608 39
	function namespacePrivateSetting($type, $name, $id = null) {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
609 39
		switch ($type) {
610
			case 'user_setting':
611 39
				if (!$id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
612
					throw new \InvalidArgumentException("You must pass the plugin id for user settings");
613
				}
614 39
				$name = ELGG_PLUGIN_USER_SETTING_PREFIX . "$id:$name";
615 39
				break;
616
617
			case 'internal':
618 20
				$name = ELGG_PLUGIN_INTERNAL_PREFIX . $name;
619 20
				break;
620
		}
621
622 39
		return $name;
623
	}
624
625
626
	/**
627
	 * Returns an array of all provides from all active plugins.
628
	 *
629
	 * Array in the form array(
630
	 *    'provide_type' => array(
631
	 *        'provided_name' => array(
632
	 *            'version' => '1.8',
633
	 *            'provided_by' => 'provider_plugin_id'
634
	 *    )
635
	 *  )
636
	 * )
637
	 *
638
	 * @param string $type The type of provides to return
639
	 * @param string $name A specific provided name to return. Requires $provide_type.
640
	 *
641
	 * @return array|false
642
	 * @access private
643
	 */
644
	function getProvides($type = null, $name = null) {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
645
		if ($this->provides_cache === null) {
646
			$active_plugins = $this->find('active');
647
648
			$provides = [];
649
650
			foreach ($active_plugins as $plugin) {
651
				$plugin_provides = [];
652
				$manifest = $plugin->getManifest();
653
				if ($manifest instanceof \ElggPluginManifest) {
654
					$plugin_provides = $plugin->getManifest()->getProvides();
655
				}
656
				if ($plugin_provides) {
657
					foreach ($plugin_provides as $provided) {
658
						$provides[$provided['type']][$provided['name']] = [
659
							'version' => $provided['version'],
660
							'provided_by' => $plugin->getID()
661
						];
662
					}
663
				}
664
			}
665
666
			$this->provides_cache = $provides;
667
		}
668
669
		if ($type && $name) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $name of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
670
			if (isset($this->provides_cache[$type][$name])) {
671
				return $this->provides_cache[$type][$name];
672
			} else {
673
				return false;
674
			}
675
		} else if ($type) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
676
			if (isset($this->provides_cache[$type])) {
677
				return $this->provides_cache[$type];
678
			} else {
679
				return false;
680
			}
681
		}
682
683
		return $this->provides_cache;
684
	}
685
686
	/**
687
	 * Deletes all cached data on plugins being provided.
688
	 *
689
	 * @return boolean
690
	 * @access private
691
	 */
692 289
	function invalidateProvidesCache() {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
693 289
		$this->provides_cache = null;
694
695 289
		return true;
696
	}
697
698
	/**
699
	 * Delete the cache holding whether plugins are active or not
700
	 *
701
	 * @return void
702
	 * @access private
703
	 */
704 3
	public function invalidateIsActiveCache() {
705 3
		$this->active_guids = [];
706 3
		$this->active_guids_known = false;
707 3
	}
708
709
	/**
710
	 * Checks if a plugin is currently providing $type and $name, and optionally
711
	 * checking a version.
712
	 *
713
	 * @param string $type       The type of the provide
714
	 * @param string $name       The name of the provide
715
	 * @param string $version    A version to check against
716
	 * @param string $comparison The comparison operator to use in version_compare()
717
	 *
718
	 * @return array An array in the form array(
719
	 *    'status' => bool Does the provide exist?,
720
	 *    'value' => string The version provided
721
	 * )
722
	 * @access private
723
	 */
724
	function checkProvides($type, $name, $version = null, $comparison = 'ge') {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
725
		$provided = $this->getProvides($type, $name);
726
		if (!$provided) {
727
			return [
728
				'status' => false,
729
				'value' => ''
730
			];
731
		}
732
733
		if ($version) {
734
			$status = version_compare($provided['version'], $version, $comparison);
735
		} else {
736
			$status = true;
737
		}
738
739
		return [
740
			'status' => $status,
741
			'value' => $provided['version']
742
		];
743
	}
744
745
	/**
746
	 * Returns an array of parsed strings for a dependency in the
747
	 * format: array(
748
	 *    'type'            =>    requires, conflicts, or provides.
749
	 *    'name'            =>    The name of the requirement / conflict
750
	 *    'value'            =>    A string representing the expected value: <1, >=3, !=enabled
751
	 *    'local_value'    =>    The current value, ("Not installed")
752
	 *    'comment'        =>    Free form text to help resovle the problem ("Enable / Search for plugin <link>")
753
	 * )
754
	 *
755
	 * @param array $dep An \ElggPluginPackage dependency array
756
	 *
757
	 * @return array
758
	 * @access private
759
	 */
760
	function getDependencyStrings($dep) {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
761
		$translator = _elgg_services()->translator;
762
		$dep_system = elgg_extract('type', $dep);
763
		$info = elgg_extract('dep', $dep);
764
		$type = elgg_extract('type', $info);
765
766
		if (!$dep_system || !$info || !$type) {
767
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
768
		}
769
770
		// rewrite some of these to be more readable
771
		$comparison = elgg_extract('comparison', $info);
772
		switch ($comparison) {
773
			case 'lt':
774
				$comparison = '<';
775
				break;
776
			case 'gt':
777
				$comparison = '>';
778
				break;
779
			case 'ge':
780
				$comparison = '>=';
781
				break;
782
			case 'le':
783
				$comparison = '<=';
784
				break;
785
			default:
786
				//keep $comparison value intact
787
				break;
788
		}
789
790
		/*
791
		'requires'	'plugin oauth_lib'	<1.3	1.3		'downgrade'
792
		'requires'	'php setting bob'	>3		3		'change it'
793
		'conflicts'	'php setting'		>3		4		'change it'
794
		'conflicted''plugin profile'	any		1.8		'disable profile'
795
		'provides'	'plugin oauth_lib'	1.3		--		--
796
		'priority'	'before blog'		--		after	'move it'
797
		*/
798
		$strings = [];
799
		$strings['type'] = $translator->translate('ElggPlugin:Dependencies:' . ucwords($dep_system));
800
801
		switch ($type) {
802
			case 'elgg_release':
803
				// 'Elgg Version'
804
				$strings['name'] = $translator->translate('ElggPlugin:Dependencies:Elgg');
805
				$strings['expected_value'] = "$comparison {$info['version']}";
806
				$strings['local_value'] = $dep['value'];
807
				$strings['comment'] = '';
808
				break;
809
810
			case 'php_version':
811
				// 'PHP version'
812
				$strings['name'] = $translator->translate('ElggPlugin:Dependencies:PhpVersion');
813
				$strings['expected_value'] = "$comparison {$info['version']}";
814
				$strings['local_value'] = $dep['value'];
815
				$strings['comment'] = '';
816
				break;
817
818
			case 'php_extension':
819
				// PHP Extension %s [version]
820
				$strings['name'] = $translator->translate('ElggPlugin:Dependencies:PhpExtension', [$info['name']]);
821
				if ($info['version']) {
822
					$strings['expected_value'] = "$comparison {$info['version']}";
823
					$strings['local_value'] = $dep['value'];
824
				} else {
825
					$strings['expected_value'] = '';
826
					$strings['local_value'] = '';
827
				}
828
				$strings['comment'] = '';
829
				break;
830
831
			case 'php_ini':
832
				$strings['name'] = $translator->translate('ElggPlugin:Dependencies:PhpIni', [$info['name']]);
833
				$strings['expected_value'] = "$comparison {$info['value']}";
834
				$strings['local_value'] = $dep['value'];
835
				$strings['comment'] = '';
836
				break;
837
838
			case 'plugin':
839
				$strings['name'] = $translator->translate('ElggPlugin:Dependencies:Plugin', [$info['name']]);
840
				$expected = $info['version'] ? "$comparison {$info['version']}" : $translator->translate('any');
841
				$strings['expected_value'] = $expected;
842
				$strings['local_value'] = $dep['value'] ? $dep['value'] : '--';
843
				$strings['comment'] = '';
844
				break;
845
846
			case 'priority':
847
				$expected_priority = ucwords($info['priority']);
848
				$real_priority = ucwords($dep['value']);
849
				$strings['name'] = $translator->translate('ElggPlugin:Dependencies:Priority');
850
				$strings['expected_value'] = $translator->translate("ElggPlugin:Dependencies:Priority:$expected_priority", [$info['plugin']]);
851
				$strings['local_value'] = $translator->translate("ElggPlugin:Dependencies:Priority:$real_priority", [$info['plugin']]);
852
				$strings['comment'] = '';
853
				break;
854
		}
855
856
		if ($dep['type'] == 'suggests') {
857
			if ($dep['status']) {
858
				$strings['comment'] = $translator->translate('ok');
859
			} else {
860
				$strings['comment'] = $translator->translate('ElggPlugin:Dependencies:Suggests:Unsatisfied');
861
			}
862
		} else {
863
			if ($dep['status']) {
864
				$strings['comment'] = $translator->translate('ok');
865
			} else {
866
				$strings['comment'] = $translator->translate('error');
867
			}
868
		}
869
870
		return $strings;
871
	}
872
873
	/**
874
	 * Get all settings (excluding user settings) for a plugin
875
	 *
876
	 * @param \ElggPlugin $plugin Plugin
877
	 *
878
	 * @return string[]
879
	 * @throws DatabaseException
880
	 */
881 39
	public function getAllSettings(ElggPlugin $plugin) {
882 39
		if (!$plugin->guid) {
883
			return [];
884
		}
885
886 39
		$values = _elgg_services()->privateSettingsCache->load($plugin->guid);
887 39
		if (isset($values)) {
888 21
			return $values;
889
		}
890
891 39
		$us_prefix = $this->namespacePrivateSetting('user_setting', '', $plugin->getID());
892
893
		// Get private settings for user
894 39
		$qb = Select::fromTable('private_settings');
895 39
		$qb->select('name')
896 39
			->addSelect('value')
897 39
			->where($qb->compare('name', 'not like', "$us_prefix%", ELGG_VALUE_STRING))
898 39
			->andWhere($qb->compare('entity_guid', '=', $plugin->guid, ELGG_VALUE_GUID));
899
900 39
		$rows = $this->db->getData($qb);
901
902 39
		$settings = [];
903
904 39
		if (!empty($rows)) {
905 18
			foreach ($rows as $row) {
906 18
				$settings[$row->name] = $row->value;
907
			}
908
		}
909
910 39
		_elgg_services()->privateSettingsCache->save($plugin->guid, $settings);
911
912 39
		return $settings;
913
	}
914
915
	/**
916
	 * Returns an array of all plugin user settings for a user
917
	 *
918
	 * @param ElggPlugin $plugin Plugin
919
	 * @param ElggUser   $user   User
920
	 *
921
	 * @return array
922
	 * @see  ElggPlugin::getAllUserSettings()
923
	 * @throws DatabaseException
924
	 */
925 1
	public function getAllUserSettings(ElggPlugin $plugin, ElggUser $user = null) {
926
927
		// send an empty name so we just get the first part of the namespace
928 1
		$prefix = $this->namespacePrivateSetting('user_setting', '', $plugin->getID());
929
930 1
		$qb = Select::fromTable('private_settings');
931 1
		$qb->select('name')
932 1
			->addSelect('value')
933 1
			->where($qb->compare('name', 'like', "{$prefix}%"));
934
935 1
		if ($user) {
936 1
			$qb->andWhere($qb->compare('entity_guid', '=', $user->guid, ELGG_VALUE_INTEGER));
937
		}
938
939 1
		$rows = $this->db->getData($qb);
940
941 1
		$settings = [];
942
943 1
		if (!empty($rows)) {
944 1
			foreach ($rows as $rows) {
945 1
				$name = substr($rows->name, strlen($prefix));
946 1
				$value = $rows->value;
947
948 1
				$settings[$name] = $value;
949
			}
950
		}
951
952 1
		return $settings;
953
	}
954
955
	/**
956
	 * Set a user specific setting for a plugin.
957
	 *
958
	 * @param string $name      The name. Note: cannot be "title".
959
	 * @param mixed  $value     The value.
960
	 * @param int    $user_guid The user GUID or 0 for the currently logged in user.
961
	 * @param string $plugin_id The plugin ID (Required)
962
	 *
963
	 * @return bool
964
	 * @see \ElggPlugin::setUserSetting()
965
	 */
966
	function setUserSetting($name, $value, $user_guid = 0, $plugin_id = null) {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
967
		$plugin = $this->get($plugin_id);
968
		if (!$plugin) {
969
			return false;
970
		}
971
972
		return $plugin->setUserSetting($name, $value, (int) $user_guid);
973
	}
974
975
	/**
976
	 * Unsets a user-specific plugin setting
977
	 *
978
	 * @param string $name      Name of the setting
979
	 * @param int    $user_guid The user GUID or 0 for the currently logged in user.
980
	 * @param string $plugin_id The plugin ID (Required)
981
	 *
982
	 * @return bool
983
	 * @see \ElggPlugin::unsetUserSetting()
984
	 */
985
	function unsetUserSetting($name, $user_guid = 0, $plugin_id = null) {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
986
		$plugin = $this->get($plugin_id);
987
		if (!$plugin) {
988
			return false;
989
		}
990
991
		return $plugin->unsetUserSetting($name, (int) $user_guid);
992
	}
993
994
	/**
995
	 * Get a user specific setting for a plugin.
996
	 *
997
	 * @param string $name      The name of the setting.
998
	 * @param int    $user_guid The user GUID or 0 for the currently logged in user.
999
	 * @param string $plugin_id The plugin ID (Required)
1000
	 * @param mixed  $default   The default value to return if none is set
1001
	 *
1002
	 * @return mixed
1003
	 * @see \ElggPlugin::getUserSetting()
1004
	 */
1005
	function getUserSetting($name, $user_guid = 0, $plugin_id = null, $default = null) {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1006
		$plugin = $this->get($plugin_id);
1007
		if (!$plugin) {
1008
			return false;
1009
		}
1010
1011
		return $plugin->getUserSetting($name, (int) $user_guid, $default);
1012
	}
1013
1014
	/**
1015
	 * Set a setting for a plugin.
1016
	 *
1017
	 * @param string $name      The name of the setting - note, can't be "title".
1018
	 * @param mixed  $value     The value.
1019
	 * @param string $plugin_id The plugin ID (Required)
1020
	 *
1021
	 * @return bool
1022
	 * @see \ElggPlugin::setSetting()
1023
	 */
1024
	function setSetting($name, $value, $plugin_id) {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1025
		$plugin = $this->get($plugin_id);
1026
		if (!$plugin) {
1027
			return false;
1028
		}
1029
1030
		return $plugin->setSetting($name, $value);
1031
	}
1032
1033
	/**
1034
	 * Get setting for a plugin.
1035
	 *
1036
	 * @param string $name      The name of the setting.
1037
	 * @param string $plugin_id The plugin ID (Required)
1038
	 * @param mixed  $default   The default value to return if none is set
1039
	 *
1040
	 * @return mixed
1041
	 * @see \ElggPlugin::getSetting()
1042
	 */
1043 31
	function getSetting($name, $plugin_id, $default = null) {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1044 31
		$plugin = $this->get($plugin_id);
1045 31
		if (!$plugin) {
1046
			return false;
1047
		}
1048
1049 31
		return $plugin->getSetting($name, $default);
1050
	}
1051
1052
	/**
1053
	 * Unsets a plugin setting.
1054
	 *
1055
	 * @param string $name      The name of the setting.
1056
	 * @param string $plugin_id The plugin ID (Required)
1057
	 *
1058
	 * @return bool
1059
	 * @see \ElggPlugin::unsetSetting()
1060
	 */
1061
	function unsetSetting($name, $plugin_id) {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1062
		$plugin = $this->get($plugin_id);
1063
		if (!$plugin) {
1064
			return false;
1065
		}
1066
1067
		return $plugin->unsetSetting($name);
1068
	}
1069
1070
	/**
1071
	 * Unsets all plugin settings for a plugin.
1072
	 *
1073
	 * @param string $plugin_id The plugin ID (Required)
1074
	 *
1075
	 * @return bool
1076
	 * @see \ElggPlugin::unsetAllSettings()
1077
	 */
1078
	function unsetAllSettings($plugin_id) {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1079
		$plugin = $this->get($plugin_id);
1080
		if (!$plugin) {
1081
			return false;
1082
		}
1083
1084
		return $plugin->unsetAllSettings();
1085
	}
1086
1087
	/**
1088
	 * Returns entities based upon plugin user settings.
1089
	 * Takes all the options for {@link elgg_get_entities_from_private_settings()}
1090
	 * in addition to the ones below.
1091
	 *
1092
	 * @param array $options Array in the format:
1093
	 *
1094
	 *    plugin_id => STR The plugin id. Required.
1095
	 *
1096
	 *    plugin_user_setting_names => null|ARR private setting names
1097
	 *
1098
	 *    plugin_user_setting_values => null|ARR metadata values
1099
	 *
1100
	 *    plugin_user_setting_name_value_pairs => null|ARR (
1101
	 *                                         name => 'name',
1102
	 *                                         value => 'value',
1103
	 *                                         'operand' => '=',
1104
	 *                                        )
1105
	 *                                 Currently if multiple values are sent via
1106
	 *                               an array (value => array('value1', 'value2')
1107
	 *                               the pair's operand will be forced to "IN".
1108
	 *
1109
	 *    plugin_user_setting_name_value_pairs_operator => null|STR The operator to use for combining
1110
	 *                                        (name = value) OPERATOR (name = value); default AND
1111
	 *
1112
	 * @return mixed int If count, int. If not count, array. false on errors.
1113
	 */
1114
	public function getEntitiesFromUserSettings(array $options = []) {
1115
		$singulars = [
1116
			'plugin_user_setting_name',
1117
			'plugin_user_setting_value',
1118
			'plugin_user_setting_name_value_pair'
1119
		];
1120
1121
		$options = _elgg_normalize_plural_options_array($options, $singulars);
1122
1123
		// rewrite plugin_user_setting_name_* to the right PS ones.
1124
		$map = [
1125
			'plugin_user_setting_names' => 'private_setting_names',
1126
			'plugin_user_setting_values' => 'private_setting_values',
1127
			'plugin_user_setting_name_value_pairs' => 'private_setting_name_value_pairs',
1128
			'plugin_user_setting_name_value_pairs_operator' => 'private_setting_name_value_pairs_operator',
1129
		];
1130
1131
		foreach ($map as $plugin => $private) {
1132
			if (!isset($options[$plugin])) {
1133
				continue;
1134
			}
1135
1136
			if (isset($options[$private])) {
1137
				if (!is_array($options[$private])) {
1138
					$options[$private] = [$options[$private]];
1139
				}
1140
1141
				$options[$private] = array_merge($options[$private], $options[$plugin]);
1142
			} else {
1143
				$options[$private] = $options[$plugin];
1144
			}
1145
		}
1146
1147
		$prefix = $this->namespacePrivateSetting('user_setting', '', $options['plugin_id']);
1148
		$options['private_setting_name_prefix'] = $prefix;
1149
1150
		return elgg_get_entities($options);
1151
	}
1152
1153
	/**
1154
	 * Set plugin priority and adjust the priorities of other plugins
1155
	 *
1156
	 * @param ElggPlugin $plugin   Plugin
1157
	 * @param int        $priority New priority
1158
	 *
1159
	 * @return int|false
1160
	 * @throws DatabaseException
1161
	 */
1162 7
	public function setPriority(ElggPlugin $plugin, $priority) {
1163
1164 7
		$old_priority = $plugin->getPriority() ? : 1;
1165
1166 7
		$name = $this->namespacePrivateSetting('internal', 'priority');
1167
1168 7
		if (!$plugin->guid) {
1169
			return false;
1170
		}
1171
1172 7
		if (!$plugin->setPrivateSetting($name, $priority)) {
1173
			return false;
1174
		}
1175
1176 7
		$qb = Update::table('private_settings');
1177 7
		$qb->where($qb->compare('name', '=', $name, ELGG_VALUE_STRING))
1178 7
			->andWhere($qb->between('CAST(value AS UNSIGNED)', $old_priority, $priority, ELGG_VALUE_INTEGER))
1179 7
			->andWhere($qb->compare('entity_guid', '!=', $plugin->guid, ELGG_VALUE_INTEGER));
1180
1181 7
		if ($priority > $old_priority) {
1182 7
			$qb->set('value', "CAST(value AS UNSIGNED) - 1");
1183
		} else {
1184 1
			$qb->set('value', "CAST(value AS UNSIGNED) + 1");
1185
		}
1186
1187 7
		if (!$this->db->updateData($qb)) {
1188
			return false;
1189
		}
1190
1191 7
		return $priority;
1192
	}
1193
}
1194