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