PageRenderTime 149ms CodeModel.GetById 31ms RepoModel.GetById 1ms app.codeStats 0ms

/core/src/main/php/peer/news/NntpConnection.class.php

https://github.com/treuter/xp-framework
PHP | 455 lines | 225 code | 59 blank | 171 comment | 42 complexity | cd7ba0e17b480116d8159a77d3965a75 MD5 | raw file
  1. <?php
  2. /* This class is part of the XP framework
  3. *
  4. * $Id$
  5. */
  6. uses(
  7. 'peer.Socket',
  8. 'peer.ProtocolException',
  9. 'peer.URL',
  10. 'peer.news.NntpReply',
  11. 'peer.news.Newsgroup',
  12. 'peer.news.Article',
  13. 'util.Date',
  14. 'util.log.Traceable'
  15. );
  16. /**
  17. * NNTP Connection
  18. *
  19. * Usage [retrieve newsgroup listing]:
  20. * <code>
  21. * $c= new NntpConnection('nntp://news.xp-framework.net');
  22. * try {
  23. * $c->connect();
  24. * $groups= $c->getGroups();
  25. * $c->close();
  26. * } catch(IOException $e) {
  27. * $e->printStackTrace();
  28. * exit();
  29. * }
  30. *
  31. * foreach ($groups as $group) {
  32. * var_dump($group->getName());
  33. * }
  34. * </code>
  35. * @see rfc://977
  36. * @purpose News protocol implementation
  37. */
  38. class NntpConnection extends Object implements Traceable {
  39. public
  40. $url = NULL,
  41. $cat = NULL,
  42. $response = array();
  43. /**
  44. * Constructor
  45. *
  46. * @param peer.URL url
  47. */
  48. public function __construct($url) {
  49. $this->url= $url;
  50. $this->_sock= new Socket(
  51. $this->url->getHost(),
  52. $this->url->getPort(119)
  53. );
  54. }
  55. /**
  56. * Set a trace for debugging
  57. *
  58. * @param util.log.LogCategory cat
  59. */
  60. public function setTrace($cat) {
  61. $this->cat= $cat;
  62. }
  63. /**
  64. * Wrapper that sends a command to the remote host.
  65. *
  66. * @param string format
  67. * @param var* args
  68. * @return bool success
  69. * @throws peer.ProtocolException in case the command is too long
  70. */
  71. protected function _sendcmd() {
  72. if (!$this->_sock->isConnected()) return FALSE;
  73. $a= func_get_args();
  74. $cmd= implode(' ', $a);
  75. // NNTP/RFC977 only allows command up to 512 (-2) chars.
  76. if (strlen($cmd) > 510) {
  77. throw new ProtocolException('Command too long! Max. 510 chars');
  78. }
  79. $this->cat && $this->cat->debug('>>>', $cmd);
  80. try {
  81. $this->_sock->write($cmd."\r\n");
  82. } catch (SocketException $e) {
  83. return FALSE;
  84. }
  85. // read first line and return
  86. // nntp statuscode
  87. return $this->_readResponse();
  88. }
  89. /**
  90. * Get status response
  91. *
  92. * @return string status
  93. */
  94. protected function _readResponse() {
  95. if (!($line= $this->_sock->readLine())) return FALSE;
  96. $this->cat && $this->cat->debug('<<<', $line);
  97. $this->response= array(
  98. (int) substr($line, 0, 3),
  99. (string) rtrim(substr($line, 4))
  100. );
  101. return $this->response[0];
  102. }
  103. /**
  104. * Get data
  105. *
  106. * @return string status
  107. */
  108. protected function _readData() {
  109. if ($this->_sock->eof()) return FALSE;
  110. $line= $this->_sock->readLine();
  111. $this->cat && $this->cat->debug('<<<', $line);
  112. if ('.' == $line) return FALSE;
  113. return $line;
  114. }
  115. /**
  116. * Connect
  117. *
  118. * @param float timeout default 2.0
  119. * @return bool success
  120. * @throws peer.ConnectException in case there's an error during connecting
  121. */
  122. public function connect($auth= FALSE) {
  123. $this->_sock->connect();
  124. // Read banner message
  125. if (!($response= $this->_readResponse()))
  126. throw new ConnectException('No valid response from server');
  127. $this->cat && $this->cat->debug('<<<', $this->getResponse());
  128. if ($auth) return $this->authenticate();
  129. return TRUE;
  130. }
  131. /**
  132. * Disconnect
  133. *
  134. * @return bool success
  135. * @throws io.IOException in case there's an error during disconnecting
  136. */
  137. public function close() {
  138. if (!$this->_sock->isConnected()) return TRUE;
  139. $status= $this->_sendcmd('QUIT');
  140. if (!NntpReply::isPositiveCompletion($status)) {
  141. throw new IOException('Error during disconnect');
  142. }
  143. $this->_sock->close();
  144. return TRUE;
  145. }
  146. /**
  147. * Authenticate
  148. *
  149. * @param string authmode
  150. * @return bool success
  151. * @throws peer.AuthenticationException in case authentication failed
  152. */
  153. public function authenticate() {
  154. $status= $this->_sendcmd('AUTHINFO user', $this->url->getUser());
  155. // Send password if requested
  156. if (NNTP_AUTH_NEEDMODE === $status) {
  157. $status= $this->_sendcmd('AUTHINFO pass', $this->url->getPassword());
  158. }
  159. switch ($status) {
  160. case NNTP_AUTH_ACCEPT: {
  161. return TRUE;
  162. break;
  163. }
  164. case NNTP_AUTH_NEEDMODE: {
  165. throw new AuthenticatorException('Authentication uncomplete');
  166. break;
  167. }
  168. case NNTP_AUTH_REJECTED: {
  169. throw new AuthenticatorException('Authentication rejected');
  170. break;
  171. }
  172. case NNTP_NOPERM: {
  173. throw new AuthenticatorException('No permission');
  174. break;
  175. }
  176. default: {
  177. throw new AuthenticatorException('Unexpected authentication error');
  178. }
  179. }
  180. }
  181. /**
  182. * Select a group
  183. *
  184. * @param string groupname
  185. * @return success
  186. */
  187. public function setGroup($group) {
  188. $status= $this->_sendcmd('GROUP', $group);
  189. if (!NntpReply::isPositiveCompletion($status))
  190. throw (new IOException('Could not set group'));
  191. return TRUE;
  192. }
  193. /**
  194. * Get groups
  195. *
  196. * @return peer.news.Newsgroup[]
  197. */
  198. public function getGroups() {
  199. $status= $this->_sendcmd('LIST');
  200. if (!NntpReply::isPositiveCompletion($status))
  201. throw new IOException('Could not get groups');
  202. while ($line= $this->_readData()) {
  203. $buf= explode(' ', $line);
  204. $groups[]= new Newsgroup($buf[0], (int)$buf[1], (int)$buf[2], $buf[3]);
  205. }
  206. return $groups;
  207. }
  208. /**
  209. * Get Article
  210. *
  211. * @param var Id eighter a messageId or an articleId
  212. * @return peer.news.Article
  213. * @throws io.IOException in case article could not be retrieved
  214. */
  215. public function getArticle($id= NULL) {
  216. $status= $this->_sendcmd('ARTICLE', $id);
  217. if (!NntpReply::isPositiveCompletion($status))
  218. throw new IOException('Could not get article');
  219. with($args= explode(' ', $this->getResponse())); {
  220. $article= new Article($args[0], $args[1]);
  221. }
  222. // retrieve headers
  223. while ($line= $this->_readData()) {
  224. if ("\t" == $line{0} || ' ' == $line{0}) {
  225. $article->setHeader(
  226. $header[0],
  227. $article->getHeader($header[0])."\n".$line
  228. );
  229. continue;
  230. }
  231. $header= explode(': ', $line, 2);
  232. $article->setHeader($header[0], $header[1]);
  233. }
  234. // retrieve body
  235. while (FALSE !== ($line= $this->_readData())) $body.= $line."\n";
  236. $article->setBody($body);
  237. return $article;
  238. }
  239. /**
  240. * Get a list of all articles in a newsgroup
  241. *
  242. * @return array articleId
  243. * @throws io.IOException in case article list could not be retrieved
  244. */
  245. public function getArticleList() {
  246. $status= $this->_sendcmd('LISTGROUP');
  247. if (!NntpReply::isPositiveCompletion($status))
  248. throw new IOException('Could not get article list');
  249. while ($line= $this->_readData()) $articles[]= $line;
  250. return $articles;
  251. }
  252. /**
  253. * Retrieve body of an article
  254. *
  255. * @param var Id eighter a messageId or an articleId default NULL
  256. * @return string body
  257. * @throws io.IOException in case body could not be retrieved
  258. */
  259. public function getBody($id= NULL) {
  260. $status= $this->_sendcmd('BODY', $id);
  261. if (!NntpReply::isPositiveCompletion($status))
  262. throw new IOException('Could not get article body');
  263. // retrieve body
  264. while (FALSE !== ($line= $this->_readData())) $body.= $line."\n";
  265. return $body;
  266. }
  267. /**
  268. * Retrieve header of an article
  269. *
  270. * @param var Id eighter a messageId or an articleId default NULL
  271. * @return array headers
  272. * @throws io.IOException in case headers could not be retrieved
  273. */
  274. public function getHeaders($id= NULL) {
  275. $status= $this->_sendcmd('HEAD', $id);
  276. if (!NntpReply::isPositiveCompletion($status))
  277. throw new IOException('Could not get article headers');
  278. // retrieve headers
  279. while ($line= $this->_readData()) {
  280. $header= explode(': ', $line, 2);
  281. $headers[$header[0]]= $header[1];
  282. }
  283. return $headers;
  284. }
  285. /**
  286. * Retrieve next article
  287. *
  288. * @return peer.news.Article
  289. * @throws io.IOException in case article could not be retrieved
  290. */
  291. public function getNextArticle() {
  292. $status= $this->_sendcmd('NEXT');
  293. if (!NntpReply::isPositiveCompletion($status))
  294. throw new IOException('Could not get next article');
  295. return $this->getArticle(current(explode(' ', $this->getResponse())));
  296. }
  297. /**
  298. * Retrieve last article
  299. *
  300. * @return peer.news.Article
  301. * @throws io.IOException in case article could not be retrieved
  302. */
  303. public function getLastArticle() {
  304. $status= $this->_sendcmd('LAST');
  305. if (!NntpReply::isPositiveCompletion($status))
  306. throw new IOException('Could not get last article');
  307. return $this->getArticle(current(explode(' ', $this->getResponse())));
  308. }
  309. /**
  310. * Get format of xover command
  311. *
  312. * @return array fields
  313. * @throws io.IOException in case format could not be retrieved
  314. */
  315. public function getOverviewFormat() {
  316. $status= $this->_sendcmd('LIST OVERVIEW.FMT');
  317. if (!NntpReply::isPositiveCompletion($status))
  318. throw new IOException('Could not get overview');
  319. while ($line= $this->_readData()) {
  320. $fields[]= current(explode(':', $line, 2));
  321. }
  322. return $fields;
  323. }
  324. /**
  325. * Get a list of articles in a given range
  326. *
  327. * @param string range default NULL
  328. * @return int[] articleId
  329. */
  330. public function getOverview($range= NULL) {
  331. $status= $this->_sendcmd('XOVER', $range);
  332. if (!NntpReply::isPositiveCompletion($status))
  333. throw new IOException('Could not get overview');
  334. while ($line= $this->_readData()) {
  335. $articles[]= current(explode("\t", $line, 9));
  336. }
  337. return $articles;
  338. }
  339. /**
  340. * Get all articles which are newer
  341. * than the given date
  342. *
  343. * @param util.Date date
  344. * @param string newsgroup
  345. * @return array messageId
  346. */
  347. public function newNews($date, $newsgroup) {
  348. $status= $this->_sendcmd(
  349. 'NEWNEWS',
  350. $newsgroup,
  351. $date->toString('ymd His')
  352. );
  353. if (!NntpReply::isPositiveCompletion($status))
  354. throw new IOException('Could not get new articles');
  355. while ($line= $this->_readData()) $articles[]= $line;
  356. return $articles;
  357. }
  358. /**
  359. * Get all groups which are newer
  360. * than the given date
  361. *
  362. * @param util.Date date
  363. * @return array &peer.news.Newsgroup
  364. */
  365. public function newGroups($date) {
  366. $status= $this->_sendcmd(
  367. 'NEWGROUPS',
  368. $date->toString('ymd His')
  369. );
  370. if (!NntpReply::isPositiveCompletion($status))
  371. throw new IOException('Could not get new groups');
  372. while ($line= $this->_readData()) {
  373. $buf= explode(' ', $line);
  374. $groups[]= new Newsgroup($buf[0], (int)$buf[1], (int)$buf[2], $buf[3]);
  375. }
  376. return $groups;
  377. }
  378. /**
  379. * Return current response
  380. *
  381. * @return string response
  382. */
  383. public function getResponse() {
  384. return $this->response[1];
  385. }
  386. /**
  387. * Return current statuscode
  388. *
  389. * @return int statuscode
  390. */
  391. public function getStatus() {
  392. return $this->response[0];
  393. }
  394. }
  395. ?>