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 | |
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; |
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(); |
b4d2fbe0 |
53 | $this->posting = ($this->lastresultcode == '200'); |
7027794f |
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 | { |
56b35f9c |
82 | if (!is_utf8($this->lasterrortext)) { |
83 | return utf8_encode($this->lasterrortext); |
84 | } else { |
85 | return $this->lasterrortext; |
86 | } |
7027794f |
87 | } |
88 | |
6a684b9b |
89 | public function backtrace() |
90 | { |
91 | if ($this->debug) { |
92 | return $this->bt; |
93 | } else { |
94 | return null; |
95 | } |
96 | } |
97 | |
7027794f |
98 | # Socket functions |
99 | |
100 | /** get a line from server |
101 | * @return STRING |
102 | */ |
103 | private function getLine() |
104 | { |
168e9acb |
105 | return rtrim(@fgets($this->ns, 1200), "\r\n"); |
7027794f |
106 | } |
107 | |
108 | /** fetch data (and on delimitor) |
109 | * @param STRING $delim string indicating and of transmission |
110 | */ |
ee472ac6 |
111 | private function &fetchResult() |
7027794f |
112 | { |
113 | $array = Array(); |
114 | while (($result = $this->getLine()) != '.') { |
168e9acb |
115 | $array[] = $result; |
7027794f |
116 | } |
6a684b9b |
117 | if ($this->debug && $this->bt) { |
118 | $this->bt[count($this->bt) - 1]['response'] = count($array); |
119 | } |
7027794f |
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); |
6a684b9b |
134 | $this->bt[] = array('action' => $db_line, 'time' => microtime(true)); |
7027794f |
135 | } |
082fb25a |
136 | return @fputs($this->ns, $line, strlen($line)); |
7027794f |
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) { |
fd57a15c |
147 | $message = preg_replace("/(^|\n)\./", '\1..', $message); |
7027794f |
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(); |
7027794f |
171 | $this->lastresultcode = substr($result, 0, 3); |
172 | $this->lastresulttext = substr($result, 4); |
6a684b9b |
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 | } |
7027794f |
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); |
2840262b |
347 | if (!Banana::$boxpattern || preg_match('@' . Banana::$boxpattern . '@i', $group)) { |
7027794f |
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 | } |
ee472ac6 |
466 | $array =& $this->fetchResult(); |
7027794f |
467 | $groups = array(); |
ee472ac6 |
468 | foreach ($array as &$result) { |
7027794f |
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 | } |
ee472ac6 |
490 | $array =& $this->fetchResult(); |
7027794f |
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 | } |
ee472ac6 |
510 | $array =& $this->fetchResult(); |
7027794f |
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 | |
598a1c53 |
520 | // vim:set et sw=4 sts=4 ts=4 enc=utf-8: |
7027794f |
521 | ?> |