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