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