ab02e8a9 |
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) { |
d759a2ba |
142 | $message = preg_replace("/(^|\n)\./", '\1..', $message); |
ab02e8a9 |
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); |
7972645b |
338 | if (!is_null(Banana::$boxpattern) || preg_match('@' . Banana::$boxpattern . '@i', $group)) { |
ab02e8a9 |
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 | |
d8d416c4 |
511 | // vim:set et sw=4 sts=4 ts=4 enc=utf-8: |
ab02e8a9 |
512 | ?> |