PageRenderTime 50ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/library/XenForo/Model/Feed.php

https://github.com/hanguyenhuu/DTUI_201105
PHP | 536 lines | 329 code | 62 blank | 145 comment | 22 complexity | c1b03a979cc5b5e81f83280bbfa8252c MD5 | raw file
Possible License(s): LGPL-2.1, GPL-2.0, BSD-3-Clause
  1. <?php
  2. class XenForo_Model_Feed extends XenForo_Model
  3. {
  4. /**
  5. * Valid values (seconds) for feed.frequency
  6. *
  7. * @var array
  8. */
  9. protected static $_frequencyValues = array(
  10. 600, // 10 mins
  11. 1200, // 20 mins
  12. 1800, // 30 mins
  13. 3600, // 1 hour
  14. 7200, // 2 hours
  15. 14400, // 4 hours
  16. 21600, // 6 hours
  17. 43200, // 12 hours
  18. );
  19. /**
  20. * Maximum number of entries to be fetched per-feed per-import.
  21. *
  22. * @var integer
  23. */
  24. protected static $_maxEntriesPerImport = 5;
  25. /**
  26. * Fetch all info for a single feed record.
  27. *
  28. * @param integer $feedId
  29. *
  30. * @return array
  31. */
  32. public function getFeedById($feedId)
  33. {
  34. return $this->_getDb()->fetchRow('
  35. SELECT feed.*, IF(feed.active AND node.node_id, 1, 0) AS active,
  36. node.title AS node_title,
  37. user.username
  38. FROM xf_feed AS feed
  39. LEFT JOIN xf_node AS node ON
  40. (node.node_id = feed.node_id)
  41. LEFT JOIN xf_user AS user ON
  42. (user.user_id = feed.user_id)
  43. WHERE feed.feed_id = ?
  44. ', $feedId);
  45. }
  46. /**
  47. * Fetch feeds matching the parameters specified
  48. *
  49. * @param array $conditions
  50. * @param array $fetchOptions (unused at present)
  51. *
  52. * @return array
  53. */
  54. public function getFeeds(array $conditions = array(), array $fetchOptions = array())
  55. {
  56. $whereConditions = $this->prepareFeedConditions($conditions);
  57. return $this->fetchAllKeyed('
  58. SELECT feed.*, IF(feed.active AND node.node_id, 1, 0) AS active,
  59. node.title AS node_title,
  60. user.username
  61. FROM xf_feed AS feed
  62. LEFT JOIN xf_node AS node ON
  63. (node.node_id = feed.node_id)
  64. LEFT JOIN xf_user AS user ON
  65. (user.user_id = feed.user_id)
  66. WHERE ' . $whereConditions . '
  67. ORDER BY feed.title
  68. ', 'feed_id');
  69. }
  70. /**
  71. * Fetch all feed records with no conditions
  72. *
  73. * @return array
  74. */
  75. public function getAllFeeds()
  76. {
  77. return $this->getFeeds();
  78. }
  79. /**
  80. * Prepares the SQL WHERE conditions for getFeeds()
  81. *
  82. * @param array $conditions
  83. *
  84. * @return string
  85. */
  86. public function prepareFeedConditions(array $conditions)
  87. {
  88. $sqlConditions = array();
  89. if (isset($conditions['time_now']))
  90. {
  91. $sqlConditions[] = "feed.last_fetch + feed.frequency < {$conditions['time_now']}";
  92. }
  93. if (isset($conditions['active']))
  94. {
  95. $sqlConditions[] = 'feed.active = ' . ($conditions['active'] ? 1 : 0) . ' AND node.node_id IS NOT NULL';
  96. }
  97. return $this->getConditionsForClause($sqlConditions);
  98. }
  99. /**
  100. * Fetch all feeds that are due to be fetched,
  101. * based on the current time and the last_fetch
  102. * time for each feed with its frequency.
  103. *
  104. * @return array
  105. */
  106. protected function _getDueFeeds()
  107. {
  108. return $this->getFeeds(array(
  109. 'time_now' => XenForo_Application::$time,
  110. 'active' => true,
  111. ));
  112. }
  113. /**
  114. * Returns an array containing the default non-empty data for a new feed
  115. *
  116. * @return array
  117. */
  118. public function getDefaultFeedArray()
  119. {
  120. return array(
  121. // non-empty values
  122. 'frequency' => 1800,
  123. 'discussion_visible' => 1,
  124. 'discussion_open' => 1,
  125. // empty values
  126. 'title' => '',
  127. 'url' => '',
  128. 'node_id' => 0,
  129. 'user_id' => 0,
  130. 'title_template' => '',
  131. 'message_template' => '{content}' . "\n\n" . '[url="{link}"]' . new XenForo_Phrase('continue_reading') . '[/url]',
  132. 'discussion_sticky' => 0,
  133. 'last_fetch' => 0
  134. );
  135. }
  136. /**
  137. * Fetch the latest data for a feed from its specified URL.
  138. * Individual entries are returned in the 'entries' key of the return array.
  139. *
  140. * @param string $url
  141. * @param Exception|null $e Exception that occurs when reading feed
  142. *
  143. * @return array
  144. */
  145. public function getFeedData($url, Exception &$e = null)
  146. {
  147. try
  148. {
  149. $feed = Zend_Feed_Reader::import($url);
  150. }
  151. catch (Exception $feedEx)
  152. {
  153. $e = $feedEx;
  154. return array();
  155. }
  156. $data = array(
  157. 'id' => $feed->getId(),
  158. 'title' => $feed->getTitle(),
  159. 'link' => $feed->getLink(),
  160. 'date_modified' => $feed->getDateModified(),
  161. 'description' => $feed->getDescription(),
  162. 'language' => $feed->getLanguage(),
  163. 'image' => $feed->getImage(),
  164. 'generator' => $feed->getGenerator(),
  165. 'entries' => array()
  166. );
  167. foreach ($feed as $entry)
  168. {
  169. $entryData = array(
  170. 'id' => $entry->getId(),
  171. 'title' => html_entity_decode($entry->getTitle(), ENT_COMPAT, 'utf-8'),
  172. 'description' => $entry->getDescription(),
  173. 'date_modified' => null,
  174. 'authors' => $entry->getAuthors(),
  175. 'link' => $entry->getLink(),
  176. 'content_html' => $entry->getContent()
  177. );
  178. if (utf8_strlen($entryData['id']) > 250)
  179. {
  180. $entryData['id'] = md5($entryData['id']);
  181. }
  182. try
  183. {
  184. $entryData['date_modified'] = $entry->getDateModified();
  185. }
  186. catch (Zend_Exception $e) {} // triggered with invalid date format
  187. if (!empty($entryData['date_modified']) && $entryData['date_modified'] instanceof Zend_Date)
  188. {
  189. $entryData['date_modified'] = $entryData['date_modified']->getTimeStamp();
  190. }
  191. else
  192. {
  193. $entryData['date_modified'] = XenForo_Application::$time;
  194. }
  195. $entryData['date_modified'] = XenForo_Locale::dateTime($entryData['date_modified'], 'absolute');
  196. $data['entries'][] = $entryData;
  197. }
  198. return $data;
  199. }
  200. /**
  201. * Prepares data fetched from getFeedData() for use in posts
  202. *
  203. * @param array $feedData
  204. * @param array $feed
  205. *
  206. * @return array
  207. */
  208. public function prepareFeedData(array $feedData, array $feed)
  209. {
  210. $feed['baseUrl'] = $this->getFeedBaseUrl($feed['url']);
  211. foreach ($feedData['entries'] AS &$entry)
  212. {
  213. $entry = $this->prepareFeedEntry($entry, $feedData, $feed);
  214. }
  215. return $feedData;
  216. }
  217. /**
  218. * Prepares the data from a single feed entry for use in posts
  219. *
  220. * @param array $entry
  221. * @param array $feedData
  222. * @param array $feed
  223. *
  224. * @return array
  225. */
  226. public function prepareFeedEntry(array $entry, array $feedData, array $feed)
  227. {
  228. $entry['content'] = XenForo_Html_Renderer_BbCode::renderFromHtml($entry['content_html'], array(
  229. 'baseUrl' => $feed['baseUrl']
  230. ));
  231. $entry['author'] = $this->_getAuthorNamesFromArray($entry['authors']);
  232. if (empty($feed['message_template']))
  233. {
  234. $entry['message'] = $entry['content'];
  235. }
  236. else
  237. {
  238. $entry['message'] = $this->_replaceTokens($feed['message_template'], $entry);
  239. }
  240. $entry['message'] = trim($entry['message']);
  241. if ($entry['message'] === '')
  242. {
  243. $entry['message'] = '[url]' . $entry['link'] . '[/url]';
  244. }
  245. if (!empty($feed['title_template']))
  246. {
  247. $entry['title'] = $this->_replaceTokens($feed['title_template'], $entry);
  248. }
  249. return $entry;
  250. }
  251. /**
  252. * Fetch the base URL for the feed
  253. *
  254. * @param string URL
  255. *
  256. * @return string Base URL
  257. */
  258. public function getFeedBaseUrl($url)
  259. {
  260. return dirname($url);
  261. }
  262. /**
  263. * Searches the given template string for {token} and replaces it with $entry[token]
  264. *
  265. * @param string $template
  266. * @param array $entry
  267. */
  268. protected function _replaceTokens($template, array $entry)
  269. {
  270. if (preg_match_all('/\{([a-z0-9_]+)\}/i', $template, $matches))
  271. {
  272. foreach ($matches[1] AS $token)
  273. {
  274. if (isset($entry[$token]))
  275. {
  276. $template = str_replace('{' . $token . '}', $entry[$token], $template);
  277. }
  278. }
  279. }
  280. return $template;
  281. }
  282. /**
  283. * Attempts to convert a Zend_Feed_Reader_Collection_Author object
  284. * into a comma-separated list of names.
  285. *
  286. * @param Zend_Feed_Reader_Collection_Author|null $feedAuthors
  287. *
  288. * @return string
  289. */
  290. protected function _getAuthorNamesFromArray($feedAuthors)
  291. {
  292. $authorNames = array();
  293. if ($feedAuthors)
  294. {
  295. foreach ($feedAuthors AS $author)
  296. {
  297. if (isset($author['name']))
  298. {
  299. $authorNames[] = $author['name'];
  300. }
  301. else if (isset($author['email']))
  302. {
  303. $authorNames[] = $author['email'];
  304. }
  305. }
  306. }
  307. return implode(', ', $authorNames);
  308. }
  309. /**
  310. * Returns the array of possible frequency values
  311. *
  312. * @return array
  313. */
  314. public function getFrequencyValues()
  315. {
  316. return self::$_frequencyValues;
  317. }
  318. /**
  319. * Checks the current feed data against a list of already-imported entries
  320. * and removes any entries from the data that have already been imported.
  321. *
  322. * @param array $feedData
  323. * @param array $feed
  324. *
  325. * @return array
  326. */
  327. protected function _checkProcessedEntries(array $feedData, array $feed)
  328. {
  329. $ids = array();
  330. foreach ($feedData['entries'] AS $i => &$entry)
  331. {
  332. $ids[$entry['id']] = $i;
  333. $entry['hash'] = md5($entry['id'] . $entry['title'] . $entry['content_html']);
  334. }
  335. if (!$ids)
  336. {
  337. return $feedData;
  338. }
  339. $existing = $this->_getDb()->fetchCol('
  340. SELECT unique_id
  341. FROM xf_feed_log
  342. WHERE feed_id = ?
  343. AND unique_id IN (' . $this->_getDb()->quote(array_keys($ids)) . ')
  344. ', $feed['feed_id']);
  345. foreach ($existing AS $id)
  346. {
  347. if (isset($ids[$id]))
  348. {
  349. unset($feedData['entries'][$ids[$id]]);
  350. }
  351. }
  352. $feedData['entries'] = $this->_limitEntries($feedData['entries'], self::$_maxEntriesPerImport);
  353. return $feedData;
  354. }
  355. /**
  356. * Limits the number of entries in the given array to the number specified.
  357. *
  358. * @param array $entries
  359. * @param integer $maxEntries
  360. */
  361. protected function _limitEntries(array $entries, $maxEntries = 0)
  362. {
  363. if ($maxEntries)
  364. {
  365. return array_slice($entries, 0, $maxEntries, true);
  366. }
  367. else
  368. {
  369. return $entries;
  370. }
  371. }
  372. /**
  373. * Inserts the data of a single feed entry
  374. *
  375. * @param array $entryData
  376. * @param array $feedData
  377. * @param array $feed
  378. */
  379. protected function _insertFeedEntry(array $entryData, array $feedData, array $feed)
  380. {
  381. $db = $this->_getDb();
  382. XenForo_Db::beginTransaction($db);
  383. $writer = XenForo_DataWriter::create('XenForo_DataWriter_Discussion_Thread', XenForo_DataWriter::ERROR_SILENT);
  384. $writer->set('node_id', $feed['node_id']);
  385. $writer->set('discussion_state', $feed['discussion_visible'] ? 'visible' : 'moderated');
  386. $writer->set('discussion_open', $feed['discussion_open']);
  387. $writer->set('sticky', $feed['discussion_sticky']);
  388. $writer->set('title', XenForo_Helper_String::wholeWordTrim($entryData['title'], 95));
  389. $writer->set('user_id', $feed['user_id']);
  390. // TODO: The wholeWordTrim() used here may not be exactly ideal. Any better ideas?
  391. if ($feed['user_id'])
  392. {
  393. // post as the specified registered user
  394. $writer->set('username', $feed['username']);
  395. }
  396. else if ($entryData['author'])
  397. {
  398. // post as guest, using the author name(s) from the entry
  399. $writer->set('username', XenForo_Helper_String::wholeWordTrim($entryData['author'], 25, 0, ''));
  400. }
  401. else
  402. {
  403. // post as guest, using the feed title
  404. $writer->set('username', XenForo_Helper_String::wholeWordTrim($feed['title'], 25, 0, ''));
  405. }
  406. $postWriter = $writer->getFirstMessageDw();
  407. $postWriter->setOption(XenForo_DataWriter_DiscussionMessage::OPTION_IS_AUTOMATED, true);
  408. $postWriter->setOption(XenForo_DataWriter_DiscussionMessage::OPTION_VERIFY_GUEST_USERNAME, false);
  409. $postWriter->set('message', $entryData['message']);
  410. $writer->save();
  411. if ($writer->get('thread_id'))
  412. {
  413. $db->query('
  414. INSERT INTO xf_feed_log
  415. (feed_id, unique_id, hash, thread_id)
  416. VALUES
  417. (?, ?, ?, ?)
  418. ON DUPLICATE KEY UPDATE
  419. hash = VALUES(hash),
  420. thread_id = VALUES(thread_id)
  421. ', array($feed['feed_id'], utf8_substr($entryData['id'], 0, 250), $entryData['hash'], $writer->get('thread_id')));
  422. }
  423. XenForo_Db::commit($db);
  424. }
  425. /**
  426. * Inserts the data from a single feed.
  427. *
  428. * @param array $feedData
  429. * @param array $feed
  430. */
  431. protected function _insertFeedData(array $feedData, array $feed)
  432. {
  433. // insert feed data and update feed
  434. $db = $this->_getDb();
  435. XenForo_Db::beginTransaction($db);
  436. $feedData['entries'] = array_reverse($feedData['entries']); // post the newest stuff at the end
  437. foreach ($feedData['entries'] AS $entry)
  438. {
  439. $this->_insertFeedEntry($entry, $feedData, $feed);
  440. }
  441. // update feed
  442. $dw = XenForo_DataWriter::create('XenForo_DataWriter_Feed');
  443. $dw->setExistingData($feed['feed_id']);
  444. $dw->set('last_fetch', XenForo_Application::$time);
  445. $dw->save();
  446. XenForo_Db::commit($db);
  447. }
  448. /**
  449. * Prepares and inserts the data from a single feed
  450. *
  451. * @param array $feed
  452. */
  453. public function importFeedData(array $feed)
  454. {
  455. $feedData = $this->getFeedData($feed['url']);
  456. if (!$feedData)
  457. {
  458. return;
  459. }
  460. $feedData = $this->_checkProcessedEntries($feedData, $feed); // filter dupes
  461. $feedData = $this->prepareFeedData($feedData, $feed);
  462. $this->_insertFeedData($feedData, $feed);
  463. }
  464. /**
  465. * Prepares and imports the data from all feeds due to be imported
  466. */
  467. public function scheduledImport()
  468. {
  469. foreach ($this->_getDueFeeds() AS $feedId => $feed)
  470. {
  471. $this->importFeedData($feed);
  472. }
  473. }
  474. }