PageRenderTime 42ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/expack/rss/parser.inc.php

http://github.com/unpush/p2-php
PHP | 526 lines | 364 code | 57 blank | 105 comment | 68 complexity | f382889bbf6a788753455c1c7624e915 MD5 | raw file
  1. <?php
  2. /**
  3. * rep2expack - RSS Parser
  4. */
  5. require_once P2EX_LIB_DIR . '/rss/common.inc.php';
  6. require_once 'XML/RSS.php';
  7. // {{{ ImageCache2との連携判定
  8. if ($GLOBALS['_conf']['expack.rss.with_imgcache'] &&
  9. ((!$GLOBALS['_conf']['ktai'] && $GLOBALS['_conf']['expack.ic2.enabled'] % 2 == 1) ||
  10. ($GLOBALS['_conf']['ktai'] && $GLOBALS['_conf']['expack.ic2.enabled'] >= 2)))
  11. {
  12. if (!class_exists('IC2_Switch', false)) {
  13. require P2EX_LIB_DIR . '/ic2/Switch.php';
  14. }
  15. if (IC2_Switch::get($GLOBALS['_conf']['ktai'])) {
  16. if (!function_exists('rss_get_image')) {
  17. require P2EX_LIB_DIR . '/rss/getimage.inc.php';
  18. }
  19. define('P2_RSS_IMAGECACHE_AVAILABLE', 1);
  20. } else {
  21. define('P2_RSS_IMAGECACHE_AVAILABLE', 0);
  22. }
  23. } else {
  24. define('P2_RSS_IMAGECACHE_AVAILABLE', 0);
  25. }
  26. // }}}
  27. // {{{ p2GetRSS()
  28. /**
  29. * RSSをダウンロードし、パース結果を返す
  30. */
  31. function p2GetRSS($remotefile, $atom = 0)
  32. {
  33. global $_conf;
  34. $refresh = (!empty($_GET['refresh']) || !empty($_POST['refresh']));
  35. $localpath = rss_get_save_path($remotefile);
  36. if (PEAR::isError($localpath)) {
  37. P2Util::pushInfoHtml('<p>' . $localpath->getMessage() . '</p>');
  38. return $localpath;
  39. }
  40. // 保存用ディレクトリがなければつくる
  41. if (!is_dir(dirname($localpath))) {
  42. FileCtl::mkdirFor($localpath);
  43. }
  44. // If-Modified-Sinceつきでダウンロード(ファイルが無いか、古いか、強制リロードのとき)
  45. if (!file_exists($localpath) || $refresh ||
  46. filemtime($localpath) < (time() - $_conf['expack.rss.check_interval'] * 60)
  47. ) {
  48. $dl = P2Util::fileDownload($remotefile, $localpath, true, 301);
  49. if ($dl->isSuccess()) {
  50. chmod($localpath, $_conf['expack.rss.setting_perm']);
  51. }
  52. }
  53. // キャッシュが更新されなかったか、ダウンロード成功ならRSSをパース
  54. if (file_exists($localpath) && (!isset($dl) || $dl->isSuccess())) {
  55. if ($atom) {
  56. $atom = (isset($dl) && $dl->code == 200) ? 2 : 1;
  57. }
  58. $rss = p2ParseRSS($localpath, $atom);
  59. return $rss;
  60. } else {
  61. return $dl;
  62. }
  63. }
  64. // }}}
  65. // {{{ p2ParseRSS()
  66. /**
  67. * RSSをパースする
  68. */
  69. function p2ParseRSS($xmlpath, $atom=0)
  70. {
  71. // $atomが真ならXSLを使ってRSS 1.0に変換
  72. // (変換済みファイルが存在しないか、$atom==2のときに実行される)
  73. // 元のXML(Atom)でencoding属性が正しく指定されていればXSLTプロセッサが自動で
  74. // 文字コードをUTF-8(XSLで指定した文字コード)に変換してくれる
  75. if ($atom) {
  76. $xslpath = P2EX_LIB_DIR . '/rss/atom03-to-rss10.xsl';
  77. $rsspath = $xmlpath . '.rss';
  78. if (file_exists($rsspath) && $atom != 2) {
  79. // OK
  80. } elseif (extension_loaded('xsl')) {
  81. if (!atom_to_rss($xmlpath, $xslpath, $rsspath)) {
  82. $retval = false;
  83. return $retval;
  84. }
  85. } else {
  86. P2Util::pushInfoHtml('<p>p2 error: Atomフィードを読むにはPHPのXSL機能拡張が必要です。</p>');
  87. $retval = false;
  88. return $retval;
  89. }
  90. } else {
  91. $rsspath = $xmlpath;
  92. }
  93. // エンコーディングを判定し、XML_RSSクラスのインスタンスを生成する
  94. // 2006-02-01 手動判定廃止
  95. /*$srcenc = 'UTF-8';
  96. $tgtenc = 'UTF-8';
  97. if ($fp = @fopen($rsspath, 'rb')) {
  98. $content = fgets($fp, 64);
  99. if (preg_match('/<\\?xml version=(["\'])1.0\\1 encoding=(["\'])(.+?)\\2 ?\\?>/', $content, $matches)) {
  100. $srcenc = $matches[3];
  101. }
  102. fclose($fp);
  103. }
  104. $rss = new XML_RSS($rsspath, $srcenc, $tgtenc);*/
  105. $rss = new XML_RSS($rsspath);
  106. if (PEAR::isError($rss)) {
  107. P2Util::pushInfoHtml('<p>p2 error: RSS - ' . $rss->getMessage() . '</p>');
  108. return $rss;
  109. }
  110. // 解析対象のタグを上書き
  111. $rss->channelTags = array_unique(array_merge($rss->channelTags, array (
  112. 'CATEGORY', 'CLOUD', 'COPYRIGHT', 'DESCRIPTION', 'DOCS', 'GENERATOR', 'IMAGE',
  113. 'ITEMS', 'LANGUAGE', 'LASTBUILDDATE', 'LINK', 'MANAGINGEditor', 'PUBDATE',
  114. 'RATING', 'SKIPDAYS', 'SKIPHOURS', 'TEXTINPUT', 'TITLE', 'TTL', 'WEBMASTER'
  115. )));
  116. $rss->itemTags = array_unique(array_merge($rss->itemTags, array (
  117. 'AUTHOR', 'CATEGORY', 'COMMENTS', 'CONTENT:ENCODED', 'DESCRIPTION',
  118. 'ENCLOSURE', 'GUID', 'LINK', 'PUBDATE', 'SOURCE', 'TITLE'
  119. )));
  120. $rss->imageTags = array_unique(array_merge($rss->imageTags, array (
  121. 'DESCRIPTION', 'HEIGHT', 'LINK', 'TITLE', 'URL', 'WIDTH'
  122. )));
  123. $rss->textinputTags = array_unique(array_merge($rss->textinputTags, array (
  124. 'DESCRIPTION', 'LINK', 'NAME', 'TITLE'
  125. )));
  126. $rss->moduleTags = array_unique(array_merge($rss->moduleTags, array (
  127. 'BLOGCHANNEL:BLOGROLL', 'BLOGCHANNEL:CHANGES', 'BLOGCHANNEL:MYSUBSCRIPTIONS',
  128. 'CC:LICENSE', 'CONTENT:ENCODED', 'DC:CONTRIBUTOR', 'DC:COVERAGE',
  129. 'DC:CREATOR', 'DC:DATE', 'DC:DESCRIPTION', 'DC:FORMAT', 'DC:IDENTIFIER',
  130. 'DC:LANGUAGE', 'DC:PUBDATE', 'DC:PUBLISHER', 'DC:RELATION', 'DC:RIGHTS',
  131. 'DC:SOURCE', 'DC:SUBJECT', 'DC:TITLE', 'DC:TYPE',
  132. 'SY:UPDATEBASE', 'SY:UPDATEFREQUENCY', 'SY:UPDATEPERIOD'
  133. )));
  134. // RSSをパース
  135. $result = $rss->parse();
  136. if (PEAR::isError($result)) {
  137. P2Util::pushInfoHtml('<p>p2 error: RSS - ' . $result->getMessage() . '</p>');
  138. return $result;
  139. }
  140. return $rss;
  141. }
  142. // }}}
  143. // {{{ atom_to_rss()
  144. /**
  145. * Atom 0.3 を RSS 1.0 に変換する(共通)
  146. */
  147. function atom_to_rss($input, $stylesheet, $output)
  148. {
  149. global $_conf;
  150. // 保存用ディレクトリがなければつくる
  151. if (!is_dir(dirname($output))) {
  152. FileCtl::mkdirFor($output);
  153. }
  154. // 変換
  155. if (extension_loaded('xslt')) { // PHP4, Sablotron
  156. $rss_content = atom_to_rss_by_xslt($input, $stylesheet, $output);
  157. } elseif (extension_loaded('xsl')) { // PHP5, LibXSLT
  158. $rss_content = atom_to_rss_by_xsl($input, $stylesheet, $output);
  159. }
  160. // チェック
  161. if (!$rss_content) {
  162. if (file_exists($output)) {
  163. unlink($output);
  164. }
  165. return FALSE;
  166. }
  167. chmod($output, $_conf['expack.rss.setting_perm']);
  168. // FreeBSD 5.3 Ports の textproc/php4-xslt ではバグのせいか変換の際に名前空間が失われるので補正する
  169. // (php4-xslt-4.3.10_2, expat-1.95.8, libiconv-1.9.2_1, Sablot-1.0.1)
  170. // バグのない環境なら何も変わらない・・・はず。
  171. $rss_fix_patterns = array(
  172. '/<(\/)?(RDF|Seq|li)( .+?)?>/u' => '<$1rdf:$2$3>',
  173. '/<(channel|item) about=/u' => '<$1 rdf:about=',
  174. '/<(\/)?(encoded)>/u' => '<$1content:$2>',
  175. '/<(\/)?(creator|subject|date|pubdate)>/u' => '<$1dc:$2>');
  176. $rss_fixed = preg_replace(array_keys($rss_fix_patterns), array_values($rss_fix_patterns), $rss_content);
  177. if (md5($rss_content) != md5($rss_fixed)) {
  178. $fp = @fopen($output, 'wb') or p2die("cannot write. ({$output})");
  179. flock($fp, LOCK_EX);
  180. fwrite($fp, $rss_fixed);
  181. flock($fp, LOCK_UN);
  182. fclose($fp);
  183. }
  184. return TRUE;
  185. }
  186. // }}}
  187. // {{{ atom_to_rss_by_xslt()
  188. /**
  189. * Atom 0.3 を RSS 1.0 に変換する(PHP4, XSLT)
  190. */
  191. function atom_to_rss_by_xslt($input, $stylesheet, $output)
  192. {
  193. $xh = xslt_create();
  194. if (!@xslt_process($xh, $input, $stylesheet, $output)) {
  195. $errmsg = xslt_errno($xh) . ': ' . xslt_error($xh);
  196. P2Util::pushInfoHtml('<p>p2 error: XSLT - AtomをRSSに変換できませんでした。(' . $errmsg . ')</p>');
  197. xslt_free($xh);
  198. return FALSE;
  199. }
  200. xslt_free($xh);
  201. return FileCtl::file_read_contents($output);
  202. }
  203. // }}}
  204. // {{{ atom_to_rss_by_xsl()
  205. /**
  206. * Atom 0.3 を RSS 1.0 に変換する(PHP5, DOM & XSL)
  207. */
  208. function atom_to_rss_by_xsl($input, $stylesheet, $output)
  209. {
  210. $xmlDoc = new DomDocument;
  211. if ($xmlDoc->load(realpath($input))) {
  212. $xslDoc = new DomDocument;
  213. $xslDoc->load(realpath($stylesheet));
  214. $proc = new XSLTProcessor;
  215. $proc->importStyleSheet($xslDoc);
  216. $rssDoc = $proc->transformToDoc($xmlDoc);
  217. $rssDoc->save($output);
  218. $rss_content = FileCtl::file_read_contents($output);
  219. } else {
  220. $rss_content = null;
  221. }
  222. if (!$rss_content) {
  223. P2Util::pushInfoHtml('<p>p2 error: XSL - AtomをRSSに変換できませんでした。</p>');
  224. return FALSE;
  225. }
  226. return $rss_content;
  227. }
  228. // }}}
  229. // {{{ rss_item_exists()
  230. /**
  231. * RSSのitem要素に任意の子要素があるかどうかをチェックする
  232. * 空要素は無視
  233. */
  234. function rss_item_exists($items, $element)
  235. {
  236. foreach ($items as $item) {
  237. if (isset($item[$element]) && strlen(trim($item[$element])) > 0) {
  238. return TRUE;
  239. }
  240. }
  241. return FALSE;
  242. }
  243. // }}}
  244. // {{{ rss_format_date()
  245. /**
  246. * RSSの日付を表示用に調整する
  247. */
  248. function rss_format_date($date)
  249. {
  250. if (preg_match('/(?P<date>(\d\d)?\d\d-\d\d-\d\d)T(?P<time>\d\d:\d\d(:\d\d)?)(?P<zone>([+\-])(\d\d):(\d\d)|Z)?/', $date, $t)) {
  251. $time = $t['date'].' '.$t['time'].' ';
  252. if ($t['zone'] && $t['zone'] != 'Z') {
  253. $time .= $t[6].$t[7].$t[8]; // [+-]HHMM
  254. } else {
  255. $time .= 'GMT';
  256. }
  257. return date('y/m/d H:i:s', strtotime($time));
  258. }
  259. return htmlspecialchars($date, ENT_QUOTES);
  260. }
  261. // }}}
  262. // {{{ rss_desc_converter()
  263. /**
  264. * RSSのdescription要素を表示用に調整する
  265. */
  266. function rss_desc_converter($description)
  267. {
  268. // HTMLタグがなければCR+LF/CR/LFを<br>+LFにするなど、軽く整形する
  269. if (!preg_match('/<(\/?[A-Za-z]+[1-6]?)( [^>]+>)?( ?\/)?>/', $description)) {
  270. return preg_replace('/[ \t]*(\r\n?|\n)[ \t]*/', "<br>\n", trim($description));
  271. }
  272. // 許可するタグ一覧
  273. $allowed_tags = '<a><b><i><u><s><strong><em><code><br><h1><h2><h3><h4><h5><h6><p><div><address><blockquote><ol><ul><li><img>';
  274. // script要素とstyle要素は中身ごとまとめて消去
  275. $description = preg_replace('/<(script|style)(?: .+?)?>(.+?)?<\/\1>/is', '', $description);
  276. // 不許可のタグを消去
  277. $description = strip_tags($description, $allowed_tags);
  278. // タグの属性チェック
  279. $description = preg_replace_callback('/<(\/?[A-Za-z]+[1-6]?)( [^>]+?)?>/', 'rss_desc_tag_cleaner', $description);
  280. return $description;
  281. }
  282. // }}}
  283. // {{{ rss_desc_tag_cleaner()
  284. /**
  285. * 無効タグ属性などを消去するコールバック関数
  286. */
  287. function rss_desc_tag_cleaner($tag)
  288. {
  289. global $_conf;
  290. $element = strtolower($tag[1]);
  291. $attributes = trim($tag[2]);
  292. $close = trim($tag[3]); // HTML 4.01形式で表示するので無視
  293. // 終了タグなら
  294. if (!$attributes || substr($element, 0, 1) == '/') {
  295. return '<'.$element.'>';
  296. }
  297. $tag = '<'.$element;
  298. if (preg_match_all('/(?:^| )([A-Za-z\-]+)\s*=\s*("[^"]*"|\'[^\']*\'|\w[^ ]*)(?: |$)/', $attributes, $matches, PREG_SET_ORDER)) {
  299. foreach ($matches as $attr) {
  300. $key = strtolower($attr[1]);
  301. $value = $attr[2];
  302. // JavaScriptイベントハンドラ・スタイルシート・ターゲットなどの属性は禁止
  303. if (preg_match('/^(on[a-z]+|style|class|id|target)$/', $key)) {
  304. continue;
  305. }
  306. // 値の引用符を削除
  307. $q = substr($value, 0, 1);
  308. if ($q == "'") {
  309. $value = str_replace('"', '&quot;', substr($value, 1, -1));
  310. } elseif ($q == '"') {
  311. $value = substr($value, 1, -1);
  312. }
  313. // 属性で分岐
  314. switch ($key) {
  315. case 'href':
  316. if ($element != 'a' || preg_match('/^javascript:/i', $value)) {
  317. break; // a要素以外はhref属性禁止
  318. }
  319. if (preg_match('|^[^/:]*/|', $value)) {
  320. $value = rss_url_rel_to_abs($value);
  321. }
  322. return '<a href="'.P2Util::throughIme($value).'"'.$_conf['ext_win_target_at'].'>';
  323. case 'src':
  324. if ($element != 'img' || preg_match('/^javascript:/i', $value)) {
  325. break; // img要素以外はsrc属性禁止
  326. }
  327. if (preg_match('|^[^/:]*/|', $value)) {
  328. $value = rss_url_rel_to_abs($value);
  329. }
  330. if (P2_RSS_IMAGECACHE_AVAILABLE) {
  331. $image = rss_get_image($value, $GLOBALS['channel']['title']);
  332. if ($image[3] != P2_IMAGECACHE_OK) {
  333. if ($_conf['ktai']) {
  334. // あぼーん画像 - 携帯
  335. switch ($image[3]) {
  336. case P2_IMAGECACHE_ABORN:return '[p2:あぼーん画像]';
  337. case P2_IMAGECACHE_BROKEN: return '[p2:壊]'; // これと
  338. case P2_IMAGECACHE_LARGE: return '[p2:大]'; // これは現状では無効
  339. case P2_IMAGECACHE_VIRUS: return '[p2:ウィルス警告]';
  340. default : return '[p2:unknown error]'; // 予備
  341. }
  342. } else {
  343. // あぼーん画像 - PC
  344. return "<img src=\"{$image[0][0]}\" {$image[0][1]}>";
  345. }
  346. } elseif ($_conf['ktai']) {
  347. // インライン表示 - 携帯(PC用サムネイルサイズ)
  348. return "<img src=\"{$image[1][0]}\" {$image[1][1]}>";
  349. } else {
  350. // インライン表示 - PC(フルサイズ)
  351. return "<img src=\"{$image[0][0]}\" {$image[0][1]}>";
  352. }
  353. }
  354. // イメージキャッシュが無効のとき画像は表示しない
  355. break '';
  356. case 'alt':
  357. if ($element == 'img' && !P2_RSS_IMAGECACHE_AVAILABLE) {
  358. return ' [img:'.$value.']'; // 画像はalt属性を代わりに表示
  359. }
  360. $tag .= ' ="'.$value.'"';
  361. break;
  362. case 'width':
  363. case 'height':
  364. // とりあえず無視
  365. break;
  366. default:
  367. $tag .= ' ="'.$value.'"';
  368. }
  369. } // endforeach
  370. // 要素で最終確認
  371. switch ($element) {
  372. // href属性がなかったa要素
  373. case 'a':
  374. return '<a>';
  375. // alt属性がなかったimg要素
  376. case 'img':
  377. return '';
  378. }
  379. } // endif
  380. $tag .= '>';
  381. return $tag;
  382. }
  383. // }}}
  384. // {{{ rss_url_rel_to_abs()
  385. /**
  386. * 相対 URL を絶対 URL にして返す関数
  387. *
  388. * グローバル変数を参照するより引数として RSS の URL を与えられる方が望ましいが
  389. * 変更が必要な箇所が多かったので手抜き
  390. */
  391. function rss_url_rel_to_abs($url)
  392. {
  393. // URL をパース
  394. $p = @parse_url($GLOBALS['channel']['link']);
  395. if (!$p || !isset($p['scheme']) || $p['scheme'] != 'http' || !isset($p['host'])) {
  396. return $url;
  397. }
  398. // ルート URL を作成
  399. $top = $p['scheme'] . '://';
  400. if (isset($p['user'])) {
  401. $top .= $p['user'];
  402. if (isset($p['pass'])) {
  403. $top .= '@' . $p['pass'];
  404. }
  405. $top .= ':';
  406. }
  407. $top .= $p['host'];
  408. if (isset($p['port'])) {
  409. $top .= ':' . $p['port'];
  410. }
  411. // 絶対パスならルート URL と結合して返す
  412. if (substr($url, 0, 1) == '/') {
  413. return $top . $url;
  414. }
  415. // ルート URL にスラッシュを付加
  416. $top .= '/';
  417. // チャンネルのパスを分解
  418. if (isset($p['path'])) {
  419. $paths1 = explode('/', trim($p['path'], '/'));
  420. } else {
  421. $paths1 = array();
  422. }
  423. // 相対 URL を分解
  424. if ($query = strstr($url, '?')) {
  425. $paths2 = explode('/', substr($url, 0, strlen($query) * -1));
  426. } else {
  427. $paths2 = explode('/', $url);
  428. $query = '';
  429. }
  430. // 分解した相対 URL のパスを絶対パスに加える
  431. while (($s = array_shift($paths2)) !== null) {
  432. $r = $s;
  433. switch ($s) {
  434. case '':
  435. case '.':
  436. // pass
  437. break;
  438. case '..':
  439. array_pop($paths1);
  440. break;
  441. default:
  442. array_push($paths1, $s);
  443. }
  444. }
  445. // 相対パスがスラッシュで終わっていたときの処理
  446. if ($r === '') {
  447. array_push($paths1, '');
  448. }
  449. //絶対 URL を返す
  450. return $top . implode('/', $paths1) . $query;
  451. }
  452. // }}}
  453. /*
  454. * Local Variables:
  455. * mode: php
  456. * coding: cp932
  457. * tab-width: 4
  458. * c-basic-offset: 4
  459. * indent-tabs-mode: nil
  460. * End:
  461. */
  462. // vim: set syn=php fenc=cp932 ai et ts=4 sw=4 sts=4 fdm=marker: