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