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

/nitrofuran/modules/reader/reader.php

https://github.com/axshavan/nitrofuran
PHP | 540 lines | 404 code | 27 blank | 109 comment | 31 complexity | 99fcf655714878e39e2bed56ca9405cb MD5 | raw file
  1. <?php
  2. /**
  3. * Описание классов моделей для модуля reader
  4. *
  5. * @author Dmitry Nikiforov <axshavan@yandex.ru>
  6. * @license http://sam.zoy.org/wtfpl WTFPL
  7. * This program is free software. It comes without any warranty, to
  8. * the extent permitted by applicable law. You can redistribute it
  9. * and/or modify it under the terms of the Do What The Fuck You Want
  10. * To Public License, Version 2, as published by Sam Hocevar. See
  11. * http://sam.zoy.org/wtfpl/COPYING for more details.
  12. */
  13. require_once(DOCUMENT_ROOT.'/nitrofuran/crud.class.php');
  14. class CReader
  15. {
  16. protected $crud; // CRUD модель
  17. // конструктор
  18. public function __construct()
  19. {
  20. $this->crud = new CRUD();
  21. $this->someProcedures();
  22. }
  23. /**
  24. * Добавление группы подписок
  25. * @param string $group_name имя новой группы
  26. * @param int $parent_group_id id группы-родителя
  27. * @param string &$error тут возвращается код ошибки
  28. * @return bool
  29. */
  30. public function addGroup($group_name, $parent_group_id, &$error)
  31. {
  32. $error = '';
  33. if(!$group_name || !sizeof($group_name))
  34. {
  35. $error = 'EMPTY_NAME';
  36. return false;
  37. }
  38. if(!$this->crud->create(READER_SUBSCRIPTION_GROUP_TABLE, array('name' => $group_name, 'group_id' => (int)$parent_group_id)))
  39. {
  40. $error = 'DB_ERROR';
  41. return false;
  42. }
  43. return true;
  44. }
  45. /**
  46. * Добавить непрочитанный элемент подписки.
  47. * @param array $item
  48. * @param array $subscription данные о подписке
  49. * @return int идентификатор добавленного элемента
  50. */
  51. public function addItem($item, $subscription)
  52. {
  53. $res = $this->crud->read
  54. (
  55. READER_SUBSCRIPTION_ITEM_TABLE,
  56. array
  57. (
  58. 'subscription_id' => $subscription['id'],
  59. 'href' => $item['href'],
  60. )
  61. );
  62. if(!$res[0] && !$res[0]['id'])
  63. {
  64. // за счёт разных часовых поясов и тупого php могут быть косяки со временными метками
  65. // накинем ещё сутки
  66. if((int)$item['date'] && $subscription['last_update'] > $item['date'] + 86400)
  67. {
  68. return -1;
  69. }
  70. $this->crud->create
  71. (
  72. READER_SUBSCRIPTION_ITEM_TABLE,
  73. array
  74. (
  75. 'name' => $item['title'],
  76. 'href' => $item['href'],
  77. 'subscription_id' => $subscription['id'],
  78. 'read_flag' => 0,
  79. 'date' => (int)$item['date'] <= gmmktime() ? (int)$item['date'] : gmmktime(),
  80. 'text' => $item['description']
  81. )
  82. );
  83. return $this->crud->id();
  84. }
  85. else
  86. {
  87. if($res[0]['read_flag'])
  88. {
  89. return -1;
  90. }
  91. return $res[0]['id'];
  92. }
  93. }
  94. /**
  95. * Добавить подписку
  96. * @param string $href собственно ссылка на фид
  97. * @param int $group_id id группы, куда добавляется подписка
  98. * @param string &$error тут возвращается ошибка
  99. * @return bool
  100. */
  101. public function addSubscription($href, $group_id, &$error)
  102. {
  103. $error = '';
  104. if(!$href || !sizeof($href))
  105. {
  106. $error = 'EMPTY_HREF';
  107. return false;
  108. }
  109. global $AUTH;
  110. if
  111. (
  112. !$this->crud->create
  113. (
  114. READER_SUBSCRIPTION_TABLE,
  115. array
  116. (
  117. 'name' => $href,
  118. 'href' => $href,
  119. 'group_id' => (int)$group_id,
  120. 'user_id' => $AUTH->sess_data['user_id']
  121. )
  122. )
  123. )
  124. {
  125. $error = 'DB_ERROR';
  126. return false;
  127. }
  128. return true;
  129. }
  130. /**
  131. * Получить список свежих элементов подписки с фида
  132. * @param array $subscription данные о подписке
  133. * @param int $mostEarlierDate таймстамп самого раннего поста
  134. * @return array
  135. */
  136. public function curlGetItems(&$subscription, &$mostEarlierDate)
  137. {
  138. $data = array();
  139. $mostEarlierDate = $mostEarlierDate ? $mostEarlierDate : gmmktime();
  140. $curl = curl_init($subscription['href']);
  141. curl_setopt($curl, CURLOPT_TIMEOUT, 30);
  142. curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5);
  143. curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
  144. curl_setopt
  145. (
  146. $curl,
  147. CURLOPT_HTTPHEADER,
  148. array
  149. (
  150. 'Accept-Encoding: ',
  151. )
  152. );
  153. curl_setopt($curl, CURLOPT_USERAGENT, 'Nitrofuran Reader: PHP bot collecting RSS feeds');
  154. ob_start();
  155. curl_exec($curl);
  156. $raw_string = ob_get_clean();
  157. $xml = simplexml_load_string($raw_string);
  158. if(!$xml)
  159. {
  160. // может быть, кто-то игнорирует заголовок Accept-Encoding и отдаёт сжатый gzip текст?
  161. $tmp_file_name = DOCUMENT_ROOT.'/tmp/'.md5(time());
  162. file_put_contents($tmp_file_name, $raw_string);
  163. $cmd = 'cat '.$tmp_file_name.' | $(which gunzip) 2>/dev/null';
  164. ob_start();
  165. echo `$cmd`;
  166. $unpacked_string = ob_get_clean();
  167. if(strlen($unpacked_string) >= strlen($raw_string))
  168. {
  169. $raw_string = $unpacked_string;
  170. }
  171. unset($unpacked_string);
  172. unlink($tmp_file_name);
  173. $xml = simplexml_load_string($raw_string);
  174. }
  175. if($xml)
  176. {
  177. // может быть, это что-то похожее на RSS 2.0
  178. if((string)$xml->attributes()->version == '2.0' && $xml->channel)
  179. {
  180. $data = $this->parseRSS20($xml);
  181. }
  182. // может быть, это что-то похожее на Atom
  183. else
  184. {
  185. $data = $this->parseAtom($xml);
  186. }
  187. }
  188. foreach($data['items'] as $k => &$item)
  189. {
  190. if(!$item['date'])
  191. {
  192. $item['date'] = gmmktime();
  193. }
  194. elseif($item['date'] < $mostEarlierDate)
  195. {
  196. $mostEarlierDate = $item['date'];
  197. }
  198. $item['id'] = $this->addItem($item, $subscription);
  199. if($item['id'] == -1)
  200. {
  201. unset($item);
  202. unset($data['items'][$k]);
  203. }
  204. }
  205. $this->crud->update(READER_SUBSCRIPTION_TABLE, array('id' => $subscription['id']), array('last_update' => gmmktime()));
  206. $subscription['last_update'] = gmmktime();
  207. return $data;
  208. }
  209. /**
  210. * Удаление подписки
  211. * @param int $id идентификатор подписки
  212. * @param string $error
  213. * @return bool
  214. */
  215. public function deleteSubscription($id, &$error)
  216. {
  217. $error = '';
  218. if(!$id)
  219. {
  220. $error = 'NO_ID';
  221. return false;
  222. }
  223. $this->crud->delete(READER_SUBSCRIPTION_TABLE, array('id' => $id));
  224. $this->crud->delete(READER_SUBSCRIPTION_ITEM_TABLE, array('subscription_id' => $id));
  225. return true;
  226. }
  227. /**
  228. * Получить список элементов подписки
  229. * @param array $subscription данные о подписке
  230. * @param bool $forceRead обновить элементы насильно
  231. * @return array
  232. */
  233. public function getItems(&$subscription, $forceRead = false)
  234. {
  235. $data = array();
  236. $mostEarlierDate = gmmktime();
  237. if(!get_param('reader', 'use_async_run') || $forceRead)
  238. {
  239. $data = $this->curlGetItems($subscription, $mostEarlierDate);
  240. }
  241. $res = $this->crud->read
  242. (
  243. READER_SUBSCRIPTION_ITEM_TABLE,
  244. array
  245. (
  246. 'subscription_id' => $subscription['id'],
  247. 'read_flag' => 0,
  248. '<date' => $mostEarlierDate
  249. )
  250. );
  251. foreach($res as $res_row)
  252. {
  253. if($res_row['date'] < $mostEarlierDate)
  254. {
  255. $mostEarlierDate = $res_row['date'];
  256. }
  257. foreach($data['items'] as &$item)
  258. {
  259. if($item['href'] == $res_row['href'])
  260. {
  261. continue 2;
  262. }
  263. }
  264. $res_row['title'] = $res_row['name'];
  265. $res_row['description'] = $res_row['text'];
  266. unset($res_row['text']);
  267. $data['items'][] = $res_row;
  268. }
  269. $res = $this->crud->read
  270. (
  271. READER_SUBSCRIPTION_ITEM_TABLE,
  272. array
  273. (
  274. 'subscription_id' => $subscription['id'],
  275. 'read_flag' => 1,
  276. '>=date' => $mostEarlierDate
  277. )
  278. );
  279. foreach($res as $res_row)
  280. {
  281. foreach($data['items'] as $k => &$item)
  282. {
  283. if($item['href'] == $res_row['href'])
  284. {
  285. unset($data['items'][$k]);
  286. }
  287. }
  288. }
  289. usort($data['items'], create_function('$a, $b', 'return $a["date"] < $b["date"] ? 1 : ($a["date"] > $b["date"] ? -1 : 0);'));
  290. return $data;
  291. }
  292. /**
  293. * Получить данные об одной подписки
  294. * @param int $id идентификатор подписки
  295. * @return array
  296. */
  297. public function getSubscription($id)
  298. {
  299. $result = $this->crud->read(READER_SUBSCRIPTION_TABLE, array('id' => (int)$id));
  300. $result = $result[0];
  301. return $result;
  302. }
  303. /**
  304. * Получить данные об одной группе подписок
  305. * @param int $id идентификатор группы подписок
  306. * @return array
  307. */
  308. public function getSubscriptionGroup($id)
  309. {
  310. $result = $this->crud->read(READER_SUBSCRIPTION_GROUP_TABLE, array('id' => (int)$id));
  311. return $result[0];
  312. }
  313. /**
  314. * Получить список папок и подписок
  315. * @return array
  316. */
  317. public function getSubscriptions()
  318. {
  319. require_once(DOCUMENT_ROOT.'/nitrofuran/graph.class.php');
  320. global $AUTH;
  321. global $DB;
  322. // надо собрать массив с ключами, соответствующими id групп
  323. $res_g = array();
  324. $tmp = $this->crud->read(READER_SUBSCRIPTION_GROUP_TABLE);
  325. foreach($tmp as $v)
  326. {
  327. $res_g[$v['id']] = $v;
  328. }
  329. unset($tmp);
  330. // выборка подписок и раскладывание их по папкам
  331. //$res_s = $this->crud->read(READER_SUBSCRIPTION_TABLE, array('user_id' => $AUTH->sess_data['user_id']));
  332. $res_s = $DB->QueryFetched("select s.*, count(i.id) as unread_count from
  333. `".READER_SUBSCRIPTION_TABLE."` s
  334. left join `".READER_SUBSCRIPTION_ITEM_TABLE."` i on (i.`subscription_id` = s.id and !i.`read_flag`)
  335. where s.`user_id` = '".$AUTH->sess_data['user_id']."'
  336. group by s.`id`");
  337. foreach($res_s as $v)
  338. {
  339. $res_g[$v['group_id']]['subscriptions'][$v['id']] = $v;
  340. }
  341. unset($res_s);
  342. // а теперь завернём всё в граф
  343. $graph = new CGraph();
  344. $graph->CreateFromArray($res_g, 'group_id');
  345. $result = $graph->GetAsArray(true);
  346. // вынос подписок без группы в корень
  347. foreach($result['children'][0]['data']['subscriptions'] as $v)
  348. {
  349. $result['data']['subscriptions'][] = $v;
  350. }
  351. unset($result['children'][0]);
  352. return $result;
  353. }
  354. /**
  355. * Пометить элемент прочитанным
  356. * @param int $id
  357. * @return bool
  358. */
  359. public function readItem($id)
  360. {
  361. $item = $this->crud->read(READER_SUBSCRIPTION_ITEM_TABLE, array('id' => $id), array('id' => 'desc'));
  362. if($item[0]['read_flag'])
  363. {
  364. return false;
  365. }
  366. $this->crud->update(READER_SUBSCRIPTION_ITEM_TABLE, array('id' => $id), array('read_flag' => 1));
  367. return true;
  368. }
  369. /**
  370. * Обновить группу подписок
  371. * @param $id идентификатор группы
  372. * @param $name новое название группы
  373. * @param $group_id новая родительяская группа для этой группы
  374. * @return bool
  375. */
  376. public function updateGroup($id, $name, $group_id)
  377. {
  378. return $this->crud->update
  379. (
  380. READER_SUBSCRIPTION_GROUP_TABLE,
  381. array('id' => (int)$id),
  382. array
  383. (
  384. 'name' => $name,
  385. 'group_id' => (int)$group_id
  386. )
  387. );
  388. }
  389. /**
  390. * Обновить подписку
  391. * @param int $id идентификатор обновляемой подписки
  392. * @param string $name новое название для подписки
  393. * @param int $group_id новая группа для подписки
  394. * @return bool
  395. */
  396. public function updateSubscription($id, $name, $group_id)
  397. {
  398. return $this->crud->update
  399. (
  400. READER_SUBSCRIPTION_TABLE,
  401. array('id' => (int)$id),
  402. array
  403. (
  404. 'name' => $name,
  405. 'group_id' => (int)$group_id
  406. )
  407. );
  408. }
  409. // PROTECTED AREA
  410. /**
  411. * Достать элементы из Atom
  412. * @param SimpleXMLElement $xml
  413. * @return array
  414. */
  415. protected function parseAtom(&$xml)
  416. {
  417. $_result = array
  418. (
  419. 'meta' => array
  420. (
  421. 'title' => (string)$xml->title
  422. ),
  423. 'items' => array()
  424. );
  425. foreach($xml->entry as $entry)
  426. {
  427. $href = '';
  428. foreach($entry->link as $link)
  429. {
  430. if
  431. (
  432. (string)$link->attributes()->type == 'text/html'
  433. && (string)$link->attributes()->rel == 'alternate'
  434. )
  435. {
  436. $href = (string)$link->attributes()->href;
  437. }
  438. }
  439. $_result['items'][] = array
  440. (
  441. 'title' => (string)$entry->title,
  442. 'href' => $href,
  443. 'description' => (string)$entry->content,
  444. 'date' => strtotime((string)$entry->updated)
  445. );
  446. }
  447. return $_result;
  448. }
  449. /**
  450. * Достать элементы из RSS 2.0
  451. * @param SimpleXMLElement $xml
  452. * @return array
  453. */
  454. protected function parseRSS20(&$xml)
  455. {
  456. $_result = array
  457. (
  458. 'meta' => array
  459. (
  460. 'title' => (string)$xml->channel->title,
  461. 'link' => (string)$xml->channel->link,
  462. 'description' => (string)$xml->channel->description,
  463. 'lastBuildDate' => (string)$xml->channel->lastBuildDate,
  464. 'image' => array
  465. (
  466. 'url' => (string)$xml->channel->image->url,
  467. 'link' => (string)$xml->channel->image->link,
  468. 'title' => (string)$xml->channel->image->title
  469. )
  470. ),
  471. 'items' => array()
  472. );
  473. foreach($xml->channel->item as $item)
  474. {
  475. $_result['items'][] = array
  476. (
  477. 'title' => (string)$item->title,
  478. 'href' => (string)$item->link,
  479. 'description' => (string)$item->description,
  480. 'date' => (string)$item->pubDateUT ? (string)$item->pubDateUT : strtotime((string)$item->pubDate)
  481. );
  482. }
  483. return $_result;
  484. }
  485. /**
  486. * Некоторые процедуры
  487. */
  488. protected function someProcedures()
  489. {
  490. // у прочитанных постов старше двух недель удалим текст
  491. $this->crud->update
  492. (
  493. READER_SUBSCRIPTION_ITEM_TABLE,
  494. array
  495. (
  496. 'read_flag' => 1,
  497. '<date' => time() - 86400 * 14
  498. ),
  499. array('text' => '')
  500. );
  501. // прочитанные посты старше месяца просто удалим
  502. $this->crud->delete
  503. (
  504. READER_SUBSCRIPTION_ITEM_TABLE,
  505. array
  506. (
  507. 'read_flag' => 1,
  508. '<date' => time() - 86400 * 31
  509. )
  510. );
  511. }
  512. }
  513. ?>