Completed
Pull Request — patch_1-1-7 (#3421)
by Spuds
13:53
created

Hooks::loadIntegrations()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 8
nc 4
nop 0
dl 0
loc 15
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file has all the main functions in it that relate to adding
5
 * removing, etc on hooks.
6
 *
7
 * @name      ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
10
 *
11
 * This file contains code covered by:
12
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
13
 * license:		BSD, See included LICENSE.TXT for terms and conditions.
14
 *
15
 * @version 1.1.7
16
 *
17
 */
18
19
/**
20
 * Class Hooks
21
 */
22
final class Hooks
23
{
24
	/**
25
	 * The instance of the class
26
	 * @var Hooks
27
	 */
28
	private static $_instance = null;
29
30
	/**
31
	 * Holds our standard path replacement array
32
	 * @var array
33
	 */
34
	protected $_path_replacements = array();
35
36
	/**
37
	 * Holds the database instance
38
	 * @var null|database
39
	 */
40
	protected $_db = null;
41
42
	/**
43
	 * If holds instance of debug class
44
	 * @var object|null
45
	 */
46
	protected $_debug = null;
47
48
	/**
49
	 * The class constructor, loads globals in to the class object
50
	 *
51
	 * @param Database $db
52
	 * @param Debug $debug
53
	 * @param string[]|string|null $paths - additional paths to add to the replacement array
54
	 */
55
	private function __construct($db, $debug, $paths = null)
56
	{
57
		$this->_path_replacements = array(
58
			'BOARDDIR' => BOARDDIR,
59
			'SOURCEDIR' => SOURCEDIR,
60
			'EXTDIR' => EXTDIR,
61
			'LANGUAGEDIR' => LANGUAGEDIR,
62
			'ADMINDIR' => ADMINDIR,
63
			'CONTROLLERDIR' => CONTROLLERDIR,
64
			'SUBSDIR' => SUBSDIR,
65
		);
66
		$this->_db = $db;
67
		$this->_debug = $debug;
68
69
		if ($paths !== null)
70
			$this->newPath($paths);
71
	}
72
73
	/**
74
	 * Allows to set a new replacement path.
75
	 *
76
	 * @param string[]|string $path an array consisting of pairs "search" => "replace with"
77
	 */
78
	public function newPath($path)
79
	{
80
		$this->_path_replacements = array_merge($this->_path_replacements, (array) $path);
81
	}
82
83
	/**
84
	 * Process functions of an integration hook.
85
	 *
86
	 * What it does:
87
	 *
88
	 * - calls all functions of the given hook.
89
	 * - supports static class method calls.
90
	 *
91
	 * @param string $hook
92
	 * @param mixed[] $parameters = array()
93
	 *
94
	 * @return mixed[] the results of the functions
95
	 */
96
	public function hook($hook, $parameters = array())
97
	{
98
		global $modSettings;
99
100
		if ($this->_debug !== null)
101
			$this->_debug->add('hooks', $hook);
102
103
		$results = array();
104
		if (empty($modSettings[$hook]))
105
			return $results;
106
107
		// Loop through each function.
108
		$functions = $this->_prepare_hooks($modSettings[$hook]);
109
		foreach ($functions as $function => $call)
110
			$results[$function] = call_user_func_array($call, $parameters);
111
112
		return $results;
113
	}
114
115
	/**
116
	 * Splits up strings from $modSettings into functions and files to include.
117
	 *
118
	 * @param string $hook_calls
119
	 */
120
	protected function _prepare_hooks($hook_calls)
121
	{
122
		// Loop through each function.
123
		$functions = explode(',', $hook_calls);
124
		$returns = array();
125
126
		foreach ($functions as $function)
127
		{
128
			$function = trim($function);
129
130
			if (strpos($function, '|') !== false)
131
				list ($call, $file) = explode('|', $function);
132
			else
133
				$call = $function;
134
135
			// OOP static method
136
			if (strpos($call, '::') !== false)
137
				$call = explode('::', $call);
138
139
			if (!empty($file))
140
			{
141
				$absPath = strtr(trim($file), $this->_path_replacements);
142
143
				if (file_exists($absPath))
144
					require_once($absPath);
145
			}
146
147
			// Is it valid?
148
			if (is_callable($call))
149
				$returns[$function] = $call;
150
		}
151
152
		return $returns;
153
	}
154
155
	/**
156
	 * Includes files for hooks that only do that (i.e. integrate_pre_include)
157
	 *
158
	 * @param string $hook
159
	 */
160
	public function include_hook($hook)
161
	{
162
		global $modSettings;
163
164
		if ($this->_debug !== null)
165
			$this->_debug->add('hooks', $hook);
166
167
		// Any file to include?
168
		if (!empty($modSettings[$hook]))
169
		{
170
			$pre_includes = explode(',', $modSettings[$hook]);
171
			foreach ($pre_includes as $include)
172
			{
173
				$include = strtr(trim($include), $this->_path_replacements);
174
175
				if (file_exists($include))
176
					require_once($include);
177
			}
178
		}
179
	}
180
181
	/**
182
	 * Special hook call executed during obExit
183
	 */
184
	public function buffer_hook()
185
	{
186
		global $modSettings;
187
188
		if ($this->_debug !== null)
189
			$this->_debug->add('hooks', 'integrate_buffer');
190
191
		if (empty($modSettings['integrate_buffer']))
192
			return;
193
194
		$buffers = $this->_prepare_hooks($modSettings['integrate_buffer']);
195
196
		foreach ($buffers as $call)
197
			ob_start($call);
198
	}
199
200
	/**
201
	 * Add a function for integration hook.
202
	 *
203
	 * - does nothing if the function is already added.
204
	 *
205
	 * @param string $hook
206
	 * @param string $function
207
	 * @param string $file
208
	 * @param bool $permanent = true if true, updates the value in settings table
209
	 */
210
	public function add($hook, $function, $file = '', $permanent = true)
211
	{
212
		global $modSettings;
213
214
		$integration_call = (!empty($file) && $file !== true) ? $function . '|' . $file : $function;
215
216
		// Is it going to be permanent?
217
		if ($permanent)
218
			$this->_store($hook, $integration_call);
219
220
		// Make current function list usable.
221
		$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
222
223
		// Do nothing, if it's already there.
224
		if (in_array($integration_call, $functions))
225
			return;
226
227
		$functions[] = $integration_call;
228
		$modSettings[$hook] = implode(',', $functions);
229
	}
230
231
	/**
232
	 * Automatically loads all the integrations enabled and that can be found.
233
	 */
234
	public function loadIntegrations()
235
	{
236
		$enabled = $this->_get_enabled_integrations();
237
238
		foreach ($enabled as $class)
239
		{
240
			if (class_exists($class) && in_array('register', get_class_methods($class)))
241
			{
242
				$hooks = $class::register();
243
244
				if (empty($hooks))
245
					continue;
246
247
				foreach ($hooks as $hook)
248
					$this->add($hook[0], $hook[1], isset($hook[2]) ? $hook[2] : '', false);
249
			}
250
		}
251
	}
252
253
	/**
254
	 * @todo
255
	 */
256
	public function loadIntegrationsSettings()
257
	{
258
		$enabled = $this->_get_enabled_integrations();
259
260
		foreach ($enabled as $class)
261
		{
262
			if (class_exists($class) && in_array('settingsRegister', get_class_methods($class)))
263
			{
264
				$hooks = $class::settingsRegister();
265
266
				if (empty($hooks))
267
					continue;
268
269
				foreach ($hooks as $hook)
270
					$this->add($hook[0], $hook[1], isset($hook[2]) ? $hook[2] : '', false);
271
			}
272
		}
273
	}
274
275
	/**
276
	 * Find all integration files (default is *.integrate.php) in the supplied directory
277
	 *
278
	 * What it does:
279
	 *
280
	 * - Searches the ADDONSDIR (and below) (by default) for xxxx.integrate.php files
281
	 * - Will use a composer.json (optional) to load basic information about the addon
282
	 * - Will set the call name as xxx_Integrate
283
	 *
284
	 * @param string $basepath
285
	 * @param string $ext
286
	 */
287
	public function discoverIntegrations($basepath, $ext = '.integrate.php')
288
	{
289
		$path = $basepath . '/*/*' . $ext;
290
		$names = array();
291
292
		$glob = new GlobIterator($path, FilesystemIterator::SKIP_DOTS);
293
294
		// Find all integration files
295
		foreach ($glob as $file)
296
		{
297
			$name = str_replace($ext, '', $file->getBasename());
298
			$composer_file = $file->getPath() . '/composer.json';
299
300
			// Already have the integration compose file, then use it, otherwise create one
301
			if (file_exists($composer_file))
302
				$composer_data = json_decode(file_get_contents($composer_file));
303
			else
304
				$composer_data = json_decode('{
305
    "name": "' . $name . '",
306
    "description": "' . $name . '",
307
    "version": "1.0.0",
308
    "type": "addon",
309
    "homepage": "https://www.elkarte.net",
310
    "time": "",
311
    "license": "",
312
    "authors": [
313
        {
314
            "name": "Unknown",
315
            "email": "notprovided",
316
            "homepage": "https://www.elkarte.net",
317
            "role": "Developer"
318
        }
319
    ],
320
    "support": {
321
        "email": "",
322
        "issues": "https://www.elkarte.net/community",
323
        "forum": "https://www.elkarte.net/community",
324
        "wiki": "",
325
        "irc": "",
326
        "source": ""
327
    },
328
    "require": {
329
        "elkarte/elkarte": "' . substr(FORUM_VERSION, 0, -5) . '"
330
    },
331
    "repositories": [
332
        {
333
            "type": "composer",
334
            "url": "http://packages.example.com"
335
        }
336
    ],
337
    "extra": {
338
        "setting_url": ""
339
    }
340
}');
341
342
			$names[] = array(
343
				'id' => $name,
344
				'class' => str_replace('.integrate.php', '_Integrate', $file->getBasename()),
345
				'title' => $composer_data->name,
346
				'description' => $composer_data->description,
347
				'path' => str_replace($basepath, '', $file->getPathname()),
348
				'details' => $composer_data,
349
			);
350
		}
351
352
		return $names;
353
	}
354
355
	/**
356
	 * Enables the autoloading of a certain addon.
357
	 *
358
	 * @param string $call A string consisting of "path/filename.integrate.php"
359
	 */
360
	public function enableIntegration($call)
361
	{
362
		$existing = $this->_get_enabled_integrations();
363
364
		$existing[] = $call;
365
366
		$this->_store_autoload_integrate($existing);
367
	}
368
369
	/**
370
	 * Disables the autoloading of a certain addon.
371
	 *
372
	 * @param string $call A string consisting of "path/filename.integrate.php"
373
	 */
374
	public function disableIntegration($call)
375
	{
376
		$existing = $this->_get_enabled_integrations();
377
378
		$existing = array_diff($existing, (array) $call);
379
380
		$this->_store_autoload_integrate($existing);
381
	}
382
383
	/**
384
	 * Retrieves from the database a set of references to files containing addons.
385
	 *
386
	 * @return string[] An array of strings consisting of "path/filename.integrate.php"
387
	 */
388
	protected function _get_enabled_integrations()
389
	{
390
		global $modSettings;
391
392
		if (!empty($modSettings['autoload_integrate']))
393
			$existing = explode(',', $modSettings['autoload_integrate']);
394
		else
395
			$existing = array();
396
397
		return $existing;
398
	}
399
400
	/**
401
	 * Saves into the database a set of references to files containing addons.
402
	 *
403
	 * @param string[] $existing An array of strings consisting of "path/filename.integrate.php"
404
	 */
405
	protected function _store_autoload_integrate($existing)
406
	{
407
		$existing = array_filter(array_unique($existing));
408
		updateSettings(array('autoload_integrate' => implode(',', $existing)));
409
	}
410
411
	/**
412
	 * Stores a function into the database.
413
	 *
414
	 * - does nothing if the function is already added.
415
	 *
416
	 * @param string $hook
417
	 * @param string $integration_call
418
	 */
419
	protected function _store($hook, $integration_call)
420
	{
421
		$request = $this->_db->query('', '
0 ignored issues
show
Bug introduced by
The method query() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

421
		/** @scrutinizer ignore-call */ 
422
  $request = $this->_db->query('', '

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
422
			SELECT 
423
				value
424
			FROM {db_prefix}settings
425
			WHERE variable = {string:variable}',
426
			array(
427
				'variable' => $hook,
428
			)
429
		);
430
		list ($current_functions) = $this->_db->fetch_row($request);
431
		$this->_db->free_result($request);
432
433
		if (!empty($current_functions))
434
		{
435
			$current_functions = explode(',', $current_functions);
436
			if (in_array($integration_call, $current_functions))
437
				return;
438
439
			$permanent_functions = array_merge($current_functions, array($integration_call));
440
		}
441
		else
442
			$permanent_functions = array($integration_call);
443
444
		updateSettings(array($hook => implode(',', $permanent_functions)));
445
	}
446
447
	/**
448
	 * Remove an integration hook function.
449
	 *
450
	 * What it does:
451
	 *
452
	 * - Removes the given function from the given hook.
453
	 * - Does nothing if the function is not available.
454
	 *
455
	 * @param string $hook
456
	 * @param string $function
457
	 * @param string $file
458
	 */
459
	public function remove($hook, $function, $file = '')
460
	{
461
		global $modSettings;
462
463
		$integration_call = (!empty($file) && $file !== true) ? $function . '|' . $file : $function;
464
465
		// Get the permanent functions.
466
		$request = $this->_db->query('', '
467
			SELECT 
468
				value
469
			FROM {db_prefix}settings
470
			WHERE variable = {string:variable}',
471
			array(
472
				'variable' => $hook,
473
			)
474
		);
475
		list ($current_functions) = $this->_db->fetch_row($request);
476
		$this->_db->free_result($request);
477
478
		// If we found entries for this hook
479
		if (!empty($current_functions))
480
		{
481
			$current_functions = explode(',', $current_functions);
482
483
			if (in_array($integration_call, $current_functions))
484
			{
485
				updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
486
				if (empty($modSettings[$hook]))
487
					removeSettings($hook);
488
			}
489
		}
490
491
		// Turn the function list into something usable.
492
		$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
493
494
		// You can only remove it if it's available.
495
		if (!in_array($integration_call, $functions))
496
			return;
497
498
		$functions = array_diff($functions, array($integration_call));
499
		$modSettings[$hook] = implode(',', $functions);
500
	}
501
502
	/**
503
	 * Instantiation is a bit more complex, so let's give it a custom function
504
	 *
505
	 * @param Database|null $db A database connection
506
	 * @param Debug|null $debug A class for debugging
507
	 * @param string[]|null $paths An array of paths for replacement
508
	 */
509
	public static function init($db = null, $debug = null, $paths = null)
510
	{
511
		if ($db === null)
512
			$db = database();
513
514
		if ($debug === null)
515
			$debug = Debug::instance();
516
517
		self::$_instance = new Hooks($db, $debug, $paths);
518
	}
519
520
	/**
521
	 * Being a singleton, this is the static method to retrieve the instance of the class
522
	 *
523
	 * @param Database|null $db A database connection
524
	 * @param Debug|null $debug A class for debugging
525
	 * @param string[]|null $paths An array of paths for replacement
526
	 *
527
	 * @return Hooks An instance of the class.
528
	 */
529
	public static function instance($db = null, $debug = null, $paths = null)
530
	{
531
		if (self::$_instance === null)
532
			self::init($db, $debug, $paths);
533
		elseif ($paths !== null)
534
			self::$_instance->newPath($paths);
535
536
		return self::$_instance;
537
	}
538
}
539