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