BioTorrents.de’s version of Gazelle
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

validate.class.php 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. <?php
  2. #declare(strict_types=1);
  3. /**
  4. * Validate
  5. * CURRENTLY UNTESTED
  6. *
  7. * WCD/OT Gazelle contains two functions by default:
  8. * - SetFields()
  9. * - ValidateForm()
  10. *
  11. * The first is responsible for initializing a number of internal variables.
  12. * The second does the actual validation with rather hideous and clumsy logic.
  13. *
  14. * Bio Gazelle seeks to improve this class and centralize site validation.
  15. * The class should serve these common and use-dependent functions:
  16. *
  17. * - Ensure that no malicious data enters the DB
  18. * - Prevent XSS and SQL injection via $_GET and $_POST
  19. * - Escape all text inputs and HTML outputs
  20. * - Generate secure input elements and DB outputs
  21. * - Enforce $_POST except when $_GET is necessary (e.g., search → RSS)
  22. *
  23. * - Ensure that no junk data enters the DB
  24. * - Enforce HTML form element limits (generated by this class)
  25. * - Prove that a date is valid and output DB-safe datetimes
  26. * - Limit acceptable input to classes/config.php constants
  27. * - Provide structured ways to support user data (e.g., $x Samples)
  28. * - Autocomplete everywhere
  29. * - Enforce DBKEY on all encryped input
  30. *
  31. * - Some more stuff, a running list
  32. * - Check if a successful function returns null, e.g.,
  33. * php > function test() { return; }
  34. * php > var_dump(test());
  35. * NULL
  36. *
  37. * - Check if a failed function returns false, e.g.,
  38. * php > function orwell() { return (2 + 2 === 5); }
  39. * php > var_dump(orwell());
  40. * bool(false)
  41. *
  42. * todo: Support form ID checks
  43. * todo: Number and date validation
  44. */
  45. class Validate
  46. {
  47. /**
  48. * title
  49. *
  50. * Check if a torrent title is valid.
  51. * If so, return the sanitized title.
  52. * If not, return an error.
  53. */
  54. public function textInput($String)
  55. {
  56. # Previously a constant
  57. $MinLength = 10;
  58. $MaxLength = 255;
  59. # Does it exist and is it valid?
  60. if (!$String || !is_string($String)) {
  61. error('No or invalid $String parameter.');
  62. }
  63. # Is it too long or short?
  64. if (count($String)) {
  65. }
  66. }
  67. /**
  68. * Torrent errors
  69. *
  70. * Responsible for the red error messages on bad upload attemps.
  71. * todo: Test $this->TorrentError() on new file checker functions
  72. */
  73. public function TorrentError($Suspect)
  74. {
  75. global $Err;
  76. if (!$Suspect) {
  77. error('No error source :^)');
  78. }
  79. switch (false) {
  80. case $this->HasExtensions($Suspect, 1):
  81. return $Err = "The torrent has one or more files without extensions:\n" . display_str($Suspect);
  82. case $this->CruftFree($Suspect):
  83. return $Err = "The torrent has one or more junk files:\n" . display_str($Suspect);
  84. case $this->SafeCharacters($Suspect):
  85. $BadChars = $this->SafeCharacters('', true);
  86. return $Err = "One or more files has the forbidden characters $BadChars:\n" . display_str($Suspect);
  87. default:
  88. return;
  89. }
  90. return;
  91. }
  92. /**
  93. * Check if a file has no extension and return false.
  94. * Otherwise, return an array of the last $x extensions.
  95. */
  96. private function HasExtensions($FileName, $x)
  97. {
  98. if (!is_int($x) || $x <= 0) {
  99. error('Requested number of extensions must be <= 0');
  100. }
  101. if (!strstr('.', $FileName)) {
  102. return false;
  103. }
  104. $Extensions = array_slice(explode('.', strtolower($FileName)), -$x, $x);
  105. return (!empty($Extensions)) ? $Extensions : false;
  106. }
  107. /**
  108. * Check if a file is junk according to a filename blacklist.
  109. * todo: Change $Keywords into an array of regexes
  110. */
  111. public function CruftFree($FileName)
  112. {
  113. $Keywords = [
  114. 'ahashare.com',
  115. 'demonoid.com',
  116. 'demonoid.me',
  117. 'djtunes.com',
  118. 'h33t',
  119. 'housexclusive.net',
  120. 'limetorrents.com',
  121. 'mixesdb.com',
  122. 'mixfiend.blogstop',
  123. 'mixtapetorrent.blogspot',
  124. 'plixid.com',
  125. 'reggaeme.com',
  126. 'scc.nfo',
  127. 'thepiratebay.org',
  128. 'torrentday',
  129. ];
  130. # $Keywords match
  131. foreach ($Keywords as &$Value) {
  132. if (strpos(strtolower($FileName), $Value) !== false) {
  133. return false;
  134. }
  135. }
  136. # Incomplete data
  137. if (preg_match('/INCOMPLETE~\*/i', $FileName)) {
  138. return false;
  139. }
  140. return true;
  141. }
  142. /**
  143. * These characters are invalid on Windows NTFS:
  144. * : ? / < > \ * | "
  145. *
  146. * If no $FileName, return the list of bad characters.
  147. * If $FileName contains, a bad character, return false.
  148. * Otherwise, return true.
  149. *
  150. * todo: Add "/" to the blacklist. This causes problems with nested dirs, apparently
  151. * todo: Make possible preg_match($AllBlockedChars, $Name, $Matches)
  152. */
  153. public function SafeCharacters($FileName, $Pretty = false)
  154. {
  155. $InvalidChars = ':?<>\*|"';
  156. if (empty($FileName)) {
  157. return (!$Pretty) ? $InvalidChars : implode(' ', str_split($InvalidChars));
  158. }
  159. # todo: Regain functionality to return the invalid character
  160. if (preg_match(implode('\\', str_split($InvalidChars)), $Name, $Matches)) {
  161. return false;
  162. }
  163. return true;
  164. }
  165. /**
  166. * Extension Parser
  167. *
  168. * Takes an associative array of file types and extension, e.g.,
  169. * $Archives = [
  170. * '7z' => ['7z'],
  171. * 'bzip2' => ['bz2', 'bzip2'],
  172. * 'gzip' => ['gz', 'gzip', 'tgz', 'tpz'],
  173. * ...
  174. * ];
  175. *
  176. * Then it finds all the extensions in a torrent file list,
  177. * organizes them by file size, and returns the heaviest match.
  178. *
  179. * That way, you can have, e.g., 5 GiB FASTQ sequence data in one file,
  180. * and 100 other small files, and get the format of the actual data.
  181. */
  182. public function ParseExtensions($FileList, $Category, $FileTypes)
  183. {
  184. # Sort $Tor->file_list() output by size
  185. $UnNested = array_values($FileList[1]);
  186. $Sorted = (usort($UnNested, function ($a, $b) {
  187. return $b <=> $a;
  188. })) ? $UnNested : null; # Ternary wrap because &uarr; returns true
  189. # Harvest the wheat
  190. # todo: Entries seem duplicated here
  191. $Heaviest = array_slice($Sorted, 0, 20);
  192. $Matches = [];
  193. # Distill the file format
  194. $FileTypes = $FileTypes[$Category];
  195. $FileTypeNames = array_keys($FileTypes);
  196. foreach ($Heaviest as $Heaviest) {
  197. # Collect the last 2 period-separated tokens
  198. $Extensions = array_slice(explode('.', strtolower($Heaviest[1])), -2, 2);
  199. $Matches = array_merge($Extensions);
  200. # todo: Reduce nesting by one level
  201. foreach ($Matches as $Match) {
  202. $Match = strtolower($Match);
  203. foreach ($FileTypeNames as $FileTypeName) {
  204. $SearchMe = [ $FileTypeName, $FileTypes[$FileTypeName] ];
  205. if (in_array($Match, $SearchMe[1])) {
  206. return $SearchMe[0];
  207. break;
  208. }
  209. }
  210. # Return the last element (Other or None)
  211. return array_key_last($FileTypes);
  212. }
  213. }
  214. }
  215. /**
  216. * Make input
  217. * https://www.w3schools.com/html/html_form_input_types.asp
  218. *
  219. * Generate a secure HTML input element.
  220. * Takes $Type, $Name (also used for id), and $Classes.
  221. * Outputs a hardened input with the requested attributes.
  222. *
  223. * todo: See if this could work, one ugly switch for all forms in sections/
  224. */
  225. public function MakeInput($Type = 'text', $Name = '', $Classes = [])
  226. {
  227. if (!is_str($Type) || !is_str($Name)) {
  228. error('$Type and $Name must be strings');
  229. }
  230. # Intentionally double quoted PHP constructing $HTML
  231. # <input type='text' id='title_jp' name='title_jp' class='one two'
  232. $HTML = "";
  233. $HTML .= "<input type='$Type' id='$Name' name='$Name'";
  234. $HTML .= (!empty($Classes)) ? " class='" . implode(' ', $Classes) . "'" : "";
  235. /*
  236. 'button'
  237. 'checkbox'
  238. 'color'
  239. 'date'
  240. 'datetime-local'
  241. 'email'
  242. 'file'
  243. 'hidden'
  244. 'image'
  245. 'month'
  246. 'number'
  247. 'password'
  248. 'radio'
  249. 'range'
  250. 'reset'
  251. 'search'
  252. 'submit'
  253. 'tel'
  254. 'text'
  255. 'time'
  256. 'url'
  257. 'week'
  258. */
  259. return;
  260. }
  261. /**
  262. * Legacy class
  263. */
  264. public $Fields = [];
  265. public function SetFields($FieldName, $Required, $FieldType, $ErrorMessage, $Options = [])
  266. {
  267. $this->Fields[$FieldName]['Type'] = strtolower($FieldType);
  268. $this->Fields[$FieldName]['Required'] = $Required;
  269. $this->Fields[$FieldName]['ErrorMessage'] = $ErrorMessage;
  270. if (!empty($Options['maxlength'])) {
  271. $this->Fields[$FieldName]['MaxLength'] = $Options['maxlength'];
  272. }
  273. if (!empty($Options['minlength'])) {
  274. $this->Fields[$FieldName]['MinLength'] = $Options['minlength'];
  275. }
  276. if (!empty($Options['comparefield'])) {
  277. $this->Fields[$FieldName]['CompareField'] = $Options['comparefield'];
  278. }
  279. if (!empty($Options['allowperiod'])) {
  280. $this->Fields[$FieldName]['AllowPeriod'] = $Options['allowperiod'];
  281. }
  282. if (!empty($Options['allowcomma'])) {
  283. $this->Fields[$FieldName]['AllowComma'] = $Options['allowcomma'];
  284. }
  285. if (!empty($Options['inarray'])) {
  286. $this->Fields[$FieldName]['InArray'] = $Options['inarray'];
  287. }
  288. if (!empty($Options['regex'])) {
  289. $this->Fields[$FieldName]['Regex'] = $Options['regex'];
  290. }
  291. }
  292. public function ValidateForm($ValidateArray)
  293. {
  294. reset($this->Fields);
  295. foreach ($this->Fields as $FieldKey => $Field) {
  296. $ValidateVar = $ValidateArray[$FieldKey];
  297. # todo: Change this to a switch statement
  298. if ($ValidateVar !== '' || !empty($Field['Required']) || $Field['Type'] === 'date') {
  299. if ($Field['Type'] === 'string') {
  300. if (isset($Field['MaxLength'])) {
  301. $MaxLength = $Field['MaxLength'];
  302. } else {
  303. $MaxLength = 255;
  304. }
  305. if (isset($Field['MinLength'])) {
  306. $MinLength = $Field['MinLength'];
  307. } else {
  308. $MinLength = 1;
  309. }
  310. if (strlen($ValidateVar) > $MaxLength) {
  311. return $Field['ErrorMessage'];
  312. } elseif (strlen($ValidateVar) < $MinLength) {
  313. return $Field['ErrorMessage'];
  314. }
  315. } elseif ($Field['Type'] === 'number') {
  316. if (isset($Field['MaxLength'])) {
  317. $MaxLength = $Field['MaxLength'];
  318. } else {
  319. $MaxLength = '';
  320. }
  321. if (isset($Field['MinLength'])) {
  322. $MinLength = $Field['MinLength'];
  323. } else {
  324. $MinLength = 0;
  325. }
  326. $Match = '0-9';
  327. if (isset($Field['AllowPeriod'])) {
  328. $Match .= '.';
  329. }
  330. if (isset($Field['AllowComma'])) {
  331. $Match .= ',';
  332. }
  333. if (preg_match('/[^'.$Match.']/', $ValidateVar) || strlen($ValidateVar) < 1) {
  334. return $Field['ErrorMessage'];
  335. } elseif ($MaxLength !== '' && $ValidateVar > $MaxLength) {
  336. return $Field['ErrorMessage'].'!!';
  337. } elseif ($ValidateVar < $MinLength) {
  338. return $Field['ErrorMessage']."$MinLength";
  339. }
  340. } elseif ($Field['Type'] === 'email') {
  341. if (isset($Field['MaxLength'])) {
  342. $MaxLength = $Field['MaxLength'];
  343. } else {
  344. $MaxLength = 255;
  345. }
  346. if (isset($Field['MinLength'])) {
  347. $MinLength = $Field['MinLength'];
  348. } else {
  349. $MinLength = 6;
  350. }
  351. if (!preg_match("/^".EMAIL_REGEX."$/i", $ValidateVar)) {
  352. return $Field['ErrorMessage'];
  353. } elseif (strlen($ValidateVar) > $MaxLength) {
  354. return $Field['ErrorMessage'];
  355. } elseif (strlen($ValidateVar) < $MinLength) {
  356. return $Field['ErrorMessage'];
  357. }
  358. } elseif ($Field['Type'] === 'link') {
  359. if (isset($Field['MaxLength'])) {
  360. $MaxLength = $Field['MaxLength'];
  361. } else {
  362. $MaxLength = 255;
  363. }
  364. if (isset($Field['MinLength'])) {
  365. $MinLength = $Field['MinLength'];
  366. } else {
  367. $MinLength = 10;
  368. }
  369. if (!preg_match('/^'.URL_REGEX.'$/i', $ValidateVar)) {
  370. return $Field['ErrorMessage'];
  371. } elseif (strlen($ValidateVar) > $MaxLength) {
  372. return $Field['ErrorMessage'];
  373. } elseif (strlen($ValidateVar) < $MinLength) {
  374. return $Field['ErrorMessage'];
  375. }
  376. } elseif ($Field['Type'] === 'username') {
  377. if (isset($Field['MaxLength'])) {
  378. $MaxLength = $Field['MaxLength'];
  379. } else {
  380. $MaxLength = 20;
  381. }
  382. if (isset($Field['MinLength'])) {
  383. $MinLength = $Field['MinLength'];
  384. } else {
  385. $MinLength = 1;
  386. }
  387. if (!preg_match(USERNAME_REGEX, $ValidateVar)) {
  388. return $Field['ErrorMessage'];
  389. } elseif (strlen($ValidateVar) > $MaxLength) {
  390. return $Field['ErrorMessage'];
  391. } elseif (strlen($ValidateVar) < $MinLength) {
  392. return $Field['ErrorMessage'];
  393. }
  394. } elseif ($Field['Type'] === 'checkbox') {
  395. if (!isset($ValidateArray[$FieldKey])) {
  396. return $Field['ErrorMessage'];
  397. }
  398. } elseif ($Field['Type'] === 'compare') {
  399. if ($ValidateArray[$Field['CompareField']] !== $ValidateVar) {
  400. return $Field['ErrorMessage'];
  401. }
  402. } elseif ($Field['Type'] === 'inarray') {
  403. if (array_search($ValidateVar, $Field['InArray']) === false) {
  404. return $Field['ErrorMessage'];
  405. }
  406. } elseif ($Field['Type'] === 'regex') {
  407. if (!preg_match($Field['Regex'], $ValidateVar)) {
  408. return $Field['ErrorMessage'];
  409. }
  410. }
  411. }
  412. } // while
  413. } // function
  414. } // class
  415. /**
  416. * File checker stub class
  417. *
  418. * Not technically part of the Validate class (yet).
  419. * Useful torrent file functions such as finding disallowed characters.
  420. * This will eventually move inside Validate for upload_handle.php.
  421. */
  422. $Keywords = array(
  423. 'ahashare.com', 'demonoid.com', 'demonoid.me', 'djtunes.com', 'h33t', 'housexclusive.net',
  424. 'limetorrents.com', 'mixesdb.com', 'mixfiend.blogstop', 'mixtapetorrent.blogspot',
  425. 'plixid.com', 'reggaeme.com' , 'scc.nfo', 'thepiratebay.org', 'torrentday');
  426. function check_file($Type, $Name)
  427. {
  428. check_name($Name);
  429. check_extensions($Type, $Name);
  430. }
  431. function check_name($Name)
  432. {
  433. global $Keywords;
  434. $NameLC = strtolower($Name);
  435. foreach ($Keywords as &$Value) {
  436. if (strpos($NameLC, $Value) !== false) {
  437. forbidden_error($Name);
  438. }
  439. }
  440. if (preg_match('/INCOMPLETE~\*/i', $Name)) {
  441. forbidden_error($Name);
  442. }
  443. /*
  444. * These characters are invalid in NTFS on Windows systems:
  445. * : ? / < > \ * | "
  446. *
  447. * todo: Add "/" to the blacklist. This causes problems with nested dirs, apparently
  448. * todo: Make possible preg_match($AllBlockedChars, $Name, $Matches)
  449. *
  450. * Only the following characters need to be escaped (see the link below):
  451. * \ - ^ ]
  452. *
  453. * http://www.php.net/manual/en/regexp.reference.character-classes.php
  454. */
  455. $AllBlockedChars = ' : ? < > \ * | " ';
  456. if (preg_match('/[\\:?<>*|"]/', $Name, $Matches)) {
  457. character_error($Matches[0], $AllBlockedChars);
  458. }
  459. }
  460. function check_extensions($Type, $Name)
  461. {
  462. # todo: Make generic or subsume into Validate->ParseExtensions()
  463. /*
  464. if (!isset($MusicExtensions[get_file_extension($Name)])) {
  465. invalid_error($Name);
  466. }
  467. */
  468. }
  469. function get_file_extension($FileName)
  470. {
  471. return strtolower(substr(strrchr($FileName, '.'), 1));
  472. }
  473. /**
  474. * Error functions
  475. *
  476. * Responsible for the red error messages on bad upload attemps.
  477. * todo: Make one function, e.g., Validate->error($type)
  478. */
  479. function invalid_error($Name)
  480. {
  481. global $Err;
  482. $Err = 'The torrent contained one or more invalid files (' . display_str($Name) . ')';
  483. }
  484. function forbidden_error($Name)
  485. {
  486. global $Err;
  487. $Err = 'The torrent contained one or more forbidden files (' . display_str($Name) . ')';
  488. }
  489. function character_error($Character, $AllBlockedChars)
  490. {
  491. global $Err;
  492. $Err = "One or more of the files or folders in the torrent has a name that contains the forbidden character '$Character'. Please rename the files as necessary and recreate the torrent.<br /><br />\nNote: The complete list of characters that are disallowed are shown below:<br />\n\t\t$AllBlockedChars";
  493. }