marginalia.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  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){
  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. if(($anubis_key = apcu_fetch("marginalia_cookie")) === false){
  257. try{
  258. $html =
  259. $this->get(
  260. $proxy,
  261. "https://old-search.marginalia.nu/"
  262. );
  263. }catch(Exception $error){
  264. throw new Exception("Failed to get anubis challenge");
  265. }
  266. try{
  267. $anubis_data = $this->anubis->scrape($html);
  268. }catch(Exception $error){
  269. throw new Exception($error);
  270. }
  271. // send anubis response & get cookies
  272. // 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
  273. try{
  274. $anubis_key =
  275. $this->get(
  276. $proxy,
  277. "https://old-search.marginalia.nu/.within.website/x/cmd/anubis/api/pass-challenge",
  278. [
  279. "response" => $anubis_data["response"],
  280. "nonce" => $anubis_data["nonce"],
  281. "redir" => "https://old-search.marginalia.nu/",
  282. "elapsedTime" => random_int(1000, 2000)
  283. ],
  284. 0
  285. );
  286. }catch(Exception $error){
  287. throw new Exception("Failed to submit anubis challenge");
  288. }
  289. apcu_store("marginalia_cookie", $anubis_key);
  290. }
  291. if($get["npt"]){
  292. [$params, $proxy] =
  293. $this->backend->get(
  294. $get["npt"],
  295. "web"
  296. );
  297. try{
  298. $html =
  299. $this->get(
  300. $proxy,
  301. "https://old-search.marginalia.nu/search?" . $params,
  302. [],
  303. $anubis_key
  304. );
  305. }catch(Exception $error){
  306. throw new Exception("Failed to get HTML");
  307. }
  308. }else{
  309. $params = [
  310. "query" => $search
  311. ];
  312. foreach(["adtech", "recent", "intitle"] as $v){
  313. if($get[$v] == "yes"){
  314. switch($v){
  315. case "adtech": $params["adtech"] = "reduce"; break;
  316. case "recent": $params["recent"] = "recent"; break;
  317. case "adtech": $params["searchTitle"] = "title"; break;
  318. }
  319. }
  320. }
  321. try{
  322. $html =
  323. $this->get(
  324. $proxy,
  325. "https://old-search.marginalia.nu/search",
  326. $params,
  327. $anubis_key
  328. );
  329. }catch(Exception $error){
  330. throw new Exception("Failed to get HTML");
  331. }
  332. }
  333. $this->fuckhtml->load($html);
  334. $sections =
  335. $this->fuckhtml
  336. ->getElementsByClassName(
  337. "card search-result",
  338. "section"
  339. );
  340. foreach($sections as $section){
  341. $this->fuckhtml->load($section);
  342. $title =
  343. $this->fuckhtml
  344. ->getElementsByClassName(
  345. "title",
  346. "a"
  347. )[0];
  348. $description =
  349. $this->fuckhtml
  350. ->getElementsByClassName(
  351. "description",
  352. "p"
  353. );
  354. if(count($description) !== 0){
  355. $description =
  356. $this->fuckhtml
  357. ->getTextContent(
  358. $description[0]
  359. );
  360. }else{
  361. $description = null;
  362. }
  363. $sublinks = [];
  364. $sublink_html =
  365. $this->fuckhtml
  366. ->getElementsByClassName("additional-results");
  367. if(count($sublink_html) !== 0){
  368. $this->fuckhtml->load($sublink_html[0]);
  369. $links =
  370. $this->fuckhtml
  371. ->getElementsByTagName("a");
  372. foreach($links as $link){
  373. $sublinks[] = [
  374. "title" =>
  375. $this->fuckhtml
  376. ->getTextContent(
  377. $link
  378. ),
  379. "date" => null,
  380. "description" => null,
  381. "url" =>
  382. $this->fuckhtml
  383. ->getTextContent(
  384. $link["attributes"]["href"]
  385. )
  386. ];
  387. }
  388. }
  389. $out["web"][] = [
  390. "title" =>
  391. $this->fuckhtml
  392. ->getTextContent(
  393. $title
  394. ),
  395. "description" => $description,
  396. "url" =>
  397. $this->fuckhtml
  398. ->getTextContent(
  399. $title["attributes"]["href"]
  400. ),
  401. "date" => null,
  402. "type" => "web",
  403. "thumb" => [
  404. "url" => null,
  405. "ratio" => null
  406. ],
  407. "sublink" => $sublinks,
  408. "table" => []
  409. ];
  410. }
  411. // get next page
  412. $this->fuckhtml->load($html);
  413. $pagination =
  414. $this->fuckhtml
  415. ->getElementsByAttributeValue(
  416. "aria-label",
  417. "pagination",
  418. "nav"
  419. );
  420. if(count($pagination) === 0){
  421. // no pagination
  422. return $out;
  423. }
  424. $this->fuckhtml->load($pagination[0]);
  425. $pages =
  426. $this->fuckhtml
  427. ->getElementsByClassName(
  428. "page-link",
  429. "a"
  430. );
  431. $found_current_page = false;
  432. foreach($pages as $page){
  433. if(
  434. stripos(
  435. $page["attributes"]["class"],
  436. "active"
  437. ) !== false
  438. ){
  439. $found_current_page = true;
  440. continue;
  441. }
  442. if($found_current_page){
  443. // we found current page index, and we iterated over
  444. // the next page <a>
  445. $out["npt"] =
  446. $this->backend->store(
  447. parse_url(
  448. $page["attributes"]["href"],
  449. PHP_URL_QUERY
  450. ),
  451. "web",
  452. $proxy
  453. );
  454. break;
  455. }
  456. }
  457. return $out;
  458. }
  459. }