/lib/deathbycaptcha/deathbycaptcha.php

https://github.com/AurelienLavorel/serposcope · PHP · 820 lines · 559 code · 67 blank · 194 comment · 47 complexity · b16405e9c28d2a39003f680a3bfb8ba1 MD5 · raw file

  1. <?php
  2. /**
  3. * @package DBCAPI
  4. * @author Sergey Kolchin <ksa242@gmail.com>
  5. */
  6. /**
  7. * Base class for DBC related exceptions.
  8. *
  9. * @package DBCAPI
  10. * @subpackage PHP
  11. */
  12. abstract class DeathByCaptcha_Exception extends Exception
  13. {}
  14. /**
  15. * Exception to throw on environment or runtime related failures.
  16. *
  17. * @package DBCAPI
  18. * @subpackage PHP
  19. */
  20. class DeathByCaptcha_RuntimeException extends DeathByCaptcha_Exception
  21. {}
  22. /**
  23. * Exception to throw on network or disk IO failures.
  24. *
  25. * @package DBCAPI
  26. * @subpackage PHP
  27. */
  28. class DeathByCaptcha_IOException extends DeathByCaptcha_Exception
  29. {}
  30. /**
  31. * Generic exception to throw on API client errors.
  32. *
  33. * @package DBCAPI
  34. * @subpackage PHP
  35. */
  36. class DeathByCaptcha_ClientException extends DeathByCaptcha_Exception
  37. {}
  38. /**
  39. * Exception to throw on rejected login attemts due to invalid DBC credentials, low balance, or when account being banned.
  40. *
  41. * @package DBCAPI
  42. * @subpackage PHP
  43. */
  44. class DeathByCaptcha_AccessDeniedException extends DeathByCaptcha_ClientException
  45. {}
  46. /**
  47. * Exception to throw on invalid CAPTCHA image payload: on empty images, on images too big, on non-image payloads.
  48. *
  49. * @package DBCAPI
  50. * @subpackage PHP
  51. */
  52. class DeathByCaptcha_InvalidCaptchaException extends DeathByCaptcha_ClientException
  53. {}
  54. /**
  55. * Generic exception to throw on API server errors.
  56. *
  57. * @package DBCAPI
  58. * @subpackage PHP
  59. */
  60. class DeathByCaptcha_ServerException extends DeathByCaptcha_Exception
  61. {}
  62. /**
  63. * Exception to throw when service is overloaded.
  64. *
  65. * @package DBCAPI
  66. * @subpackage PHP
  67. */
  68. class DeathByCaptcha_ServiceOverloadException extends DeathByCaptcha_ServerException
  69. {}
  70. /**
  71. * Base Death by Captcha API client
  72. *
  73. * @property-read array|null $user User's details
  74. * @property-read float|null $balance User's balance (in US cents)
  75. *
  76. * @package DBCAPI
  77. * @subpackage PHP
  78. */
  79. abstract class DeathByCaptcha_Client
  80. {
  81. const API_VERSION = 'DBC/PHP v4.1.1';
  82. const DEFAULT_TIMEOUT = 60;
  83. const POLLS_INTERVAL = 5;
  84. /**
  85. * DBC account credentials
  86. *
  87. * @var array
  88. */
  89. protected $_userpwd = array();
  90. /**
  91. * Verbosity flag.
  92. * When it's set to true, the client will produce debug output on every API call.
  93. *
  94. * @var bool
  95. */
  96. public $is_verbose = false;
  97. /**
  98. * Parses URL query encoded responses
  99. *
  100. * @param string $s
  101. * @return array
  102. */
  103. static public function parse_plain_response($s)
  104. {
  105. parse_str($s, $a);
  106. return $a;
  107. }
  108. /**
  109. * Parses JSON encoded response
  110. *
  111. * @param string $s
  112. * @return array
  113. */
  114. static public function parse_json_response($s)
  115. {
  116. return json_decode(rtrim($s), true);
  117. }
  118. /**
  119. * Checks if CAPTCHA is valid (not empty)
  120. *
  121. * @param string $img Raw CAPTCHA image
  122. * @throws DeathByCaptcha_InvalidCaptchaException On invalid CAPTCHA images
  123. */
  124. protected function _is_valid_captcha($img)
  125. {
  126. if (0 == strlen($img)) {
  127. throw new DeathByCaptcha_InvalidCaptchaException(
  128. 'CAPTCHA image file is empty'
  129. );
  130. } else {
  131. return true;
  132. }
  133. }
  134. protected function _load_captcha($captcha)
  135. {
  136. if (is_resource($captcha)) {
  137. $img = '';
  138. rewind($captcha);
  139. while ($s = fread($captcha, 8192)) {
  140. $img .= $s;
  141. }
  142. return $img;
  143. } else if (is_array($captcha)) {
  144. return implode('', array_map('chr', $captcha));
  145. } else if ('base64:' == substr($captcha, 0, 7)) {
  146. return base64_decode(substr($captcha, 7));
  147. } else {
  148. return file_get_contents($captcha);
  149. }
  150. }
  151. /**
  152. * Closes opened connection (if any), as gracefully as possible
  153. *
  154. * @return DeathByCaptcha_Client
  155. */
  156. abstract public function close();
  157. /**
  158. * Returns user details
  159. *
  160. * @return array|null
  161. */
  162. abstract public function get_user();
  163. /**
  164. * Returns user's balance (in US cents)
  165. *
  166. * @uses DeathByCaptcha_Client::get_user()
  167. * @return float|null
  168. */
  169. public function get_balance()
  170. {
  171. return ($user = $this->get_user()) ? $user['balance'] : null;
  172. }
  173. /**
  174. * Returns CAPTCHA details
  175. *
  176. * @param int $cid CAPTCHA ID
  177. * @return array|null
  178. */
  179. abstract public function get_captcha($cid);
  180. /**
  181. * Returns CAPTCHA text
  182. *
  183. * @uses DeathByCaptcha_Client::get_captcha()
  184. * @param int $cid CAPTCHA ID
  185. * @return string|null
  186. */
  187. public function get_text($cid)
  188. {
  189. return ($captcha = $this->get_captcha($cid)) ? $captcha['text'] : null;
  190. }
  191. /**
  192. * Reports an incorrectly solved CAPTCHA
  193. *
  194. * @param int $cid CAPTCHA ID
  195. * @return bool
  196. */
  197. abstract public function report($cid);
  198. /**
  199. * Uploads a CAPTCHA
  200. *
  201. * @param string|array|resource $captcha CAPTCHA image file name, vector of bytes, or file handle
  202. * @return array|null Uploaded CAPTCHA details on success
  203. * @throws DeathByCaptcha_InvalidCaptchaException On invalid CAPTCHA file
  204. */
  205. abstract public function upload($captcha);
  206. /**
  207. * Tries to solve CAPTCHA by uploading it and polling for its status/text
  208. * with arbitrary timeout. See {@link DeathByCaptcha_Client::upload()} for
  209. * $captcha param details.
  210. *
  211. * @uses DeathByCaptcha_Client::upload()
  212. * @uses DeathByCaptcha_Client::get_captcha()
  213. * @param int $timeout Optional solving timeout (in seconds)
  214. * @return array|null CAPTCHA details hash on success
  215. */
  216. public function decode($captcha, $timeout=self::DEFAULT_TIMEOUT)
  217. {
  218. $deadline = time() + (0 < $timeout ? $timeout : self::DEFAULT_TIMEOUT);
  219. if ($c = $this->upload($captcha)) {
  220. while ($deadline > time() && $c && !$c['text']) {
  221. sleep(self::POLLS_INTERVAL);
  222. $c = $this->get_captcha($c['captcha']);
  223. }
  224. if ($c && $c['text'] && $c['is_correct']) {
  225. return $c;
  226. }
  227. }
  228. return null;
  229. }
  230. /**
  231. * @param string $username DBC account username
  232. * @param string $password DBC account password
  233. * @throws DeathByCaptcha_RuntimeException On missing/empty DBC account credentials
  234. * @throws DeathByCaptcha_RuntimeException When required extensions/functions not found
  235. */
  236. public function __construct($username, $password)
  237. {
  238. foreach (array('username', 'password') as $k) {
  239. if (!$$k) {
  240. throw new DeathByCaptcha_RuntimeException(
  241. "Account {$k} is missing or empty"
  242. );
  243. }
  244. }
  245. $this->_userpwd = array($username, $password);
  246. }
  247. /**
  248. * @ignore
  249. */
  250. public function __destruct()
  251. {
  252. $this->close();
  253. }
  254. /**
  255. * @ignore
  256. */
  257. public function __get($key)
  258. {
  259. switch ($key) {
  260. case 'user':
  261. return $this->get_user();
  262. case 'balance':
  263. return $this->get_balance();
  264. }
  265. }
  266. }
  267. /**
  268. * Death by Captcha HTTP API Client
  269. *
  270. * @see DeathByCaptcha_Client
  271. * @package DBCAPI
  272. * @subpackage PHP
  273. */
  274. class DeathByCaptcha_HttpClient extends DeathByCaptcha_Client
  275. {
  276. const BASE_URL = 'http://api.dbcapi.me/api';
  277. protected $_conn = null;
  278. protected $_response_type = '';
  279. protected $_response_parser = null;
  280. /**
  281. * Sets up CURL connection
  282. */
  283. protected function _connect()
  284. {
  285. if (!is_resource($this->_conn)) {
  286. if ($this->is_verbose) {
  287. fputs(STDERR, time() . " CONN\n");
  288. }
  289. if (!($this->_conn = curl_init())) {
  290. throw new DeathByCaptcha_RuntimeException(
  291. 'Failed initializing a CURL connection'
  292. );
  293. }
  294. curl_setopt_array($this->_conn, array(
  295. CURLOPT_TIMEOUT => self::DEFAULT_TIMEOUT,
  296. CURLOPT_CONNECTTIMEOUT => (int)(self::DEFAULT_TIMEOUT / 4),
  297. CURLOPT_HEADER => false,
  298. CURLOPT_RETURNTRANSFER => true,
  299. CURLOPT_FOLLOWLOCATION => true,
  300. CURLOPT_AUTOREFERER => false,
  301. CURLOPT_HTTPHEADER => array(
  302. 'Accept: ' . $this->_response_type,
  303. 'Expect: ',
  304. 'User-Agent: ' . self::API_VERSION
  305. )
  306. ));
  307. }
  308. return $this;
  309. }
  310. /**
  311. * Makes an API call
  312. *
  313. * @param string $cmd API command
  314. * @param array $payload API call payload, essentially HTTP POST fields
  315. * @return array|null API response hash table on success
  316. * @throws DeathByCaptcha_IOException On network related errors
  317. * @throws DeathByCaptcha_AccessDeniedException On failed login attempt
  318. * @throws DeathByCaptcha_InvalidCaptchaException On invalid CAPTCHAs rejected by the service
  319. * @throws DeathByCaptcha_ServerException On API server errors
  320. */
  321. protected function _call($cmd, $payload=null)
  322. {
  323. if (null !== $payload) {
  324. $payload = array_merge($payload, array(
  325. 'username' => $this->_userpwd[0],
  326. 'password' => $this->_userpwd[1],
  327. ));
  328. }
  329. $this->_connect();
  330. $opts = array(CURLOPT_URL => self::BASE_URL . '/' . trim($cmd, '/'),
  331. CURLOPT_REFERER => '');
  332. if (null !== $payload) {
  333. $opts[CURLOPT_POST] = true;
  334. $opts[CURLOPT_POSTFIELDS] = array_key_exists('captchafile', $payload)
  335. ? $payload
  336. : http_build_query($payload);
  337. } else {
  338. $opts[CURLOPT_HTTPGET] = true;
  339. }
  340. curl_setopt_array($this->_conn, $opts);
  341. if ($this->is_verbose) {
  342. fputs(STDERR, time() . " SEND: {$cmd} " . serialize($payload) . "\n");
  343. }
  344. $response = curl_exec($this->_conn);
  345. if (0 < ($err = curl_errno($this->_conn))) {
  346. throw new DeathByCaptcha_IOException(
  347. "API connection failed: [{$err}] " . curl_error($this->_conn)
  348. );
  349. }
  350. if ($this->is_verbose) {
  351. fputs(STDERR, time() . " RECV: {$response}\n");
  352. }
  353. $status_code = curl_getinfo($this->_conn, CURLINFO_HTTP_CODE);
  354. if (403 == $status_code) {
  355. throw new DeathByCaptcha_AccessDeniedException(
  356. 'Access denied, check your credentials and/or balance'
  357. );
  358. } else if (400 == $status_code || 413 == $status_code) {
  359. throw new DeathByCaptcha_InvalidCaptchaException(
  360. "CAPTCHA was rejected by the service, check if it's a valid image"
  361. );
  362. } else if (503 == $status_code) {
  363. throw new DeathByCaptcha_ServiceOverloadException(
  364. "CAPTCHA was rejected due to service overload, try again later"
  365. );
  366. } else if (!($response = call_user_func($this->_response_parser, $response))) {
  367. throw new DeathByCaptcha_ServerException(
  368. 'Invalid API response'
  369. );
  370. } else {
  371. return $response;
  372. }
  373. }
  374. /**
  375. * @see DeathByCaptcha_Client::__construct()
  376. */
  377. public function __construct($username, $password)
  378. {
  379. if (!extension_loaded('curl')) {
  380. throw new DeathByCaptcha_RuntimeException(
  381. 'CURL extension not found'
  382. );
  383. }
  384. if (function_exists('json_decode')) {
  385. $this->_response_type = 'application/json';
  386. $this->_response_parser = array($this, 'parse_json_response');
  387. } else {
  388. $this->_response_type = 'text/plain';
  389. $this->_response_parser = array($this, 'parse_plain_response');
  390. }
  391. parent::__construct($username, $password);
  392. }
  393. /**
  394. * @see DeathByCaptcha_Client::close()
  395. */
  396. public function close()
  397. {
  398. if (is_resource($this->_conn)) {
  399. if ($this->is_verbose) {
  400. fputs(STDERR, time() . " CLOSE\n");
  401. }
  402. curl_close($this->_conn);
  403. $this->_conn = null;
  404. }
  405. return $this;
  406. }
  407. /**
  408. * @see DeathByCaptcha_Client::get_user()
  409. */
  410. public function get_user()
  411. {
  412. $user = $this->_call('user', array());
  413. return (0 < ($id = (int)@$user['user']))
  414. ? array('user' => $id,
  415. 'balance' => (float)@$user['balance'],
  416. 'is_banned' => (bool)@$user['is_banned'])
  417. : null;
  418. }
  419. /**
  420. * @see DeathByCaptcha_Client::upload()
  421. * @throws DeathByCaptcha_RuntimeException When failed to save CAPTCHA image to a temporary file
  422. */
  423. public function upload($captcha)
  424. {
  425. $img = $this->_load_captcha($captcha);
  426. if ($this->_is_valid_captcha($img)) {
  427. $tmp_fn = tempnam(null, 'captcha');
  428. file_put_contents($tmp_fn, $img);
  429. try {
  430. $captcha = $this->_call('captcha', array(
  431. 'captchafile' => '@'. $tmp_fn,
  432. ));
  433. } catch (Exception $e) {
  434. @unlink($tmp_fn);
  435. throw $e;
  436. }
  437. @unlink($tmp_fn);
  438. if (0 < ($cid = (int)@$captcha['captcha'])) {
  439. return array(
  440. 'captcha' => $cid,
  441. 'text' => (!empty($captcha['text']) ? $captcha['text'] : null),
  442. 'is_correct' => (bool)@$captcha['is_correct'],
  443. );
  444. }
  445. }
  446. return null;
  447. }
  448. /**
  449. * @see DeathByCaptcha_Client::get_captcha()
  450. */
  451. public function get_captcha($cid)
  452. {
  453. $captcha = $this->_call('captcha/' . (int)$cid);
  454. return (0 < ($cid = (int)@$captcha['captcha']))
  455. ? array('captcha' => $cid,
  456. 'text' => (!empty($captcha['text']) ? $captcha['text'] : null),
  457. 'is_correct' => (bool)$captcha['is_correct'])
  458. : null;
  459. }
  460. /**
  461. * @see DeathByCaptcha_Client::report()
  462. */
  463. public function report($cid)
  464. {
  465. $captcha = $this->_call('captcha/' . (int)$cid . '/report', array());
  466. return !(bool)@$captcha['is_correct'];
  467. }
  468. }
  469. /**
  470. * Death by Captcha socket API Client
  471. *
  472. * @see DeathByCaptcha_Client
  473. * @package DBCAPI
  474. * @subpackage PHP
  475. */
  476. class DeathByCaptcha_SocketClient extends DeathByCaptcha_Client
  477. {
  478. const HOST = 'api.dbcapi.me';
  479. const FIRST_PORT = 8123;
  480. const LAST_PORT = 8130;
  481. const TERMINATOR = "\r\n";
  482. protected $_sock = null;
  483. /**
  484. * Opens a socket connection to the API server
  485. *
  486. * @throws DeathByCaptcha_IOException When API connection fails
  487. * @throws DeathByCaptcha_RuntimeException When socket operations fail
  488. */
  489. protected function _connect()
  490. {
  491. if (null === $this->_sock) {
  492. if ($this->is_verbose) {
  493. fputs(STDERR, time() . " CONN\n");
  494. }
  495. $errno = 0;
  496. $error = '';
  497. $port = rand(self::FIRST_PORT, self::LAST_PORT);
  498. $sock = null;
  499. if (!($sock = @fsockopen(self::HOST, $port, $errno, $error, self::DEFAULT_TIMEOUT))) {
  500. throw new DeathByCaptcha_IOException(
  501. 'Failed connecting to ' . self::HOST . ":{$port}: fsockopen(): [{$errno}] {$error}"
  502. );
  503. } else if (!@stream_set_timeout($sock, self::DEFAULT_TIMEOUT / 4)) {
  504. fclose($sock);
  505. throw new DeathByCaptcha_IOException(
  506. 'Failed setting socket timeout'
  507. );
  508. } else {
  509. $this->_sock = $sock;
  510. }
  511. }
  512. return $this;
  513. }
  514. /**
  515. * Socket send()/recv() wrapper
  516. *
  517. * @param string $buf Raw API request to send
  518. * @return string Raw API response on success
  519. * @throws DeathByCaptcha_IOException On network failures
  520. */
  521. protected function _sendrecv($buf)
  522. {
  523. if ($this->is_verbose) {
  524. fputs(STDERR, time() . ' SEND: ' . strlen($buf) . ' ' . rtrim($buf) . "\n");
  525. }
  526. $buf .= self::TERMINATOR;
  527. $response = '';
  528. while (true) {
  529. if ($buf) {
  530. if (!($n = fwrite($this->_sock, $buf))) {
  531. throw new DeathByCaptcha_IOException(
  532. 'Connection lost while sending API request'
  533. );
  534. } else {
  535. $buf = substr($buf, $n);
  536. }
  537. }
  538. if (!$buf) {
  539. if (!($s = fread($this->_sock, 4096))) {
  540. throw new DeathByCaptcha_IOException(
  541. 'Connection lost while receiving API response'
  542. );
  543. } else {
  544. $response .= $s;
  545. if (self::TERMINATOR == substr($s, strlen($s) - 2)) {
  546. $response = rtrim($response, self::TERMINATOR);
  547. if ($this->is_verbose) {
  548. fputs(STDERR, time() . ' RECV: ' . strlen($response) . " {$response}\n");
  549. }
  550. return $response;
  551. }
  552. }
  553. }
  554. }
  555. throw new DeathByCaptcha_IOException('API request timed out');
  556. }
  557. /**
  558. * Makes an API call
  559. *
  560. * @param string $cmd API command to call
  561. * @param array $payload API request payload
  562. * @return array|null API response hash map on success
  563. * @throws DeathByCaptcha_IOException On network errors
  564. * @throws DeathByCaptcha_AccessDeniedException On failed login attempt
  565. * @throws DeathByCaptcha_InvalidCaptchaException On invalid CAPTCHAs rejected by the service
  566. * @throws DeathByCaptcha_ServerException On API server errors
  567. */
  568. protected function _call($cmd, $payload=null)
  569. {
  570. if (null === $payload) {
  571. $payload = array();
  572. }
  573. $payload = array_merge($payload, array(
  574. 'cmd' => $cmd,
  575. 'version' => self::API_VERSION,
  576. ));
  577. $payload = json_encode($payload);
  578. $response = null;
  579. for ($attempt = 2; 0 < $attempt && null === $response; $attempt--) {
  580. if (null === $this->_sock && 'login' != $cmd) {
  581. $this->_call('login', array(
  582. 'username' => $this->_userpwd[0],
  583. 'password' => $this->_userpwd[1],
  584. ));
  585. }
  586. $this->_connect();
  587. try {
  588. $response = $this->_sendrecv($payload);
  589. } catch (DeathByCaptcha_Exception $e) {
  590. $this->close();
  591. }
  592. }
  593. try {
  594. if (null === $response) {
  595. throw new DeathByCaptcha_IOException(
  596. 'API connection lost or timed out'
  597. );
  598. } else if (!($response = $this->parse_json_response($response))) {
  599. throw new DeathByCaptcha_ServerException(
  600. 'Invalid API response'
  601. );
  602. }
  603. if (!empty($response['error'])) {
  604. switch ($response['error']) {
  605. case 'not-logged-in':
  606. throw new DeathByCaptcha_AccessDeniedException(
  607. 'Access denied, check your credentials'
  608. );
  609. case 'banned':
  610. throw new DeathByCaptcha_AccessDeniedException(
  611. 'Access denied, account suspended'
  612. );
  613. case 'insufficient-funds':
  614. throw new DeathByCaptcha_AccessDeniedException(
  615. 'Access denied, balance is too low'
  616. );
  617. case 'invalid-captcha':
  618. throw new DeathByCaptcha_InvalidCaptchaException(
  619. "CAPTCHA was rejected by the service, check if it's a valid image"
  620. );
  621. case 'service-overload':
  622. throw new DeathByCaptcha_ServiceOverloadException(
  623. 'CAPTCHA was rejected due to service overload, try again later'
  624. );
  625. default:
  626. throw new DeathByCaptcha_ServerException(
  627. 'API server error occured: ' . $error
  628. );
  629. }
  630. } else {
  631. return $response;
  632. }
  633. } catch (Exception $e) {
  634. $this->close();
  635. throw $e;
  636. }
  637. }
  638. /**
  639. * @see DeathByCaptcha_Client::__construct()
  640. */
  641. public function __construct($username, $password)
  642. {
  643. // PHP for Windows lacks EAGAIN errno constant
  644. if (!defined('SOCKET_EAGAIN')) {
  645. define('SOCKET_EAGAIN', 11);
  646. }
  647. foreach (array('json', ) as $k) {
  648. if (!extension_loaded($k)) {
  649. throw new DeathByCaptcha_RuntimeException(
  650. "Required {$k} extension not found, check your PHP configuration"
  651. );
  652. }
  653. }
  654. foreach (array('json_encode', 'json_decode', 'base64_encode') as $k) {
  655. if (!function_exists($k)) {
  656. throw new DeathByCaptcha_RuntimeException(
  657. "Required {$k}() function not found, check your PHP configuration"
  658. );
  659. }
  660. }
  661. parent::__construct($username, $password);
  662. }
  663. /**
  664. * @see DeathByCaptcha_Client::close()
  665. */
  666. public function close()
  667. {
  668. if (null !== $this->_sock) {
  669. if ($this->is_verbose) {
  670. fputs(STDERR, time() . " CLOSE\n");
  671. }
  672. fclose($this->_sock);
  673. $this->_sock = null;
  674. }
  675. return $this;
  676. }
  677. /**
  678. * @see DeathByCaptcha_Client::get_user()
  679. */
  680. public function get_user()
  681. {
  682. $user = $this->_call('user');
  683. return (0 < ($id = (int)@$user['user']))
  684. ? array('user' => $id,
  685. 'balance' => (float)@$user['balance'],
  686. 'is_banned' => (bool)@$user['is_banned'])
  687. : null;
  688. }
  689. /**
  690. * @see DeathByCaptcha_Client::get_user()
  691. */
  692. public function upload($captcha)
  693. {
  694. $img = $this->_load_captcha($captcha);
  695. if ($this->_is_valid_captcha($img)) {
  696. $captcha = $this->_call('upload', array(
  697. 'captcha' => base64_encode($img),
  698. ));
  699. if (0 < ($cid = (int)@$captcha['captcha'])) {
  700. return array(
  701. 'captcha' => $cid,
  702. 'text' => (!empty($captcha['text']) ? $captcha['text'] : null),
  703. 'is_correct' => (bool)@$captcha['is_correct'],
  704. );
  705. }
  706. }
  707. return null;
  708. }
  709. /**
  710. * @see DeathByCaptcha_Client::get_captcha()
  711. */
  712. public function get_captcha($cid)
  713. {
  714. $captcha = $this->_call('captcha', array('captcha' => (int)$cid));
  715. return (0 < ($cid = (int)@$captcha['captcha']))
  716. ? array('captcha' => $cid,
  717. 'text' => (!empty($captcha['text']) ? $captcha['text'] : null),
  718. 'is_correct' => (bool)$captcha['is_correct'])
  719. : null;
  720. }
  721. /**
  722. * @see DeathByCaptcha_Client::report()
  723. */
  724. public function report($cid)
  725. {
  726. $captcha = $this->_call('report', array('captcha' => (int)$cid));
  727. return !@$captcha['is_correct'];
  728. }
  729. }