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(); |
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 | { |
082fb25a |
101 | return rtrim(@fgets($this->ns, 1200)); |
7027794f |
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 | } |
6a684b9b |
125 | if ($this->debug && $this->bt) { |
126 | $this->bt[count($this->bt) - 1]['response'] = count($array); |
127 | } |
7027794f |
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); |
6a684b9b |
142 | $this->bt[] = array('action' => $db_line, 'time' => microtime(true)); |
7027794f |
143 | } |
082fb25a |
144 | return @fputs($this->ns, $line, strlen($line)); |
7027794f |
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) { |
fd57a15c |
155 | $message = preg_replace("/(^|\n)\./", '\1..', $message); |
7027794f |
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(); |
7027794f |
179 | $this->lastresultcode = substr($result, 0, 3); |
180 | $this->lastresulttext = substr($result, 4); |
6a684b9b |
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 | } |
7027794f |
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); |
2840262b |
355 | if (!Banana::$boxpattern || preg_match('@' . Banana::$boxpattern . '@i', $group)) { |
7027794f |
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 | |
598a1c53 |
528 | // vim:set et sw=4 sts=4 ts=4 enc=utf-8: |
7027794f |
529 | ?> |