/core/src/main/php/peer/news/NntpConnection.class.php
PHP | 455 lines | 225 code | 59 blank | 171 comment | 42 complexity | cd7ba0e17b480116d8159a77d3965a75 MD5 | raw file
1<?php
2/* This class is part of the XP framework
3 *
4 * $Id$
5 */
6
7 uses(
8 'peer.Socket',
9 'peer.ProtocolException',
10 'peer.URL',
11 'peer.news.NntpReply',
12 'peer.news.Newsgroup',
13 'peer.news.Article',
14 'util.Date',
15 'util.log.Traceable'
16 );
17
18 /**
19 * NNTP Connection
20 *
21 * Usage [retrieve newsgroup listing]:
22 * <code>
23 * $c= new NntpConnection('nntp://news.xp-framework.net');
24 * try {
25 * $c->connect();
26 * $groups= $c->getGroups();
27 * $c->close();
28 * } catch(IOException $e) {
29 * $e->printStackTrace();
30 * exit();
31 * }
32 *
33 * foreach ($groups as $group) {
34 * var_dump($group->getName());
35 * }
36 * </code>
37
38 * @see rfc://977
39 * @purpose News protocol implementation
40 */
41 class NntpConnection extends Object implements Traceable {
42 public
43 $url = NULL,
44 $cat = NULL,
45 $response = array();
46
47 /**
48 * Constructor
49 *
50 * @param peer.URL url
51 */
52 public function __construct($url) {
53 $this->url= $url;
54 $this->_sock= new Socket(
55 $this->url->getHost(),
56 $this->url->getPort(119)
57 );
58 }
59
60 /**
61 * Set a trace for debugging
62 *
63 * @param util.log.LogCategory cat
64 */
65 public function setTrace($cat) {
66 $this->cat= $cat;
67 }
68
69 /**
70 * Wrapper that sends a command to the remote host.
71 *
72 * @param string format
73 * @param var* args
74 * @return bool success
75 * @throws peer.ProtocolException in case the command is too long
76 */
77 protected function _sendcmd() {
78 if (!$this->_sock->isConnected()) return FALSE;
79
80 $a= func_get_args();
81 $cmd= implode(' ', $a);
82
83 // NNTP/RFC977 only allows command up to 512 (-2) chars.
84 if (strlen($cmd) > 510) {
85 throw new ProtocolException('Command too long! Max. 510 chars');
86 }
87
88 $this->cat && $this->cat->debug('>>>', $cmd);
89 try {
90 $this->_sock->write($cmd."\r\n");
91 } catch (SocketException $e) {
92 return FALSE;
93 }
94
95 // read first line and return
96 // nntp statuscode
97 return $this->_readResponse();
98 }
99
100 /**
101 * Get status response
102 *
103 * @return string status
104 */
105 protected function _readResponse() {
106 if (!($line= $this->_sock->readLine())) return FALSE;
107 $this->cat && $this->cat->debug('<<<', $line);
108
109 $this->response= array(
110 (int) substr($line, 0, 3),
111 (string) rtrim(substr($line, 4))
112 );
113 return $this->response[0];
114 }
115
116 /**
117 * Get data
118 *
119 * @return string status
120 */
121 protected function _readData() {
122 if ($this->_sock->eof()) return FALSE;
123
124 $line= $this->_sock->readLine();
125 $this->cat && $this->cat->debug('<<<', $line);
126
127 if ('.' == $line) return FALSE;
128 return $line;
129 }
130
131 /**
132 * Connect
133 *
134 * @param float timeout default 2.0
135 * @return bool success
136 * @throws peer.ConnectException in case there's an error during connecting
137 */
138 public function connect($auth= FALSE) {
139 $this->_sock->connect();
140
141 // Read banner message
142 if (!($response= $this->_readResponse()))
143 throw new ConnectException('No valid response from server');
144
145 $this->cat && $this->cat->debug('<<<', $this->getResponse());
146 if ($auth) return $this->authenticate();
147
148 return TRUE;
149 }
150
151 /**
152 * Disconnect
153 *
154 * @return bool success
155 * @throws io.IOException in case there's an error during disconnecting
156 */
157 public function close() {
158 if (!$this->_sock->isConnected()) return TRUE;
159
160 $status= $this->_sendcmd('QUIT');
161 if (!NntpReply::isPositiveCompletion($status)) {
162 throw new IOException('Error during disconnect');
163 }
164 $this->_sock->close();
165 return TRUE;
166 }
167
168 /**
169 * Authenticate
170 *
171 * @param string authmode
172 * @return bool success
173 * @throws peer.AuthenticationException in case authentication failed
174 */
175 public function authenticate() {
176 $status= $this->_sendcmd('AUTHINFO user', $this->url->getUser());
177
178 // Send password if requested
179 if (NNTP_AUTH_NEEDMODE === $status) {
180 $status= $this->_sendcmd('AUTHINFO pass', $this->url->getPassword());
181 }
182
183 switch ($status) {
184 case NNTP_AUTH_ACCEPT: {
185 return TRUE;
186 break;
187 }
188 case NNTP_AUTH_NEEDMODE: {
189 throw new AuthenticatorException('Authentication uncomplete');
190 break;
191 }
192 case NNTP_AUTH_REJECTED: {
193 throw new AuthenticatorException('Authentication rejected');
194 break;
195 }
196 case NNTP_NOPERM: {
197 throw new AuthenticatorException('No permission');
198 break;
199 }
200 default: {
201 throw new AuthenticatorException('Unexpected authentication error');
202 }
203 }
204 }
205
206 /**
207 * Select a group
208 *
209 * @param string groupname
210 * @return success
211 */
212 public function setGroup($group) {
213 $status= $this->_sendcmd('GROUP', $group);
214 if (!NntpReply::isPositiveCompletion($status))
215 throw (new IOException('Could not set group'));
216
217 return TRUE;
218 }
219
220 /**
221 * Get groups
222 *
223 * @return peer.news.Newsgroup[]
224 */
225 public function getGroups() {
226 $status= $this->_sendcmd('LIST');
227 if (!NntpReply::isPositiveCompletion($status))
228 throw new IOException('Could not get groups');
229
230 while ($line= $this->_readData()) {
231 $buf= explode(' ', $line);
232 $groups[]= new Newsgroup($buf[0], (int)$buf[1], (int)$buf[2], $buf[3]);
233 }
234
235 return $groups;
236 }
237
238 /**
239 * Get Article
240 *
241 * @param var Id eighter a messageId or an articleId
242 * @return peer.news.Article
243 * @throws io.IOException in case article could not be retrieved
244 */
245 public function getArticle($id= NULL) {
246 $status= $this->_sendcmd('ARTICLE', $id);
247 if (!NntpReply::isPositiveCompletion($status))
248 throw new IOException('Could not get article');
249
250 with($args= explode(' ', $this->getResponse())); {
251 $article= new Article($args[0], $args[1]);
252 }
253
254 // retrieve headers
255 while ($line= $this->_readData()) {
256 if ("\t" == $line{0} || ' ' == $line{0}) {
257 $article->setHeader(
258 $header[0],
259 $article->getHeader($header[0])."\n".$line
260 );
261 continue;
262 }
263 $header= explode(': ', $line, 2);
264 $article->setHeader($header[0], $header[1]);
265 }
266
267 // retrieve body
268 while (FALSE !== ($line= $this->_readData())) $body.= $line."\n";
269 $article->setBody($body);
270
271 return $article;
272 }
273
274 /**
275 * Get a list of all articles in a newsgroup
276 *
277 * @return array articleId
278 * @throws io.IOException in case article list could not be retrieved
279 */
280 public function getArticleList() {
281 $status= $this->_sendcmd('LISTGROUP');
282 if (!NntpReply::isPositiveCompletion($status))
283 throw new IOException('Could not get article list');
284
285 while ($line= $this->_readData()) $articles[]= $line;
286
287 return $articles;
288 }
289
290 /**
291 * Retrieve body of an article
292 *
293 * @param var Id eighter a messageId or an articleId default NULL
294 * @return string body
295 * @throws io.IOException in case body could not be retrieved
296 */
297 public function getBody($id= NULL) {
298 $status= $this->_sendcmd('BODY', $id);
299 if (!NntpReply::isPositiveCompletion($status))
300 throw new IOException('Could not get article body');
301
302 // retrieve body
303 while (FALSE !== ($line= $this->_readData())) $body.= $line."\n";
304 return $body;
305 }
306
307 /**
308 * Retrieve header of an article
309 *
310 * @param var Id eighter a messageId or an articleId default NULL
311 * @return array headers
312 * @throws io.IOException in case headers could not be retrieved
313 */
314 public function getHeaders($id= NULL) {
315 $status= $this->_sendcmd('HEAD', $id);
316 if (!NntpReply::isPositiveCompletion($status))
317 throw new IOException('Could not get article headers');
318
319 // retrieve headers
320 while ($line= $this->_readData()) {
321 $header= explode(': ', $line, 2);
322 $headers[$header[0]]= $header[1];
323 }
324
325 return $headers;
326 }
327
328 /**
329 * Retrieve next article
330 *
331 * @return peer.news.Article
332 * @throws io.IOException in case article could not be retrieved
333 */
334 public function getNextArticle() {
335 $status= $this->_sendcmd('NEXT');
336 if (!NntpReply::isPositiveCompletion($status))
337 throw new IOException('Could not get next article');
338
339 return $this->getArticle(current(explode(' ', $this->getResponse())));
340 }
341
342 /**
343 * Retrieve last article
344 *
345 * @return peer.news.Article
346 * @throws io.IOException in case article could not be retrieved
347 */
348 public function getLastArticle() {
349 $status= $this->_sendcmd('LAST');
350 if (!NntpReply::isPositiveCompletion($status))
351 throw new IOException('Could not get last article');
352
353 return $this->getArticle(current(explode(' ', $this->getResponse())));
354 }
355
356 /**
357 * Get format of xover command
358 *
359 * @return array fields
360 * @throws io.IOException in case format could not be retrieved
361 */
362 public function getOverviewFormat() {
363 $status= $this->_sendcmd('LIST OVERVIEW.FMT');
364 if (!NntpReply::isPositiveCompletion($status))
365 throw new IOException('Could not get overview');
366
367 while ($line= $this->_readData()) {
368 $fields[]= current(explode(':', $line, 2));
369
370 }
371 return $fields;
372 }
373
374 /**
375 * Get a list of articles in a given range
376 *
377 * @param string range default NULL
378 * @return int[] articleId
379 */
380 public function getOverview($range= NULL) {
381 $status= $this->_sendcmd('XOVER', $range);
382 if (!NntpReply::isPositiveCompletion($status))
383 throw new IOException('Could not get overview');
384
385 while ($line= $this->_readData()) {
386 $articles[]= current(explode("\t", $line, 9));
387 }
388 return $articles;
389 }
390
391 /**
392 * Get all articles which are newer
393 * than the given date
394 *
395 * @param util.Date date
396 * @param string newsgroup
397 * @return array messageId
398 */
399 public function newNews($date, $newsgroup) {
400 $status= $this->_sendcmd(
401 'NEWNEWS',
402 $newsgroup,
403 $date->toString('ymd His')
404 );
405 if (!NntpReply::isPositiveCompletion($status))
406 throw new IOException('Could not get new articles');
407
408 while ($line= $this->_readData()) $articles[]= $line;
409
410 return $articles;
411 }
412
413 /**
414 * Get all groups which are newer
415 * than the given date
416 *
417 * @param util.Date date
418 * @return array &peer.news.Newsgroup
419 */
420 public function newGroups($date) {
421 $status= $this->_sendcmd(
422 'NEWGROUPS',
423 $date->toString('ymd His')
424 );
425 if (!NntpReply::isPositiveCompletion($status))
426 throw new IOException('Could not get new groups');
427
428 while ($line= $this->_readData()) {
429 $buf= explode(' ', $line);
430 $groups[]= new Newsgroup($buf[0], (int)$buf[1], (int)$buf[2], $buf[3]);
431 }
432
433 return $groups;
434 }
435
436 /**
437 * Return current response
438 *
439 * @return string response
440 */
441 public function getResponse() {
442 return $this->response[1];
443 }
444
445 /**
446 * Return current statuscode
447 *
448 * @return int statuscode
449 */
450 public function getStatus() {
451 return $this->response[0];
452 }
453
454 }
455?>