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