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

engine/classes/Elgg/Database/Plugins.php (11 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) {
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) {
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() {
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) {
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) {
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() {
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) {
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) {
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() {
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') {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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