startpage.php 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516
  1. <?php
  2. class startpage{
  3. public function __construct(){
  4. include "lib/backend.php";
  5. $this->backend = new backend("startpage");
  6. include "lib/fuckhtml.php";
  7. $this->fuckhtml = new fuckhtml();
  8. include "lib/anubis.php";
  9. $this->anubis = new anubis();
  10. $this->anubis_error = "all gucci";
  11. }
  12. public function getfilters($page){
  13. switch($page){
  14. case "web":
  15. return [
  16. "country" => [
  17. "display" => "Country",
  18. "option" => [
  19. "any" => "All Regions",
  20. "es_AR" => "Argentina",
  21. "en_AU" => "Australia",
  22. "de_AT" => "Austria",
  23. "ru_BY" => "Belarus",
  24. "fr_BE" => "Belgium (FR)",
  25. "nl_BE" => "Belgium (NL)",
  26. "bg_BG" => "Bulgaria",
  27. "en_CA" => "Canada (EN)",
  28. "fr_CA" => "Canada (FR)",
  29. "es_CL" => "Chile",
  30. "es_CO" => "Colombia",
  31. "cs_CZ" => "Czech Republic",
  32. "da_DK" => "Denmark",
  33. "ar_EG" => "Egypt",
  34. "et_EE" => "Estonia",
  35. "fi_FI" => "Finland",
  36. "fr_FR" => "France",
  37. "de_DE" => "Germany",
  38. "el_GR" => "Greece",
  39. "hu_HU" => "Hungary",
  40. "hi_IN" => "India (HI)",
  41. "en_IN" => "India (EN)",
  42. "id_ID" => "Indonesia (ID)",
  43. "en_ID" => "Indonesia (EN)",
  44. "en_IE" => "Ireland",
  45. "it_IT" => "Italy",
  46. "ja_JP" => "Japan",
  47. "ko_KR" => "Korea",
  48. "ms_MY" => "Malaysia (MS)",
  49. "en_MY" => "Malaysia (EN)",
  50. "es_MX" => "Mexico",
  51. "nl_NL" => "Netherlands",
  52. "en_NZ" => "New Zealand",
  53. "no_NO" => "Norway",
  54. "es_PE" => "Peru",
  55. "fil_PH" => "Philippines (FIL)",
  56. "en_PH" => "Philippines (EN)",
  57. "pl_PL" => "Poland",
  58. "pt_PT" => "Portugal",
  59. "ro_RO" => "Romania",
  60. "ru_RU" => "Russia",
  61. "ms_SG" => "Singapore (MS)",
  62. "en_SG" => "Singapore (EN)",
  63. "es_ES" => "Spain (ES)",
  64. "ca_ES" => "Spain (CA)",
  65. "sv_SE" => "Sweden",
  66. "de_CH" => "Switzerland (DE)",
  67. "fr_CH" => "Switzerland (FR)",
  68. "it_CH" => "Switzerland (IT)",
  69. "tr_TR" => "Turkey",
  70. "uk_UA" => "Ukraine",
  71. "en_US" => "US (EN)",
  72. "es_US" => "US (ES)",
  73. "es_UY" => "Uruguay",
  74. "es_VE" => "Venezuela",
  75. "vi_VN" => "Vietnam (VI)",
  76. "en_VN" => "Vietnam (EN)",
  77. "en_ZA" => "South Africa"
  78. ]
  79. ],
  80. "nsfw" => [ // qadf
  81. "display" => "NSFW",
  82. "option" => [
  83. "yes" => "Yes", // qadf=none
  84. "no" => "No" // qadf=heavy
  85. ]
  86. ],
  87. "time" => [ // with_date
  88. "display" => "Time posted",
  89. "option" => [
  90. "any" => "Any time",
  91. "d" => "Past 24 hours",
  92. "w" => "Past week",
  93. "m" => "Past month",
  94. "y" => "Past year",
  95. ]
  96. ],
  97. "extendedsearch" => [
  98. // undefined display, so it wont show in frontend
  99. "option" => [
  100. "yes" => "Yes",
  101. "no" => "No"
  102. ]
  103. ]
  104. ];
  105. break;
  106. case "images":
  107. return [
  108. "nsfw" => [ // qadf
  109. "display" => "NSFW",
  110. "option" => [
  111. "yes" => "Yes", // qadf=none
  112. "no" => "No" // qadf=heavy
  113. ]
  114. ],
  115. "size" => [ // flimgsize
  116. "display" => "Size",
  117. "option" => [
  118. "any" => "Any size",
  119. "Small" => "Small",
  120. "Medium" => "Medium",
  121. "Large" => "Large",
  122. "Wallpaper" => "Wallpaper",
  123. // from here, image-size-select, var prefix = isz:lt,islt:
  124. "qsvgs" => "Larger than 400x300",
  125. "vga" => "Larger than 640x480",
  126. "svga" => "Larger than 800x600",
  127. "xga" => "Larger than 1024x768",
  128. "qsvgs" => "Larger than 400x300",
  129. "2mp" => "Larger than 2 MP (1600x1200)",
  130. "4mp" => "Larger than 4 MP (2272x1704)",
  131. "6mp" => "Larger than 6 MP (2816x2112)",
  132. "8mp" => "Larger than 8 MP (3264x2448)",
  133. "10mp" => "Larger than 10 MP (3648x2736)",
  134. "12mp" => "Larger than 12 MP (4096x3072)",
  135. "15mp" => "Larger than 15 MP (4480x3360)",
  136. "20mp" => "Larger than 20 MP (5120x3840)",
  137. "40mp" => "Larger than 40 MP (7216x5412)",
  138. "70mp" => "Larger than 70 MP (9600x7200)"
  139. ]
  140. ],
  141. "color" => [ // flimgcolor
  142. "display" => "Color",
  143. "option" => [
  144. "any" => "Any color",
  145. // from here, var prefix = ic:
  146. "color" => "Color only",
  147. "bnw" => "Black & white", // set to "gray"
  148. // from here, var prefix = ic:specific,isc:
  149. "red" => "Red",
  150. "orange" => "Orange",
  151. "yellow" => "Yellow",
  152. "green" => "Green",
  153. "teal" => "Teal",
  154. "blue" => "Blue",
  155. "purple" => "Purple",
  156. "pink" => "Pink",
  157. "white" => "White",
  158. "gray" => "Gray",
  159. "black" => "Black",
  160. "brown" => "Brown"
  161. ]
  162. ],
  163. "type" => [ // flimgtype
  164. "display" => "Type",
  165. "option" => [
  166. "any" => "Any type",
  167. "AnimatedGif" => "Animated GIF",
  168. "Clipart" => "Clip Art",
  169. "Line" => "Line Drawing",
  170. "Photo" => "Photograph",
  171. "Transparent" => "Transparent Background"
  172. ]
  173. ],
  174. "license" => [ // flimglicense
  175. "display" => "License",
  176. "option" => [
  177. "any" => "Any license",
  178. "p" => "Public domain",
  179. "s" => "Free to share",
  180. "sc" => "Free to share commercially",
  181. "m" => "Free to modify",
  182. "mc" => "Free to modify commercially"
  183. ]
  184. ]
  185. ];
  186. break;
  187. case "videos":
  188. return [
  189. "nsfw" => [ // qadf
  190. "display" => "NSFW",
  191. "option" => [
  192. "yes" => "Yes", // qadf=none
  193. "no" => "No" // qadf=heavy
  194. ]
  195. ],
  196. "sort" => [
  197. "display" => "Sort by",
  198. "option" => [
  199. "relevance" => "Most relevant",
  200. "popular" => "Most popular",
  201. "recent" => "Most recent"
  202. ]
  203. ],
  204. "duration" => [ // with_duration
  205. "display" => "Duration",
  206. "option" => [
  207. "any" => "Any duration",
  208. "short" => "Short",
  209. "medium" => "Medium",
  210. "long" => "Long"
  211. ]
  212. ]
  213. ];
  214. break;
  215. case "news":
  216. return [
  217. "nsfw" => [ // qadf
  218. "display" => "NSFW",
  219. "option" => [
  220. "yes" => "Yes", // qadf=none
  221. "no" => "No" // qadf=heavy
  222. ]
  223. ],
  224. "time" => [ // with_date
  225. "display" => "Time posted",
  226. "option" => [
  227. "any" => "Any time",
  228. "d" => "Past 24 hours",
  229. "w" => "Past week",
  230. "m" => "Past month"
  231. ]
  232. ]
  233. ];
  234. break;
  235. //preferences=date_timeEEEworldN1Ndisable_family_filterEEE1N1Ndisable_open_in_new_windowEEE0N1Nenable_post_methodEEE1N1Nenable_proxy_safety_suggestEEE0N1Nenable_stay_controlEEE0N1Ninstant_answersEEE1N1Nlang_homepageEEEs%2Fdevice%2FenN1NlanguageEEEazerbaijaniN1Nlanguage_uiEEEenglishN1Nnum_of_resultsEEE20N1Nsearch_results_regionEEEallN1NsuggestionsEEE1N1Nwt_unitEEEcelsius; Domain=startpage.com; Expires=Mon, 28 Oct 2024 20:21:58 GMT; Secure; Path=/
  236. //preferences=date_timeEEEworldN1Ndisable_family_filterEEE1N1Ndisable_open_in_new_windowEEE0N1Nenable_post_methodEEE1N1Nenable_proxy_safety_suggestEEE0N1Nenable_stay_controlEEE0N1Ninstant_answersEEE1N1Nlang_homepageEEEs%2Fdevice%2FenN1NlanguageEEEenglishN1Nlanguage_uiEEEenglishN1Nnum_of_resultsEEE20N1Nsearch_results_regionEEEallN1NsuggestionsEEE1N1Nwt_unitEEEcelsius; Domain=startpage.com; Expires=Mon, 28 Oct 2024 20:22:52 GMT; Secure; Path=/
  237. }
  238. }
  239. private function get($proxy, $url, $get = [], $post = false, $is_xhr = false, $return_cookies = false, $passed_cookies = []){
  240. $curlproc = curl_init();
  241. if($post === true){
  242. curl_setopt($curlproc, CURLOPT_POST, true);
  243. curl_setopt($curlproc, CURLOPT_POSTFIELDS, $get);
  244. }elseif($get !== []){
  245. $get = http_build_query($get);
  246. $url .= "?" . $get;
  247. }
  248. if($return_cookies){
  249. $cookies = [];
  250. curl_setopt($curlproc, CURLOPT_HEADERFUNCTION, function($curlproc, $header) use (&$cookies){
  251. $length = strlen($header);
  252. $header = explode(":", $header, 2);
  253. if(trim(strtolower($header[0])) == "set-cookie"){
  254. $cookie_tmp = explode("=", trim($header[1]), 2);
  255. $cookies[trim($cookie_tmp[0])] =
  256. explode(";", $cookie_tmp[1], 2)[0];
  257. }
  258. return $length;
  259. });
  260. }
  261. curl_setopt($curlproc, CURLOPT_URL, $url);
  262. // http2 bypass
  263. curl_setopt($curlproc, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
  264. curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
  265. $passed_cookies["preferences"] = "date_timeEEEworldN1Ndisable_family_filterEEE1N1Ndisable_open_in_new_windowEEE0N1Nenable_post_methodEEE1N1Nenable_proxy_safety_suggestEEE0N1Nenable_stay_controlEEE0N1Ninstant_answersEEE1N1Nlang_homepageEEEs%2Fdevice%2FenN1NlanguageEEEenglishN1Nlanguage_uiEEEenglishN1Nnum_of_resultsEEE20N1Nsearch_results_regionEEEallN1NsuggestionsEEE1N1Nwt_unitEEEcelsius";
  266. foreach($passed_cookies as $name => $val){
  267. $header_cookies[] = "$name=$val";
  268. }
  269. $header_cookies = rtrim(implode("; ", $header_cookies));
  270. if($is_xhr === true){
  271. curl_setopt($curlproc, CURLOPT_HTTPHEADER,
  272. ["User-Agent: " . config::USER_AGENT,
  273. "Accept: application/json",
  274. "Accept-Language: en-US,en;q=0.5",
  275. "Accept-Encoding: gzip",
  276. "Referer: https://www.startpage.com/",
  277. "Content-Type: application/json",
  278. "Content-Length: " . strlen($get),
  279. "Origin: https://www.startpage.com/",
  280. "DNT: 1",
  281. "Connection: keep-alive",
  282. "Cookie: $header_cookies",
  283. "Sec-Fetch-Dest: empty",
  284. "Sec-Fetch-Mode: cors",
  285. "Sec-Fetch-Site: same-origin",
  286. "TE: trailers"]
  287. );
  288. }elseif($post === true){
  289. curl_setopt($curlproc, CURLOPT_HTTPHEADER,
  290. ["User-Agent: " . config::USER_AGENT,
  291. "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
  292. "Accept-Language: en-US,en;q=0.5",
  293. "Accept-Encoding: gzip",
  294. "Referer: https://www.startpage.com/",
  295. "Content-Type: application/x-www-form-urlencoded",
  296. "Content-Length: " . strlen($get),
  297. "DNT: 1",
  298. "Connection: keep-alive",
  299. "Cookie: $header_cookies",
  300. "Upgrade-Insecure-Requests: 1",
  301. "Sec-Fetch-Dest: document",
  302. "Sec-Fetch-Mode: navigate",
  303. "Sec-Fetch-Site: none",
  304. "Sec-Fetch-User: ?1",
  305. "Priority: u=0, i",
  306. "TE: trailers"]
  307. );
  308. }else{
  309. curl_setopt($curlproc, CURLOPT_HTTPHEADER,
  310. ["User-Agent: " . config::USER_AGENT,
  311. "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
  312. "Accept-Language: en-US,en;q=0.5",
  313. "Accept-Encoding: gzip",
  314. "DNT: 1",
  315. "Connection: keep-alive",
  316. "Cookie: $header_cookies",
  317. "Sec-Fetch-Dest: document",
  318. "Sec-Fetch-Mode: navigate",
  319. "Sec-Fetch-Site: none",
  320. "Sec-Fetch-User: ?1",
  321. "Priority: u=0, i",
  322. "TE: trailers"]
  323. );
  324. }
  325. curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true);
  326. curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2);
  327. curl_setopt($curlproc, CURLOPT_SSL_VERIFYPEER, true);
  328. curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30);
  329. curl_setopt($curlproc, CURLOPT_TIMEOUT, 30);
  330. $this->backend->assign_proxy($curlproc, $proxy);
  331. $data = curl_exec($curlproc);
  332. if(curl_errno($curlproc)){
  333. throw new Exception(curl_error($curlproc));
  334. }
  335. curl_close($curlproc);
  336. if($return_cookies){
  337. return [
  338. "cookies" => $cookies,
  339. "data" => $data
  340. ];
  341. }
  342. return $data;
  343. }
  344. public function web($get){
  345. if($get["npt"]){
  346. $this->anubis_error = "Not ran on pagination";
  347. [$post, $proxy] = $this->backend->get($get["npt"], "web");
  348. $post = json_decode($post, true);
  349. $query = $post["query"];
  350. $cookies = $post["cookies"];
  351. try{
  352. $html = $this->get(
  353. $proxy,
  354. "https://www.startpage.com/sp/search",
  355. $query,
  356. true,
  357. false,
  358. false,
  359. $cookies
  360. );
  361. }catch(Exception $error){
  362. throw new Exception("Failed to fetch search page");
  363. }
  364. $get_instant_answer = false;
  365. }else{
  366. $proxy = $this->backend->get_ip();
  367. $params = [
  368. "query" => $get["s"],
  369. "cat" => "web",
  370. "pl" => "opensearch"
  371. ];
  372. if($get["nsfw"] == "no"){
  373. $params["qadf"] = "heavy";
  374. $get_instant_answer = false;
  375. }else{
  376. $get_instant_answer = true;
  377. }
  378. if($get["country"] !== "any"){
  379. $params["qsr"] = $get["country"];
  380. }
  381. if($get["time"] !== "any"){
  382. $params["with_date"] = $get["time"];
  383. }
  384. try{
  385. $html = $this->get(
  386. $proxy,
  387. "https://www.startpage.com/sp/search",
  388. $params,
  389. false,
  390. false,
  391. true,
  392. []
  393. );
  394. }catch(Exception $error){
  395. throw new Exception("Failed to fetch search page");
  396. }
  397. //$html = file_get_contents("scraper/startpage.html");
  398. // detect captcha on that page
  399. $this->detect_captcha($html["data"]);
  400. // solve anubis if need be
  401. $cookies = $this->solve_anubis($proxy, $html);
  402. if($cookies === false){
  403. $cookies = [];
  404. $html = $html["data"];
  405. }else{
  406. try{
  407. $html = $this->get(
  408. $proxy,
  409. "https://www.startpage.com/sp/search",
  410. $params,
  411. false,
  412. false,
  413. false,
  414. $cookies
  415. );
  416. }catch(Exception $error){
  417. throw new Exception("Failed to re-fetch search page. Anubis error: {$this->anubis_error}");
  418. }
  419. }
  420. }
  421. // detect captcha even if we passed anubis challenge
  422. $this->detect_captcha($html);
  423. //echo htmlspecialchars($html);
  424. if(
  425. preg_match(
  426. '/React\.createElement\(UIStartpage\.AppSerpWeb, ?(.+)\),?$/m',
  427. $html,
  428. $matches
  429. ) === 0
  430. ){
  431. throw new Exception("Failed to grep JSON object. Anubis error: {$this->anubis_error}");
  432. }
  433. $json = json_decode($matches[1], true);
  434. if($json === null){
  435. throw new Exception("Failed to decode JSON. Anubis error: {$this->anubis_error}");
  436. }
  437. //print_r($json);
  438. $out = [
  439. "status" => "ok",
  440. "spelling" => [
  441. "type" => "no_correction",
  442. "using" => null,
  443. "correction" => null
  444. ],
  445. "npt" => null,
  446. "answer" => [],
  447. "web" => [],
  448. "image" => [],
  449. "video" => [],
  450. "news" => [],
  451. "related" => []
  452. ];
  453. // get npt
  454. $out["npt"] = $this->parse_npt($json, "web", $proxy, $cookies);
  455. foreach($json["render"]["presenter"]["regions"]["mainline"] as $category){
  456. if(!isset($category["display_type"])){
  457. continue;
  458. }
  459. switch($category["display_type"]){
  460. case "web-google":
  461. foreach($category["results"] as $result){
  462. $sublinks = [];
  463. foreach($result["siteLinks"] as $sublink){
  464. $sublinks[] = [
  465. "title" => $sublink["title"],
  466. "description" => null,
  467. "url" => $sublink["clickUrl"]
  468. ];
  469. }
  470. $description =
  471. explode(
  472. "...",
  473. $this->titledots(
  474. html_entity_decode(
  475. $this->fuckhtml
  476. ->getTextContent(
  477. $result["description"]
  478. )
  479. )
  480. ),
  481. 2
  482. );
  483. $date = strtotime(trim($description[0]));
  484. if(
  485. $date === false ||
  486. count($description) !== 2 ||
  487. strlen($description[0]) > 14
  488. ){
  489. // no date found
  490. $description =
  491. implode(
  492. " ... ",
  493. $description
  494. );
  495. $date = null;
  496. }else{
  497. // date found
  498. $description = ltrim($description[1]);
  499. }
  500. $out["web"][] = [
  501. "title" =>
  502. $this->titledots(
  503. html_entity_decode(
  504. $this->fuckhtml
  505. ->getTextContent(
  506. $result["title"]
  507. )
  508. )
  509. ),
  510. "description" => $description,
  511. "url" => $result["clickUrl"],
  512. "date" => $date,
  513. "type" => "web",
  514. "thumb" => [
  515. "url" => null,
  516. "ratio" => null
  517. ],
  518. "sublink" => $sublinks,
  519. "table" => []
  520. ];
  521. }
  522. break;
  523. case "images-qi-top":
  524. foreach($category["results"] as $result){
  525. $out["image"][] = [
  526. "title" =>
  527. $this->titledots(
  528. html_entity_decode(
  529. $this->fuckhtml
  530. ->getTextContent(
  531. $result["title"]
  532. )
  533. )
  534. ),
  535. "source" => [
  536. [
  537. "url" => $result["rawImageUrl"],
  538. "width" => (int)$result["width"],
  539. "height" => (int)$result["height"]
  540. ],
  541. [
  542. "url" => $this->unshitimage($result["mdThumbnailUrl"]),
  543. "width" => (int)$result["mdThumbnailWidth"],
  544. "height" => (int)$result["mdThumbnailHeight"]
  545. ]
  546. ],
  547. "url" =>
  548. $result["altClickUrl"]
  549. ];
  550. }
  551. break;
  552. case "spellsuggest-google":
  553. if(isset($category["results"][0]["query"])){
  554. $out["spelling"] =
  555. [
  556. "type" => "including",
  557. "using" => rawurldecode($json["render"]["query"]),
  558. "correction" => html_entity_decode($category["results"][0]["query"])
  559. ];
  560. }
  561. break;
  562. case "dictionary-qi":
  563. foreach($category["results"] as $result){
  564. $answer = [
  565. "title" => $result["word"],
  566. "description" => [],
  567. "url" => null,
  568. "thumb" => null,
  569. "table" => [],
  570. "sublink" => []
  571. ];
  572. foreach($result["lexical_categories"] as $lexic_type => $definitions){
  573. $answer["description"][] = [
  574. "type" => "title",
  575. "value" => $lexic_type
  576. ];
  577. $i = 0;
  578. foreach($definitions as $definition){
  579. $text_definition = trim($definition["definition"]);
  580. $text_example = trim($definition["example"]);
  581. $text_synonyms = implode(", ", $definition["synonyms"]);
  582. if($text_definition != ""){
  583. $i++;
  584. $c = count($answer["description"]) - 1;
  585. if(
  586. $c !== 0 &&
  587. $answer["description"][$c]["type"] == "text"
  588. ){
  589. $answer["description"][$c]["value"] .=
  590. "\n\n" . $i . ". " . $text_definition;
  591. }else{
  592. $answer["description"][] = [
  593. "type" => "text",
  594. "value" => $i . ". " . $text_definition
  595. ];
  596. }
  597. }
  598. if($text_example != ""){
  599. $answer["description"][] = [
  600. "type" => "quote",
  601. "value" => $text_example
  602. ];
  603. }
  604. if($text_synonyms != ""){
  605. $answer["description"][] = [
  606. "type" => "text",
  607. "value" => "Synonyms: " . $text_synonyms
  608. ];
  609. }
  610. }
  611. }
  612. $out["answer"][] = $answer;
  613. }
  614. break;
  615. }
  616. }
  617. return $out;
  618. }
  619. public function image($get){
  620. if($get["npt"]){
  621. $this->anubis_error = "Not ran on pagination";
  622. [$post, $proxy] = $this->backend->get($get["npt"], "images");
  623. $post = json_decode($post, true);
  624. $query = $post["query"];
  625. $cookies = $post["cookies"];
  626. try{
  627. $html = $this->get(
  628. $proxy,
  629. "https://www.startpage.com/sp/search",
  630. $query,
  631. true,
  632. false,
  633. false,
  634. $cookies
  635. );
  636. }catch(Exception $error){
  637. throw new Exception("Failed to fetch search page");
  638. }
  639. }else{
  640. $search = $get["s"];
  641. if(strlen($search) === 0){
  642. throw new Exception("Search term is empty!");
  643. }
  644. $proxy = $this->backend->get_ip();
  645. $params = [
  646. "query" => $get["s"],
  647. "cat" => "images",
  648. "pl" => "opensearch"
  649. ];
  650. if($get["nsfw"] == "no"){
  651. $params["qadf"] = "heavy";
  652. }
  653. if($get["size"] != "any"){
  654. if(
  655. $get["size"] == "Small" ||
  656. $get["size"] == "Medium" ||
  657. $get["size"] == "Large" ||
  658. $get["size"] == "Wallpaper"
  659. ){
  660. $params["flimgsize"] = $get["size"];
  661. }else{
  662. $params["image-size-select"] = "isz:lt,islt:" . $get["size"];
  663. }
  664. }
  665. if($get["color"] != "any"){
  666. if($get["color"] == "color"){
  667. $params["flimgcolor"] = "ic:color";
  668. }elseif($get["color"] == "bnw"){
  669. $params["flimgcolor"] = "ic:gray";
  670. }else{
  671. $params["flimgcolor"] = "ic:specific,isc:" . $get["color"];
  672. }
  673. }
  674. if($get["type"] != "any"){
  675. $params["flimgtype"] = $get["type"];
  676. }
  677. if($get["license"] != "any"){
  678. $params["flimglicense"] = $get["license"];
  679. }
  680. try{
  681. $html = $this->get(
  682. $proxy,
  683. "https://www.startpage.com/sp/search",
  684. $params,
  685. false,
  686. false,
  687. true,
  688. []
  689. );
  690. }catch(Exception $error){
  691. throw new Exception("Failed to fetch search page");
  692. }
  693. //$html = file_get_contents("scraper/startpage.html");
  694. // detect captcha on that page
  695. $this->detect_captcha($html["data"]);
  696. // solve anubis if need be
  697. $cookies = $this->solve_anubis($proxy, $html);
  698. if($cookies === false){
  699. $cookies = [];
  700. $html = $html["data"];
  701. }else{
  702. try{
  703. $html = $this->get(
  704. $proxy,
  705. "https://www.startpage.com/sp/search",
  706. $params,
  707. false,
  708. false,
  709. false,
  710. $cookies
  711. );
  712. }catch(Exception $error){
  713. throw new Exception("Failed to re-fetch search page. Anubis error: {$this->anubis_error}");
  714. }
  715. }
  716. }
  717. $this->detect_captcha($html);
  718. $out = [
  719. "status" => "ok",
  720. "npt" => null,
  721. "image" => []
  722. ];
  723. if(
  724. preg_match(
  725. '/React\.createElement\(UIStartpage\.AppSerpImages, ?(.+)\),?$/m',
  726. $html,
  727. $matches
  728. ) === 0
  729. ){
  730. throw new Exception("Failed to grep JSON object. Anubis error: {$this->anubis_error}");
  731. }
  732. $json = json_decode($matches[1], true);
  733. if($json === null){
  734. throw new Exception("Failed to decode JSON object. Anubis error: {$this->anubis_error}");
  735. }
  736. // get npt
  737. $out["npt"] = $this->parse_npt($json, "images", $proxy, $cookies);
  738. // get images
  739. foreach($json["render"]["presenter"]["regions"]["mainline"] as $category){
  740. if($category["display_type"] != "images-bing"){
  741. // ignore ads and !! suggestions !! @todo
  742. continue;
  743. }
  744. foreach($category["results"] as $image){
  745. $out["image"][] = [
  746. "title" => $this->titledots($image["title"]),
  747. "source" => [
  748. [
  749. "url" => $this->unshitimage($image["clickUrl"]),
  750. "width" => (int)$image["width"],
  751. "height" => (int)$image["height"]
  752. ],
  753. [
  754. "url" => $this->unshitimage($image["thumbnailUrl"]),
  755. "width" => (int)$image["thumbnailWidth"],
  756. "height" => (int)$image["thumbnailHeight"]
  757. ]
  758. ],
  759. "url" => $image["altClickUrl"]
  760. ];
  761. }
  762. }
  763. return $out;
  764. }
  765. public function video($get){
  766. if($get["npt"]){
  767. $this->anubis_error = "Not ran on pagination";
  768. [$post, $proxy] = $this->backend->get($get["npt"], "videos");
  769. $post = json_decode($post, true);
  770. $query = $post["query"];
  771. $cookies = $post["cookies"];
  772. try{
  773. $html = $this->get(
  774. $proxy,
  775. "https://www.startpage.com/sp/search",
  776. $query,
  777. true,
  778. false,
  779. false,
  780. $cookies
  781. );
  782. }catch(Exception $error){
  783. throw new Exception("Failed to fetch search page");
  784. }
  785. }else{
  786. $search = $get["s"];
  787. if(strlen($search) === 0){
  788. throw new Exception("Search term is empty!");
  789. }
  790. $proxy = $this->backend->get_ip();
  791. $params = [
  792. "query" => $get["s"],
  793. "cat" => "video",
  794. "pl" => "opensearch"
  795. ];
  796. if($get["nsfw"] == "no"){
  797. $params["qadf"] = "heavy";
  798. }
  799. if($get["sort"] != "relevance"){
  800. $params["sort_by"] = $get["sort"];
  801. }
  802. if($get["duration"] != "any"){
  803. $params["with_duration"] = $get["duration"];
  804. }
  805. try{
  806. $html = $this->get(
  807. $proxy,
  808. "https://www.startpage.com/sp/search",
  809. $params,
  810. false,
  811. false,
  812. true,
  813. []
  814. );
  815. }catch(Exception $error){
  816. throw new Exception("Failed to fetch search page");
  817. }
  818. //$html = file_get_contents("scraper/startpage.html");
  819. // detect captcha on that page
  820. $this->detect_captcha($html["data"]);
  821. // solve anubis if need be
  822. $cookies = $this->solve_anubis($proxy, $html);
  823. if($cookies === false){
  824. $cookies = [];
  825. $html = $html["data"];
  826. }else{
  827. try{
  828. $html = $this->get(
  829. $proxy,
  830. "https://www.startpage.com/sp/search",
  831. $params,
  832. false,
  833. false,
  834. false,
  835. $cookies
  836. );
  837. }catch(Exception $error){
  838. throw new Exception("Failed to re-fetch search page. Anubis error: {$this->anubis_error}");
  839. }
  840. }
  841. }
  842. $this->detect_captcha($html);
  843. if(
  844. preg_match(
  845. '/React\.createElement\(UIStartpage\.AppSerpVideos, ?(.+)\),?$/m',
  846. $html,
  847. $matches
  848. ) === 0
  849. ){
  850. throw new Exception("Failed to get JSON object. Anubis error: {$this->anubis_error}");
  851. }
  852. $json = json_decode($matches[1], true);
  853. if($json === null){
  854. throw new Exception("Failed to decode JSON object. Anubis error: {$this->anubis_error}");
  855. }
  856. $out = [
  857. "status" => "ok",
  858. "npt" => null,
  859. "video" => [],
  860. "author" => [],
  861. "livestream" => [],
  862. "playlist" => [],
  863. "reel" => []
  864. ];
  865. // get npt
  866. $out["npt"] = $this->parse_npt($json, "video", $proxy, $cookies);
  867. // get results
  868. foreach($json["render"]["presenter"]["regions"]["mainline"] as $category){
  869. if(
  870. preg_match(
  871. '/^video-/i',
  872. $category["display_type"]
  873. )
  874. ){
  875. foreach($category["results"] as $video){
  876. if(
  877. isset($video["thumbnailUrl"]) &&
  878. $video["thumbnailUrl"] !== null
  879. ){
  880. $thumb = [
  881. "ratio" => "16:9",
  882. "url" => $this->unshitimage($video["thumbnailUrl"])
  883. ];
  884. }else{
  885. $thumb = [
  886. "ratio" => null,
  887. "url" => null
  888. ];
  889. }
  890. $out["video"][] = [
  891. "title" => str_replace(["", ""], "", $video["title"]),
  892. "description" => $this->limitstrlen($video["description"]),
  893. "author" => [
  894. "name" => $video["channelTitle"],
  895. "url" => null,
  896. "avatar" => null
  897. ],
  898. "date" => strtotime($video["publishDate"]),
  899. "duration" => $this->hms2int($category["display_type"] == "video-youtube" ? $video["duration"] : $video["duration"] / 1000),
  900. "views" => (int)$video["viewCount"],
  901. "thumb" => $thumb,
  902. "url" => $video["clickUrl"]
  903. ];
  904. }
  905. }
  906. }
  907. return $out;
  908. }
  909. public function news($get){
  910. if($get["npt"]){
  911. $this->anubis_error = "Not ran on pagination";
  912. [$post, $proxy] = $this->backend->get($get["npt"], "news");
  913. $post = json_decode($post, true);
  914. $query = $post["query"];
  915. $cookies = $post["cookies"];
  916. try{
  917. $html = $this->get(
  918. $proxy,
  919. "https://www.startpage.com/sp/search",
  920. $query,
  921. true,
  922. false,
  923. false,
  924. $cookies
  925. );
  926. }catch(Exception $error){
  927. throw new Exception("Failed to fetch search page");
  928. }
  929. }else{
  930. $search = $get["s"];
  931. if(strlen($search) === 0){
  932. throw new Exception("Search term is empty!");
  933. }
  934. $proxy = $this->backend->get_ip();
  935. $params = [
  936. "query" => $get["s"],
  937. "cat" => "news",
  938. "pl" => "opensearch"
  939. ];
  940. if($get["nsfw"] == "no"){
  941. $params["qadf"] = "heavy";
  942. }
  943. if($get["time"] != "any"){
  944. $params["with_date"] = $get["time"];
  945. }
  946. try{
  947. $html = $this->get(
  948. $proxy,
  949. "https://www.startpage.com/sp/search",
  950. $params,
  951. false,
  952. false,
  953. true,
  954. []
  955. );
  956. }catch(Exception $error){
  957. throw new Exception("Failed to fetch search page. Anubis error: {$this->anubis_error}");
  958. }
  959. //$html = file_get_contents("scraper/startpage.html");
  960. // detect captcha on that page
  961. $this->detect_captcha($html["data"]);
  962. // solve anubis if need be
  963. $cookies = $this->solve_anubis($proxy, $html);
  964. if($cookies === false){
  965. $cookies = [];
  966. $html = $html["data"];
  967. }else{
  968. try{
  969. $html = $this->get(
  970. $proxy,
  971. "https://www.startpage.com/sp/search",
  972. $params,
  973. false,
  974. false,
  975. false,
  976. $cookies
  977. );
  978. }catch(Exception $error){
  979. throw new Exception("Failed to re-fetch search page. Anubis error: {$this->anubis_error}");
  980. }
  981. }
  982. }
  983. $this->detect_captcha($html);
  984. if(
  985. preg_match(
  986. '/React\.createElement\(UIStartpage\.AppSerpNews, ?(.+)\),?$/m',
  987. $html,
  988. $matches
  989. ) === 0
  990. ){
  991. throw new Exception("Failed to get JSON object. Anubis error: {$this->anubis_error}");
  992. }
  993. $json = json_decode($matches[1], true);
  994. if($json === null){
  995. throw new Exception("Failed to decode JSON object. Anubis error: {$this->anubis_error}");
  996. }
  997. $out = [
  998. "status" => "ok",
  999. "npt" => null,
  1000. "news" => []
  1001. ];
  1002. // get npt
  1003. $out["npt"] = $this->parse_npt($json, "news", $proxy, $cookies);
  1004. foreach($json["render"]["presenter"]["regions"]["mainline"] as $category){
  1005. if($category["display_type"] != "news-bing"){
  1006. // unsupported category
  1007. continue;
  1008. }
  1009. foreach($category["results"] as $news){
  1010. if(
  1011. isset($news["thumbnailUrl"]) &&
  1012. $news["thumbnailUrl"] !== null
  1013. ){
  1014. $thumb = [
  1015. "ratio" => "16:9",
  1016. "url" => $this->unshitimage($news["thumbnailUrl"])
  1017. ];
  1018. }else{
  1019. $thumb = [
  1020. "ratio" => null,
  1021. "url" => null
  1022. ];
  1023. }
  1024. $out["news"][] = [
  1025. "title" => $this->titledots($this->remove_penguins($news["title"])),
  1026. "author" => $news["source"],
  1027. "description" => $this->titledots($this->remove_penguins($news["description"])),
  1028. "date" => (int)substr((string)$news["date"], 0, -3),
  1029. "thumb" => $thumb,
  1030. "url" => $news["clickUrl"]
  1031. ];
  1032. }
  1033. }
  1034. return $out;
  1035. }
  1036. private function parse_npt($json, $pagetype, $proxy, $cookies){
  1037. foreach($json["render"]["presenter"]["pagination"]["pages"] as $page){
  1038. if($page["name"] == "Next"){
  1039. return
  1040. $this->backend->store(
  1041. json_encode([
  1042. "query" =>
  1043. http_build_query(
  1044. [
  1045. "lui" => "english",
  1046. "language" => "english",
  1047. "sc" => $json["render"]["search_sc"],
  1048. "t" => "device",
  1049. "cat" => $pagetype,
  1050. "segment" => "startpage.udog", // "original" sometimes
  1051. "abd" => "0",
  1052. "abe" => "0",
  1053. "query" => rawurldecode($json["render"]["query"]),
  1054. "page" => $page["number"],
  1055. "qsr" => "all",
  1056. "qadf" => "none", // @ todo fix (??)
  1057. ...(isset($page["token"]) ? ["page_token" => $page["token"]] : [])
  1058. ]
  1059. ),
  1060. "cookies" => $cookies
  1061. ]),
  1062. $pagetype,
  1063. $proxy
  1064. );
  1065. break;
  1066. }
  1067. }
  1068. return null;
  1069. }
  1070. private function unshitimage($url){
  1071. $query = parse_url($url, PHP_URL_QUERY);
  1072. parse_str($query, $query);
  1073. if(isset($query["piurl"])){
  1074. if(strpos($query["piurl"], "gstatic.com/")){
  1075. return
  1076. explode(
  1077. "&",
  1078. $query["piurl"],
  1079. 2
  1080. )[0];
  1081. }
  1082. if(
  1083. strpos($query["piurl"], "bing.net/") ||
  1084. strpos($query["piurl"], "bing.com/")
  1085. ){
  1086. return
  1087. explode(
  1088. "&",
  1089. $query["piurl"],
  1090. 2
  1091. )[0];
  1092. }
  1093. return $query["piurl"];
  1094. }
  1095. return $url;
  1096. }
  1097. private function limitstrlen($text){
  1098. return
  1099. explode(
  1100. "\n",
  1101. wordwrap(
  1102. str_replace(
  1103. ["\n\r", "\r\n", "\n", "\r"],
  1104. " ",
  1105. $text
  1106. ),
  1107. 300,
  1108. "\n"
  1109. ),
  1110. 2
  1111. )[0];
  1112. }
  1113. private function titledots($title){
  1114. return trim($title, " .\t\n\r\0\x0B…");
  1115. }
  1116. private function hms2int($time){
  1117. $parts = explode(":", $time, 3);
  1118. $time = 0;
  1119. if(count($parts) === 3){
  1120. // hours
  1121. $time = $time + ((int)$parts[0] * 3600);
  1122. array_shift($parts);
  1123. }
  1124. if(count($parts) === 2){
  1125. // minutes
  1126. $time = $time + ((int)$parts[0] * 60);
  1127. array_shift($parts);
  1128. }
  1129. // seconds
  1130. $time = $time + (int)$parts[0];
  1131. return $time;
  1132. }
  1133. private function remove_penguins($text){
  1134. return str_replace(
  1135. ["", ""],
  1136. "",
  1137. $text
  1138. );
  1139. }
  1140. private function detect_captcha($html){
  1141. $this->fuckhtml->load($html);
  1142. $title =
  1143. $this->fuckhtml
  1144. ->getElementsByTagName(
  1145. "title"
  1146. );
  1147. if(
  1148. count($title) !== 0 &&
  1149. $title[0]["innerHTML"] == "Redirecting..."
  1150. ){
  1151. // check if it's a captcha
  1152. $as =
  1153. $this->fuckhtml
  1154. ->getElementsByTagName(
  1155. "a"
  1156. );
  1157. foreach($as as $a){
  1158. if(
  1159. strpos(
  1160. $this->fuckhtml
  1161. ->getTextContent(
  1162. $a["innerHTML"]
  1163. ),
  1164. "https://www.startpage.com/sp/captcha"
  1165. ) !== false
  1166. ){
  1167. throw new Exception("Startpage returned a captcha");
  1168. }
  1169. }
  1170. throw new Exception("Startpage redirected the scraper to an unhandled page");
  1171. }
  1172. }
  1173. private function solve_anubis($proxy, $html){
  1174. //
  1175. // solve anubis challenge, if theres one
  1176. //
  1177. $res = null;
  1178. $cookies = [];
  1179. try{
  1180. $res = $this->anubis->scrape($html["data"]);
  1181. }catch(Exception $error){
  1182. // do nothang
  1183. $this->anubis_error = $error->getMessage();
  1184. }
  1185. if($res !== null){
  1186. // https://www.startpage.com/.within.website/x/cmd/anubis/api/pass-challenge
  1187. // ?id=01a00241-da22-7d62-b64f-e7a6a0e8bda8
  1188. // &response=0000460826adc23e0363e97e713e4aa36ca46e1a7cd933184442772f3d6b994c
  1189. // &nonce=3098
  1190. // &redir=https%3A%2F%2Fwww.startpage.com%2Fsp%2Fsearch
  1191. // &elapsedTime=111
  1192. $anubis_response =
  1193. $this->get(
  1194. $proxy,
  1195. "https://www.startpage.com/.within.website/x/cmd/anubis/api/pass-challenge",
  1196. [
  1197. "id" => $res["id"],
  1198. "response" => $res["response"],
  1199. "nonce" => $res["nonce"],
  1200. "redir" => "https://www.startpage.com/sp/search",
  1201. "elapsedTime" => $res["elapsedTime"]
  1202. ],
  1203. false,
  1204. false,
  1205. true,
  1206. $html["cookies"]
  1207. );
  1208. // re-fetch search page AGAIN with the anubis bullshit
  1209. $cookies = array_merge($anubis_response["cookies"], $html["cookies"]);
  1210. return $cookies;
  1211. }
  1212. return false;
  1213. }
  1214. }