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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  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. * mirrors
  49. *
  50. * @param string $String The raw textarea, e.g., from torrent_form.class.php
  51. * @param string $Regex The regex to test against, e.g., from $ENV
  52. * @return array An array of unique values that match the regex
  53. */
  54. public function textarea2array(string $String, string $Regex)
  55. {
  56. $ENV = ENV::go();
  57. $String = array_map(
  58. 'trim',
  59. explode(
  60. "\n",
  61. $String
  62. )
  63. );
  64. return array_unique(
  65. preg_grep(
  66. "/^$Regex$/i",
  67. $String
  68. )
  69. );
  70. }
  71. /**
  72. * title
  73. *
  74. * Check if a torrent title is valid.
  75. * If so, return the sanitized title.
  76. * If not, return an error.
  77. */
  78. public function textInput($String)
  79. {
  80. # Previously a constant
  81. $MinLength = 10;
  82. $MaxLength = 255;
  83. # Does it exist and is it valid?
  84. if (!$String || !is_string($String)) {
  85. error('No or invalid $String parameter.');
  86. }
  87. # Is it too long or short?
  88. if (count($String)) {
  89. }
  90. }
  91. /**
  92. * Torrent errors
  93. *
  94. * Responsible for the red error messages on bad upload attemps.
  95. * todo: Test $this->TorrentError() on new file checker functions
  96. */
  97. public function TorrentError($Suspect)
  98. {
  99. global $Err;
  100. if (!$Suspect) {
  101. error('No error source :^)');
  102. }
  103. switch (false) {
  104. case $this->HasExtensions($Suspect, 1):
  105. return $Err = "The torrent has one or more files without extensions:\n" . display_str($Suspect);
  106. case $this->CruftFree($Suspect):
  107. return $Err = "The torrent has one or more junk files:\n" . display_str($Suspect);
  108. case $this->SafeCharacters($Suspect):
  109. $BadChars = $this->SafeCharacters('', true);
  110. return $Err = "One or more files has the forbidden characters $BadChars:\n" . display_str($Suspect);
  111. default:
  112. return;
  113. }
  114. return;
  115. }
  116. /**
  117. * Check if a file has no extension and return false.
  118. * Otherwise, return an array of the last $x extensions.
  119. */
  120. private function HasExtensions($FileName, $x)
  121. {
  122. if (!is_int($x) || $x <= 0) {
  123. error('Requested number of extensions must be <= 0');
  124. }
  125. if (!strstr('.', $FileName)) {
  126. return false;
  127. }
  128. $Extensions = array_slice(explode('.', strtolower($FileName)), -$x, $x);
  129. return (!empty($Extensions)) ? $Extensions : false;
  130. }
  131. /**
  132. * Check if a file is junk according to a filename blacklist.
  133. * todo: Change $Keywords into an array of regexes
  134. */
  135. public function CruftFree($FileName)
  136. {
  137. $Keywords = [
  138. 'ahashare.com',
  139. 'demonoid.com',
  140. 'demonoid.me',
  141. 'djtunes.com',
  142. 'h33t',
  143. 'housexclusive.net',
  144. 'limetorrents.com',
  145. 'mixesdb.com',
  146. 'mixfiend.blogstop',
  147. 'mixtapetorrent.blogspot',
  148. 'plixid.com',
  149. 'reggaeme.com',
  150. 'scc.nfo',
  151. 'thepiratebay.org',
  152. 'torrentday',
  153. ];
  154. # $Keywords match
  155. foreach ($Keywords as &$Value) {
  156. if (strpos(strtolower($FileName), $Value) !== false) {
  157. return false;
  158. }
  159. }
  160. # Incomplete data
  161. if (preg_match('/INCOMPLETE~\*/i', $FileName)) {
  162. return false;
  163. }
  164. return true;
  165. }
  166. /**
  167. * These characters are invalid on Windows NTFS:
  168. * : ? / < > \ * | "
  169. *
  170. * If no $FileName, return the list of bad characters.
  171. * If $FileName contains, a bad character, return false.
  172. * Otherwise, return true.
  173. *
  174. * todo: Add "/" to the blacklist. This causes problems with nested dirs, apparently
  175. * todo: Make possible preg_match($AllBlockedChars, $Name, $Matches)
  176. */
  177. public function SafeCharacters($FileName, $Pretty = false)
  178. {
  179. $InvalidChars = ':?<>\*|"';
  180. if (empty($FileName)) {
  181. return (!$Pretty) ? $InvalidChars : implode(' ', str_split($InvalidChars));
  182. }
  183. # todo: Regain functionality to return the invalid character
  184. if (preg_match(implode('\\', str_split($InvalidChars)), $Name, $Matches)) {
  185. return false;
  186. }
  187. return true;
  188. }
  189. /**
  190. * Extension Parser
  191. *
  192. * Takes an associative array of file types and extension, e.g.,
  193. * $ENV->META->Archives = [
  194. * '7z' => ['7z'],
  195. * 'bzip2' => ['bz2', 'bzip2'],
  196. * 'gzip' => ['gz', 'gzip', 'tgz', 'tpz'],
  197. * ...
  198. * ];
  199. *
  200. * Then it finds all the extensions in a torrent file list,
  201. * organizes them by file size, and returns the heaviest match.
  202. *
  203. * That way, you can have, e.g., 5 GiB FASTQ sequence data in one file,
  204. * and 100 other small files, and get the format of the actual data.
  205. */
  206. public function ParseExtensions($FileList, $Category, $FileTypes)
  207. {
  208. # Sort $Tor->file_list() output by size
  209. $UnNested = array_values($FileList[1]);
  210. $Sorted = (usort($UnNested, function ($a, $b) {
  211. return $b <=> $a;
  212. })) ? $UnNested : null; # Ternary wrap because &uarr; returns true
  213. # Harvest the wheat
  214. # todo: Entries seem duplicated here
  215. $Heaviest = array_slice($Sorted, 0, 20);
  216. $Matches = [];
  217. # Distill the file format
  218. $FileTypes = $FileTypes[$Category];
  219. $FileTypeNames = array_keys($FileTypes);
  220. foreach ($Heaviest as $Heaviest) {
  221. # Collect the last 2 period-separated tokens
  222. $Extensions = array_slice(explode('.', strtolower($Heaviest[1])), -2, 2);
  223. $Matches = array_merge($Extensions);
  224. # todo: Reduce nesting by one level
  225. foreach ($Matches as $Match) {
  226. $Match = strtolower($Match);
  227. foreach ($FileTypeNames as $FileTypeName) {
  228. $SearchMe = [ $FileTypeName, $FileTypes[$FileTypeName] ];
  229. if (in_array($Match, $SearchMe[1])) {
  230. return $SearchMe[0];
  231. break;
  232. }
  233. }
  234. # Return the last element (Other or None)
  235. return array_key_last($FileTypes);
  236. }
  237. }
  238. }
  239. /**
  240. * Legacy class
  241. */
  242. public $Fields = [];
  243. public function SetFields($FieldName, $Required, $FieldType, $ErrorMessage, $Options = [])
  244. {
  245. $this->Fields[$FieldName]['Type'] = strtolower($FieldType);
  246. $this->Fields[$FieldName]['Required'] = $Required;
  247. $this->Fields[$FieldName]['ErrorMessage'] = $ErrorMessage;
  248. if (!empty($Options['maxlength'])) {
  249. $this->Fields[$FieldName]['MaxLength'] = $Options['maxlength'];
  250. }
  251. if (!empty($Options['minlength'])) {
  252. $this->Fields[$FieldName]['MinLength'] = $Options['minlength'];
  253. }
  254. if (!empty($Options['comparefield'])) {
  255. $this->Fields[$FieldName]['CompareField'] = $Options['comparefield'];
  256. }
  257. if (!empty($Options['allowperiod'])) {
  258. $this->Fields[$FieldName]['AllowPeriod'] = $Options['allowperiod'];
  259. }
  260. if (!empty($Options['allowcomma'])) {
  261. $this->Fields[$FieldName]['AllowComma'] = $Options['allowcomma'];
  262. }
  263. if (!empty($Options['inarray'])) {
  264. $this->Fields[$FieldName]['InArray'] = $Options['inarray'];
  265. }
  266. if (!empty($Options['regex'])) {
  267. $this->Fields[$FieldName]['Regex'] = $Options['regex'];
  268. }
  269. }
  270. public function ValidateForm($ValidateArray)
  271. {
  272. reset($this->Fields);
  273. foreach ($this->Fields as $FieldKey => $Field) {
  274. $ValidateVar = $ValidateArray[$FieldKey];
  275. # todo: Change this to a switch statement
  276. if ($ValidateVar !== '' || !empty($Field['Required']) || $Field['Type'] === 'date') {
  277. if ($Field['Type'] === 'string') {
  278. if (isset($Field['MaxLength'])) {
  279. $MaxLength = $Field['MaxLength'];
  280. } else {
  281. $MaxLength = 255;
  282. }
  283. if (isset($Field['MinLength'])) {
  284. $MinLength = $Field['MinLength'];
  285. } else {
  286. $MinLength = 1;
  287. }
  288. if (strlen($ValidateVar) > $MaxLength) {
  289. return $Field['ErrorMessage'];
  290. } elseif (strlen($ValidateVar) < $MinLength) {
  291. return $Field['ErrorMessage'];
  292. }
  293. } elseif ($Field['Type'] === 'number') {
  294. if (isset($Field['MaxLength'])) {
  295. $MaxLength = $Field['MaxLength'];
  296. } else {
  297. $MaxLength = '';
  298. }
  299. if (isset($Field['MinLength'])) {
  300. $MinLength = $Field['MinLength'];
  301. } else {
  302. $MinLength = 0;
  303. }
  304. $Match = '0-9';
  305. if (isset($Field['AllowPeriod'])) {
  306. $Match .= '.';
  307. }
  308. if (isset($Field['AllowComma'])) {
  309. $Match .= ',';
  310. }
  311. if (preg_match('/[^'.$Match.']/', $ValidateVar) || strlen($ValidateVar) < 1) {
  312. return $Field['ErrorMessage'];
  313. } elseif ($MaxLength !== '' && $ValidateVar > $MaxLength) {
  314. return $Field['ErrorMessage'].'!!';
  315. } elseif ($ValidateVar < $MinLength) {
  316. return $Field['ErrorMessage']."$MinLength";
  317. }
  318. } elseif ($Field['Type'] === 'email') {
  319. if (isset($Field['MaxLength'])) {
  320. $MaxLength = $Field['MaxLength'];
  321. } else {
  322. $MaxLength = 255;
  323. }
  324. if (isset($Field['MinLength'])) {
  325. $MinLength = $Field['MinLength'];
  326. } else {
  327. $MinLength = 6;
  328. }
  329. if (!preg_match("/^".EMAIL_REGEX."$/i", $ValidateVar)) {
  330. return $Field['ErrorMessage'];
  331. } elseif (strlen($ValidateVar) > $MaxLength) {
  332. return $Field['ErrorMessage'];
  333. } elseif (strlen($ValidateVar) < $MinLength) {
  334. return $Field['ErrorMessage'];
  335. }
  336. } elseif ($Field['Type'] === 'link') {
  337. if (isset($Field['MaxLength'])) {
  338. $MaxLength = $Field['MaxLength'];
  339. } else {
  340. $MaxLength = 255;
  341. }
  342. if (isset($Field['MinLength'])) {
  343. $MinLength = $Field['MinLength'];
  344. } else {
  345. $MinLength = 10;
  346. }
  347. if (!preg_match('/^'.URL_REGEX.'$/i', $ValidateVar)) {
  348. return $Field['ErrorMessage'];
  349. } elseif (strlen($ValidateVar) > $MaxLength) {
  350. return $Field['ErrorMessage'];
  351. } elseif (strlen($ValidateVar) < $MinLength) {
  352. return $Field['ErrorMessage'];
  353. }
  354. } elseif ($Field['Type'] === 'username') {
  355. if (isset($Field['MaxLength'])) {
  356. $MaxLength = $Field['MaxLength'];
  357. } else {
  358. $MaxLength = 20;
  359. }
  360. if (isset($Field['MinLength'])) {
  361. $MinLength = $Field['MinLength'];
  362. } else {
  363. $MinLength = 1;
  364. }
  365. if (!preg_match(USERNAME_REGEX, $ValidateVar)) {
  366. return $Field['ErrorMessage'];
  367. } elseif (strlen($ValidateVar) > $MaxLength) {
  368. return $Field['ErrorMessage'];
  369. } elseif (strlen($ValidateVar) < $MinLength) {
  370. return $Field['ErrorMessage'];
  371. }
  372. } elseif ($Field['Type'] === 'checkbox') {
  373. if (!isset($ValidateArray[$FieldKey])) {
  374. return $Field['ErrorMessage'];
  375. }
  376. } elseif ($Field['Type'] === 'compare') {
  377. if ($ValidateArray[$Field['CompareField']] !== $ValidateVar) {
  378. return $Field['ErrorMessage'];
  379. }
  380. } elseif ($Field['Type'] === 'inarray') {
  381. if (array_search($ValidateVar, $Field['InArray']) === false) {
  382. return $Field['ErrorMessage'];
  383. }
  384. } elseif ($Field['Type'] === 'regex') {
  385. if (!preg_match($Field['Regex'], $ValidateVar)) {
  386. return $Field['ErrorMessage'];
  387. }
  388. }
  389. }
  390. } // while
  391. } // function
  392. } // class
  393. /**
  394. * File checker stub class
  395. *
  396. * Not technically part of the Validate class (yet).
  397. * Useful torrent file functions such as finding disallowed characters.
  398. * This will eventually move inside Validate for upload_handle.php.
  399. */
  400. $Keywords = array(
  401. 'ahashare.com', 'demonoid.com', 'demonoid.me', 'djtunes.com', 'h33t', 'housexclusive.net',
  402. 'limetorrents.com', 'mixesdb.com', 'mixfiend.blogstop', 'mixtapetorrent.blogspot',
  403. 'plixid.com', 'reggaeme.com' , 'scc.nfo', 'thepiratebay.org', 'torrentday');
  404. function check_file($Type, $Name)
  405. {
  406. check_name($Name);
  407. check_extensions($Type, $Name);
  408. }
  409. function check_name($Name)
  410. {
  411. global $Keywords;
  412. $NameLC = strtolower($Name);
  413. foreach ($Keywords as &$Value) {
  414. if (strpos($NameLC, $Value) !== false) {
  415. forbidden_error($Name);
  416. }
  417. }
  418. if (preg_match('/INCOMPLETE~\*/i', $Name)) {
  419. forbidden_error($Name);
  420. }
  421. /*
  422. * These characters are invalid in NTFS on Windows systems:
  423. * : ? / < > \ * | "
  424. *
  425. * todo: Add "/" to the blacklist. This causes problems with nested dirs, apparently
  426. * todo: Make possible preg_match($AllBlockedChars, $Name, $Matches)
  427. *
  428. * Only the following characters need to be escaped (see the link below):
  429. * \ - ^ ]
  430. *
  431. * http://www.php.net/manual/en/regexp.reference.character-classes.php
  432. */
  433. $AllBlockedChars = ' : ? < > \ * | " ';
  434. if (preg_match('/[\\:?<>*|"]/', $Name, $Matches)) {
  435. character_error($Matches[0], $AllBlockedChars);
  436. }
  437. }
  438. function check_extensions($Type, $Name)
  439. {
  440. # todo: Make generic or subsume into Validate->ParseExtensions()
  441. /*
  442. if (!isset($MusicExtensions[get_file_extension($Name)])) {
  443. invalid_error($Name);
  444. }
  445. */
  446. }
  447. function get_file_extension($FileName)
  448. {
  449. return strtolower(substr(strrchr($FileName, '.'), 1));
  450. }
  451. /**
  452. * Error functions
  453. *
  454. * Responsible for the red error messages on bad upload attemps.
  455. * todo: Make one function, e.g., Validate->error($type)
  456. */
  457. function invalid_error($Name)
  458. {
  459. global $Err;
  460. $Err = 'The torrent contained one or more invalid files (' . display_str($Name) . ')';
  461. }
  462. function forbidden_error($Name)
  463. {
  464. global $Err;
  465. $Err = 'The torrent contained one or more forbidden files (' . display_str($Name) . ')';
  466. }
  467. function character_error($Character, $AllBlockedChars)
  468. {
  469. global $Err;
  470. $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";
  471. }