PageRenderTime 60ms CodeModel.GetById 33ms app.highlight 22ms RepoModel.GetById 1ms app.codeStats 0ms

/core/src/main/php/peer/news/NntpConnection.class.php

https://github.com/treuter/xp-framework
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?>