Fixes vim mode line.
[banana.git] / banana / nntpcore.inc.php
1 <?php
2 /********************************************************************************
3 * include/nntpcore.inc.php : NNTP subroutines
4 * -------------------------
5 *
6 * This file is part of the banana distribution
7 * Copyright: See COPYING files that comes with this distribution
8 ********************************************************************************/
9
10 require_once dirname(__FILE__) . '/banana.inc.php';
11
12 /** Class NNTPCore
13 * implements some basic functions for NNTP protocol
14 */
15 class BananaNNTPCore
16 {
17 /** socket filehandle */
18 private $ns;
19 /** posting allowed */
20 private $posting;
21 /** last NNTP error code */
22 private $lasterrorcode;
23 /** last NNTP error text */
24 private $lasterrortext;
25 /** last NNTP result code */
26 private $lastresultcode;
27 /** last NNTP result */
28 private $lastresulttext;
29
30 /** debug mode */
31 private $debug = false;
32 private $bt = array();
33
34 /** constructor
35 * @param $host STRING NNTP host
36 * @parma $port STRING NNTP port
37 * @param $timeout INTEGER socket timeout
38 * @param $reader BOOLEAN sends a "MODE READER" at connection if true
39 */
40 public function __construct($host, $port = 119, $timeout = 120, $reader = true)
41 {
42 if (Banana::$debug_nntp) {
43 $this->debug = true;
44 }
45 $this->ns = fsockopen($host, $port, $errno, $errstr, $timeout);
46 $this->lasterrorcode = $errno;
47 $this->lasterrortext = $errstr;
48 if (is_null($this->ns)) {
49 return;
50 }
51
52 $this->checkState();
53 $this->posting = ($this->lastresultcode == '200');
54 if ($reader && $this->posting) {
55 $this->execLine('MODE READER');
56 $this->posting = ($this->lastresultcode == '200');
57 }
58 if (!$this->posting) {
59 $this->quit();
60 }
61 }
62
63 public function __destruct()
64 {
65 $this->quit();
66 }
67
68 # Accessors
69
70 public function isValid()
71 {
72 return !is_null($this->ns) && $this->posting;
73 }
74
75 public function lastErrNo()
76 {
77 return $this->lasterrorcode;
78 }
79
80 public function lastError()
81 {
82 if (!is_utf8($this->lasterrortext)) {
83 return utf8_encode($this->lasterrortext);
84 } else {
85 return $this->lasterrortext;
86 }
87 }
88
89 public function backtrace()
90 {
91 if ($this->debug) {
92 return $this->bt;
93 } else {
94 return null;
95 }
96 }
97
98 # Socket functions
99
100 /** get a line from server
101 * @return STRING
102 */
103 private function &getLine()
104 {
105 $data = rtrim(@fgets($this->ns, 1200), "\r\n");
106 return $data;
107 }
108
109 /** fetch data (and on delimitor)
110 * @param STRING $delim string indicating and of transmission
111 */
112 private function &fetchResult()
113 {
114 $array = Array();
115 while (($result =& $this->getLine()) != '.') {
116 $array[] =& $result;
117 }
118 if ($this->debug && $this->bt) {
119 $trace =& $this->bt[count($this->bt) - 1];
120 $trace['response'] = count($array);
121 $trace['time'] = microtime(true) - $trace['start'];
122 }
123 return $array;
124 }
125
126 /** puts a line on server
127 * @param STRING $line line to put
128 */
129 private function putLine($line, $format = false)
130 {
131 if ($format) {
132 $line = str_replace(array("\r", "\n"), '', $line);
133 $line .= "\r\n";
134 }
135 if ($this->debug) {
136 $db_line = preg_replace('/PASS .*/', 'PASS *******', $line);
137 $this->bt[] = array('action' => $db_line, 'start' => microtime(true));
138 }
139 return @fputs($this->ns, $line, strlen($line));
140 }
141
142 /** put a message (multiline)
143 */
144 private function putMessage($message = false)
145 {
146 if (is_array($message)) {
147 $message = join("\n", $_message);
148 }
149 if ($message) {
150 $message = preg_replace("/(^|\n)\./", '\1..', $message);
151 $this->putLine("$message\r\n", false);
152 }
153 return $this->execLine('.');
154 }
155
156
157 /** exec a command a check result
158 * @param STRING $line line to exec
159 */
160 private function execLine($line, $strict_state = true)
161 {
162 if (!$this->putLine($line, true)) {
163 return null;
164 }
165 return $this->checkState($strict_state);
166 }
167
168 /** check if last command was successfull (read one line)
169 * @param BOOL $strict indicate if 1XX codes are interpreted as errors (true) or success (false)
170 */
171 private function checkState($strict = true)
172 {
173 $result = $this->getLine();
174 $this->lastresultcode = substr($result, 0, 3);
175 $this->lastresulttext = substr($result, 4);
176 if ($this->debug && $this->bt) {
177 $trace =& $this->bt[count($this->bt) - 1];
178 $trace['time'] = microtime(true) - $trace['start'];
179 $trace['code'] = $this->lastresultcode;
180 $trace['message'] = $this->lastresulttext;
181 $trace['response'] = 0;
182 }
183 $c = $this->lastresultcode{0};
184 if ($c == '2' || (($c == '1' || $c == '3') && !$strict)) {
185 return true;
186 } else {
187 $this->lasterrorcode = $this->lastresultcode;
188 $this->lasterrortext = $this->lastresulttext;
189 return false;
190 }
191 }
192
193 # strict NNTP Functions [RFC 977]
194 # see http://www.faqs.org/rfcs/rfc977.html
195
196 /** authentification
197 * @param $user STRING login
198 * @param $pass INTEGER password
199 * @return BOOLEAN true if authentication was successful
200 */
201 public function authinfo($user, $pass)
202 {
203 if ($this->execLine("AUTHINFO USER $user", false)) {
204 return $this->execline("AUTHINFO PASS $pass");
205 }
206 return false;
207 }
208
209 /** retrieves an article
210 * MSGID is a numeric ID a shown in article's headers. MSGNUM is a
211 * server-dependent ID (see X-Ref on many servers) and retriving
212 * an article by this way will change the current article pointer.
213 * If an error occur, false is returned.
214 * @param $_msgid STRING MSGID or MSGNUM of article
215 * @return ARRAY lines of the article
216 * @see body
217 * @see head
218 */
219 public function article($msgid = "")
220 {
221 if (!$this->execLine("ARTICLE $msgid")) {
222 return false;
223 }
224 return $this->fetchResult();
225 }
226
227 /** post a message
228 * if an error occur, false is returned
229 * @param $_message STRING message to post
230 * @return STRING MSGID of article
231 */
232 public function post($message)
233 {
234 if (!$this->execLine("POST ", false)) {
235 return false;
236 }
237 if (!$this->putMessage($message)) {
238 return false;
239 }
240 if (preg_match("/(<[^@>]+@[^@>]+>)/", $this->lastresulttext, $regs)) {
241 return $regs[0];
242 } else {
243 return true;
244 }
245 }
246
247 /** fetches the body of an article
248 * params are the same as article
249 * @param $_msgid STRING MSGID or MSGNUM of article
250 * @return ARRAY lines of the article
251 * @see article
252 * @see head
253 */
254 public function body($msgid = '')
255 {
256 if ($this->execLine("BODY $msgid")) {
257 return false;
258 }
259 return $this->fetchResult();
260 }
261
262 /** fetches the headers of an article
263 * params are the same as article
264 * @param $_msgid STRING MSGID or MSGNUM of article
265 * @return ARRAY lines of the article
266 * @see article
267 * @see body
268 */
269 public function head($msgid = '')
270 {
271 if (!$this->execLine("HEAD $msgid")) {
272 return false;
273 }
274 return $this->fetchResult();
275 }
276
277 /** set current group
278 * @param $_group STRING
279 * @return ARRAY array : nb of articles in group, MSGNUM of first article, MSGNUM of last article, and group name
280 */
281 public function group($group)
282 {
283 if (!$this->execLine("GROUP $group")) {
284 return false;
285 }
286 $array = explode(' ', $this->lastresulttext);
287 if (count($array) >= 4) {
288 return array_slice($array, 0, 4);
289 }
290 return false;
291 }
292
293 /** set the article pointer to the previous article in current group
294 * @return STRING MSGID of article
295 * @see next
296 */
297 public function last()
298 {
299 if (!$this->execLine("LAST ")) {
300 return false;
301 }
302 if (preg_match("/^\d+ (<[^>]+>)/", $this->lastresulttext, $regs)) {
303 return $regs[1];
304 }
305 return false;
306 }
307
308 /** set the article pointer to the next article in current group
309 * @return STRING MSGID of article
310 * @see last
311 */
312
313 public function next()
314 {
315 if (!$this->execLine('NEXT ')) {
316 return false;
317 }
318 if (preg_match("/^\d+ (<[^>]+>)/", $this->lastresulttext, $regs)) {
319 return $regs[1];
320 }
321 return false;
322 }
323
324 /** set the current article pointer
325 * @param $_msgid STRING MSGID or MSGNUM of article
326 * @return BOOLEAN true if authentication was successful, error code otherwise
327 * @see article
328 * @see body
329 */
330 public function nntpstat($msgid)
331 {
332 if (!$this->execLine("STAT $msgid")) {
333 return false;
334 }
335 if (preg_match("/^\d+ (<[^>]+>)/", $this->lastresulttext, $regs)) {
336 return $regs[1];
337 }
338 return false;
339 }
340
341 /** filter group list
342 */
343 private function filterGroups()
344 {
345 $list = $this->fetchResult();
346
347 $groups = array();
348 foreach ($list as $result) {
349 list($group, $last, $first, $p) = explode(' ', $result, 4);
350 if (!Banana::$boxpattern || preg_match('@' . Banana::$boxpattern . '@i', $group)) {
351 $groups[$group] = array(intval($last), intval($first), $p);
352 }
353 }
354 return $groups;
355 }
356
357 /** gets information about all active newsgroups
358 * @return ARRAY group name => (MSGNUM of first article, MSGNUM of last article, NNTP flags)
359 * @see newgroups
360 */
361 public function listGroups()
362 {
363 if (!$this->execLine('LIST')) {
364 return false;
365 }
366 return $this->filterGroups();
367 }
368
369 /** format date for news server
370 * @param since UNIX TIMESTAMP
371 */
372 protected function formatDate($since)
373 {
374 return gmdate("ymd His", $since) . ' GMT';
375 }
376
377 /** get information about recent newsgroups
378 * same as list, but information are limited to newgroups created after $_since
379 * @param $_since INTEGER unix timestamp
380 * @param $_distributions STRING distributions
381 * @return ARRAY same format as liste
382 * @see liste
383 */
384 public function newgroups($since, $distributions = '')
385 {
386 if (!($since = $this->formatDate($since))) {
387 return false;
388 }
389 if (!$this->execLine("NEWGROUPS $since $distributions")) {
390 return false;
391 }
392 return $this->filterGroups();
393 }
394
395 /** gets a list of new articles
396 * @param $_since INTEGER unix timestamp
397 * @parma $_groups STRING pattern of intersting groups
398 * @return ARRAY MSGID of new articles
399 */
400 public function newnews($groups = '*', $since = 0, $distributions = '')
401 {
402 if (!($since = $this->formatDate($since))) {
403 return false;
404 }
405 if (!$this->execLine("NEWNEWS $groups $since $distributions")) {
406 return false;
407 }
408 return $this->fetchResult();
409 }
410
411 /** Tell the remote server that I am not a user client, but probably another news server
412 * @return BOOLEAN true if sucessful
413 */
414 public function slave()
415 {
416 return $this->execLine("SLAVE ");
417 }
418
419 /** implements IHAVE method
420 * @param $_msgid STRING MSGID of article
421 * @param $_message STRING article
422 * @return BOOLEAN
423 */
424 public function ihave($msgid, $message = false)
425 {
426 if (!$this->execLine("IHAVE $msgid ")) {
427 return false;
428 }
429 return $this->putMessage($message);
430 }
431
432 /** closes connection to server
433 */
434 public function quit()
435 {
436 $this->execLine('QUIT');
437 fclose($this->ns);
438 $this->ns = null;
439 $this->posting = false;
440 }
441
442 # NNTP Extensions [RFC 2980]
443
444 /** Returns the date on the remote server
445 * @return INTEGER timestamp
446 */
447
448 public function date()
449 {
450 if (!$this->execLine('DATE ', false)) {
451 return false;
452 }
453 if (preg_match("/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/", $this->lastresulttext, $r)) {
454 return gmmktime($r[4], $r[5], $r[6], $r[2], $r[3], $r[1]);
455 }
456 return false;
457 }
458
459 /** returns group descriptions
460 * @param $_pattern STRING pattern of intersting groups
461 * @return ARRAY group name => description
462 */
463
464 public function xgtitle($pattern = '*')
465 {
466 if (!$this->execLine("XGTITLE $pattern ")) {
467 return false;
468 }
469 $array =& $this->fetchResult();
470 $groups = array();
471 foreach ($array as &$result) {
472 @list($group, $desc) = preg_split('/[ \t]/', $result, 2);
473 $groups[$group] = $desc;
474 }
475 return $groups;
476 }
477
478 /** obtain the header field $hdr for all the messages specified
479 * @param $_hdr STRING name of the header (eg: 'From')
480 * @param $_range STRING range of articles
481 * @return ARRAY MSGNUM => header value
482 */
483 public function xhdr($hdr, $first = null, $last = null)
484 {
485 if (is_null($first) && is_null($last)) {
486 $range = "";
487 } else {
488 $range = $first . '-' . $last;
489 }
490 if (!$this->execLine("XHDR $hdr $range ")) {
491 return false;
492 }
493 $array =& $this->fetchResult();
494 $headers = array();
495 foreach ($array as &$result) {
496 @list($head, $value) = explode(' ', $result, 2);
497 $headers[$head] = $value;
498 }
499 return $headers;
500 }
501
502 /** obtain the header field $_hdr matching $_pat for all the messages specified
503 * @param $_hdr STRING name of the header (eg: 'From')
504 * @param $_range STRING range of articles
505 * @param $_pat STRING pattern
506 * @return ARRAY MSGNUM => header value
507 */
508 public function xpat($_hdr, $_range, $_pat)
509 {
510 if (!$this->execLine("XPAT $hdr $range $pat")) {
511 return false;
512 }
513 $array =& $this->fetchResult();
514 $headers = array();
515 foreach ($array as &$result) {
516 list($head, $value) = explode(' ', $result, 2);
517 $headers[$head] = $result;
518 }
519 return $headers;
520 }
521 }
522
523 // vim:set et sw=4 sts=4 ts=4 fenc=utf-8:
524 ?>