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