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