startpage.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802
  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. }
  9. public function getfilters($page){
  10. switch($page){
  11. case "web":
  12. return [
  13. "country" => [
  14. "display" => "Country",
  15. "option" => [
  16. "any" => "All Regions",
  17. "es_AR" => "Argentina",
  18. "en_AU" => "Australia",
  19. "de_AT" => "Austria",
  20. "ru_BY" => "Belarus",
  21. "fr_BE" => "Belgium (FR)",
  22. "nl_BE" => "Belgium (NL)",
  23. "bg_BG" => "Bulgaria",
  24. "en_CA" => "Canada (EN)",
  25. "fr_CA" => "Canada (FR)",
  26. "es_CL" => "Chile",
  27. "es_CO" => "Colombia",
  28. "cs_CZ" => "Czech Republic",
  29. "da_DK" => "Denmark",
  30. "ar_EG" => "Egypt",
  31. "et_EE" => "Estonia",
  32. "fi_FI" => "Finland",
  33. "fr_FR" => "France",
  34. "de_DE" => "Germany",
  35. "el_GR" => "Greece",
  36. "hu_HU" => "Hungary",
  37. "hi_IN" => "India (HI)",
  38. "en_IN" => "India (EN)",
  39. "id_ID" => "Indonesia (ID)",
  40. "en_ID" => "Indonesia (EN)",
  41. "en_IE" => "Ireland",
  42. "it_IT" => "Italy",
  43. "ja_JP" => "Japan",
  44. "ko_KR" => "Korea",
  45. "ms_MY" => "Malaysia (MS)",
  46. "en_MY" => "Malaysia (EN)",
  47. "es_MX" => "Mexico",
  48. "nl_NL" => "Netherlands",
  49. "en_NZ" => "New Zealand",
  50. "no_NO" => "Norway",
  51. "es_PE" => "Peru",
  52. "fil_PH" => "Philippines (FIL)",
  53. "en_PH" => "Philippines (EN)",
  54. "pl_PL" => "Poland",
  55. "pt_PT" => "Portugal",
  56. "ro_RO" => "Romania",
  57. "ru_RU" => "Russia",
  58. "ms_SG" => "Singapore (MS)",
  59. "en_SG" => "Singapore (EN)",
  60. "es_ES" => "Spain (ES)",
  61. "ca_ES" => "Spain (CA)",
  62. "sv_SE" => "Sweden",
  63. "de_CH" => "Switzerland (DE)",
  64. "fr_CH" => "Switzerland (FR)",
  65. "it_CH" => "Switzerland (IT)",
  66. "tr_TR" => "Turkey",
  67. "uk_UA" => "Ukraine",
  68. "en_US" => "US (EN)",
  69. "es_US" => "US (ES)",
  70. "es_UY" => "Uruguay",
  71. "es_VE" => "Venezuela",
  72. "vi_VN" => "Vietnam (VI)",
  73. "en_VN" => "Vietnam (EN)",
  74. "en_ZA" => "South Africa"
  75. ]
  76. ],
  77. "nsfw" => [ // qadf
  78. "display" => "NSFW",
  79. "option" => [
  80. "yes" => "Yes", // qadf=none
  81. "no" => "No" // qadf=heavy
  82. ]
  83. ],
  84. "time" => [ // with_date
  85. "display" => "Time fetched",
  86. "option" => [
  87. "any" => "Any time",
  88. "d" => "Past 24 hours",
  89. "w" => "Past week",
  90. "m" => "Past month",
  91. "y" => "Past year",
  92. ]
  93. ],
  94. "extendedsearch" => [
  95. // undefined display, so it wont show in frontend
  96. "option" => [
  97. "yes" => "Yes",
  98. "no" => "No"
  99. ]
  100. ]
  101. ];
  102. break;
  103. }
  104. }
  105. private function get($proxy, $url, $get = [], $post = false, $is_xhr = false){
  106. $curlproc = curl_init();
  107. if($post === true){
  108. curl_setopt($curlproc, CURLOPT_POST, true);
  109. curl_setopt($curlproc, CURLOPT_POSTFIELDS, $get);
  110. }elseif($get !== []){
  111. $get = http_build_query($get);
  112. $url .= "?" . $get;
  113. }
  114. curl_setopt($curlproc, CURLOPT_URL, $url);
  115. // http2 bypass
  116. curl_setopt($curlproc, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
  117. curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
  118. if($is_xhr === true){
  119. curl_setopt($curlproc, CURLOPT_HTTPHEADER,
  120. ["User-Agent: " . config::USER_AGENT,
  121. "Accept: application/json",
  122. "Accept-Language: en-US,en;q=0.5",
  123. "Accept-Encoding: gzip",
  124. "Referer: https://www.startpage.com/",
  125. "Content-Type: application/json",
  126. "Content-Length: " . strlen($get),
  127. "Origin: https://www.startpage.com/",
  128. "DNT: 1",
  129. "Connection: keep-alive",
  130. "Cookie: 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",
  131. "Sec-Fetch-Dest: empty",
  132. "Sec-Fetch-Mode: cors",
  133. "Sec-Fetch-Site: same-origin",
  134. "TE: trailers"]
  135. );
  136. }elseif($post === true){
  137. curl_setopt($curlproc, CURLOPT_HTTPHEADER,
  138. ["User-Agent: " . config::USER_AGENT,
  139. "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
  140. "Accept-Language: en-US,en;q=0.5",
  141. "Accept-Encoding: gzip",
  142. "Referer: https://www.startpage.com/",
  143. "Content-Type: application/x-www-form-urlencoded",
  144. "Content-Length: " . strlen($get),
  145. "DNT: 1",
  146. "Connection: keep-alive",
  147. "Cookie: 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",
  148. "Upgrade-Insecure-Requests: 1",
  149. "Sec-Fetch-Dest: document",
  150. "Sec-Fetch-Mode: navigate",
  151. "Sec-Fetch-Site: none",
  152. "Sec-Fetch-User: ?1",
  153. "Priority: u=0, i",
  154. "TE: trailers"]
  155. );
  156. }else{
  157. curl_setopt($curlproc, CURLOPT_HTTPHEADER,
  158. ["User-Agent: " . config::USER_AGENT,
  159. "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
  160. "Accept-Language: en-US,en;q=0.5",
  161. "Accept-Encoding: gzip",
  162. "DNT: 1",
  163. "Connection: keep-alive",
  164. "Cookie: 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",
  165. "Sec-Fetch-Dest: document",
  166. "Sec-Fetch-Mode: navigate",
  167. "Sec-Fetch-Site: none",
  168. "Sec-Fetch-User: ?1",
  169. "Priority: u=0, i",
  170. "TE: trailers"]
  171. );
  172. }
  173. curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true);
  174. curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2);
  175. curl_setopt($curlproc, CURLOPT_SSL_VERIFYPEER, true);
  176. curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30);
  177. curl_setopt($curlproc, CURLOPT_TIMEOUT, 30);
  178. $this->backend->assign_proxy($curlproc, $proxy);
  179. $data = curl_exec($curlproc);
  180. if(curl_errno($curlproc)){
  181. throw new Exception(curl_error($curlproc));
  182. }
  183. curl_close($curlproc);
  184. return $data;
  185. }
  186. public function web($get){
  187. if($get["npt"]){
  188. [$post, $proxy] = $this->backend->get($get["npt"], "web");
  189. try{
  190. $html = $this->get(
  191. $proxy,
  192. "https://www.startpage.com/sp/search",
  193. $post,
  194. true
  195. );
  196. }catch(Exception $error){
  197. throw new Exception("Failed to fetch search page");
  198. }
  199. $get_instant_answer = false;
  200. }else{
  201. $proxy = $this->backend->get_ip();
  202. $params = [
  203. "query" => $get["s"],
  204. "cat" => "web",
  205. "pl" => "opensearch"
  206. ];
  207. if($get["nsfw"] == "no"){
  208. $params["qadf"] = "heavy";
  209. $get_instant_answer = false;
  210. }else{
  211. $get_instant_answer = true;
  212. }
  213. if($get["country"] !== "any"){
  214. $params["qsr"] = $get["country"];
  215. }
  216. if($get["time"] !== "any"){
  217. $params["with_date"] = $get["time"];
  218. }
  219. try{
  220. $html = $this->get(
  221. $proxy,
  222. "https://www.startpage.com/sp/search",
  223. $params
  224. );
  225. }catch(Exception $error){
  226. throw new Exception("Failed to fetch search page");
  227. }
  228. //$html = file_get_contents("scraper/startpage.html");
  229. }
  230. if(
  231. preg_match(
  232. '/React\.createElement\(UIStartpage\.AppSerpWeb, ?(.+)\),$/m',
  233. $html,
  234. $matches
  235. ) === 0
  236. ){
  237. throw new Exception("Failed to grep JSON object");
  238. }
  239. $json = json_decode($matches[1], true);
  240. if($json === null){
  241. throw new Exception("Failed to decode JSON");
  242. }
  243. $out = [
  244. "status" => "ok",
  245. "spelling" => [
  246. "type" => "no_correction",
  247. "using" => null,
  248. "correction" => null
  249. ],
  250. "npt" => null,
  251. "answer" => [],
  252. "web" => [],
  253. "image" => [],
  254. "video" => [],
  255. "news" => [],
  256. "related" => []
  257. ];
  258. // get npt
  259. foreach($json["render"]["presenter"]["pagination"]["pages"] as $page){
  260. if($page["name"] == "Next"){
  261. parse_str(
  262. explode(
  263. "?",
  264. $page["url"],
  265. 2
  266. )[1],
  267. $str
  268. );
  269. $out["npt"] =
  270. $this->backend->store(
  271. http_build_query(
  272. [
  273. "lui" => "english",
  274. "language" => "english",
  275. "query" => $str["q"],
  276. "cat" => "web",
  277. "sc" => $str["sc"],
  278. "t" => "device",
  279. "segment" => "startpage.udog",
  280. "page" => $str["page"]
  281. ]
  282. ),
  283. "web",
  284. $proxy
  285. );
  286. break;
  287. }
  288. }
  289. foreach($json["render"]["presenter"]["regions"]["mainline"] as $category){
  290. if(!isset($category["display_type"])){
  291. continue;
  292. }
  293. switch($category["display_type"]){
  294. case "web-google":
  295. foreach($category["results"] as $result){
  296. $sublinks = [];
  297. foreach($result["siteLinks"] as $sublink){
  298. $sublinks[] = [
  299. "title" => $sublink["title"],
  300. "description" => null,
  301. "url" => $sublink["clickUrl"]
  302. ];
  303. }
  304. $description =
  305. explode(
  306. "...",
  307. $this->titledots(
  308. html_entity_decode(
  309. $this->fuckhtml
  310. ->getTextContent(
  311. $result["description"]
  312. )
  313. )
  314. ),
  315. 2
  316. );
  317. $date = strtotime(trim($description[0]));
  318. if(
  319. $date === false ||
  320. count($description) !== 2 ||
  321. strlen($description[0]) > 14
  322. ){
  323. // no date found
  324. $description =
  325. implode(
  326. " ... ",
  327. $description
  328. );
  329. $date = null;
  330. }else{
  331. // date found
  332. $description = ltrim($description[1]);
  333. }
  334. $out["web"][] = [
  335. "title" =>
  336. $this->titledots(
  337. html_entity_decode(
  338. $this->fuckhtml
  339. ->getTextContent(
  340. $result["title"]
  341. )
  342. )
  343. ),
  344. "description" => $description,
  345. "url" => $result["clickUrl"],
  346. "date" => $date,
  347. "type" => "web",
  348. "thumb" => [
  349. "url" => null,
  350. "ratio" => null
  351. ],
  352. "sublink" => $sublinks,
  353. "table" => []
  354. ];
  355. }
  356. break;
  357. case "images-qi-top":
  358. foreach($category["results"] as $result){
  359. $out["image"][] = [
  360. "title" =>
  361. $this->titledots(
  362. html_entity_decode(
  363. $this->fuckhtml
  364. ->getTextContent(
  365. $result["title"]
  366. )
  367. )
  368. ),
  369. "source" => [
  370. [
  371. "url" => $result["rawImageUrl"],
  372. "width" => (int)$result["width"],
  373. "height" => (int)$result["height"]
  374. ],
  375. [
  376. "url" => $this->unshitimage($result["mdThumbnailUrl"]),
  377. "width" => (int)$result["mdThumbnailWidth"],
  378. "height" => (int)$result["mdThumbnailHeight"]
  379. ]
  380. ],
  381. "url" =>
  382. $result["altClickUrl"]
  383. ];
  384. }
  385. break;
  386. }
  387. }
  388. // parse instant answers
  389. if(
  390. $get["extendedsearch"] == "yes" &&
  391. $get_instant_answer === true
  392. ){
  393. // https://www.startpage.com/sp/qi?qimsn=ex&sxap=%2Fv1%2Fquery&sc=BqZ3inqrAgF701&sr=1
  394. try{
  395. $post = [
  396. "se" => "n0vze2y9dqwy",
  397. "q" => $json["render"]["query"],
  398. "results" => [], // populate
  399. "enableKnowledgePanel" => true,
  400. "enableMediaThumbBar" => false,
  401. "enableSearchSuggestions" => false,
  402. "enableTripadvisorProperties" => [],
  403. "enableTripadvisorPlaces" => [],
  404. "enableTripadvisorPlacesForLocations" => [],
  405. "enableWebProducts" => false,
  406. "tripadvisorPartnerId" => null,
  407. "tripadvisorMapColorMode" => "light",
  408. "tripadvisorDisablesKnowledgePanel" => false,
  409. "instantAnswers" => [
  410. "smartAnswers",
  411. "youtube",
  412. "tripadvisor"
  413. ],
  414. "iaType" => null,
  415. "forceEnhancedKnowledgePanel" => false,
  416. "shoppingOnly" => false,
  417. "allowAdultProducts" => true,
  418. "lang" => "en",
  419. "browserLang" => "en-US",
  420. "browserTimezone" => "America/New_York",
  421. "market" => null,
  422. "userLocation" => null,
  423. "userDate" => date("Y-m-d"),
  424. "userAgentType" => "unknown"
  425. ];
  426. foreach($out["web"] as $result){
  427. $post["results"][] = [
  428. "url" => $result["url"],
  429. "title" => $result["title"]
  430. ];
  431. }
  432. $post = json_encode($post, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_IGNORE);
  433. $additional_data =
  434. $this->get(
  435. $proxy,
  436. "https://www.startpage.com/sp/qi?qimsn=ex&sxap=%2Fv1%2Fquery&sc=" . $json["render"]["callback_sc"] . "&sr=1",
  437. $post,
  438. true,
  439. true
  440. );
  441. $additional_data = json_decode($additional_data, true);
  442. if($additional_data === null){
  443. throw new Exception("Failed to decode JSON"); // just break out, dont fail completely
  444. }
  445. if(!isset($additional_data["knowledgePanel"])){
  446. throw new Exception("Response has missing data (knowledgePanel)");
  447. }
  448. $additional_data = $additional_data["knowledgePanel"];
  449. $answer = [
  450. "title" => $additional_data["meta"]["title"],
  451. "description" => [
  452. [
  453. "type" => "quote",
  454. "value" => $additional_data["meta"]["description"]
  455. ]
  456. ],
  457. "url" => $additional_data["meta"]["origWikiUrl"],
  458. "thumb" => $additional_data["meta"]["image"],
  459. "table" => [],
  460. "sublink" => []
  461. ];
  462. // parse html for instant answer
  463. $this->fuckhtml->load($additional_data["html"]);
  464. $div =
  465. $this->fuckhtml
  466. ->getElementsByTagName(
  467. "div"
  468. );
  469. // get description
  470. $description =
  471. $this->fuckhtml
  472. ->getElementsByClassName(
  473. "sx-kp-short-extract sx-kp-short-extract-complete",
  474. $div
  475. );
  476. if(count($description) !== 0){
  477. $answer["description"][] = [
  478. "type" => "text",
  479. "value" =>
  480. $this->fuckhtml
  481. ->getTextContent(
  482. $description[0]
  483. )
  484. ];
  485. }
  486. // get socials
  487. $socials =
  488. $this->fuckhtml
  489. ->getElementsByClassName(
  490. "sx-wiki-social-link",
  491. "a"
  492. );
  493. foreach($socials as $social){
  494. $title =
  495. $this->fuckhtml
  496. ->getTextContent(
  497. $social["attributes"]["title"]
  498. );
  499. $url =
  500. $this->fuckhtml
  501. ->getTextContent(
  502. $social["attributes"]["href"]
  503. );
  504. switch($title){
  505. case "Official Website":
  506. $title = "Website";
  507. break;
  508. }
  509. $answer["sublink"][$title] = $url;
  510. }
  511. // get videos
  512. $videos =
  513. $this->fuckhtml
  514. ->getElementsByClassName(
  515. "sx-kp-video-grid-item",
  516. $div
  517. );
  518. foreach($videos as $video){
  519. $this->fuckhtml->load($video);
  520. $as =
  521. $this->fuckhtml
  522. ->getElementsByTagName(
  523. "a"
  524. );
  525. if(count($as) === 0){
  526. // ?? invalid
  527. continue;
  528. }
  529. $image =
  530. $this->fuckhtml
  531. ->getElementsByAttributeName(
  532. "data-sx-src",
  533. "img"
  534. );
  535. if(count($image) !== 0){
  536. $thumb = [
  537. "ratio" => "16:9",
  538. "url" =>
  539. $this->fuckhtml
  540. ->getTextContent(
  541. $image[0]["attributes"]["data-sx-src"]
  542. )
  543. ];
  544. }else{
  545. $thumb = [
  546. "ratio" => null,
  547. "url" => null
  548. ];
  549. }
  550. $out["video"][] = [
  551. "title" =>
  552. $this->fuckhtml
  553. ->getTextContent(
  554. $as[0]["attributes"]["title"]
  555. ),
  556. "description" => null,
  557. "date" => null,
  558. "duration" => null,
  559. "views" => null,
  560. "thumb" => $thumb,
  561. "url" =>
  562. $this->fuckhtml
  563. ->getTextContent(
  564. $as[0]["attributes"]["href"]
  565. )
  566. ];
  567. }
  568. // reset
  569. $this->fuckhtml->load($additional_data["html"]);
  570. // get table elements
  571. $table =
  572. $this->fuckhtml
  573. ->getElementsByClassName(
  574. "sx-infobox",
  575. "table"
  576. );
  577. if(count($table) !== 0){
  578. $trs =
  579. $this->fuckhtml
  580. ->getElementsByTagName(
  581. "tr"
  582. );
  583. foreach($trs as $tr){
  584. $this->fuckhtml->load($tr);
  585. // ok so startpage devs cant fucking code a table
  586. // td = content
  587. // th (AAAHH) = title
  588. $tds =
  589. $this->fuckhtml
  590. ->getElementsByTagName(
  591. "td"
  592. );
  593. $ths =
  594. $this->fuckhtml
  595. ->getElementsByTagName(
  596. "th"
  597. );
  598. if(
  599. count($ths) === 1 &&
  600. count($tds) === 1
  601. ){
  602. $title =
  603. $this->fuckhtml
  604. ->getTextContent(
  605. $ths[0]
  606. );
  607. $description = [];
  608. $this->fuckhtml->load($tds[0]);
  609. $lis =
  610. $this->fuckhtml
  611. ->getElementsByTagName(
  612. "li"
  613. );
  614. if(count($lis) !== 0){
  615. foreach($lis as $li){
  616. $description[] =
  617. $this->fuckhtml
  618. ->getTextContent(
  619. $li
  620. );
  621. }
  622. $description = implode(", ", $description);
  623. }else{
  624. $description =
  625. $this->fuckhtml
  626. ->getTextContent(
  627. $tds[0]
  628. );
  629. }
  630. $answer["table"][$title] = $description;
  631. }
  632. }
  633. }
  634. $out["answer"][] = $answer;
  635. }catch(Exception $error){
  636. // do nothing
  637. //echo "error!";
  638. }
  639. }
  640. return $out;
  641. }
  642. private function unshitimage($url){
  643. $query = parse_url($url, PHP_URL_QUERY);
  644. parse_str($query, $query);
  645. if(isset($query["piurl"])){
  646. if(strpos($query["piurl"], "gstatic.com/")){
  647. return
  648. explode(
  649. "&",
  650. $query["piurl"],
  651. 2
  652. )[0];
  653. }
  654. return $query["piurl"];
  655. }
  656. return $url;
  657. }
  658. private function titledots($title){
  659. return trim($title, " .\t\n\r\0\x0B…");
  660. }
  661. }