PageRenderTime 59ms CodeModel.GetById 31ms RepoModel.GetById 1ms app.codeStats 0ms

/util/rss.php

https://github.com/hrs113355/Maple-XDBBS
PHP | 387 lines | 317 code | 12 blank | 58 comment | 76 complexity | b02a2e8b9127dd07b1d0aafd742d88af MD5 | raw file
  1. #!/usr/local/bin/php -q
  2. <?
  3. /****************************************************
  4. Maple-xdbbs RSS 訂閱器- RSS/ATOM 下載/解讀/配信
  5. ****************************************************
  6. 作者: albb0920.bbs <AT> xdbbs.twbbs.org
  7. hrs113355.bbs <AT> xdbbs.twbbs.org
  8. 運行需求: PHP 5.1 以上
  9. 無法於 safe_mode 下運作
  10. 本檔案必須存為 Big5 編碼,並以 bbs 權限執行
  11. *****************************************************/
  12. define(SHORTURL_API, 'http://loli.tw/apiadd.php?url=');
  13. define(SITEHOST,'xdbbs.twbbs.org');
  14. define(RSSNICK,'叉滴小站 RSS 訂閱器');
  15. // 下載失敗時嘗試以 Proxy 下載
  16. // 主要解決 domain 含底線"_"這個非法字元時
  17. // 部分 OS 拒絕解析的問題
  18. define(ALT_Proxy,'tcp://proxy.edu.tw:3128');
  19. define(FOOTER,
  20. "--
  21. 發信站: 叉滴小站(XDBBS.twbbs.org)
  22. \x1B[1;30m 作者: XDBBS RSS Reader\x1B[m\n");
  23. define(RSSPATH,'/home/bbs/etc/rss/');
  24. /*------ 以上為設定 ------*/
  25. define(FLAG_OUTGO, 1);
  26. define(FLAG_GET_UPDATE, 2);
  27. define(FLAG_LABEL, 4);
  28. define(FLAG_SKIP, 8);
  29. /* 檢查設定檔 */
  30. if(!file_exists(RSSPATH.$argv[1]))
  31. die('設定檔: '.RSSPATH.$argv[1].' 不存在 EXIT');
  32. $url = str_replace('#%slash%#','/',$argv[1]);
  33. // $lastLog=microtime(true); // For performance test,you also need to edit vlog() to enable
  34. vlog('==> 開始抓取 '.$url);
  35. $rss = new DOMDocument();
  36. if (!$rss->load($url))
  37. {
  38. $context=stream_context_create(array('http'=>array('proxy'=>ALT_Proxy)));
  39. $data=file_get_contents($url,0,$context);
  40. if($data===FALSE)
  41. die("下載失敗\n"); // todo: Maybe need to mark rss as failed
  42. else{
  43. $rss->loadXML($data);
  44. unset($data);
  45. }
  46. }
  47. vlog('*** OK');
  48. /* 讀入設定檔 */
  49. $fp = fopen(RSSPATH.$argv[1],'r+');
  50. fseek($fp,192);
  51. fputs($fp,pack('i',time())); // 實際更新時間,僅供介面顯示用
  52. fseek($fp,200);
  53. $subscriber=array();
  54. // typedef struct 必須與 struct.h 中定義相同
  55. // 0~12 char brdname[13+1];
  56. // 13~25 char owner[13+1];
  57. // 26~41 char prefix[16];
  58. // 42~113 char white[72];
  59. // 114~185 char black[72];
  60. // 186~187 char pad2[2];
  61. // 188~191 time_t chrono;
  62. // 192~195 time_t update;
  63. // 196~199 usint attr;
  64. // } rssbrdlist_t; // 200 bytes
  65. while(($buf=fread($fp,200))!=''){ // feof() 需要一次抓不到東西的fread,所以以此取代
  66. $sub=unpack('a13brd/a13owner/a16perfix/a72wfilter/a72bfilter/@192/iupdate/Iattr',$buf);
  67. // 字串切到 \0
  68. stripnull($sub['brd']);
  69. stripnull($sub['owner']);
  70. stripnull($sub['perfix']);
  71. stripnull($sub['wfilter']);
  72. stripnull($sub['bfilter']);
  73. array_push($subscriber,$sub);
  74. if(!isset($lastFetch))
  75. $lastFetch=$sub['update'];
  76. elseif($sub['update']<$lastFetch)
  77. $lastFetch=$sub['update'];
  78. }
  79. unset($buf);
  80. fclose($fp);
  81. $latest_time = $lastFetch; // 存最新文章,因為有些站台時間不照順序,亂跳一通
  82. dl('maplebbs3.so');
  83. $rssobj = $rss->getElementsByTagName('item');
  84. if($rssobj->length==0){ // 判斷是不是 atom feed
  85. $rssobj = $rss->getElementsByTagName('entry');
  86. if($rssobj->length==0)
  87. die('*** 異常中止! 不支援的 Feed 格式 ***');
  88. else{
  89. $IsATOM = true;
  90. if($rss->getElementsByTagName('feed')->item(0)->getAttribute('version')=='0.3')
  91. $IsOldSpec = true; // 向下支援 ATOM 0.3 (非標準,已廢棄)
  92. }
  93. }
  94. $channel=str_replace(chr(10),' ',iconv('UTF-8','Big5//IGNORE',
  95. $rss->getElementsByTagName(($IsATOM)?'feed':'channel')->item(0)
  96. ->getElementsByTagName('title')->item(0)->nodeValue));
  97. for($i = $rssobj->length - 1;$i>=0;--$i) // 從最舊的 RRS(通常在下面) 往新的掃
  98. {
  99. $entry = $rssobj->item($i);
  100. // TODO: Maybe more timezone handle
  101. $pubDate = $entry->getElementsByTagName(($IsATOM)?($IsOldSpec)?'issued':'published':'pubDate');
  102. $pubDate = ($pubDate->length)?date_create($pubDate->item(0)->nodeValue)->format('U'):0;
  103. $upDate = $entry->getElementsByTagName(($IsATOM)?($IsOldSpec)?'modified':'updated':'lastBuildDate');
  104. $upDate = ($upDate->length)?date_create($upDate->item(0)->nodeValue)->format('U'):0;
  105. if($pubDate < $lastFetch && $upDate < $lastFetch)
  106. continue; // 抓過了
  107. vlog('文章時間: '.$pubDate.' update:'.$upDate.' > '.$lastFetch);
  108. $title = $entry->getElementsByTagName('title');
  109. $title = ($title->length)?
  110. iconv('UTF-8','Big5//IGNORE',html_entity_decode($title->item(0)->nodeValue,ENT_QUOTES,'UTF-8')):'';
  111. $description=$entry->getElementsByTagName(($IsATOM)?'content':'encoded');
  112. if(!$IsATOM && !$description->length)
  113. $description = $entry->getElementsByTagName('description');
  114. if($description->length)
  115. if($IsATOM && $description->item(0)->getAttribute('src')!='')
  116. $description = shorturl('詳見: ',$description->item(0)->getAttribute('src'));
  117. elseif($IsATOM && $description->item(0)->getAttribute('type')=='html')
  118. $description = format($description->item(0)->nodeValue,1);
  119. else
  120. $description = format($description->item(0)->nodeValue);
  121. elseif($IsATOM && $entry->getElementsByTagName('summary')->length)
  122. $description = format($entry->getElementsByTagName('summary')->item(0)->nodeValue);
  123. else
  124. $description = $title;
  125. vlog('@-> Stage1 Done '.$title);
  126. // 重建 $description ,方便自動斷行,直接把不因看板不同的 Header 寫入
  127. $copy=&$description;
  128. unset($description);
  129. $description='標題: '.substr($title,0,70).chr(10).
  130. '發信站: '.strftime('%D %a %X').chr(10).
  131. '轉信: '.RSSNICK.chr(10).
  132. chr(10);
  133. if($pubDate)
  134. $description.="\x1B[1;30m發佈時間: \x1B[;1m".strftime('%D %a %X',$pubDate)."\x1B[m ";
  135. if($upDate)
  136. $description.="\x1B[1;30m修改時間: \x1B[;1m".strftime('%D %a %X',$upDate)."\x1B[m";
  137. $link=$entry->getElementsByTagName('link');
  138. if(strlen($title)>70)
  139. $description.=chr(10)."\x1B[1;30m完整標題:\x1B[m\n\x1B[1m".graceful_cut($title,79,1,"\x1B[m\n\x1B[1m")."\x1B[m";
  140. if($link->length){
  141. if($IsATOM)
  142. $link = $link->item(0)->getAttribute('href');
  143. else
  144. $link = $link->item(0)->nodeValue;
  145. $description.=chr(10).shorturl("\x1B[1;30m原文鏈結:\x1B[;1m ",iconv('UTF-8','Big5//IGNORE',$link),true)."\x1B[m";
  146. }
  147. $description.=chr(10).
  148. '───────────────────────────────────────'.chr(10);
  149. $now=0;
  150. $len=strlen($copy);
  151. vlog('++> 自動斷行 START');
  152. while($now<$len){
  153. $next=strpos($copy,chr(10),$now);
  154. if($next===false)
  155. $next=$len;
  156. if($next-$now>80){ // > 80 過長,實際上希望 70~80 之間斷
  157. for($next=$now+78;$next>$now+70;$next--)
  158. if(in_array($copy[$next],array(' ','.',',')))
  159. break;
  160. if($next==$now+70){ // 沒找到適合的切斷點, TODO:寫法待檢討
  161. for($next=$now;$next-$now<78;$next++)
  162. if(ord($copy[$next])>128){
  163. $next++;//跳過低位元
  164. if($next-$now>70)
  165. if(ord($copy[$next+1])<128){ //提前換行
  166. $next++;
  167. break;}
  168. }
  169. $next--;
  170. }
  171. $description.=substr($copy,$now,$next-$now+1).chr(10);
  172. }else
  173. $description.=substr($copy,$now,$next-$now).chr(10);
  174. $now=$next+1;
  175. }
  176. unset($copy);
  177. vlog('**> 過長處理 DONE');
  178. // 以下是可以讀,但是沒解讀的,
  179. // 理由是大部分 Feed 提供的這些資料對我們沒參考性
  180. // rss/atom category , rss comments
  181. $description .= chr(10);
  182. if($entry->getElementsByTagName('author')->length){
  183. $description.="\x1B[1;30m作者:\x1B[;1m ";
  184. $author = $entry->getElementsByTagName('author')->item(0);
  185. if($IsATOM){
  186. $description.=iconv('UTF-8','Big5//IGNORE',
  187. $author->getElementsByTagName('name')->item(0)->nodeValue).chr(27).'[m'.chr(10);
  188. if( $author->getElementsByTagName('email')->length )
  189. $description.=chr(27).'[1m'.' '.iconv('UTF-8','Big5//IGNORE',$author->getElementsByTagName('email')->item(0)->nodeValue).chr(27).'[m'.chr(10);
  190. if($author->getElementsByTagName('uri')->length)
  191. $description.=chr(27).'[1m'.shorturl(' ',$author->getElementsByTagName('uri')->item(0)->nodeValue).chr(27).'[m'.chr(10);
  192. }else
  193. $description.=iconv('UTF-8','Big5//IGNORE',$author->nodeValue).chr(10);
  194. }
  195. $description.="\x1B[1;30m".shorturl("來源:\x1B[;1m ",$url)."\x1B[m".chr(10).
  196. "\x1B[1;30m類型: \x1B[;1m".(($IsATOM)?'Atom Feed':'RSS Feed')."\x1B[m".chr(10).
  197. FOOTER;
  198. /* 配信 */
  199. foreach($subscriber as &$sub){
  200. if($sub['brd']=='' || $sub['owner']=='' || $sub['attr'] & FLAG_SKIP)
  201. continue;
  202. if($sub['update'] > $pubDate){
  203. if($sub['update'] > $upDate)
  204. continue;
  205. if(!($sub['attr'] & FLAG_GET_UPDATE) && $pubDate > 0)
  206. continue;
  207. }
  208. vlog('brd->'.$sub['brd']);
  209. /* filter */
  210. if($sub['wfilter']!=''){ // 白名單
  211. $filter=explode('/',$sub['wfilter']);
  212. $ubound=count($filter);
  213. for($p=0;$p<=$ubound;$p++)
  214. if($filter[$p]!='' && stripos($title,$filter[$p])!==FALSE)
  215. break;
  216. if($p>$ubound)
  217. continue;
  218. }
  219. if($sub['bfilter']!=''){ // 黑名單
  220. $filter=explode('/',$sub['bfilter']);
  221. $ubound=count($filter);
  222. for($p=0;$p<=$ubound;$p++)
  223. if($filter[$p]!='' && stripos($title,$filter[$p])!==FALSE)
  224. break;
  225. if($p<=$ubound)
  226. continue;
  227. }
  228. doPost($sub['brd'],
  229. ($sub['attr'] & FLAG_LABEL)?($title[0]=='[')?'['.$sub['perfix'].']'.$title:
  230. '['.$sub['perfix'].'] '.$title:$title,
  231. $sub['owner'].'.rss@'.SITEHOST,$channel,
  232. '發信人: '.$channel.' 看板: '.$sub['brd'].chr(10).$description,$sub['attr'] & FLAG_OUTGO);
  233. // doPost 的第二個參數是標題,對於不處理中文斷字的系統(ex:M3-itoc原廠)
  234. // 建議丟到 graceful_cut()去,第二個參數給 45(maybe 44)
  235. vlog(' > '.'OK');
  236. }
  237. unset($sub); // clean up
  238. if($upDate>$pubDate)
  239. $pubDate=$upDate;
  240. if(++$pubDate>$latest_time)
  241. $latest_time=$pubDate;
  242. }
  243. /* 避免來自未來的 RSS 發生,造成一抓再抓,標記 lastFech 為最新RSS時間 */
  244. if($latest_time>$lastFetch){
  245. $latest_time=pack('i',$latest_time); // 換成二進位
  246. /* 把 lastFetch 寫給所以人 */
  247. $fp = fopen(RSSPATH.$argv[1],'r+');
  248. $ubound=count($subscriber);
  249. for($p=1;$p<=$ubound;++$p){
  250. fseek($fp,200*$p+192);
  251. fputs($fp,$latest_time);
  252. }
  253. fclose($fp);
  254. }
  255. vlog('** ALL DONE **');
  256. /* 把 HTML/純文字 處理搬過來 */
  257. function format(&$str,$knownHTML=0){
  258. //todo: 待改善
  259. $str=html_entity_decode($str,ENT_COMPAT,'UTF-8');
  260. if($knownHTML || stripos($str,'<br')!==FALSE || stripos($str,'<p')!==FALSE){ // HTML
  261. $str=preg_replace_callback('% +|\n\r|\n|\r|<pre>.*?</pre>%si', 'format_callback', $str);
  262. // 處理換行
  263. // TODO:
  264. // 0. ATOM feed 支援純文字
  265. // 1. <p></p> 詳細判定
  266. $str = str_ireplace(array('<br>','<br/>','<br />','<p>','</p>'),array(chr(10),chr(10),chr(10),chr(10),chr(10)),$str);
  267. vlog(' -> 換行處理 Done ');
  268. $str=iconv('UTF-8','Big5//IGNORE',$str); // 拖到現在才做,應該會快一點吧 Orz
  269. // 處理圖片
  270. $str=preg_replace_callback('/\n?<img.*?src="(.*?)".*?>\n?/i','img_callback',$str);
  271. vlog(' -> 圖片處理 Done ');
  272. // 處理鏈結
  273. $str=preg_replace_callback('/[\n<\[\( ]*<a.*?href="(.*?)".*?>(.*?)<\/a>[>\]\)\n ]*/i','ahref_callback',$str);
  274. vlog(' -> 鏈結處理 Done ');
  275. // 其他標籤
  276. $str=strip_tags($str);
  277. return $str;
  278. }else{ // 純文字
  279. $str=iconv('UTF-8','Big5//IGNORE',$str);
  280. // 還是無法保證完全沒有 HTML,所以各自搜尋
  281. if(stripos($str,'<img')!==FALSE)
  282. $str=preg_replace_callback('/\n?<img.*?src="(.*?)".*?>\n?/i','img_callback',$str);
  283. if(stripos($str,'<a')!==FALSE)
  284. $str=preg_replace_callback('/[\n<\[\( ]*<a.*?href="(.*?)".*?>(.*?)<\/a>[>\]\)\n ]*/i','ahref_callback',$str);
  285. else
  286. $str=preg_replace_callback('%[a-zA-Z0-9]*?://[^ "<>\[\]\^`\{\}\n]+%','texturl',$str);// 硬抓出鏈結
  287. return $str;
  288. }
  289. }
  290. /* callbacks */
  291. function format_callback($matches){
  292. switch($matches[0][0]){
  293. case ' ':
  294. return ' ';
  295. case chr(10):
  296. case chr(13):
  297. return '';
  298. case '<':
  299. return $matches[0];
  300. }
  301. }
  302. function img_callback($matchs){
  303. return chr(10).shorturl('[圖片] ',$matchs[1]).chr(10);
  304. }
  305. function texturl($matchs){
  306. return chr(10).shorturl('[鏈結] ',$matchs[0]).chr(10);
  307. }
  308. function ahref_callback($matchs){
  309. $matchs[2]=strip_tags($matchs[2]);
  310. if( $matchs[2]==$matchs[1]||
  311. substr($matchs[2],0,7)=='http://'||
  312. substr($matchs[2],0,8)=='https://'||
  313. substr($matchs[2],0,6)=='ftp://'||
  314. substr($matchs[2],0,9)=='telnet://'||
  315. substr($matchs[2],0,6)=='mms://'||
  316. substr($matchs[2],0,7)=='fd2k://'||
  317. substr($matchs[2],0,7)=='rtsp://'||
  318. substr($matchs[2],0,7)=='mailto:')
  319. return chr(10).shorturl('[鏈結] ',$matchs[1]).chr(10);
  320. else{
  321. if(strlen($matchs[2])>51)
  322. $matchs[2]=graceful_cut($matchs[2],49).'…';
  323. return chr(10).shorturl('[鏈結:"'.$matchs[2].'"] ',$matchs[1]).chr(10);
  324. }
  325. }
  326. /* Utilities */
  327. function stripnull(&$str){
  328. if(($pos=strpos($str,chr(0)))!==FALSE)
  329. $str=substr($str,0,$pos);
  330. }
  331. function shorturl($prompt,$url,$oneline=false){
  332. if(stripos($url,':/')==false&&substr($matchs[4],0,7)!='mailto:'){
  333. if($url[0]=='/'){ // TODO:其實應該判斷更多的,但是很麻煩,先這樣
  334. global $entry,$IsATOM,$rss;
  335. if(($base=$entry->getAttribute('base'))=='')
  336. $base=$rss->getElementsByTagName(($IsATOM)?'feed':'channel')->item(0)->
  337. getElementsByTagName('link')->item(0)->getAttribute('href');
  338. $url=substr($base,0,strpos($base,'/',8)).$url;
  339. }
  340. else
  341. $url='http://'.$url;
  342. }
  343. $len = strlen($url);
  344. if( $len>80 || ($oneline && $len>79-strlen($prompt) ) )
  345. return $prompt.file_get_contents(SHORTURL_API.urlencode($url));
  346. elseif($len>79-strlen($prompt))
  347. return $prompt.chr(10).$url;
  348. else
  349. return $prompt.$url;
  350. }
  351. function vlog($msg){
  352. /* Uncomment these if you need performance test.
  353. global $lastLog;
  354. $now=microtime(true);
  355. echo sprintf('%05.2f',$now-$lastLog),' ',$msg,chr(10);
  356. $lastLog=$now;*/
  357. echo $msg,chr(10);
  358. }
  359. function graceful_cut($str,$len,$recursive=0,$between="\n"){
  360. if(strlen($str)<$len)
  361. return $str;
  362. for($ptr=0;$ptr<$len;$ptr++){
  363. if(ord($str[$ptr])>128)
  364. if($ptr==$len-1)
  365. break;
  366. else
  367. $ptr++;
  368. }
  369. if($recursive && $len < strlen($str))
  370. return substr($str,0,$ptr).$between.graceful_cut(substr($str,$ptr),$len,1);
  371. else
  372. return substr($str,0,$ptr);
  373. }
  374. ?>