Common.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. <?php
  2. declare(strict_types = 1);
  3. // {{{ License
  4. // This file is part of GNU social - https://www.gnu.org/software/social
  5. //
  6. // GNU social is free software: you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // GNU social is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  18. // }}}
  19. /**
  20. * Common utility functions
  21. *
  22. * @package GNUsocial
  23. * @category Util
  24. *
  25. * @author Hugo Sales <hugo@hsal.es>
  26. * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
  27. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  28. */
  29. namespace App\Util;
  30. use App\Core\I18n\I18n;
  31. use App\Core\Router\Router;
  32. use App\Core\Security;
  33. use App\Entity\Actor;
  34. use App\Entity\LocalUser;
  35. use App\Util\Exception\NoLoggedInUser;
  36. use Component\Language\Entity\Language;
  37. use Egulias\EmailValidator\EmailValidator;
  38. use Egulias\EmailValidator\Validation\RFCValidation as RFCEmailValidation;
  39. use Functional as F;
  40. use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
  41. use Symfony\Component\HttpFoundation\Request;
  42. use Symfony\Component\Routing\Exception\ResourceNotFoundException;
  43. use Symfony\Component\Yaml;
  44. abstract class Common
  45. {
  46. private static array $defaults;
  47. private static ?array $config = null;
  48. public static function setupConfig(ContainerBagInterface $config)
  49. {
  50. $components = $config->has('components') ? $config->get('components') : [];
  51. $plugins = $config->has('plugins') ? $config->get('plugins') : [];
  52. self::$config = array_merge_recursive($config->get('gnusocial'), ['components' => $components], ['plugins' => $plugins]);
  53. self::$defaults = $config->get('gnusocial_defaults');
  54. }
  55. private static ?Request $request = null;
  56. public static function setRequest(Request $req)
  57. {
  58. self::$request = $req;
  59. }
  60. /**
  61. * Don't use this
  62. */
  63. public static function getRequest(): ?Request
  64. {
  65. return self::$request;
  66. }
  67. public static function route()
  68. {
  69. return self::$request->attributes->get('_route');
  70. }
  71. public static function isRoute(string|array $routes)
  72. {
  73. return \in_array(self::route(), \is_array($routes) ? $routes : [$routes]);
  74. }
  75. /**
  76. * Access sysadmin's configuration preferences for GNU social
  77. * Returns value if exists, null if not set
  78. */
  79. public static function config(string $section, ?string $setting = null)
  80. {
  81. if (!\array_key_exists($section, self::$config)) {
  82. return null;
  83. } else {
  84. if ($setting !== null) {
  85. if (\array_key_exists($setting, self::$config[$section])) {
  86. return self::$config[$section][$setting];
  87. } else {
  88. return null;
  89. }
  90. } else {
  91. return self::$config[$section];
  92. }
  93. }
  94. }
  95. /**
  96. * Set sysadmin's configuration preferences for GNU social
  97. *
  98. * @param bool $transient keep this setting in memory only
  99. */
  100. public static function setConfig(string $section, string $setting, $value, bool $transient = false): void
  101. {
  102. self::$config[$section][$setting] = $value;
  103. if (!$transient) {
  104. $diff = self::arrayDiffRecursive(self::$config, self::$defaults);
  105. $yaml = (new Yaml\Dumper(indentation: 2))->dump(['parameters' => ['locals' => ['gnusocial' => $diff]]], Yaml\Yaml::DUMP_OBJECT_AS_MAP);
  106. rename(INSTALLDIR . '/social.local.yaml', INSTALLDIR . '/social.local.yaml.back');
  107. file_put_contents(INSTALLDIR . '/social.local.yaml', $yaml);
  108. }
  109. }
  110. public static function getConfigDefaults()
  111. {
  112. return self::$defaults;
  113. }
  114. public static function user(): ?LocalUser
  115. {
  116. // This returns the user stored in the session. We only use
  117. // LocalUser, but this is more generic and returns
  118. // UserInterface, so we need a type cast
  119. /** @var LocalUser */
  120. return Security::getUser();
  121. }
  122. public static function actor(): ?Actor
  123. {
  124. return self::user()?->getActor();
  125. }
  126. public static function userNickname(): ?string
  127. {
  128. return self::ensureLoggedIn()?->getNickname();
  129. }
  130. public static function userId(): ?int
  131. {
  132. return self::ensureLoggedIn()?->getId();
  133. }
  134. /**
  135. * @throws NoLoggedInUser
  136. */
  137. public static function ensureLoggedIn(): LocalUser
  138. {
  139. if (\is_null($user = self::user())) {
  140. throw new NoLoggedInUser();
  141. // TODO Maybe redirect to login page and back
  142. } else {
  143. return $user;
  144. }
  145. }
  146. /**
  147. * checks if user is logged in
  148. *
  149. * @return bool true if user is logged; false if it isn't
  150. */
  151. public static function isLoggedIn(): bool
  152. {
  153. return !\is_null(self::user());
  154. }
  155. /**
  156. * Is the given string identical to a system path or route?
  157. * This could probably be put in some other class, but at
  158. * at the moment, only Nickname requires this functionality.
  159. */
  160. public static function isSystemPath(string $str): bool
  161. {
  162. try {
  163. $route = Router::match('/' . $str);
  164. return $route['is_system_path'] ?? true;
  165. } catch (ResourceNotFoundException $e) {
  166. return false;
  167. }
  168. }
  169. /**
  170. * A recursive `array_diff`, while PHP itself doesn't provide one
  171. */
  172. public static function arrayDiffRecursive($array1, $array2): array
  173. {
  174. $diff = [];
  175. foreach ($array1 as $key => $value) {
  176. if (\array_key_exists($key, $array2)) {
  177. if (\is_array($value)) {
  178. $recursive_diff = static::arrayDiffRecursive($value, $array2[$key]);
  179. if (\count($recursive_diff)) {
  180. $diff[$key] = $recursive_diff;
  181. }
  182. } else {
  183. if ($value != $array2[$key]) {
  184. $diff[$key] = $value;
  185. }
  186. }
  187. } else {
  188. $diff[$key] = $value;
  189. }
  190. }
  191. return $diff;
  192. }
  193. /**
  194. * Remove keys from the _values_ of $keys from the array $from
  195. */
  196. public static function arrayRemoveKeys(array $from, array $keys, bool $strict = false)
  197. {
  198. return F\filter($from, fn ($_, $key) => !\in_array($key, $keys, $strict));
  199. }
  200. /**
  201. * An internal helper function that converts a $size from php.ini for
  202. * file size limit from the 'human-readable' shorthand into a int. If
  203. * $size is empty (the value is not set in php.ini), returns a default
  204. * value (3M)
  205. *
  206. * @return int the php.ini upload limit in machine-readable format
  207. */
  208. public static function sizeStrToInt(string $size): int
  209. {
  210. // `memory_limit` can be -1 and `post_max_size` can be 0
  211. // for unlimited. Consistency.
  212. if (empty($size) || $size === '-1' || $size === '0') {
  213. $size = '3M';
  214. }
  215. $suffix = mb_substr($size, -1);
  216. $size = (int) mb_substr($size, 0, -1);
  217. switch (mb_strtoupper($suffix)) {
  218. case 'P':
  219. $size *= 1024;
  220. // no break
  221. case 'T':
  222. $size *= 1024;
  223. // no break
  224. case 'G':
  225. $size *= 1024;
  226. // no break
  227. case 'M':
  228. $size *= 1024;
  229. // no break
  230. case 'K':
  231. $size *= 1024;
  232. break;
  233. default:
  234. if ($suffix >= '0' && $suffix <= '9') {
  235. $size = (int) "{$size}{$suffix}";
  236. }
  237. }
  238. return $size;
  239. }
  240. /**
  241. * Uses `size_str_to_int()` to find the smallest value for uploads in php.ini
  242. */
  243. public static function getPreferredPhpUploadLimit(): int
  244. {
  245. return min(
  246. self::sizeStrToInt(ini_get('post_max_size')),
  247. self::sizeStrToInt(ini_get('upload_max_filesize')),
  248. self::sizeStrToInt(ini_get('memory_limit')),
  249. );
  250. }
  251. /**
  252. * Uses common config 'attachments' 'file_quota' while respecting PreferredPhpUploadLimit
  253. */
  254. public static function getUploadLimit(): int
  255. {
  256. return min(
  257. self::getPreferredPhpUploadLimit(),
  258. self::config('attachments', 'file_quota'),
  259. );
  260. }
  261. /**
  262. * Clamps a value between 2 numbers
  263. *
  264. * @return float|int clamped value
  265. */
  266. public static function clamp(int|float $value, int|float $min, int|float $max): int|float
  267. {
  268. return min(max($value, $min), $max);
  269. }
  270. /**
  271. * If $ensure_secure is true, only allow https URLs to pass
  272. */
  273. public static function isValidHttpUrl(string $url, bool $ensure_secure = false)
  274. {
  275. if (empty($url)) {
  276. return false;
  277. }
  278. // (if false, we use '?' in 'https?' to say the 's' is optional)
  279. $regex = $ensure_secure ? '/^https$/' : '/^https?$/';
  280. return filter_var($url, \FILTER_VALIDATE_URL) !== false
  281. && preg_match($regex, parse_url($url, \PHP_URL_SCHEME));
  282. }
  283. public static function isValidEmail(string $email): bool
  284. {
  285. return (new EmailValidator())->isValid($email, new RFCEmailValidation());
  286. }
  287. /**
  288. * Flatten an array of ['note' => note, 'replies' => [notes]] to an array of notes
  289. */
  290. public static function flattenNoteArray(array $a): array
  291. {
  292. $notes = [];
  293. foreach ($a as $n) {
  294. $notes[] = $n['note'];
  295. if (isset($n['replies'])) {
  296. $notes = array_merge($notes, static::flattenNoteArray($n['replies']));
  297. }
  298. }
  299. return $notes;
  300. }
  301. public static function currentLanguage(): Language
  302. {
  303. return self::actor()?->getTopLanguage() ?? Language::getByLocale(self::$request->headers->has('accept-language') ? I18n::clientPreferredLanguage(self::$request->headers->get('accept-language')) : self::config('site', 'language'));
  304. }
  305. // Convert the ArrayBuffer to string using Uint8 array.
  306. // btoa takes chars from 0-255 and base64 encodes.
  307. // Then convert the base64 encoded to base64url encoded.
  308. // (replace + with -, replace / with _, trim trailing =)
  309. public static function base64url_encode(string $data): string
  310. {
  311. return rtrim(strtr(strtr(base64_encode($data), '+', '-'), '/', '_'), '=');
  312. }
  313. public static function base64url_decode(string $data): string
  314. {
  315. return base64_decode(str_pad(strtr(strtr($data, '_', '/'), '-', '+'), mb_strlen($data) % 4, '=', \STR_PAD_RIGHT));
  316. }
  317. }