Completed
Push — master ( fab0a4...8e249d )
by Jeroen
41:43 queued 16:43
created

ElggPlugin::__set()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 20
Code Lines 10

Duplication

Lines 20
Ratio 100 %

Code Coverage

Tests 8
CRAP Score 5.5069

Importance

Changes 0
Metric Value
cc 5
eloc 10
nc 4
nop 2
dl 20
loc 20
rs 8.8571
c 0
b 0
f 0
ccs 8
cts 11
cp 0.7272
crap 5.5069
1
<?php
2
3
use Elgg\Includer;
4
use Elgg\Application;
5
6
/**
7
 * Stores site-side plugin settings as private data.
8
 *
9
 * This class is currently a stub, allowing a plugin to
10
 * save settings in an object's private settings for each site.
11
 *
12
 * @package    Elgg.Core
13
 * @subpackage Plugins.Settings
14
 */
15
class ElggPlugin extends \ElggObject {
16
	private $package;
17
	private $manifest;
18
19
	/**
20
	 * Data from static config file. null if not yet read.
21
	 *
22
	 * @var array|null
23
	 */
24
	private $static_config;
25
26
	private $path;
27
	private $errorMsg = '';
28
29
	/**
30
	 * Set subtype to 'plugin'
31
	 *
32
	 * @return void
33
	 */
34 311
	protected function initializeAttributes() {
35 311
		parent::initializeAttributes();
36
37 311
		$this->attributes['subtype'] = "plugin";
38
39
		// plugins must be public.
40 311
		$this->access_id = ACCESS_PUBLIC;
41 311
	}
42
43
	/**
44
	 * Creates a new plugin from path
45
	 *
46
	 * @note Internal: also supports database objects
47
	 *
48
	 * @warning Unlike other \ElggEntity objects, you cannot null instantiate
49
	 *          \ElggPlugin. You must provide the path to the plugin directory.
50
	 *
51
	 * @param string $path The absolute path of the plugin
52
	 *
53
	 * @throws PluginException
54
	 */
55 311
	public function __construct($path) {
56 311
		if (!$path) {
57
			throw new \PluginException("ElggPlugin cannot be null instantiated. You must pass a full path.");
58
		}
59
		
60 311
		if (is_object($path)) {
61
			// database object
62 311
			parent::__construct($path);
63 311
			$this->path = _elgg_config()->plugins_path . $this->getID();
64 311
			_elgg_cache_plugin_by_id($this);
65 311
			return;
66
		}
67
68 2
		if (is_numeric($path)) {
69
			// guid
70
			// @todo plugins with directory names of '12345'
71
			throw new \InvalidArgumentException('$path cannot be a GUID');
72
		}
73
74 2
		$this->initializeAttributes();
75
76
		// path checking is done in the package
77 2
		$path = sanitise_filepath($path);
78 2
		$this->path = $path;
79 2
		$path_parts = explode('/', rtrim($path, '/'));
80 2
		$plugin_id = array_pop($path_parts);
81 2
		$this->title = $plugin_id;
82
83
		// check if we're loading an existing plugin
84 2
		$existing_plugin = elgg_get_plugin_from_id($plugin_id);
85
86 2
		if ($existing_plugin) {
87 2
			$this->load((object) ['guid' => $existing_plugin->guid]);
88
		}
89
90 2
		_elgg_cache_plugin_by_id($this);
91 2
	}
92
93
	/**
94
	 * Save the plugin object.  Make sure required values exist.
95
	 *
96
	 * @see \ElggObject::save()
97
	 * @return bool
98
	 */
99
	public function save() {
100
		// own by the current site so users can be deleted without affecting plugins
101
		$site = elgg_get_site_entity();
102
		$this->attributes['owner_guid'] = $site->guid;
103
		$this->attributes['container_guid'] = $site->guid;
104
		
105
		if (parent::save()) {
106
			// make sure we have a priority
107
			$priority = $this->getPriority();
108
			if ($priority === false || $priority === null) {
109
				return $this->setPriority('last');
110
			}
111
		} else {
112
			return false;
113
		}
114
	}
115
116
117
	// Plugin ID and path
118
119
	/**
120
	 * Returns the ID (dir name) of this plugin
121
	 *
122
	 * @return string
123
	 */
124 311
	public function getID() {
125 311
		return $this->title;
126
	}
127
128
	/**
129
	 * Returns the manifest's name if available, otherwise the ID.
130
	 *
131
	 * @return string
132
	 * @since 3.0
133
	 */
134
	public function getDisplayName() {
135
		$manifest = $this->getManifest();
136
		if ($manifest) {
137
			return $manifest->getName();
138
		}
139
140
		return $this->getID();
141
	}
142
143
	/**
144
	 * Returns the manifest's name if available, otherwise the ID.
145
	 *
146
	 * @return string
147
	 * @since 1.8.1
148
	 * @deprecated 3.0
149
	 */
150
	public function getFriendlyName() {
151
		elgg_deprecated_notice(__METHOD__ . ' is deprecated. Use getDisplayName().', '3.0');
152
	
153
		return $this->getDisplayName();
154
	}
155
156
	/**
157
	 * Returns the plugin's full path with trailing slash.
158
	 *
159
	 * @return string
160
	 */
161 311
	public function getPath() {
162 311
		return sanitise_filepath($this->path);
163
	}
164
165
	/**
166
	 * Get a value from the plugins's static config file.
167
	 *
168
	 * @note If the system cache is on, Elgg APIs should not call this on every request.
169
	 *
170
	 * @param string $key     Config key
171
	 * @param mixed  $default Value returned if missing
172
	 *
173
	 * @return mixed
174
	 * @throws PluginException
175
	 * @access private
176
	 * @internal For Elgg internal use only
177
	 */
178 311
	public function getStaticConfig($key, $default = null) {
179 311
		if ($this->static_config === null) {
180 311
			$this->static_config = [];
181
182 311
			if ($this->canReadFile(ElggPluginPackage::STATIC_CONFIG_FILENAME)) {
183 311
				$this->static_config = $this->includeFile(ElggPluginPackage::STATIC_CONFIG_FILENAME);
184
			}
185
		}
186
187 311
		if (!empty($this->static_config[$key])) {
188 311
			return $this->static_config[$key];
189
		} else {
190 311
			return $default;
191
		}
192
	}
193
194
	/**
195
	 * Sets the location of this plugin.
196
	 *
197
	 * @param string $id The path to the plugin's dir.
198
	 * @return bool
199
	 */
200
	public function setID($id) {
201
		return $this->title = $id;
202
	}
203
204
	/**
205
	 * Returns an array of available markdown files for this plugin
206
	 *
207
	 * @return array
208
	 */
209
	public function getAvailableTextFiles() {
210
		$filenames = $this->getPackage()->getTextFilenames();
211
212
		$files = [];
213
		foreach ($filenames as $filename) {
214
			if ($this->canReadFile($filename)) {
215
				$files[$filename] = "$this->path/$filename";
216
			}
217
		}
218
219
		return $files;
220
	}
221
222
	// Load Priority
223
224
	/**
225
	 * Gets the plugin's load priority.
226
	 *
227
	 * @return int
228
	 */
229 1
	public function getPriority() {
230 1
		$name = _elgg_namespace_plugin_private_setting('internal', 'priority');
231 1
		return $this->getSetting($name);
232
	}
233
234
	/**
235
	 * Sets the priority of the plugin
236
	 *
237
	 * @param mixed $priority The priority to set. One of +1, -1, first, last, or a number.
238
	 *                        If given a number, this will displace all plugins at that number
239
	 *                        and set their priorities +1
240
	 * @return bool
241
	 */
242 1
	public function setPriority($priority) {
243 1
		if (!$this->guid) {
244
			return false;
245
		}
246
247 1
		$db = $this->getDatabase();
248 1
		$name = _elgg_namespace_plugin_private_setting('internal', 'priority');
249
		// if no priority assume a priority of 1
250 1
		$old_priority = (int) $this->getPriority();
251 1
		$old_priority = (!$old_priority) ? 1 : $old_priority;
252 1
		$max_priority = _elgg_get_max_plugin_priority();
253
254
		// can't use switch here because it's not strict and
255
		// php evaluates +1 == 1
256 1
		if ($priority === '+1') {
257
			$priority = $old_priority + 1;
258 1
		} elseif ($priority === '-1') {
259
			$priority = $old_priority - 1;
260 1
		} elseif ($priority === 'first') {
261
			$priority = 1;
262 1
		} elseif ($priority === 'last') {
263
			$priority = $max_priority;
264
		}
265
266
		// should be a number by now
267 1
		if ($priority > 0) {
268
			if (!is_numeric($priority)) {
269
				return false;
270
			}
271
272
			// there's nothing above the max.
273
			if ($priority > $max_priority) {
274
				$priority = $max_priority;
275
			}
276
277
			// there's nothing below 1.
278
			if ($priority < 1) {
279
				$priority = 1;
280
			}
281
282
			if ($priority > $old_priority) {
283
				$op = '-';
284
				$where = "CAST(value as unsigned) BETWEEN $old_priority AND $priority";
285
			} else {
286
				$op = '+';
287
				$where = "CAST(value as unsigned) BETWEEN $priority AND $old_priority";
288
			}
289
290
			// displace the ones affected by this change
291
			$q = "UPDATE {$db->prefix}private_settings
292
				SET value = CAST(value as unsigned) $op 1
293
				WHERE entity_guid != $this->guid
294
				AND name = '$name'
295
				AND $where";
296
297
			if (!$db->updateData($q)) {
298
				return false;
299
			}
300
301
			// set this priority
302
			if ($this->setPrivateSetting($name, $priority)) {
303
				return true;
304
			} else {
305
				return false;
306
			}
307
		}
308
309 1
		return false;
310
	}
311
312
313
	// Plugin settings
314
315
	/**
316
	 * Returns a plugin setting
317
	 *
318
	 * @param string $name    The setting name
319
	 * @param mixed  $default The default value to return if none is set
320
	 * @return mixed
321
	 */
322 311
	public function getSetting($name, $default = null) {
323 311
		$values = $this->getAllSettings();
324
325 311
		return elgg_extract($name, $values, $default);
0 ignored issues
show
Security Bug introduced by
It seems like $values defined by $this->getAllSettings() on line 323 can also be of type false; however, elgg_extract() does only seem to accept array, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
326
	}
327
328
	/**
329
	 * Returns an array of all settings saved for this plugin.
330
	 *
331
	 * @note Unlike user settings, plugin settings are not namespaced.
332
	 *
333
	 * @return array An array of key/value pairs.
334
	 */
335 311
	public function getAllSettings() {
336
		
337 311
		$defaults = $this->getStaticConfig('settings', []);
338 311
		$values = _elgg_services()->pluginSettingsCache->getAll($this->guid);
339
		
340 311
		if ($values !== null) {
341 311
			return array_merge($defaults, $values);
342
		}
343
344 2
		if (!$this->guid) {
345
			return false;
346
		}
347
348 2
		$db_prefix = _elgg_config()->dbprefix;
349
		// need to remove all namespaced private settings.
350 2
		$us_prefix = _elgg_namespace_plugin_private_setting('user_setting', '', $this->getID());
351
352
		// Get private settings for user
353 2
		$q = "SELECT * FROM {$db_prefix}private_settings
354 2
			WHERE entity_guid = $this->guid
355 2
			AND name NOT LIKE '$us_prefix%'";
356
357 2
		$private_settings = $this->getDatabase()->getData($q);
358
359 2
		$return = [];
360
361 2
		if ($private_settings) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $private_settings 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...
362 2
			foreach ($private_settings as $setting) {
363 2
				$return[$setting->name] = $setting->value;
364
			}
365
		}
366
367 2
		return array_merge($defaults, $return);
368
	}
369
370
	/**
371
	 * Set a plugin setting for the plugin
372
	 *
373
	 * @todo This will only work once the plugin has a GUID.
374
	 *
375
	 * @param string $name  The name to set
376
	 * @param string $value The value to set
377
	 *
378
	 * @return bool
379
	 */
380
	public function setSetting($name, $value) {
381
		if (!$this->guid) {
382
			return false;
383
		}
384
		
385
		// Hook to validate setting
386
		$value = elgg_trigger_plugin_hook('setting', 'plugin', [
387
			'plugin_id' => $this->getID(),
388
			'plugin' => $this,
389
			'name' => $name,
390
			'value' => $value,
391
		], $value);
392
		
393
		if (is_array($value)) {
394
			elgg_log('Plugin settings cannot store arrays.', 'ERROR');
395
			return false;
396
		}
397
		
398
		return $this->setPrivateSetting($name, $value);
399
	}
400
401
	/**
402
	 * Removes a plugin setting name and value.
403
	 *
404
	 * @param string $name The setting name to remove
405
	 *
406
	 * @return bool
407
	 */
408
	public function unsetSetting($name) {
409
		return remove_private_setting($this->guid, $name);
410
	}
411
412
	/**
413
	 * Removes all settings for this plugin.
414
	 *
415
	 * @todo Should be a better way to do this without dropping to raw SQL.
416
	 * @todo If we could namespace the plugin settings this would be cleaner.
417
	 * @todo this shouldn't work because ps_prefix will be empty string
418
	 * @return bool
419
	 */
420
	public function unsetAllSettings() {
421
		_elgg_services()->pluginSettingsCache->clear($this->guid);
422
		_elgg_services()->boot->invalidateCache();
423
424
		$db = $this->getDatabase();
425
		$us_prefix = _elgg_namespace_plugin_private_setting('user_setting', '', $this->getID());
426
		$is_prefix = _elgg_namespace_plugin_private_setting('internal', '', $this->getID());
427
428
		$q = "DELETE FROM {$db->prefix}private_settings
429
			WHERE entity_guid = $this->guid
430
			AND name NOT LIKE '$us_prefix%'
431
			AND name NOT LIKE '$is_prefix%'";
432
433
		return $db->deleteData($q);
434
	}
435
436
437
	// User settings
438
439
	/**
440
	 * Returns a user's setting for this plugin
441
	 *
442
	 * @param string $name      The setting name
443
	 * @param int    $user_guid The user GUID
444
	 * @param mixed  $default   The default value to return if none is set
445
	 *
446
	 * @return mixed The setting string value, the default value or false if there is no user
447
	 */
448
	public function getUserSetting($name, $user_guid = 0, $default = null) {
449
		$values = $this->getAllUserSettings($user_guid);
450
		if ($values === false) {
451
			return false;
452
		}
453
		
454
		return elgg_extract($name, $values, $default);
455
	}
456
457
	/**
458
	 * Returns an array of all user settings saved for this plugin for the user.
459
	 *
460
	 * @note Plugin settings are saved with a prefix. This removes that prefix.
461
	 *
462
	 * @param int $user_guid The user GUID. Defaults to logged in.
463
	 * @return array An array of key/value pairs.
464
	 */
465
	public function getAllUserSettings($user_guid = 0) {
466
		$user_guid = (int) $user_guid;
467
468
		if ($user_guid) {
469
			$user = get_entity($user_guid);
470
		} else {
471
			$user = _elgg_services()->session->getLoggedInUser();
472
		}
473
474
		if (!($user instanceof \ElggUser)) {
475
			return false;
476
		}
477
478
		$defaults = $this->getStaticConfig('user_settings', []);
479
		
480
		$db_prefix = _elgg_config()->dbprefix;
481
		// send an empty name so we just get the first part of the namespace
482
		$ps_prefix = _elgg_namespace_plugin_private_setting('user_setting', '', $this->getID());
483
		$ps_prefix_len = strlen($ps_prefix);
484
485
		// Get private settings for user
486
		$q = "SELECT * FROM {$db_prefix}private_settings
487
			WHERE entity_guid = {$user->guid}
488
			AND name LIKE '$ps_prefix%'";
489
490
		$private_settings = $this->getDatabase()->getData($q);
491
492
		$return = [];
493
494
		if ($private_settings) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $private_settings 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...
495
			foreach ($private_settings as $setting) {
496
				$name = substr($setting->name, $ps_prefix_len);
497
				$value = $setting->value;
498
499
				$return[$name] = $value;
500
			}
501
		}
502
503
		return array_merge($defaults, $return);
504
	}
505
506
	/**
507
	 * Sets a user setting for a plugin
508
	 *
509
	 * @param string $name      The setting name
510
	 * @param string $value     The setting value
511
	 * @param int    $user_guid The user GUID
512
	 *
513
	 * @return mixed The new setting ID or false
514
	 */
515
	public function setUserSetting($name, $value, $user_guid = 0) {
516
		$user_guid = (int) $user_guid;
517
518
		if ($user_guid) {
519
			$user = get_entity($user_guid);
520
		} else {
521
			$user = _elgg_services()->session->getLoggedInUser();
522
		}
523
524
		if (!($user instanceof \ElggUser)) {
525
			return false;
526
		}
527
528
		// Hook to validate setting
529
		// note: this doesn't pass the namespaced name
530
		$value = _elgg_services()->hooks->trigger('usersetting', 'plugin', [
531
			'user' => $user,
532
			'plugin' => $this,
533
			'plugin_id' => $this->getID(),
534
			'name' => $name,
535
			'value' => $value
536
		], $value);
537
		
538
		if (is_array($value)) {
539
			elgg_log('Plugin user settings cannot store arrays.', 'ERROR');
540
			return false;
541
		}
542
		
543
		// set the namespaced name.
544
		$name = _elgg_namespace_plugin_private_setting('user_setting', $name, $this->getID());
545
546
		return set_private_setting($user->guid, $name, $value);
547
	}
548
549
	/**
550
	 * Removes a user setting name and value.
551
	 *
552
	 * @param string $name      The user setting name
553
	 * @param int    $user_guid The user GUID
554
	 * @return bool
555
	 */
556
	public function unsetUserSetting($name, $user_guid = 0) {
557
		$user_guid = (int) $user_guid;
558
559
		if ($user_guid) {
560
			$user = get_entity($user_guid);
561
		} else {
562
			$user = _elgg_services()->session->getLoggedInUser();
563
		}
564
565
		if (!($user instanceof \ElggUser)) {
566
			return false;
567
		}
568
569
		// set the namespaced name.
570
		$name = _elgg_namespace_plugin_private_setting('user_setting', $name, $this->getID());
571
572
		return remove_private_setting($user->guid, $name);
573
	}
574
575
	/**
576
	 * Removes all User Settings for this plugin for a particular user
577
	 *
578
	 * Use {@link removeAllUsersSettings()} to remove all user
579
	 * settings for all users.  (Note the plural 'Users'.)
580
	 *
581
	 * @warning 0 does not equal logged in user for this method!
582
	 * @todo fix that
583
	 *
584
	 * @param int $user_guid The user GUID to remove user settings.
585
	 * @return bool
586
	 */
587 View Code Duplication
	public function unsetAllUserSettings($user_guid) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
588
		$db = $this->getDatabase();
589
		$ps_prefix = _elgg_namespace_plugin_private_setting('user_setting', '', $this->getID());
590
591
		$q = "DELETE FROM {$db->prefix}private_settings
592
			WHERE entity_guid = $user_guid
593
			AND name LIKE '$ps_prefix%'";
594
595
		return $db->deleteData($q);
596
	}
597
598
	/**
599
	 * Removes this plugin's user settings for all users.
600
	 *
601
	 * Use {@link removeAllUserSettings()} if you just want to remove
602
	 * settings for a single user.
603
	 *
604
	 * @return bool
605
	 */
606 View Code Duplication
	public function unsetAllUsersSettings() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
607
		$db = $this->getDatabase();
608
		$ps_prefix = _elgg_namespace_plugin_private_setting('user_setting', '', $this->getID());
609
610
		$q = "DELETE FROM {$db->prefix}private_settings
611
			WHERE name LIKE '$ps_prefix%'";
612
613
		return $db->deleteData($q);
614
	}
615
616
617
	// validation
618
619
	/**
620
	 * Returns if the plugin is complete, meaning has all required files
621
	 * and Elgg can read them and they make sense.
622
	 *
623
	 * @todo bad name? This could be confused with isValid() from \ElggPluginPackage.
624
	 *
625
	 * @return bool
626
	 */
627 1
	public function isValid() {
628 1
		if (!$this->getID()) {
629
			$this->errorMsg = _elgg_services()->translator->translate('ElggPlugin:MissingID', [$this->guid]);
630
			return false;
631
		}
632
633 1
		if (!$this->getPackage() instanceof \ElggPluginPackage) {
634
			$this->errorMsg = _elgg_services()->translator->translate('ElggPlugin:NoPluginPackagePackage', [$this->getID(), $this->guid]);
635
			return false;
636
		}
637
638 1
		if (!$this->getPackage()->isValid()) {
639
			$this->errorMsg = $this->getPackage()->getError();
640
			return false;
641
		}
642
643 1
		return true;
644
	}
645
646
	/**
647
	 * Is this plugin active?
648
	 *
649
	 * @return bool
650
	 */
651 1
	public function isActive() {
652 1
		if (!$this->guid) {
653
			return false;
654
		}
655
656 1
		$site = elgg_get_site_entity();
657
658 1
		if (!($site instanceof \ElggSite)) {
659
			return false;
660
		}
661
662 1
		return check_entity_relationship($this->guid, 'active_plugin', $site->guid) instanceof ElggRelationship;
663
	}
664
665
	/**
666
	 * Checks if this plugin can be activated on the current
667
	 * Elgg installation.
668
	 *
669
	 * @return bool
670
	 */
671 1
	public function canActivate() {
672 1
		if ($this->isActive()) {
673
			return false;
674
		}
675
676 1
		if ($this->getPackage()) {
677 1
			$result = $this->getPackage()->isValid() && $this->getPackage()->checkDependencies();
678 1
			if (!$result) {
679
				$this->errorMsg = $this->getPackage()->getError();
680
			}
681
682 1
			return $result;
683
		}
684
685
		return false;
686
	}
687
688
689
	// activating and deactivating
690
691
	/**
692
	 * Actives the plugin for the current site.
693
	 *
694
	 * @return bool
695
	 */
696 1
	public function activate() {
697 1
		if ($this->isActive()) {
698
			return false;
699
		}
700
701 1
		if (!$this->canActivate()) {
702
			return false;
703
		}
704
705
		// Check this before setting status because the file could potentially throw
706 1
		if (!$this->isStaticConfigValid()) {
707
			return false;
708
		}
709
710 1
		if (!$this->setStatus(true)) {
711
			return false;
712
		}
713
714
		// perform tasks and emit events
715
		// emit an event. returning false will make this not be activated.
716
		// we need to do this after it's been fully activated
717
		// or the deactivate will be confused.
718
		$params = [
719 1
			'plugin_id' => $this->getID(),
720 1
			'plugin_entity' => $this,
721
		];
722
723 1
		$return = _elgg_services()->hooks->getEvents()->trigger('activate', 'plugin', $params);
724
725
		// if there are any on_enable functions, start the plugin now and run them
726
		// Note: this will not run re-run the init hooks!
727 1
		if ($return) {
728 1
			$this->activateEntities();
729
			
730 1
			if ($this->canReadFile('activate.php')) {
731
				$flags = ELGG_PLUGIN_INCLUDE_START | ELGG_PLUGIN_REGISTER_CLASSES |
732
						ELGG_PLUGIN_REGISTER_LANGUAGES | ELGG_PLUGIN_REGISTER_VIEWS | ELGG_PLUGIN_REGISTER_WIDGETS | ELGG_PLUGIN_REGISTER_ACTIONS;
733
734
				$this->start($flags);
735
736
				$return = $this->includeFile('activate.php');
737
			}
738
		}
739
740 1
		if ($return === false) {
741
			$this->deactivate();
742
		}
743
744 1
		_elgg_services()->hooks->getEvents()->trigger('cache:flush', 'system');
745
746 1
		_elgg_services()->logger->notice("Plugin {$this->getID()} has been activated");
747 1
		return $return;
748
	}
749
750
	/**
751
	 * Checks if this plugin can be deactivated on the current
752
	 * Elgg installation. Validates that this plugin has no
753
	 * active dependants.
754
	 *
755
	 * @return bool
756
	 */
757 1
	public function canDeactivate() {
758 1
		if (!$this->isActive()) {
759
			return false;
760
		}
761
762 1
		$dependents = [];
763
764 1
		$active_plugins = elgg_get_plugins();
765
766 1
		foreach ($active_plugins as $plugin) {
767 1
			$manifest = $plugin->getManifest();
768 1
			if (!$manifest) {
769
				return true;
770
			}
771 1
			$requires = $manifest->getRequires();
772
773 1
			foreach ($requires as $required) {
774 1
				if ($required['type'] == 'plugin' && $required['name'] == $this->getID()) {
775
					// there are active dependents
776 1
					$dependents[$manifest->getPluginID()] = $plugin;
777
				}
778
			}
779
		}
780
781 1
		if (!empty($dependents)) {
782
			$list = array_map(function(\ElggPlugin $plugin) {
783
				$css_id = preg_replace('/[^a-z0-9-]/i', '-', $plugin->getManifest()->getID());
784
				return elgg_view('output/url', [
785
					'text' => $plugin->getDisplayName(),
786
					'href' => "#$css_id",
787
				]);
788
			}, $dependents);
789
			$name = $this->getDisplayName();
790
			$list = implode(', ', $list);
791
			$this->errorMsg = elgg_echo('ElggPlugin:Dependencies:ActiveDependent', [$name, $list]);
792
			return false;
793
		}
794
795 1
		return true;
796
	}
797
798
	/**
799
	 * Deactivates the plugin.
800
	 *
801
	 * @return bool
802
	 */
803 1
	public function deactivate() {
804 1
		if (!$this->isActive()) {
805
			return false;
806
		}
807
808 1
		if (!$this->canDeactivate()) {
809
			return false;
810
		}
811
		
812
		// emit an event. returning false will cause this to not be deactivated.
813
		$params = [
814 1
			'plugin_id' => $this->getID(),
815 1
			'plugin_entity' => $this,
816
		];
817
818 1
		$return = _elgg_services()->hooks->getEvents()->trigger('deactivate', 'plugin', $params);
819 1
		if ($return === false) {
820
			return false;
821
		}
822
		
823
		// run any deactivate code
824 1
		if ($this->canReadFile('deactivate.php')) {
825
			// allows you to prevent disabling a plugin by returning false in a deactivate.php file
826
			if ($this->includeFile('deactivate.php') === false) {
827
				return false;
828
			}
829
		}
830
		
831 1
		$this->deactivateEntities();
832
		
833 1
		_elgg_services()->hooks->getEvents()->trigger('cache:flush', 'system');
834
835 1
		_elgg_services()->logger->notice("Plugin {$this->getID()} has been deactivated");
836
837 1
		return $this->setStatus(false);
838
	}
839
840
	/**
841
	 * Start the plugin.
842
	 *
843
	 * @param int $flags Start flags for the plugin. See the constants in lib/plugins.php for details.
844
	 * @return true
845
	 * @throws PluginException
846
	 */
847 311
	public function start($flags) {
848
849
		// Detect plugins errors early and throw so that plugins service can disable the plugin
850 311
		if (!$this->getManifest()) {
851
			throw new Exception($this->getError());
852
		}
853
854
		// Preload static config file
855 311
		$this->getStaticConfig('');
856
857
		// include classes
858 311
		if ($flags & ELGG_PLUGIN_REGISTER_CLASSES) {
859 311
			$this->registerClasses();
860
			
861 311
			$autoload_file = 'vendor/autoload.php';
862 311
			if ($this->canReadFile($autoload_file)) {
863
				Application::requireSetupFileOnce("{$this->path}/{$autoload_file}");
864
			}
865
		}
866
867
		// include languages
868 311
		if ($flags & ELGG_PLUGIN_REGISTER_LANGUAGES) {
869
			// should be loaded before the first function that touches the static config (elgg-plugin.php)
870
			// so translations can be used... for example in registering widgets
871 1
			$this->registerLanguages();
872
		}
873
		
874
		// include start file if it exists
875 311
		if ($flags & ELGG_PLUGIN_INCLUDE_START) {
876 311
			if ($this->canReadFile('start.php')) {
877 311
				$result = Application::requireSetupFileOnce("{$this->path}/start.php");
878 311
				if ($result instanceof \Closure) {
879
					$result();
880
				}
881
			}
882
			
883 311
			$this->registerEntities();
884
		}
885
		
886
		// include views
887 311
		if ($flags & ELGG_PLUGIN_REGISTER_VIEWS) {
888 1
			$this->registerViews();
889
		}
890
891
		// include actions
892 311
		if ($flags & ELGG_PLUGIN_REGISTER_ACTIONS) {
893 311
			$this->registerActions();
894
		}
895
896
		// include widgets
897 311
		if ($flags & ELGG_PLUGIN_REGISTER_WIDGETS) {
898
			// should load after views because those are used during registration
899 311
			$this->registerWidgets();
900
		}
901
902 311
		return true;
903
	}
904
905
	/**
906
	 * Includes one of the plugins files
907
	 *
908
	 * @param string $filename The name of the file
909
	 *
910
	 * @throws PluginException
911
	 * @return mixed The return value of the included file (or 1 if there is none)
912
	 */
913 311
	protected function includeFile($filename) {
914 311
		$filepath = "$this->path/$filename";
915
916 311
		if (!$this->canReadFile($filename)) {
917
			$msg = _elgg_services()->translator->translate('ElggPlugin:Exception:CannotIncludeFile',
918
							[$filename, $this->getID(), $this->guid, $this->path]);
919
			throw new \PluginException($msg);
920
		}
921
922
		try {
923 311
			$ret = Includer::includeFile($filepath);
924
		} catch (Exception $e) {
925
			$msg = _elgg_services()->translator->translate('ElggPlugin:Exception:IncludeFileThrew',
926
				[$filename, $this->getID(), $this->guid, $this->path]);
927
			throw new \PluginException($msg, 0, $e);
928
		}
929
930 311
		return $ret;
931
	}
932
933
	/**
934
	 * Checks whether a plugin file with the given name exists
935
	 *
936
	 * @param string $filename The name of the file
937
	 * @return bool
938
	 */
939 311
	protected function canReadFile($filename) {
940 311
		$path = "{$this->path}/$filename";
941 311
		return is_file($path) && is_readable($path);
942
	}
943
944
	/**
945
	 * If a static config file is present, is it a serializable array?
946
	 *
947
	 * @return bool
948
	 * @throws PluginException
949
	 */
950 1
	private function isStaticConfigValid() {
951 1
		if (!$this->canReadFile(ElggPluginPackage::STATIC_CONFIG_FILENAME)) {
952
			return true;
953
		}
954
955 1
		ob_start();
956 1
		$value = $this->includeFile(ElggPluginPackage::STATIC_CONFIG_FILENAME);
957 1
		if (ob_get_clean() !== '') {
958
			$this->errorMsg = _elgg_services()->translator->translate('ElggPlugin:activate:ConfigSentOutput');
959
			return false;
960
		}
961
962
		// make sure can serialize
963 1
		$value = @unserialize(serialize($value));
964 1
		if (!is_array($value)) {
965
			$this->errorMsg = _elgg_services()->translator->translate('ElggPlugin:activate:BadConfigFormat');
966
			return false;
967
		}
968
969 1
		return true;
970
	}
971
972
	/**
973
	 * Registers the plugin's views
974
	 *
975
	 * @throws PluginException
976
	 * @return void
977
	 */
978 1
	protected function registerViews() {
979 1
		$views = _elgg_services()->views;
980
981
		// Declared views first
982 1
		$file = "{$this->path}/views.php";
983 1 View Code Duplication
		if (is_file($file)) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
984 1
			$spec = Includer::includeFile($file);
985 1
			if (is_array($spec)) {
986 1
				$views->mergeViewsSpec($spec);
987
			}
988
		}
989
990 1
		$spec = $this->getStaticConfig('views');
991 1
		if ($spec) {
992 1
			$views->mergeViewsSpec($spec);
993
		}
994
995
		// Allow /views directory files to override
996 1
		if (!$views->registerPluginViews($this->path, $failed_dir)) {
997
			$key = 'ElggPlugin:Exception:CannotRegisterViews';
998
			$args = [$this->getID(), $this->guid, $failed_dir];
999
			$msg = _elgg_services()->translator->translate($key, $args);
1000
			throw new \PluginException($msg);
1001
		}
1002 1
	}
1003
1004
	/**
1005
	 * Registers the plugin's entities
1006
	 *
1007
	 * @return void
1008
	 */
1009 311 View Code Duplication
	protected function registerEntities() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1010
1011 311
		$spec = (array) $this->getStaticConfig('entities', []);
1012 311
		if (empty($spec)) {
1013 311
			return;
1014
		}
1015
		
1016 311
		foreach ($spec as $entity) {
1017 311
			if (isset($entity['type'], $entity['subtype'], $entity['searchable']) && $entity['searchable']) {
1018 311
				elgg_register_entity_type($entity['type'], $entity['subtype']);
1019
			}
1020
		}
1021 311
	}
1022
1023
	/**
1024
	 * Registers the plugin's actions provided in the plugin config file
1025
	 *
1026
	 * @return void
1027
	 */
1028 311
	protected function registerActions() {
1029 311
		self::addActionsFromStaticConfig($this->getStaticConfig('actions', []), $this->getPath());
1030 311
	}
1031
1032
	/**
1033
	 * Register a plugin's actions provided in the config file
1034
	 *
1035
	 * @todo move to a static config service
1036
	 *
1037
	 * @param array  $spec      'actions' section of static config
1038
	 * @param string $root_path Plugin path
1039
	 *
1040
	 * @return void
1041
	 * @access private
1042
	 * @internal
1043
	 */
1044 311
	public static function addActionsFromStaticConfig(array $spec, $root_path) {
1045 311
		$actions = _elgg_services()->actions;
1046 311
		$root_path = rtrim($root_path, '/\\');
1047
1048 311
		foreach ($spec as $action => $action_spec) {
1049 311
			if (!is_array($action_spec)) {
1050
				continue;
1051
			}
1052
1053
			$options = [
1054 311
				'access' => 'logged_in',
1055
				'filename' => '', // assuming core action is registered
1056
			];
1057
1058 311
			$options = array_merge($options, $action_spec);
1059
1060 311
			$filename = "$root_path/actions/{$action}.php";
1061 311
			if (is_file($filename)) {
1062 311
				$options['filename'] = $filename;
1063
			}
1064
1065 311
			$actions->register($action, $options['filename'], $options['access']);
1066
		}
1067 311
	}
1068
1069
	/**
1070
	 * Registers the plugin's widgets provided in the plugin config file
1071
	 *
1072
	 * @throws PluginException
1073
	 * @return void
1074
	 */
1075 311
	protected function registerWidgets() {
1076 311
		$widgets = _elgg_services()->widgets;
1077
		
1078 311
		$spec = (array) $this->getStaticConfig('widgets', []);
1079 311
		foreach ($spec as $widget_id => $widget_definition) {
1080 311
			if (!is_array($widget_definition)) {
1081
				continue;
1082
			}
1083 311
			if (!isset($widget_definition['id'])) {
1084 311
				$widget_definition['id'] = $widget_id;
1085
			}
1086
			
1087 311
			$definition = \Elgg\WidgetDefinition::factory($widget_definition);
1088
			
1089 311
			$widgets->registerType($definition);
1090
		}
1091 311
	}
1092
1093
	/**
1094
	 * Registers the plugin's languages
1095
	 *
1096
	 * @throws PluginException
1097
	 * @return true
1098
	 */
1099 1
	protected function registerLanguages() {
1100 1
		return _elgg_services()->translator->registerPluginTranslations($this->path);
1101
	}
1102
1103
	/**
1104
	 * Registers the plugin's classes
1105
	 *
1106
	 * @throws PluginException
1107
	 * @return true
1108
	 */
1109 311
	protected function registerClasses() {
1110 311
		$classes_path = "$this->path/classes";
1111
1112 311
		if (is_dir($classes_path)) {
1113 311
			_elgg_services()->autoloadManager->addClasses($classes_path);
1114
		}
1115
1116 311
		return true;
1117
	}
1118
1119
	/**
1120
	 * Activates the plugin's entities
1121
	 *
1122
	 * @return void
1123
	 */
1124 1
	protected function activateEntities() {
1125 1
		$spec = (array) $this->getStaticConfig('entities', []);
1126 1
		if (empty($spec)) {
1127 1
			return;
1128
		}
1129
		
1130
		foreach ($spec as $entity) {
1131
			if (isset($entity['type'], $entity['subtype'], $entity['class'])) {
1132
				if (get_subtype_id($entity['type'], $entity['subtype'])) {
1133
					update_subtype($entity['type'], $entity['subtype'], $entity['class']);
1134
				} else {
1135
					add_subtype($entity['type'], $entity['subtype'], $entity['class']);
1136
				}
1137
			}
1138
		}
1139
	}
1140
1141
	/**
1142
	 * Deactivates the plugin's entities
1143
	 *
1144
	 * @return void
1145
	 */
1146 1 View Code Duplication
	protected function deactivateEntities() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1147 1
		$spec = (array) $this->getStaticConfig('entities', []);
1148 1
		if (empty($spec)) {
1149 1
			return;
1150
		}
1151
		
1152
		foreach ($spec as $entity) {
1153
			if (isset($entity['type'], $entity['subtype'], $entity['class'])) {
1154
				update_subtype($entity['type'], $entity['subtype']);
1155
			}
1156
		}
1157
	}
1158
1159
	/**
1160
	 * Get an attribute, metadata or private setting value
1161
	 *
1162
	 * @param string $name Name of the attribute or private setting
1163
	 * @return mixed
1164
	 */
1165 311
	public function __get($name) {
1166
		// See if its in our base attribute
1167 311
		if (array_key_exists($name, $this->attributes)) {
1168 311
			return $this->attributes[$name];
1169
		}
1170
		
1171
		// object title and description are stored as metadata
1172 311
		if (in_array($name, ['title', 'description'])) {
1173 311
			return parent::__get($name);
1174
		}
1175
1176
		$result = $this->getPrivateSetting($name);
1177
		if ($result !== null) {
1178
			return $result;
1179
		}
1180
		
1181
		$defaults = $this->getStaticConfig('settings', []);
1182
		return elgg_extract($name, $defaults, $result);
1183
	}
1184
1185
	/**
1186
	 * Set a value as attribute, metadata or private setting.
1187
	 *
1188
	 * Metadata applies to title and description.
1189
	 *
1190
	 * @param string $name  Name of the attribute or private_setting
1191
	 * @param mixed  $value Value to be set
1192
	 * @return void
1193
	 */
1194 311 View Code Duplication
	public function __set($name, $value) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1195 311
		if (array_key_exists($name, $this->attributes)) {
1196
			// Check that we're not trying to change the guid!
1197 311
			if ((array_key_exists('guid', $this->attributes)) && ($name == 'guid')) {
1198
				return;
1199
			}
1200
1201 311
			$this->attributes[$name] = $value;
1202 311
			return;
1203
		}
1204
			
1205
		// object title and description are stored as metadata
1206 2
		if (in_array($name, ['title', 'description'])) {
1207 2
			parent::__set($name, $value);
1208 2
			return;
1209
		}
1210
		
1211
		// to make sure we trigger the correct hooks
1212
		$this->setSetting($name, $value);
1213
	}
1214
1215
	/**
1216
	 * Sets the plugin to active or inactive.
1217
	 *
1218
	 * @param bool $active Set to active or inactive
1219
	 *
1220
	 * @return bool
1221
	 */
1222 1
	private function setStatus($active) {
1223 1
		if (!$this->guid) {
1224
			return false;
1225
		}
1226
1227 1
		$site = elgg_get_site_entity();
1228 1
		if ($active) {
1229 1
			$result = add_entity_relationship($this->guid, 'active_plugin', $site->guid);
1230
		} else {
1231 1
			$result = remove_entity_relationship($this->guid, 'active_plugin', $site->guid);
1232
		}
1233
1234 1
		_elgg_invalidate_plugins_provides_cache();
1235 1
		_elgg_services()->boot->invalidateCache();
1236
1237 1
		return $result;
1238
	}
1239
1240
	/**
1241
	 * Returns the last error message registered.
1242
	 *
1243
	 * @return string|null
1244
	 */
1245
	public function getError() {
1246
		return $this->errorMsg;
1247
	}
1248
1249
	/**
1250
	 * Returns this plugin's \ElggPluginManifest object
1251
	 *
1252
	 * @return \ElggPluginManifest|null
1253
	 */
1254 311
	public function getManifest() {
1255 311
		if ($this->manifest instanceof \ElggPluginManifest) {
1256 1
			return $this->manifest;
1257
		}
1258
1259
		try {
1260 311
			$package = $this->getPackage();
1261 311
			if (!$package) {
1262
				throw new \Exception('Package cannot be loaded');
1263
			}
1264 311
			$this->manifest = $package->getManifest();
1265
		} catch (Exception $e) {
1266
			_elgg_services()->logger->warn("Failed to load manifest for plugin $this->guid. " . $e->getMessage());
1267
			$this->errorMsg = $e->getmessage();
1268
		}
1269
1270 311
		return $this->manifest;
1271
	}
1272
1273
	/**
1274
	 * Returns this plugin's \ElggPluginPackage object
1275
	 *
1276
	 * @return \ElggPluginPackage|null
1277
	 */
1278 311
	public function getPackage() {
1279 311
		if ($this->package instanceof \ElggPluginPackage) {
1280 2
			return $this->package;
1281
		}
1282
1283
		try {
1284 311
			$this->package = new \ElggPluginPackage($this->path, false);
1285
		} catch (Exception $e) {
1286
			_elgg_services()->logger->warn("Failed to load package for $this->guid. " . $e->getMessage());
1287
			$this->errorMsg = $e->getmessage();
1288
		}
1289
1290 311
		return $this->package;
1291
	}
1292
}
1293