marginalia.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  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. $sections =
  391. $this->fuckhtml
  392. ->getElementsByClassName(
  393. "card search-result",
  394. "section"
  395. );
  396. foreach($sections as $section){
  397. $this->fuckhtml->load($section);
  398. $title =
  399. $this->fuckhtml
  400. ->getElementsByClassName(
  401. "title",
  402. "a"
  403. )[0];
  404. $description =
  405. $this->fuckhtml
  406. ->getElementsByClassName(
  407. "description",
  408. "p"
  409. );
  410. if(count($description) !== 0){
  411. $description =
  412. $this->fuckhtml
  413. ->getTextContent(
  414. $description[0]
  415. );
  416. }else{
  417. $description = null;
  418. }
  419. $sublinks = [];
  420. $sublink_html =
  421. $this->fuckhtml
  422. ->getElementsByClassName("additional-results");
  423. if(count($sublink_html) !== 0){
  424. $this->fuckhtml->load($sublink_html[0]);
  425. $links =
  426. $this->fuckhtml
  427. ->getElementsByTagName("a");
  428. foreach($links as $link){
  429. $sublinks[] = [
  430. "title" =>
  431. $this->fuckhtml
  432. ->getTextContent(
  433. $link
  434. ),
  435. "date" => null,
  436. "description" => null,
  437. "url" =>
  438. $this->fuckhtml
  439. ->getTextContent(
  440. $link["attributes"]["href"]
  441. )
  442. ];
  443. }
  444. }
  445. $out["web"][] = [
  446. "title" =>
  447. $this->fuckhtml
  448. ->getTextContent(
  449. $title
  450. ),
  451. "description" => $description,
  452. "url" =>
  453. $this->fuckhtml
  454. ->getTextContent(
  455. $title["attributes"]["href"]
  456. ),
  457. "date" => null,
  458. "type" => "web",
  459. "thumb" => [
  460. "url" => null,
  461. "ratio" => null
  462. ],
  463. "sublink" => $sublinks,
  464. "table" => []
  465. ];
  466. }
  467. // get next page
  468. $this->fuckhtml->load($html);
  469. $pagination =
  470. $this->fuckhtml
  471. ->getElementsByAttributeValue(
  472. "aria-label",
  473. "pagination",
  474. "nav"
  475. );
  476. if(count($pagination) === 0){
  477. // no pagination
  478. return $out;
  479. }
  480. $this->fuckhtml->load($pagination[0]);
  481. $pages =
  482. $this->fuckhtml
  483. ->getElementsByClassName(
  484. "page-link",
  485. "a"
  486. );
  487. $found_current_page = false;
  488. foreach($pages as $page){
  489. if(
  490. stripos(
  491. $page["attributes"]["class"],
  492. "active"
  493. ) !== false
  494. ){
  495. $found_current_page = true;
  496. continue;
  497. }
  498. if($found_current_page){
  499. // we found current page index, and we iterated over
  500. // the next page <a>
  501. $out["npt"] =
  502. $this->backend->store(
  503. parse_url(
  504. $page["attributes"]["href"],
  505. PHP_URL_QUERY
  506. ),
  507. "web",
  508. $proxy
  509. );
  510. break;
  511. }
  512. }
  513. return $out;
  514. }
  515. }