marginalia.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  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. $json = json_decode($json, true);
  234. foreach($json["results"] as $result){
  235. $out["web"][] = [
  236. "title" => $result["title"],
  237. "description" => str_replace("\n", " ", $result["description"]),
  238. "url" => $result["url"],
  239. "date" => null,
  240. "type" => "web",
  241. "thumb" => [
  242. "url" => null,
  243. "ratio" => null
  244. ],
  245. "sublink" => [],
  246. "table" => []
  247. ];
  248. }
  249. return $out;
  250. }
  251. // HTML parser
  252. $proxy = $this->backend->get_ip();
  253. //
  254. // Bypass anubis check
  255. //
  256. /*
  257. if(($anubis_key = apcu_fetch("marginalia_cookie")) === false){
  258. try{
  259. $html =
  260. $this->get(
  261. $proxy,
  262. "https://old-search.marginalia.nu/search",
  263. [
  264. "query" => $search
  265. ]
  266. );
  267. }catch(Exception $error){
  268. throw new Exception("Failed to get anubis challenge");
  269. }
  270. try{
  271. $anubis_data = $this->anubis->scrape($html);
  272. }catch(Exception $error){
  273. throw new Exception($error);
  274. }
  275. // send anubis response & get cookies
  276. // 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
  277. try{
  278. $anubis_key =
  279. $this->get(
  280. $proxy,
  281. "https://old-search.marginalia.nu/.within.website/x/cmd/anubis/api/pass-challenge",
  282. [
  283. "response" => $anubis_data["response"],
  284. "nonce" => $anubis_data["nonce"],
  285. "redir" => "https://old-search.marginalia.nu/",
  286. "elapsedTime" => random_int(1000, 2000)
  287. ],
  288. 0
  289. );
  290. }catch(Exception $error){
  291. throw new Exception("Failed to submit anubis challenge");
  292. }
  293. apcu_store("marginalia_cookie", $anubis_key);
  294. }*/
  295. if($get["npt"]){
  296. [$params, $proxy] =
  297. $this->backend->get(
  298. $get["npt"],
  299. "web"
  300. );
  301. try{
  302. $html =
  303. $this->get(
  304. $proxy,
  305. "https://old-search.marginalia.nu/search?" . $params,
  306. [],
  307. //$anubis_key
  308. );
  309. }catch(Exception $error){
  310. throw new Exception("Failed to get HTML");
  311. }
  312. }else{
  313. if($ss == ""){
  314. $params = [
  315. "query" => $search
  316. ];
  317. foreach(["adtech", "recent", "intitle"] as $v){
  318. if($get[$v] == "yes"){
  319. switch($v){
  320. case "adtech": $params["adtech"] = "reduce"; break;
  321. case "recent": $params["recent"] = "recent"; break;
  322. case "adtech": $params["searchTitle"] = "title"; break;
  323. }
  324. }
  325. }
  326. }else{
  327. $params = [];
  328. }
  329. try{
  330. $html =
  331. $this->get(
  332. $proxy,
  333. "https://old-search.marginalia.nu/search" . $ss,
  334. $params,
  335. //$anubis_key
  336. );
  337. }catch(Exception $error){
  338. throw new Exception("Failed to get HTML");
  339. }
  340. }
  341. $this->fuckhtml->load($html);
  342. // detect meta redirect
  343. $title =
  344. $this->fuckhtml
  345. ->getElementsByTagName(
  346. "title"
  347. );
  348. if(
  349. count($title) !== 0 &&
  350. $this->fuckhtml
  351. ->getTextContent(
  352. $title[0]
  353. ) == "Error"
  354. ){
  355. // redirect detected
  356. // get timeout
  357. $timeout =
  358. $this->fuckhtml
  359. ->getElementById(
  360. "countdown",
  361. "b"
  362. );
  363. if(count($timeout) === null){
  364. throw new Exception("Failed to find timeout value");
  365. }
  366. $timeout =
  367. $this->fuckhtml
  368. ->getTextContent(
  369. $timeout
  370. );
  371. preg_match(
  372. '/location\.replace\(\'([^\']+)\'\)/',
  373. $html,
  374. $redirect
  375. );
  376. if(!isset($redirect[1])){
  377. throw new Exception("Failed to grep redirect value");
  378. }
  379. $one = 1;
  380. $redirect =
  381. str_replace(
  382. "/search",
  383. "",
  384. $redirect[1],
  385. $one
  386. );
  387. sleep((int)$timeout);
  388. return $this->web($get, $redirect);
  389. }
  390. // detect internal error
  391. $infobox =
  392. $this->fuckhtml
  393. ->getElementsByClassName("infobox");
  394. if(count($infobox) !== 0){
  395. foreach($infobox as $i){
  396. $this->fuckhtml->load($i);
  397. $h2 =
  398. $this->fuckhtml
  399. ->getElementsByTagName("h2");
  400. if(
  401. count($h2) !== 0 &&
  402. $this->fuckhtml
  403. ->getTextContent(
  404. $h2[0]
  405. ) == "Internal error"
  406. ){
  407. throw new Exception("Marginalia returned an internal server error");
  408. }
  409. }
  410. // reset
  411. $this->fuckhtml->load($html);
  412. }
  413. $sections =
  414. $this->fuckhtml
  415. ->getElementsByClassName(
  416. "card search-result",
  417. "section"
  418. );
  419. foreach($sections as $section){
  420. $this->fuckhtml->load($section);
  421. $title =
  422. $this->fuckhtml
  423. ->getElementsByClassName(
  424. "title",
  425. "a"
  426. )[0];
  427. $description =
  428. $this->fuckhtml
  429. ->getElementsByClassName(
  430. "description",
  431. "p"
  432. );
  433. if(count($description) !== 0){
  434. $description =
  435. $this->fuckhtml
  436. ->getTextContent(
  437. $description[0]
  438. );
  439. }else{
  440. $description = null;
  441. }
  442. $sublinks = [];
  443. $sublink_html =
  444. $this->fuckhtml
  445. ->getElementsByClassName("additional-results");
  446. if(count($sublink_html) !== 0){
  447. $this->fuckhtml->load($sublink_html[0]);
  448. $links =
  449. $this->fuckhtml
  450. ->getElementsByTagName("a");
  451. foreach($links as $link){
  452. $sublinks[] = [
  453. "title" =>
  454. $this->fuckhtml
  455. ->getTextContent(
  456. $link
  457. ),
  458. "date" => null,
  459. "description" => null,
  460. "url" =>
  461. $this->fuckhtml
  462. ->getTextContent(
  463. $link["attributes"]["href"]
  464. )
  465. ];
  466. }
  467. }
  468. $out["web"][] = [
  469. "title" =>
  470. $this->fuckhtml
  471. ->getTextContent(
  472. $title
  473. ),
  474. "description" => $description,
  475. "url" =>
  476. $this->fuckhtml
  477. ->getTextContent(
  478. $title["attributes"]["href"]
  479. ),
  480. "date" => null,
  481. "type" => "web",
  482. "thumb" => [
  483. "url" => null,
  484. "ratio" => null
  485. ],
  486. "sublink" => $sublinks,
  487. "table" => []
  488. ];
  489. }
  490. // get next page
  491. $this->fuckhtml->load($html);
  492. $pagination =
  493. $this->fuckhtml
  494. ->getElementsByAttributeValue(
  495. "aria-label",
  496. "pagination",
  497. "nav"
  498. );
  499. if(count($pagination) === 0){
  500. // no pagination
  501. return $out;
  502. }
  503. $this->fuckhtml->load($pagination[0]);
  504. $pages =
  505. $this->fuckhtml
  506. ->getElementsByClassName(
  507. "page-link",
  508. "a"
  509. );
  510. $found_current_page = false;
  511. foreach($pages as $page){
  512. if(
  513. stripos(
  514. $page["attributes"]["class"],
  515. "active"
  516. ) !== false
  517. ){
  518. $found_current_page = true;
  519. continue;
  520. }
  521. if($found_current_page){
  522. // we found current page index, and we iterated over
  523. // the next page <a>
  524. $out["npt"] =
  525. $this->backend->store(
  526. parse_url(
  527. $page["attributes"]["href"],
  528. PHP_URL_QUERY
  529. ),
  530. "web",
  531. $proxy
  532. );
  533. break;
  534. }
  535. }
  536. return $out;
  537. }
  538. }