1
0

marginalia.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. <?php
  2. class marginalia{
  3. public function __construct(){
  4. include "lib/anubis.php";
  5. $this->anubis = new anubis();
  6. include_once "lib/fuckhtml.php";
  7. $this->fuckhtml = new fuckhtml();
  8. include "lib/backend.php";
  9. $this->backend = new backend("marginalia");
  10. }
  11. public function getfilters($page){
  12. if(config::MARGINALIA_API_KEY === null){
  13. $base = [
  14. "adtech" => [
  15. "display" => "Reduce adtech",
  16. "option" => [
  17. "no" => "No",
  18. "yes" => "Yes"
  19. ]
  20. ],
  21. "recent" => [
  22. "display" => "Recent results",
  23. "option" => [
  24. "no" => "No",
  25. "yes" => "Yes"
  26. ]
  27. ],
  28. "intitle" => [
  29. "display" => "Search in title",
  30. "option" => [
  31. "no" => "No",
  32. "yes" => "Yes"
  33. ]
  34. ]
  35. ];
  36. }else{
  37. $base = [];
  38. }
  39. return array_merge(
  40. $base,
  41. [
  42. "format" => [
  43. "display" => "Format",
  44. "option" => [
  45. "any" => "Any format",
  46. "html5" => "html5",
  47. "xhtml" => "xhtml",
  48. "html123" => "html123"
  49. ]
  50. ],
  51. "file" => [
  52. "display" => "Filetype",
  53. "option" => [
  54. "any" => "Any filetype",
  55. "nomedia" => "Deny media",
  56. "media" => "Contains media",
  57. "audio" => "Contains audio",
  58. "video" => "Contains video",
  59. "archive" => "Contains archive",
  60. "document" => "Contains document"
  61. ]
  62. ],
  63. "javascript" => [
  64. "display" => "Javascript",
  65. "option" => [
  66. "any" => "Allow JS",
  67. "deny" => "Deny JS",
  68. "require" => "Require JS"
  69. ]
  70. ],
  71. "trackers" => [
  72. "display" => "Trackers",
  73. "option" => [
  74. "any" => "Allow trackers",
  75. "deny" => "Deny trackers",
  76. "require" => "Require trackers"
  77. ]
  78. ],
  79. "cookies" => [
  80. "display" => "Cookies",
  81. "option" => [
  82. "any" => "Allow cookies",
  83. "deny" => "Deny cookies",
  84. "require" => "Require cookies"
  85. ]
  86. ],
  87. "affiliate" => [
  88. "display" => "Affiliate links in body",
  89. "option" => [
  90. "any" => "Allow affiliate links",
  91. "deny" => "Deny affiliate links",
  92. "require" => "Require affiliate links"
  93. ]
  94. ]
  95. ]
  96. );
  97. }
  98. private function get($proxy, $url, $get = [], $get_cookies = 1){
  99. $curlproc = curl_init();
  100. switch($get_cookies){
  101. case 0:
  102. $cookies = "";
  103. $cookies_tmp = [];
  104. curl_setopt($curlproc, CURLOPT_HEADERFUNCTION, function($curlproc, $header) use (&$cookies_tmp){
  105. $length = strlen($header);
  106. $header = explode(":", $header, 2);
  107. if(trim(strtolower($header[0])) == "set-cookie"){
  108. $cookie_tmp = explode("=", trim($header[1]), 2);
  109. $cookies_tmp[trim($cookie_tmp[0])] =
  110. explode(";", $cookie_tmp[1], 2)[0];
  111. }
  112. return $length;
  113. });
  114. break;
  115. case 1:
  116. $cookies = "";
  117. break;
  118. default:
  119. $cookies = "Cookie: " . $get_cookies;
  120. }
  121. $headers = [
  122. "User-Agent: " . config::USER_AGENT,
  123. "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
  124. "Accept-Language: en-US,en;q=0.5",
  125. "Accept-Encoding: gzip",
  126. "DNT: 1",
  127. $cookies,
  128. "Connection: keep-alive",
  129. "Upgrade-Insecure-Requests: 1",
  130. "Sec-Fetch-Dest: document",
  131. "Sec-Fetch-Mode: navigate",
  132. "Sec-Fetch-Site: none",
  133. "Sec-Fetch-User: ?1"
  134. ];
  135. if($get !== []){
  136. $get = http_build_query($get);
  137. $url .= "?" . $get;
  138. }
  139. curl_setopt($curlproc, CURLOPT_URL, $url);
  140. curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
  141. curl_setopt($curlproc, CURLOPT_HTTPHEADER, $headers);
  142. curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true);
  143. curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2);
  144. curl_setopt($curlproc, CURLOPT_SSL_VERIFYPEER, true);
  145. curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30);
  146. curl_setopt($curlproc, CURLOPT_TIMEOUT, 30);
  147. $this->backend->assign_proxy($curlproc, $proxy);
  148. $data = curl_exec($curlproc);
  149. if(curl_errno($curlproc)){
  150. throw new Exception(curl_error($curlproc));
  151. }
  152. if($get_cookies === 0){
  153. $cookie = [];
  154. foreach($cookies_tmp as $key => $value){
  155. $cookie[] = $key . "=" . $value;
  156. }
  157. curl_close($curlproc);
  158. return implode(";", $cookie);
  159. }
  160. return $data;
  161. }
  162. public function web($get, $ss = ""){
  163. $search = [$get["s"]];
  164. if(strlen($get["s"]) === 0){
  165. throw new Exception("Search term is empty!");
  166. }
  167. $format = $get["format"];
  168. $file = $get["file"];
  169. foreach(
  170. [
  171. "javascript" => $get["javascript"],
  172. "trackers" => $get["trackers"],
  173. "cookies" => $get["cookies"],
  174. "affiliate" => $get["affiliate"]
  175. ]
  176. as $key => $value
  177. ){
  178. if($value == "any"){ continue; }
  179. switch($key){
  180. case "javascript": $str = "js:true"; break;
  181. case "trackers": $str = "special:tracking"; break;
  182. case "cookies": $str = "special:cookies"; break;
  183. case "affiliate": $str = "special:affiliate"; break;
  184. }
  185. if($value == "deny"){
  186. $str = "-" . $str;
  187. }
  188. $search[] = $str;
  189. }
  190. if($format != "any"){
  191. $search[] = "format:$format";
  192. }
  193. switch($file){
  194. case "any": break;
  195. case "nomedia": $search[] = "-special:media"; break;
  196. case "media": $search[] = "special:media"; break;
  197. default:
  198. $search[] = "file:$file";
  199. }
  200. $search = implode(" ", $search);
  201. $out = [
  202. "status" => "ok",
  203. "spelling" => [
  204. "type" => "no_correction",
  205. "using" => null,
  206. "correction" => null
  207. ],
  208. "npt" => null,
  209. "answer" => [],
  210. "web" => [],
  211. "image" => [],
  212. "video" => [],
  213. "news" => [],
  214. "related" => []
  215. ];
  216. // API scraper
  217. if(config::MARGINALIA_API_KEY !== null){
  218. try{
  219. $json =
  220. $this->get(
  221. $this->backend->get_ip(), // no nextpage
  222. "https://api.marginalia-search.com/" . config::MARGINALIA_API_KEY . "/search/" . urlencode($search),
  223. [
  224. "count" => 20
  225. ]
  226. );
  227. }catch(Exception $error){
  228. throw new Exception("Failed to get JSON");
  229. }
  230. if($json == "Slow down"){
  231. throw new Exception("The API key used is rate limited. Please try again in a few minutes.");
  232. }
  233. if(strpos($json, "<title>Bad Gateway (502)</title>")){
  234. throw new Exception("Marginalia returned a 502 error");
  235. }
  236. $json = json_decode($json, true);
  237. if($json === null){
  238. throw new Exception("Failed to decode JSON");
  239. }
  240. foreach($json["results"] as $result){
  241. $out["web"][] = [
  242. "title" => $result["title"],
  243. "description" => str_replace("\n", " ", $result["description"]),
  244. "url" => $result["url"],
  245. "date" => null,
  246. "type" => "web",
  247. "thumb" => [
  248. "url" => null,
  249. "ratio" => null
  250. ],
  251. "sublink" => [],
  252. "table" => []
  253. ];
  254. }
  255. return $out;
  256. }
  257. // HTML parser
  258. $proxy = $this->backend->get_ip();
  259. //
  260. // Bypass anubis check
  261. //
  262. /*
  263. if(($anubis_key = apcu_fetch("marginalia_cookie")) === false){
  264. try{
  265. $html =
  266. $this->get(
  267. $proxy,
  268. "https://old-search.marginalia.nu/search",
  269. [
  270. "query" => $search
  271. ]
  272. );
  273. }catch(Exception $error){
  274. throw new Exception("Failed to get anubis challenge");
  275. }
  276. try{
  277. $anubis_data = $this->anubis->scrape($html);
  278. }catch(Exception $error){
  279. throw new Exception($error);
  280. }
  281. // send anubis response & get cookies
  282. // https://old-search.marginalia.nu/.within.website/x/cmd/anubis/api/pass-challenge?response=0000018966b086834f738bacba6031028adb5aa875974ead197a8b75778baf3a&nonce=39947&redir=https%3A%2F%2Fold-search.marginalia.nu%2F&elapsedTime=1164
  283. try{
  284. $anubis_key =
  285. $this->get(
  286. $proxy,
  287. "https://old-search.marginalia.nu/.within.website/x/cmd/anubis/api/pass-challenge",
  288. [
  289. "response" => $anubis_data["response"],
  290. "nonce" => $anubis_data["nonce"],
  291. "redir" => "https://old-search.marginalia.nu/",
  292. "elapsedTime" => random_int(1000, 2000)
  293. ],
  294. 0
  295. );
  296. }catch(Exception $error){
  297. throw new Exception("Failed to submit anubis challenge");
  298. }
  299. apcu_store("marginalia_cookie", $anubis_key);
  300. }*/
  301. if($get["npt"]){
  302. [$params, $proxy] =
  303. $this->backend->get(
  304. $get["npt"],
  305. "web"
  306. );
  307. try{
  308. $html =
  309. $this->get(
  310. $proxy,
  311. "https://old-search.marginalia.nu/search?" . $params,
  312. [],
  313. //$anubis_key
  314. );
  315. }catch(Exception $error){
  316. throw new Exception("Failed to get HTML");
  317. }
  318. }else{
  319. if($ss == ""){
  320. $params = [
  321. "query" => $search
  322. ];
  323. foreach(["adtech", "recent", "intitle"] as $v){
  324. if($get[$v] == "yes"){
  325. switch($v){
  326. case "adtech": $params["adtech"] = "reduce"; break;
  327. case "recent": $params["recent"] = "recent"; break;
  328. case "adtech": $params["searchTitle"] = "title"; break;
  329. }
  330. }
  331. }
  332. }else{
  333. $params = [];
  334. }
  335. try{
  336. $html =
  337. $this->get(
  338. $proxy,
  339. "https://old-search.marginalia.nu/search" . $ss,
  340. $params,
  341. //$anubis_key
  342. );
  343. }catch(Exception $error){
  344. throw new Exception("Failed to get HTML");
  345. }
  346. }
  347. $this->fuckhtml->load($html);
  348. // detect meta redirect
  349. $title =
  350. $this->fuckhtml
  351. ->getElementsByTagName(
  352. "title"
  353. );
  354. if(
  355. count($title) !== 0 &&
  356. $this->fuckhtml
  357. ->getTextContent(
  358. $title[0]
  359. ) == "Error"
  360. ){
  361. // redirect detected
  362. // get timeout
  363. $timeout =
  364. $this->fuckhtml
  365. ->getElementById(
  366. "countdown",
  367. "b"
  368. );
  369. if(count($timeout) === null){
  370. throw new Exception("Failed to find timeout value");
  371. }
  372. $timeout =
  373. $this->fuckhtml
  374. ->getTextContent(
  375. $timeout
  376. );
  377. preg_match(
  378. '/location\.replace\(\'([^\']+)\'\)/',
  379. $html,
  380. $redirect
  381. );
  382. if(!isset($redirect[1])){
  383. throw new Exception("Failed to grep redirect value");
  384. }
  385. $one = 1;
  386. $redirect =
  387. str_replace(
  388. "/search",
  389. "",
  390. $redirect[1],
  391. $one
  392. );
  393. sleep((int)$timeout);
  394. return $this->web($get, $redirect);
  395. }
  396. // detect internal error
  397. $infobox =
  398. $this->fuckhtml
  399. ->getElementsByClassName("infobox");
  400. if(count($infobox) !== 0){
  401. foreach($infobox as $i){
  402. $this->fuckhtml->load($i);
  403. $h2 =
  404. $this->fuckhtml
  405. ->getElementsByTagName("h2");
  406. if(
  407. count($h2) !== 0 &&
  408. $this->fuckhtml
  409. ->getTextContent(
  410. $h2[0]
  411. ) == "Internal error"
  412. ){
  413. throw new Exception("Marginalia returned an internal server error");
  414. }
  415. }
  416. // reset
  417. $this->fuckhtml->load($html);
  418. }
  419. $sections =
  420. $this->fuckhtml
  421. ->getElementsByClassName(
  422. "card search-result",
  423. "section"
  424. );
  425. foreach($sections as $section){
  426. $this->fuckhtml->load($section);
  427. $title =
  428. $this->fuckhtml
  429. ->getElementsByClassName(
  430. "title",
  431. "a"
  432. )[0];
  433. $description =
  434. $this->fuckhtml
  435. ->getElementsByClassName(
  436. "description",
  437. "p"
  438. );
  439. if(count($description) !== 0){
  440. $description =
  441. $this->fuckhtml
  442. ->getTextContent(
  443. $description[0]
  444. );
  445. }else{
  446. $description = null;
  447. }
  448. $sublinks = [];
  449. $sublink_html =
  450. $this->fuckhtml
  451. ->getElementsByClassName("additional-results");
  452. if(count($sublink_html) !== 0){
  453. $this->fuckhtml->load($sublink_html[0]);
  454. $links =
  455. $this->fuckhtml
  456. ->getElementsByTagName("a");
  457. foreach($links as $link){
  458. $sublinks[] = [
  459. "title" =>
  460. $this->fuckhtml
  461. ->getTextContent(
  462. $link
  463. ),
  464. "date" => null,
  465. "description" => null,
  466. "url" =>
  467. $this->fuckhtml
  468. ->getTextContent(
  469. $link["attributes"]["href"]
  470. )
  471. ];
  472. }
  473. }
  474. $out["web"][] = [
  475. "title" =>
  476. $this->fuckhtml
  477. ->getTextContent(
  478. $title
  479. ),
  480. "description" => $description,
  481. "url" =>
  482. $this->fuckhtml
  483. ->getTextContent(
  484. $title["attributes"]["href"]
  485. ),
  486. "date" => null,
  487. "type" => "web",
  488. "thumb" => [
  489. "url" => null,
  490. "ratio" => null
  491. ],
  492. "sublink" => $sublinks,
  493. "table" => []
  494. ];
  495. }
  496. // get next page
  497. $this->fuckhtml->load($html);
  498. $pagination =
  499. $this->fuckhtml
  500. ->getElementsByAttributeValue(
  501. "aria-label",
  502. "pagination",
  503. "nav"
  504. );
  505. if(count($pagination) === 0){
  506. // no pagination
  507. return $out;
  508. }
  509. $this->fuckhtml->load($pagination[0]);
  510. $pages =
  511. $this->fuckhtml
  512. ->getElementsByClassName(
  513. "page-link",
  514. "a"
  515. );
  516. $found_current_page = false;
  517. foreach($pages as $page){
  518. if(
  519. stripos(
  520. $page["attributes"]["class"],
  521. "active"
  522. ) !== false
  523. ){
  524. $found_current_page = true;
  525. continue;
  526. }
  527. if($found_current_page){
  528. // we found current page index, and we iterated over
  529. // the next page <a>
  530. $out["npt"] =
  531. $this->backend->store(
  532. parse_url(
  533. $page["attributes"]["href"],
  534. PHP_URL_QUERY
  535. ),
  536. "web",
  537. $proxy
  538. );
  539. break;
  540. }
  541. }
  542. return $out;
  543. }
  544. }