]> git.mxchange.org Git - friendica.git/blob - src/Core/System.php
Merge remote-tracking branch 'upstream/develop' into restricted-access
[friendica.git] / src / Core / System.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\Content\Text\BBCode;
25 use Friendica\Content\Text\HTML;
26 use Friendica\Core\Config\Capability\IManageConfigValues;
27 use Friendica\DI;
28 use Friendica\Model\User;
29 use Friendica\Module\Response;
30 use Friendica\Network\HTTPException\FoundException;
31 use Friendica\Network\HTTPException\MovedPermanentlyException;
32 use Friendica\Network\HTTPException\TemporaryRedirectException;
33 use Friendica\Util\BasePath;
34 use Friendica\Util\XML;
35 use Psr\Log\LoggerInterface;
36
37 /**
38  * Contains the class with system relevant stuff
39  */
40 class System
41 {
42         /**
43          * @var LoggerInterface
44          */
45         private $logger;
46
47         /**
48          * @var IManageConfigValues
49          */
50         private $config;
51
52         /**
53          * @var string
54          */
55         private $basePath;
56
57         public function __construct(LoggerInterface $logger, IManageConfigValues $config, string $basepath)
58         {
59                 $this->logger   = $logger;
60                 $this->config   = $config;
61                 $this->basePath = $basepath;
62         }
63
64         /**
65          * Checks if the maximum number of database processes is reached
66          *
67          * @return bool Is the limit reached?
68          */
69         public function isMaxProcessesReached(): bool
70         {
71                 // Deactivated, needs more investigating if this check really makes sense
72                 return false;
73
74                 /*
75                  * Commented out to suppress static analyzer issues
76                  *
77                 if ($this->mode->isBackend()) {
78                         $process = 'backend';
79                         $max_processes = $this->config->get('system', 'max_processes_backend');
80                         if (intval($max_processes) == 0) {
81                                 $max_processes = 5;
82                         }
83                 } else {
84                         $process = 'frontend';
85                         $max_processes = $this->config->get('system', 'max_processes_frontend');
86                         if (intval($max_processes) == 0) {
87                                 $max_processes = 20;
88                         }
89                 }
90
91                 $processlist = DBA::processlist();
92                 if ($processlist['list'] != '') {
93                         $this->logger->debug('Processcheck: Processes: ' . $processlist['amount'] . ' - Processlist: ' . $processlist['list']);
94
95                         if ($processlist['amount'] > $max_processes) {
96                                 $this->logger->debug('Processcheck: Maximum number of processes for ' . $process . ' tasks (' . $max_processes . ') reached.');
97                                 return true;
98                         }
99                 }
100                 return false;
101                  */
102         }
103
104         /**
105          * Checks if the minimal memory is reached
106          *
107          * @return bool Is the memory limit reached?
108          */
109         public function isMinMemoryReached(): bool
110         {
111                 // Deactivated, needs more investigating if this check really makes sense
112                 return false;
113
114                 /*
115                  * Commented out to suppress static analyzer issues
116                  *
117                 $min_memory = $this->config->get('system', 'min_memory', 0);
118                 if ($min_memory == 0) {
119                         return false;
120                 }
121
122                 if (!is_readable('/proc/meminfo')) {
123                         return false;
124                 }
125
126                 $memdata = explode("\n", file_get_contents('/proc/meminfo'));
127
128                 $meminfo = [];
129                 foreach ($memdata as $line) {
130                         $data = explode(':', $line);
131                         if (count($data) != 2) {
132                                 continue;
133                         }
134                         [$key, $val]     = $data;
135                         $meminfo[$key]   = (int)trim(str_replace('kB', '', $val));
136                         $meminfo[$key]   = (int)($meminfo[$key] / 1024);
137                 }
138
139                 if (!isset($meminfo['MemFree'])) {
140                         return false;
141                 }
142
143                 $free = $meminfo['MemFree'];
144
145                 $reached = ($free < $min_memory);
146
147                 if ($reached) {
148                         $this->logger->warning('Minimal memory reached.', ['free' => $free, 'memtotal' => $meminfo['MemTotal'], 'limit' => $min_memory]);
149                 }
150
151                 return $reached;
152                  */
153         }
154
155         /**
156          * Checks if the maximum load is reached
157          *
158          * @return bool Is the load reached?
159          */
160         public function isMaxLoadReached(): bool
161         {
162                 $maxsysload = intval($this->config->get('system', 'maxloadavg'));
163                 if ($maxsysload < 1) {
164                         $maxsysload = 50;
165                 }
166
167                 $load = System::currentLoad();
168                 if ($load) {
169                         if (intval($load) > $maxsysload) {
170                                 $this->logger->notice('system load for process too high.', ['load' => $load, 'process' => 'backend', 'maxsysload' => $maxsysload]);
171                                 return true;
172                         }
173                 }
174                 return false;
175         }
176
177         /**
178          * Executes a child process with 'proc_open'
179          *
180          * @param string $command The command to execute
181          * @param array  $args    Arguments to pass to the command ( [ 'key' => value, 'key2' => value2, ... ]
182          */
183         public function run(string $command, array $args)
184         {
185                 if (!function_exists('proc_open')) {
186                         $this->logger->warning('"proc_open" not available - quitting');
187                         return;
188                 }
189
190                 $cmdline = $this->config->get('config', 'php_path', 'php') . ' ' . escapeshellarg($command);
191
192                 foreach ($args as $key => $value) {
193                         if (!is_null($value) && is_bool($value) && !$value) {
194                                 continue;
195                         }
196
197                         $cmdline .= ' --' . $key;
198                         if (!is_null($value) && !is_bool($value)) {
199                                 $cmdline .= ' ' . $value;
200                         }
201                 }
202
203                 if ($this->isMinMemoryReached()) {
204                         $this->logger->warning('Memory limit reached - quitting');
205                         return;
206                 }
207
208                 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
209                         $resource = proc_open('cmd /c start /b ' . $cmdline, [], $foo, $this->basePath);
210                 } else {
211                         $resource = proc_open($cmdline . ' &', [], $foo, $this->basePath);
212                 }
213
214                 if (!is_resource($resource)) {
215                         $this->logger->warning('We got no resource for command.', ['command' => $cmdline]);
216                         return;
217                 }
218
219                 proc_close($resource);
220
221                 $this->logger->info('Executed "proc_open"', ['command' => $cmdline, 'callstack' => System::callstack(10)]);
222         }
223
224         /**
225          * Returns a string with a callstack. Can be used for logging.
226          *
227          * @param integer $depth  How many calls to include in the stacks after filtering
228          * @param int     $offset How many calls to shave off the top of the stack, for example if
229          *                        this is called from a centralized method that isn't relevant to the callstack
230          * @param bool    $full   If enabled, the callstack is not compacted
231          * @return string
232          */
233         public static function callstack(int $depth = 4, int $offset = 0, bool $full = false): string
234         {
235                 $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
236
237                 // We remove at least the first two items from the list since they contain data that we don't need.
238                 $trace = array_slice($trace, 2 + $offset);
239
240                 $callstack = [];
241                 $previous = ['class' => '', 'function' => '', 'database' => false];
242
243                 // The ignore list contains all functions that are only wrapper functions
244                 $ignore = ['call_user_func_array'];
245
246                 while ($func = array_pop($trace)) {
247                         if (!empty($func['class'])) {
248                                 if (!$full && in_array($previous['function'], ['insert', 'fetch', 'toArray', 'exists', 'count', 'selectFirst', 'selectToArray',
249                                         'select', 'update', 'delete', 'selectFirstForUser', 'selectForUser'])
250                                         && (substr($previous['class'], 0, 15) === 'Friendica\Model')) {
251                                         continue;
252                                 }
253
254                                 // Don't show multiple calls from the Database classes to show the essential parts of the callstack
255                                 $func['database'] = in_array($func['class'], ['Friendica\Database\DBA', 'Friendica\Database\Database']);
256                                 if ($full || !$previous['database'] || !$func['database']) {
257                                         $classparts = explode("\\", $func['class']);
258                                         $callstack[] = array_pop($classparts).'::'.$func['function'] . (isset($func['line']) ? ' (' . $func['line'] . ')' : '');
259                                         $previous = $func;
260                                 }
261                         } elseif (!in_array($func['function'], $ignore)) {
262                                 $func['database'] = ($func['function'] == 'q');
263                                 $callstack[] = $func['function'] . (isset($func['line']) ? ' (' . $func['line'] . ')' : '');
264                                 $func['class'] = '';
265                                 $previous = $func;
266                         }
267                 }
268
269                 $callstack2 = [];
270                 while ((count($callstack2) < $depth) && (count($callstack) > 0)) {
271                         $callstack2[] = array_pop($callstack);
272                 }
273
274                 return implode(', ', $callstack2);
275         }
276
277         /**
278          * Generic XML return
279          * Outputs a basic dfrn XML status structure to STDOUT, with a <status> variable
280          * of $st and an optional text <message> of $message and terminates the current process.
281          *
282          * @param        $st
283          * @param string $message
284          * @throws \Exception
285          */
286         public static function xmlExit($st, $message = '')
287         {
288                 $result = ['status' => $st];
289
290                 if ($message != '') {
291                         $result['message'] = $message;
292                 }
293
294                 if ($st) {
295                         Logger::notice('xml_status returning non_zero: ' . $st . " message=" . $message);
296                 }
297
298                 DI::apiResponse()->setType(Response::TYPE_XML);
299                 DI::apiResponse()->addContent(XML::fromArray(['result' => $result]));
300                 DI::page()->exit(DI::apiResponse()->generate());
301
302                 self::exit();
303         }
304
305         /**
306          * Send HTTP status header and exit.
307          *
308          * @param integer $val     HTTP status result value
309          * @param string  $message Error message. Optional.
310          * @param string  $content Response body. Optional.
311          * @throws \Exception
312          */
313         public static function httpError($httpCode, $message = '', $content = '')
314         {
315                 if ($httpCode >= 400) {
316                         Logger::debug('Exit with error', ['code' => $httpCode, 'message' => $message, 'callstack' => System::callstack(20), 'method' => DI::args()->getMethod(), 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '']);
317                 }
318                 DI::apiResponse()->setStatus($httpCode, $message);
319                 DI::apiResponse()->addContent($content);
320                 DI::page()->exit(DI::apiResponse()->generate());
321
322                 self::exit();
323         }
324
325         /**
326          * This function adds the content and a content-type HTTP header to the output.
327          * After finishing the process is getting killed.
328          *
329          * @param string $content
330          * @param string $type
331          * @param string|null $content_type
332          * @return void
333          */
334         public static function httpExit(string $content, string $type = Response::TYPE_HTML, ?string $content_type = null) {
335                 DI::apiResponse()->setType($type, $content_type);
336                 DI::apiResponse()->addContent($content);
337                 DI::page()->exit(DI::apiResponse()->generate());
338
339                 self::exit();
340         }
341
342         public static function jsonError($httpCode, $content, $content_type = 'application/json')
343         {
344                 if ($httpCode >= 400) {
345                         Logger::debug('Exit with error', ['code' => $httpCode, 'content_type' => $content_type, 'callstack' => System::callstack(20), 'method' => DI::args()->getMethod(), 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '']);
346                 }
347                 DI::apiResponse()->setStatus($httpCode);
348                 self::jsonExit($content, $content_type);
349         }
350
351         /**
352          * Encodes content to json.
353          *
354          * This function encodes an array to json format
355          * and adds an application/json HTTP header to the output.
356          * After finishing the process is getting killed.
357          *
358          * @param mixed   $content      The input content
359          * @param string  $content_type Type of the input (Default: 'application/json')
360          * @param integer $options      JSON options
361          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
362          */
363         public static function jsonExit($content, $content_type = 'application/json', int $options = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) {
364                 DI::apiResponse()->setType(Response::TYPE_JSON, $content_type);
365                 DI::apiResponse()->addContent(json_encode($content, $options));
366                 DI::page()->exit(DI::apiResponse()->generate());
367
368                 self::exit();
369         }
370
371         /**
372          * Exit the program execution.
373          */
374         public static function exit()
375         {
376                 DI::page()->logRuntime(DI::config(), 'exit');
377                 exit();
378         }
379
380         /**
381          * Generates a random string in the UUID format
382          *
383          * @param bool|string $prefix A given prefix (default is empty)
384          * @return string a generated UUID
385          * @throws \Exception
386          */
387         public static function createUUID($prefix = '')
388         {
389                 $guid = System::createGUID(32, $prefix);
390                 return substr($guid, 0, 8) . '-' . substr($guid, 8, 4) . '-' . substr($guid, 12, 4) . '-' . substr($guid, 16, 4) . '-' . substr($guid, 20, 12);
391         }
392
393         /**
394          * Generates a GUID with the given parameters
395          *
396          * @param int         $size   The size of the GUID (default is 16)
397          * @param bool|string $prefix A given prefix (default is empty)
398          * @return string a generated GUID
399          * @throws \Exception
400          */
401         public static function createGUID($size = 16, $prefix = '')
402         {
403                 if (is_bool($prefix) && !$prefix) {
404                         $prefix = '';
405                 } elseif (empty($prefix)) {
406                         $prefix = hash('crc32', DI::baseUrl()->getHost());
407                 }
408
409                 while (strlen($prefix) < ($size - 13)) {
410                         $prefix .= mt_rand();
411                 }
412
413                 if ($size >= 24) {
414                         $prefix = substr($prefix, 0, $size - 22);
415                         return str_replace('.', '', uniqid($prefix, true));
416                 } else {
417                         $prefix = substr($prefix, 0, max($size - 13, 0));
418                         return uniqid($prefix);
419                 }
420         }
421
422         /**
423          * Returns the current Load of the System
424          *
425          * @return integer
426          */
427         public static function currentLoad()
428         {
429                 if (!function_exists('sys_getloadavg')) {
430                         return false;
431                 }
432
433                 $load_arr = sys_getloadavg();
434
435                 if (!is_array($load_arr)) {
436                         return false;
437                 }
438
439                 return max($load_arr[0], $load_arr[1]);
440         }
441
442         /**
443          * Fetch the load and number of processes
444          *
445          * @param bool $get_processes
446          * @return array
447          */
448         public static function getLoadAvg(bool $get_processes = true): array
449         {
450                 $load_arr = sys_getloadavg();
451                 if (empty($load_arr)) {
452                         return [];
453                 }
454
455                 $load = [
456                         'average1'  => $load_arr[0],
457                         'average5'  => $load_arr[1],
458                         'average15' => $load_arr[2],
459                         'runnable'  => 0,
460                         'scheduled' => 0
461                 ];
462
463                 if ($get_processes && @is_readable('/proc/loadavg')) {
464                         $content = @file_get_contents('/proc/loadavg');
465                         if (!empty($content) && preg_match("#([.\d]+)\s([.\d]+)\s([.\d]+)\s(\d+)/(\d+)#", $content, $matches)) {
466                                 $load['runnable']  = (float)$matches[4];
467                                 $load['scheduled'] = (float)$matches[5];
468                         }
469                 }
470
471                 return $load;
472         }
473
474         /**
475          * Redirects to an external URL (fully qualified URL)
476          * If you want to route relative to the current Friendica base, use App->internalRedirect()
477          *
478          * @param string $url  The new Location to redirect
479          * @param int    $code The redirection code, which is used (Default is 302)
480          *
481          * @throws FoundException
482          * @throws MovedPermanentlyException
483          * @throws TemporaryRedirectException
484          *
485          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
486          */
487         public static function externalRedirect($url, $code = 302)
488         {
489                 if (empty(parse_url($url, PHP_URL_SCHEME))) {
490                         Logger::warning('No fully qualified URL provided', ['url' => $url, 'callstack' => self::callstack(20)]);
491                         DI::baseUrl()->redirect($url);
492                 }
493
494                 header("Location: $url");
495
496                 switch ($code) {
497                         case 302:
498                                 throw new FoundException();
499                         case 301:
500                                 throw new MovedPermanentlyException();
501                         case 307:
502                                 throw new TemporaryRedirectException();
503                 }
504                 self::exit();
505         }
506
507         /**
508          * Returns the system user that is executing the script
509          *
510          * This mostly returns something like "www-data".
511          *
512          * @return string system username
513          */
514         public static function getUser()
515         {
516                 if (!function_exists('posix_getpwuid') || !function_exists('posix_geteuid')) {
517                         return '';
518                 }
519
520                 $processUser = posix_getpwuid(posix_geteuid());
521                 return $processUser['name'];
522         }
523
524         /**
525          * Checks if a given directory is usable for the system
526          *
527          * @param      $directory
528          *
529          * @return boolean the directory is usable
530          */
531         private static function isDirectoryUsable(string $directory): bool
532         {
533                 if (empty($directory)) {
534                         Logger::warning('Directory is empty. This shouldn\'t happen.');
535                         return false;
536                 }
537
538                 if (!file_exists($directory)) {
539                         Logger::info('Path does not exist', ['directory' => $directory, 'user' => static::getUser()]);
540                         return false;
541                 }
542
543                 if (is_file($directory)) {
544                         Logger::warning('Path is a file', ['directory' => $directory, 'user' => static::getUser()]);
545                         return false;
546                 }
547
548                 if (!is_dir($directory)) {
549                         Logger::warning('Path is not a directory', ['directory' => $directory, 'user' => static::getUser()]);
550                         return false;
551                 }
552
553                 if (!is_writable($directory)) {
554                         Logger::warning('Path is not writable', ['directory' => $directory, 'user' => static::getUser()]);
555                         return false;
556                 }
557
558                 return true;
559         }
560
561         /**
562          * Exit method used by asynchronous update modules
563          *
564          * @param string $o
565          */
566         public static function htmlUpdateExit($o)
567         {
568                 DI::apiResponse()->setType(Response::TYPE_HTML);
569                 echo "<!DOCTYPE html><html><body>\r\n";
570                 // We can remove this hack once Internet Explorer recognises HTML5 natively
571                 echo "<section>";
572                 // reportedly some versions of MSIE don't handle tabs in XMLHttpRequest documents very well
573                 echo str_replace("\t", "       ", $o);
574                 echo "</section>";
575                 echo "</body></html>\r\n";
576                 self::exit();
577         }
578
579         /**
580          * Fetch the temp path of the system
581          *
582          * @return string Path for temp files
583          */
584         public static function getTempPath()
585         {
586                 $temppath = DI::config()->get("system", "temppath");
587
588                 if (($temppath != "") && self::isDirectoryUsable($temppath)) {
589                         // We have a temp path and it is usable
590                         return BasePath::getRealPath($temppath);
591                 }
592
593                 // We don't have a working preconfigured temp path, so we take the system path.
594                 $temppath = sys_get_temp_dir();
595
596                 // Check if it is usable
597                 if (($temppath != "") && self::isDirectoryUsable($temppath)) {
598                         // Always store the real path, not the path through symlinks
599                         $temppath = BasePath::getRealPath($temppath);
600
601                         // To avoid any interferences with other systems we create our own directory
602                         $new_temppath = $temppath . "/" . DI::baseUrl()->getHost();
603                         if (!is_dir($new_temppath)) {
604                                 /// @TODO There is a mkdir()+chmod() upwards, maybe generalize this (+ configurable) into a function/method?
605                                 @mkdir($new_temppath);
606                         }
607
608                         if (self::isDirectoryUsable($new_temppath)) {
609                                 // The new path is usable, we are happy
610                                 DI::config()->set("system", "temppath", $new_temppath);
611                                 return $new_temppath;
612                         } else {
613                                 // We can't create a subdirectory, strange.
614                                 // But the directory seems to work, so we use it but don't store it.
615                                 return $temppath;
616                         }
617                 }
618
619                 // Reaching this point means that the operating system is configured badly.
620                 return '';
621         }
622
623         /**
624          * Returns the path where spool files are stored
625          *
626          * @return string Spool path
627          */
628         public static function getSpoolPath()
629         {
630                 $spoolpath = DI::config()->get('system', 'spoolpath');
631                 if (($spoolpath != "") && self::isDirectoryUsable($spoolpath)) {
632                         // We have a spool path and it is usable
633                         return $spoolpath;
634                 }
635
636                 // We don't have a working preconfigured spool path, so we take the temp path.
637                 $temppath = self::getTempPath();
638
639                 if ($temppath != "") {
640                         // To avoid any interferences with other systems we create our own directory
641                         $spoolpath = $temppath . "/spool";
642                         if (!is_dir($spoolpath)) {
643                                 mkdir($spoolpath);
644                         }
645
646                         if (self::isDirectoryUsable($spoolpath)) {
647                                 // The new path is usable, we are happy
648                                 DI::config()->set("system", "spoolpath", $spoolpath);
649                                 return $spoolpath;
650                         } else {
651                                 // We can't create a subdirectory, strange.
652                                 // But the directory seems to work, so we use it but don't store it.
653                                 return $temppath;
654                         }
655                 }
656
657                 // Reaching this point means that the operating system is configured badly.
658                 return "";
659         }
660
661         /**
662          * Fetch the system rules
663          * @param bool $numeric_id If set to "true", the rules are returned with a numeric id as key.
664          *
665          * @return array
666          */
667         public static function getRules(bool $numeric_id = false): array
668         {
669                 $rules = [];
670                 $id    = 0;
671
672                 if (DI::config()->get('system', 'tosdisplay')) {
673                         $rulelist = DI::config()->get('system', 'tosrules') ?: DI::config()->get('system', 'tostext');
674                         $msg = BBCode::toPlaintext($rulelist, false);
675                         foreach (explode("\n", trim($msg)) as $line) {
676                                 $line = trim($line);
677                                 if ($line) {
678                                         if ($numeric_id) {
679                                                 $rules[++$id] = $line;
680                                         } else {
681                                                 $rules[] = ['id' => (string)++$id, 'text' => $line];
682                                         }
683                                 }
684                         }
685                 }
686
687                 return $rules;
688         }
689 }