]> git.mxchange.org Git - friendica.git/blob - src/Core/Hook.php
Merge pull request #13704 from MrPetovan/bug/13693-infinite-indentation-level
[friendica.git] / src / Core / Hook.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2023, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Core;
23
24 use Friendica\App;
25 use Friendica\Database\DBA;
26 use Friendica\DI;
27 use Friendica\Util\Strings;
28
29 /**
30  * Some functions to handle hooks
31  */
32 class Hook
33 {
34         /**
35          * Array of registered hooks
36          *
37          * Format:
38          * [
39          *              ["<hook name>"] => [
40          *                      0 => "<hook file>",
41          *                      1 => "<hook function name>"
42          *              ],
43          *              ...
44          * ]
45          *
46          * @var array
47          */
48         private static $hooks = [];
49
50         /**
51          * Load hooks
52          *
53          * @return void
54          */
55         public static function loadHooks()
56         {
57                 self::$hooks = [];
58                 $stmt = DBA::select('hook', ['hook', 'file', 'function'], [], ['order' => ['priority' => 'desc', 'file']]);
59
60                 while ($hook = DBA::fetch($stmt)) {
61                         self::add($hook['hook'], $hook['file'], $hook['function']);
62                 }
63                 DBA::close($stmt);
64         }
65
66         /**
67          * Adds a new hook to the hooks array.
68          *
69          * This function is meant to be called by modules on each page load as it works after loadHooks has been called.
70          *
71          * @param string $hook
72          * @param string $file
73          * @param string $function
74          * @return void
75          */
76         public static function add(string $hook, string $file, string $function)
77         {
78                 if (!array_key_exists($hook, self::$hooks)) {
79                         self::$hooks[$hook] = [];
80                 }
81                 self::$hooks[$hook][] = [$file, $function];
82         }
83
84         /**
85          * Registers a hook.
86          *
87          * This function is meant to be called once when an addon is enabled for example as it doesn't add to the current hooks.
88          *
89          * @param string $hook     the name of the hook
90          * @param string $file     the name of the file that hooks into
91          * @param string $function the name of the function that the hook will call
92          * @param int    $priority A priority (defaults to 0)
93          * @return mixed|bool
94          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
95          */
96         public static function register(string $hook, string $file, string $function, int $priority = 0)
97         {
98                 $file = str_replace(DI::app()->getBasePath() . DIRECTORY_SEPARATOR, '', $file);
99
100                 $condition = ['hook' => $hook, 'file' => $file, 'function' => $function];
101                 if (DBA::exists('hook', $condition)) {
102                         return true;
103                 }
104
105                 return self::insert(['hook' => $hook, 'file' => $file, 'function' => $function, 'priority' => $priority]);
106         }
107
108         /**
109          * Unregisters a hook.
110          *
111          * @param string $hook     the name of the hook
112          * @param string $file     the name of the file that hooks into
113          * @param string $function the name of the function that the hook called
114          * @return boolean
115          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
116          */
117         public static function unregister(string $hook, string $file, string $function): bool
118         {
119                 $relative_file = str_replace(DI::app()->getBasePath() . DIRECTORY_SEPARATOR, '', $file);
120
121                 // This here is only needed for fixing a problem that existed on the develop branch
122                 $condition = ['hook' => $hook, 'file' => $file, 'function' => $function];
123                 self::delete($condition);
124
125                 $condition = ['hook' => $hook, 'file' => $relative_file, 'function' => $function];
126
127                 return self::delete($condition);
128         }
129
130         /**
131          * Returns the list of callbacks for a single hook
132          *
133          * @param  string $name Name of the hook
134          * @return array
135          */
136         public static function getByName(string $name): array
137         {
138                 $return = [];
139
140                 if (isset(self::$hooks[$name])) {
141                         $return = self::$hooks[$name];
142                 }
143
144                 return $return;
145         }
146
147         /**
148          * Forks a hook.
149          *
150          * Use this function when you want to fork a hook via the worker.
151          *
152          * @param integer $priority of the hook
153          * @param string  $name     of the hook to call
154          * @param mixed   $data     to transmit to the callback handler
155          * @return void
156          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
157          */
158         public static function fork(int $priority, string $name, $data = null)
159         {
160                 if (array_key_exists($name, self::$hooks)) {
161                         foreach (self::$hooks[$name] as $hook) {
162                                 // Call a hook to check if this hook call needs to be forked
163                                 if (array_key_exists('hook_fork', self::$hooks)) {
164                                         $hookdata = ['name' => $name, 'data' => $data, 'execute' => true];
165
166                                         foreach (self::$hooks['hook_fork'] as $fork_hook) {
167                                                 if ($hook[0] != $fork_hook[0]) {
168                                                         continue;
169                                                 }
170                                                 self::callSingle('hook_fork', $fork_hook, $hookdata);
171                                         }
172
173                                         if (!$hookdata['execute']) {
174                                                 continue;
175                                         }
176                                 }
177
178                                 Worker::add($priority, 'ForkHook', $name, $hook, $data);
179                         }
180                 }
181         }
182
183         /**
184          * Calls a hook.
185          *
186          * Use this function when you want to be able to allow a hook to manipulate
187          * the provided data.
188          *
189          * @param string        $name of the hook to call
190          * @param string|array &$data to transmit to the callback handler
191          * @return void
192          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
193          */
194         public static function callAll(string $name, &$data = null)
195         {
196                 if (array_key_exists($name, self::$hooks)) {
197                         foreach (self::$hooks[$name] as $hook) {
198                                 self::callSingle($name, $hook, $data);
199                         }
200                 }
201         }
202
203         /**
204          * Calls a single hook.
205          *
206          * @param string          $name of the hook to call
207          * @param array           $hook Hook data
208          * @param string|array   &$data to transmit to the callback handler
209          * @return void
210          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
211          */
212         public static function callSingle(string $name, array $hook, &$data = null)
213         {
214                 // Don't run a theme's hook if the user isn't using the theme
215                 if (strpos($hook[0], 'view/theme/') !== false && strpos($hook[0], 'view/theme/' . DI::app()->getCurrentTheme()) === false) {
216                         return;
217                 }
218
219                 @include_once($hook[0]);
220                 if (function_exists($hook[1])) {
221                         $func = $hook[1];
222                         $func($data);
223                 } else {
224                         // remove orphan hooks
225                         $condition = ['hook' => $name, 'file' => $hook[0], 'function' => $hook[1]];
226                         self::delete($condition);
227                 }
228         }
229
230         /**
231          * Checks if an app_menu hook exist for the provided addon name.
232          * Return true if the addon is an app
233          *
234          * @param string $name Name of the addon
235          * @return boolean
236          */
237         public static function isAddonApp(string $name): bool
238         {
239                 $name = Strings::sanitizeFilePathItem($name);
240
241                 if (array_key_exists('app_menu', self::$hooks)) {
242                         foreach (self::$hooks['app_menu'] as $hook) {
243                                 if ($hook[0] == 'addon/' . $name . '/' . $name . '.php') {
244                                         return true;
245                                 }
246                         }
247                 }
248
249                 return false;
250         }
251
252         /**
253          * Deletes one or more hook records
254          *
255          * We have to clear the cached routerDispatchData because addons can provide routes
256          *
257          * @param array $condition
258          * @return bool
259          * @throws \Exception
260          */
261         public static function delete(array $condition): bool
262         {
263                 $result = DBA::delete('hook', $condition);
264
265                 if ($result) {
266                         DI::cache()->delete('routerDispatchData');
267                 }
268
269                 return $result;
270         }
271
272         /**
273          * Inserts a hook record
274          *
275          * We have to clear the cached routerDispatchData because addons can provide routes
276          *
277          * @param array $condition
278          * @return bool
279          * @throws \Exception
280          */
281         private static function insert(array $condition): bool
282         {
283                 $result = DBA::insert('hook', $condition);
284
285                 if ($result) {
286                         DI::cache()->delete('routerDispatchData');
287                 }
288
289                 return $result;
290         }
291 }