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