summoner.class.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. <?php
  2. /**
  3. * Insipid
  4. * Personal web-bookmark-system
  5. *
  6. * Copyright 2016-2023 Johannes Keßler
  7. *
  8. * Development starting from 2011: Johannes Keßler
  9. * https://www.bananas-playground.net/projekt/insipid/
  10. *
  11. * creator:
  12. * Luke Reeves <luke@neuro-tech.net>
  13. *
  14. * This program is free software: you can redistribute it and/or modify
  15. * it under the terms of the GNU General Public License as published by
  16. * the Free Software Foundation, either version 3 of the License, or
  17. * (at your option) any later version.
  18. *
  19. * This program is distributed in the hope that it will be useful,
  20. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  21. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  22. * GNU General Public License for more details.
  23. *
  24. * You should have received a copy of the GNU General Public License
  25. * along with this program. If not, see http://www.gnu.org/licenses/gpl-3.0.
  26. *
  27. */
  28. /**
  29. * a static helper class
  30. */
  31. class Summoner {
  32. private const BROWSER_AGENT_STRING = 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0';
  33. /**
  34. * validate the given string with the given type. Optional check the string
  35. * length
  36. *
  37. * @param string $input The string to check
  38. * @param string $mode How the string should be checked
  39. * @param int $limit If int given the string is checked for length
  40. *
  41. * @see http://de.php.net/manual/en/regexp.reference.unicode.php
  42. * http://www.sql-und-xml.de/unicode-database/#pc
  43. *
  44. * the pattern replaces all that is allowed. the correct result after
  45. * the replace should be empty, otherwise are there chars which are not
  46. * allowed
  47. *
  48. * @return bool
  49. */
  50. static function validate(string $input, string $mode='text', int $limit=0): bool {
  51. // check if we have input
  52. $input = trim($input);
  53. if($input == "") return false;
  54. $ret = false;
  55. switch ($mode) {
  56. case 'mail':
  57. if(filter_var($input,FILTER_VALIDATE_EMAIL) === $input) {
  58. return true;
  59. }
  60. else {
  61. return false;
  62. }
  63. break;
  64. case 'url':
  65. if(filter_var($input,FILTER_VALIDATE_URL) === $input) {
  66. return true;
  67. }
  68. else {
  69. return false;
  70. }
  71. break;
  72. case 'nospace':
  73. // text without any whitespace and special chars
  74. $pattern = '/[\p{L}\p{N}]/u';
  75. break;
  76. case 'nospaceP':
  77. // text without any whitespace and special chars
  78. // but with Punctuation other
  79. # http://www.sql-und-xml.de/unicode-database/po.html
  80. $pattern = '/[\p{L}\p{N}\p{Po}\-_]/u';
  81. break;
  82. case 'digit':
  83. // only numbers and digit
  84. // warning with negative numbers...
  85. $pattern = '/[\p{N}\-]/';
  86. break;
  87. case 'pageTitle':
  88. // text with whitespace and without special chars
  89. // but with Punctuation
  90. $pattern = '/[\p{L}\p{N}\p{Po}\p{Z}\s\-_]/u';
  91. break;
  92. # strange. the \p{M} is needed.. don't know why..
  93. case 'filename':
  94. $pattern = '/[\p{L}\p{N}\p{M}\-_\.\p{Zs}]/u';
  95. break;
  96. case 'text':
  97. default:
  98. $pattern = '/[\p{L}\p{N}\p{P}\p{S}\p{Z}\p{M}\s]/u';
  99. }
  100. $value = preg_replace($pattern, '', $input);
  101. if($value === "") {
  102. $ret = true;
  103. }
  104. if(!empty($limit)) {
  105. # isset starts with 0
  106. if(isset($input[$limit])) {
  107. # too long
  108. $ret = false;
  109. }
  110. }
  111. return $ret;
  112. }
  113. /**
  114. * execute a curl call to the given $url
  115. *
  116. * @param string $url The request url
  117. * @param int $port
  118. * @return array
  119. */
  120. static function curlCall(string $url, int $port=0): array {
  121. $ret = array('status' => false, 'message' => 'Unknown');
  122. $ch = curl_init();
  123. curl_setopt($ch, CURLOPT_URL, $url);
  124. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  125. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
  126. curl_setopt($ch, CURLOPT_TIMEOUT, 30);
  127. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
  128. curl_setopt($ch, CURLOPT_MAXREDIRS, 2);
  129. curl_setopt($ch, CURLOPT_USERAGENT,self::BROWSER_AGENT_STRING);
  130. // curl_setopt($ch, CURLOPT_VERBOSE, true);
  131. // curl_setopt($ch, CURLOPT_HEADER, true);
  132. if(!empty($port)) {
  133. curl_setopt($ch, CURLOPT_PORT, $port);
  134. }
  135. $do = curl_exec($ch);
  136. if(is_string($do) === true) {
  137. $ret['status'] = true;
  138. $ret['message'] = $do;
  139. }
  140. else {
  141. $ret['message'] = curl_error($ch);
  142. }
  143. curl_close($ch);
  144. return $ret;
  145. }
  146. /**
  147. * Download given url to given file
  148. *
  149. * @param string $url
  150. * @param string $whereToStore
  151. * @param int $port
  152. * @return bool
  153. */
  154. static function downloadFile(string $url, string $whereToStore, int $port=0): bool {
  155. $fh = fopen($whereToStore, 'w+');
  156. $ret = false;
  157. if($fh !== false) {
  158. $ch = curl_init($url);
  159. curl_setopt($ch, CURLOPT_FILE, $fh);
  160. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
  161. curl_setopt($ch, CURLOPT_TIMEOUT, 30);
  162. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
  163. curl_setopt($ch, CURLOPT_MAXREDIRS, 2);
  164. curl_setopt($ch, CURLOPT_USERAGENT, self::BROWSER_AGENT_STRING);
  165. if(!empty($port)) {
  166. curl_setopt($ch, CURLOPT_PORT, $port);
  167. }
  168. curl_exec($ch);
  169. curl_close($ch);
  170. $ret = true;
  171. }
  172. fclose($fh);
  173. return $ret;
  174. }
  175. /**
  176. * try to gather meta information from given URL
  177. *
  178. * @param string $url
  179. * @return array
  180. */
  181. static function gatherInfoFromURL(string $url): array {
  182. $ret = array();
  183. if(self::validate($url,'url')) {
  184. $data = self::curlCall($url);
  185. if(!empty($data['status'])) {
  186. $ret = self::socialMetaInfos($data['message']);
  187. }
  188. }
  189. return $ret;
  190. }
  191. /**
  192. * get as much as possible social meta infos from given string
  193. * the string is usually a HTML source
  194. *
  195. * @param string $string
  196. * @return array
  197. */
  198. static function socialMetaInfos(string $string): array {
  199. #http://www.w3bees.com/2013/11/fetch-facebook-og-meta-tags-with-php.html
  200. #http://www.9lessons.info/2014/01/social-meta-tags-for-google-twitter-and.html
  201. #http://ogp.me/
  202. #https://moz.com/blog/meta-data-templates-123
  203. $dom = new DomDocument;
  204. # surpress invalid html warnings
  205. @$dom->loadHTML($string);
  206. $xpath = new DOMXPath($dom);
  207. $metas = $xpath->query('//*/meta');
  208. $mediaInfos = array();
  209. # meta tags
  210. foreach($metas as $meta) {
  211. if($meta->getAttribute('property')) {
  212. $prop = $meta->getAttribute('property');
  213. $prop = mb_strtolower($prop);
  214. # minimum required information
  215. # http://ogp.me/#metadata
  216. if($prop == "og:title") {
  217. $mediaInfos['title'] = $meta->getAttribute('content');
  218. }
  219. elseif($prop == "og:image") {
  220. $mediaInfos['image'] = $meta->getAttribute('content');
  221. }
  222. elseif($prop == "og:url") {
  223. $mediaInfos['link'] = $meta->getAttribute('content');
  224. }
  225. elseif($prop == "og:description") {
  226. $mediaInfos['description'] = $meta->getAttribute('content');
  227. }
  228. }
  229. elseif($meta->getAttribute('name')) {
  230. $name = $meta->getAttribute('name');
  231. $name = mb_strtolower($name);
  232. # twitter
  233. # https://dev.twitter.com/cards/overview
  234. if($name == "twitter:title") {
  235. $mediaInfos['title'] = $meta->getAttribute('content');
  236. }
  237. elseif($name == "twitter:description") {
  238. $mediaInfos['description'] = $meta->getAttribute('content');
  239. }
  240. elseif($name == "twitter:image") {
  241. $mediaInfos['image'] = $meta->getAttribute('content');
  242. }
  243. elseif($name == "description") {
  244. $mediaInfos['description'] = $meta->getAttribute('content');
  245. }
  246. }
  247. elseif($meta->getAttribute('itemprop')) {
  248. $itemprop = $meta->getAttribute('itemprop');
  249. $itemprop = mb_strtolower($itemprop);
  250. # google plus
  251. if($itemprop == "name") {
  252. $mediaInfos['title'] = $meta->getAttribute('content');
  253. }
  254. elseif($itemprop == "description") {
  255. $mediaInfos['description'] = $meta->getAttribute('content');
  256. }
  257. elseif($itemprop == "image") {
  258. $mediaInfos['image'] = $meta->getAttribute('content');
  259. }
  260. }
  261. }
  262. if(!isset($mediaInfos['title'])) {
  263. $titleDom = $xpath->query('//title');
  264. $mediaInfos['title'] = $titleDom->item(0)->nodeValue;
  265. }
  266. return $mediaInfos;
  267. }
  268. /**
  269. * at creation a category or tag can be a string with multiple values.
  270. * separated with space or ,
  271. * category and tag is a single string without any separators
  272. *
  273. * @param string $string
  274. * @return array
  275. */
  276. static function prepareTagOrCategoryStr(string $string): array {
  277. $ret = array();
  278. $_ret = array();
  279. $string = trim($string, ", ");
  280. if(strstr($string, ",")) {
  281. $_t = explode(",", $string);
  282. foreach($_t as $n) {
  283. $_ret[$n] = $n;
  284. }
  285. unset($_t);
  286. unset($n);
  287. foreach($_ret as $e) {
  288. if(strstr($e, " ")) {
  289. unset($ret[$e]);
  290. $_t = explode(" ", $e);
  291. foreach($_t as $new) {
  292. $new = trim($new);
  293. $_c = self::validate($new,'nospace');
  294. if(!empty($new) && $_c === true) {
  295. $ret[$new] = $new;
  296. }
  297. }
  298. }
  299. else {
  300. $new = trim($e);
  301. $_c = self::validate($new,'nospace');
  302. if(!empty($new) && $_c === true) {
  303. $ret[$new] = $new;
  304. }
  305. }
  306. }
  307. }
  308. else {
  309. $_t = explode(" ", $string);
  310. foreach($_t as $new) {
  311. $new = trim($new);
  312. $_c = self::validate($new,'nospace');
  313. if(!empty($new) && $_c === true) {
  314. $ret[$new] = $new;
  315. }
  316. }
  317. }
  318. return $ret;
  319. }
  320. /**
  321. * a very simple HTTP_AUTH authentication.
  322. */
  323. static function simpleAuth(): void {
  324. if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])
  325. || $_SERVER['PHP_AUTH_USER'] !== FRONTEND_USERNAME || $_SERVER['PHP_AUTH_PW'] !== FRONTEND_PASSWORD
  326. ) {
  327. header('WWW-Authenticate: Basic realm="Insipid edit area"');
  328. header('HTTP/1.0 401 Unauthorized');
  329. echo 'No Access.';
  330. exit;
  331. }
  332. }
  333. /**
  334. * check if we have a valid auth. Nothing more.
  335. *
  336. * @see Summoner::simpleAuth to trigger the auth
  337. * @return bool
  338. */
  339. static function simpleAuthCheck(): bool {
  340. if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])
  341. && $_SERVER['PHP_AUTH_USER'] === FRONTEND_USERNAME && $_SERVER['PHP_AUTH_PW'] === FRONTEND_PASSWORD
  342. ) {
  343. return true;
  344. }
  345. return false;
  346. }
  347. /**
  348. * Checks if in the given urlstring a scheme is existent. If not add http:// to it
  349. *
  350. * @param string $urlString
  351. * @return string
  352. */
  353. static function addSchemeToURL(string $urlString): string {
  354. $ret = $urlString;
  355. if(empty(parse_url($ret, PHP_URL_SCHEME))) {
  356. $ret = "http://".$ret;
  357. }
  358. return $ret;
  359. }
  360. /**
  361. * retrieve the folder size with its children of given folder path
  362. *
  363. * @param string $folder
  364. * @return int
  365. */
  366. static function folderSize(string $folder): int {
  367. $ret = 0;
  368. if(file_exists($folder) && is_readable($folder)) {
  369. foreach (glob(rtrim($folder, '/') . '/*', GLOB_NOSORT) as $each) {
  370. $ret += is_file($each) ? filesize($each) : self::folderSize($each);
  371. }
  372. }
  373. return $ret;
  374. }
  375. /**
  376. * Calculate the given byte size in more human readable format.
  377. *
  378. * @param integer $size
  379. * @param string $unit
  380. * @return string
  381. */
  382. static function humanFileSize(int $size, string $unit=""): string {
  383. $ret = number_format($size)." bytes";
  384. if((!$unit && $size >= 1<<30) || $unit == "GB") {
  385. $ret = number_format($size / (1 << 30), 2)."GB";
  386. }
  387. elseif((!$unit && $size >= 1<<20) || $unit == "MB") {
  388. $ret = number_format($size / (1 << 20), 2) . "MB";
  389. }
  390. elseif( (!$unit && $size >= 1<<10) || $unit == "KB") {
  391. $ret = number_format($size / (1 << 10), 2) . "KB";
  392. }
  393. return $ret;
  394. }
  395. /**
  396. * delete and/or empty a directory
  397. *
  398. * $empty = true => empty the directory but do not delete it
  399. *
  400. * @param string $directory
  401. * @param boolean $empty
  402. * @param int $fTime If not false remove files older then this value in sec.
  403. * @return boolean
  404. */
  405. static function recursive_remove_directory(string $directory, bool $empty=false, int $fTime=0): bool {
  406. if(substr($directory,-1) == '/') {
  407. $directory = substr($directory,0,-1);
  408. }
  409. if(!file_exists($directory) || !is_dir($directory)) {
  410. return false;
  411. }
  412. elseif(!is_readable($directory)) {
  413. return false;
  414. }
  415. else {
  416. $handle = opendir($directory);
  417. // and scan through the items inside
  418. while (false !== ($item = readdir($handle))) {
  419. if($item[0] != '.') {
  420. $path = $directory.'/'.$item;
  421. if(is_dir($path)) {
  422. recursive_remove_directory($path);
  423. }
  424. else {
  425. if(!empty($fTime) && is_int($fTime)) {
  426. $ft = filemtime($path);
  427. $offset = time()-$fTime;
  428. if($ft <= $offset) {
  429. unlink($path);
  430. }
  431. }
  432. else {
  433. unlink($path);
  434. }
  435. }
  436. }
  437. }
  438. closedir($handle);
  439. if($empty === false) {
  440. if(!rmdir($directory)) {
  441. return false;
  442. }
  443. }
  444. return true;
  445. }
  446. }
  447. /**
  448. * http_build_query with modify array
  449. * modify will add: key AND value not empty
  450. * modify will remove: only key with no value
  451. *
  452. * @param array $array
  453. * @param array $modify
  454. * @return string
  455. */
  456. static function createFromParameterLinkQuery(array $array, array $modify=array()): string {
  457. $ret = '';
  458. if(!empty($modify)) {
  459. foreach($modify as $k=>$v) {
  460. if(empty($v)) {
  461. unset($array[$k]);
  462. }
  463. else {
  464. $array[$k] = $v;
  465. }
  466. }
  467. }
  468. if(!empty($array)) {
  469. $ret = http_build_query($array);
  470. }
  471. return $ret;
  472. }
  473. /**
  474. * Make the input more safe for logging
  475. *
  476. * @param mixed $input The string to be made more safe
  477. * @return string
  478. */
  479. static function cleanForLog(mixed $input): string {
  480. $input = var_export($input, true);
  481. $input = preg_replace( "/[\t\n\r]/", " ", $input);
  482. return addcslashes($input, "\000..\037\177..\377\\");
  483. }
  484. /**
  485. * error_log with a dedicated destination
  486. * Uses LOGFILE const
  487. *
  488. * @param string $msg The string to be written to the log
  489. */
  490. static function sysLog(string $msg): void {
  491. error_log(date("c")." ".$msg."\n", 3, LOGFILE);
  492. }
  493. }