119b207277d9cce897578a51180e18ba9165350e
[stapibas.git] / src / stapibas / Feed / UpdateEntries.php
1 <?php
2 namespace stapibas;
3
4 /**
5  * Fetches entries that need an update and extracts their links
6  */
7 class Feed_UpdateEntries
8 {
9     public $db;
10     public $log;
11
12     public function __construct(Dependencies $deps)
13     {
14         $this->deps = $deps;
15         $this->db   = $deps->db;
16         $this->log  = $deps->log;
17     }
18
19     public function updateAll()
20     {
21         $this->log->info('Updating feed entries..');
22         $res = $this->db->query(
23             'SELECT * FROM feedentries'
24             . ' WHERE ' . $this->sqlNeedsUpdate()
25         );
26         $items = 0;
27         while ($entryRow = $res->fetch(\PDO::FETCH_OBJ)) {
28             ++$items;
29             $this->updateEntry($entryRow);
30         }
31         $this->log->info('Finished updating %d entries.', $items);
32     }
33
34     public function updateSome($urlOrIds)
35     {
36         $options = array();
37         foreach ($urlOrIds as $urlOrId) {
38             if (is_numeric($urlOrId)) {
39                 $options[] = 'fe_id = ' . intval($urlOrId);
40             } else {
41                 $options[] = 'fe_url = ' . $this->db->quote($urlOrId);
42             }
43         }
44
45         $this->log->info('Updating %d feed entries..', count($options));
46         $res = $this->db->query(
47             'SELECT * FROM feedentries'
48             . ' WHERE ' . $this->sqlNeedsUpdate()
49             . ' AND (' . implode(' OR ', $options) . ')'
50         );
51
52         $items = 0;
53         while ($entryRow = $res->fetch(\PDO::FETCH_OBJ)) {
54             ++$items;
55             $this->updateEntry($entryRow);
56         }
57         $this->log->info('Finished updating %d entries.', $items);
58     }
59
60     protected function updateEntry($entryRow)
61     {
62         $this->log->info(
63             'Updating feed entry #%d: %s', $entryRow->fe_id, $entryRow->fe_url
64         );
65
66         $req = new \HTTP_Request2($entryRow->fe_url);
67         $req->setHeader('User-Agent', 'stapibas');
68         $req->setHeader(
69             'Accept',
70             'application/xhtml+xml; q=1'
71             . ', application/xml; q=0.9'
72             . ', text/xml; q=0.9'
73             . ', text/html; q=0.5'
74             . ', */*; q=0.1'
75         );
76
77         if ($entryRow->fe_updated != '0000-00-00 00:00:00') {
78             $req->setHeader(
79                 'If-Modified-Since',
80                 gmdate('r', strtotime($entryRow->fe_updated))
81             );
82         }
83
84         $res = $req->send();
85         if ($res->getStatus() == 304) {
86             //not modified
87             $this->setNoUpdate($entryRow);
88             $this->log->info('Not modified');
89             return;
90         }
91
92         if (intval($res->getStatus() / 100) != 2) {
93             //no 2xx is an error for us
94             $this->log->err('Error fetching feed entry URL');
95             return;
96         }
97
98         $urls = $this->extractUrls($entryRow, $res);
99         $this->updateUrls($entryRow, $urls);
100         $this->setUpdated($entryRow, $res);
101     }
102
103     protected function updateUrls($entryRow, $urls)
104     {
105         $res = $this->db->query(
106             'SELECT * FROM feedentryurls'
107             . ' WHERE feu_fe_id = ' . $this->db->quote($entryRow->fe_id)
108         );
109         $urlRows = array();
110         while ($urlRow = $res->fetch(\PDO::FETCH_OBJ)) {
111             $urlRows[$urlRow->feu_url] = $urlRow;
112         }
113
114         $urls = array_unique($urls);
115
116         $new = $updated = $deleted = 0;
117         $items = count($urls);
118
119         foreach ($urls as $url) {
120             if (!isset($urlRows[$url])) {
121                 //URL is not known - insert it
122                 $this->db->exec(
123                     'INSERT INTO feedentryurls SET'
124                     . '  feu_fe_id = ' . $this->db->quote($entryRow->fe_id)
125                     . ', feu_url = ' . $this->db->quote($url)
126                     . ', feu_active = 1'
127                     . ', feu_pinged = 0'
128                     . ', feu_updated = NOW()'
129                 );
130                 ++$new;
131             } else if ($urlRows[$url]->feu_active == 0) {
132                 //URL is known already, but was once deleted and is back now
133                 $this->db->exec(
134                     'UPDATE feedentryurls SET'
135                     . '  feu_active = 1'
136                     . ', feu_updated = NOW()'
137                     . ' WHERE feu_id = ' . $this->db->quote($urlRows[$url]->feu_id)
138                 );
139                 ++$updated;
140                 unset($urlRows[$url]);
141             } else {
142                 //already known, all fine
143                 unset($urlRows[$url]);
144             }
145         }
146
147         //these URLs are in DB but not on the page anymore
148         foreach ($urlRows as $urlRow) {
149             ++$deleted;
150             $this->db->exec(
151                 'UPDATE feedentryurls SET'
152                 . '  feu_active = 0'
153                 . ', feu_updated = NOW()'
154                 . ' WHERE feu_id = ' . $this->db->quote($urlRow->feu_id)
155             );
156         }
157         $this->log->info(
158             'Feed entry #%d: %d new, %d updated, %d deleted of %d URLs',
159             $entryRow->fe_id, $new, $updated, $deleted, $items
160         );
161     }
162
163     protected function extractUrls($entryRow, \HTTP_Request2_Response $res)
164     {
165         $doc = new \DOMDocument();
166         $typeParts = explode(';', $res->getHeader('content-type'));
167         $type = $typeParts[0];
168         if ($type == 'application/xhtml+xml'
169             || $type == 'application/xml'
170             || $type == 'text/xml'
171         ) {
172             $doc->loadXML($res->getBody());
173         } else { 
174             $doc->loadHTML($res->getBody());
175         }
176
177         $xpath = new \DOMXPath($doc);
178         $xpath->registerNamespace('h', 'http://www.w3.org/1999/xhtml');
179         $query = '//*[' . $this->xpc('h-entry') . ' or ' . $this->xpc('hentry') . ']'
180             . '//*[' . $this->xpc('e-content') . ' or ' . $this->xpc('entry-content') . ']'
181             . '//*[(self::a or self::h:a) and @href and not(starts-with(@href, "#"))]';
182         $links = $xpath->query($query);
183         $this->log->info('%d links found', $links->length);
184
185         $entryUrl = new \Net_URL2($entryRow->fe_url);
186         //FIXME: base URL in html code
187
188         $urls = array();
189         foreach ($links as $link) {
190             $url = (string)$entryUrl->resolve(
191                 $link->attributes->getNamedItem('href')->nodeValue
192             );
193             $this->log->info('URL in entry: ' . $url);
194             $urls[] = $url;
195         }
196         return $urls;
197     }
198
199     protected function xpc($class)
200     {
201         return 'contains('
202             . 'concat(" ", normalize-space(@class), " "),'
203             . '" ' . $class . ' "'
204             . ')';
205     }
206
207     protected function setNoUpdate($entryRow)
208     {
209         $this->db->exec(
210             'UPDATE feedentries SET fe_needs_update = 0'
211             . ' WHERE fe_id = ' . $this->db->quote($entryRow->fe_id)
212         );
213     }
214
215     protected function setUpdated($entryRow, \HTTP_Request2_Response $res)
216     {
217         $this->db->exec(
218             'UPDATE feedentries'
219             . ' SET fe_needs_update = 0'
220             . ', fe_updated = ' . $this->db->quote(
221                 gmdate('Y-m-d H:i:s', strtotime($res->getHeader('last-modified')))
222             )
223             . ' WHERE fe_id = ' . $this->db->quote($entryRow->fe_id)
224         );
225     }
226
227     protected function sqlNeedsUpdate()
228     {
229         if ($this->deps->options['force']) {
230             return ' 1';
231         }
232         return ' (fe_needs_update = 1 OR fe_updated = "0000-00-00 00:00:00")';
233     }
234 }
235 ?>