mollom.class.inc 57 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598
  1. <?php
  2. /**
  3. * @file
  4. * Mollom client class.
  5. *
  6. * @license MIT|GNU GPL v2
  7. * See LICENSE-MIT.txt or LICENSE-GPL.txt shipped with this library.
  8. */
  9. /**
  10. * The base class for Mollom client implementations.
  11. */
  12. abstract class Mollom {
  13. /**
  14. * The Mollom API version, used in HTTP requests.
  15. */
  16. const API_VERSION = 'v1';
  17. /**
  18. * Network communication failure code: Server could not be reached.
  19. *
  20. * @see MollomNetworkException
  21. */
  22. const NETWORK_ERROR = 900;
  23. /**
  24. * Server communication failure code: Unexpected server response.
  25. *
  26. * Using the 5xx HTTP status code range, but not re-using an existing HTTP
  27. * code to prevent bogus bug reports. 511 is the closest comparable code
  28. * 501 (Not Implemented) plus 10.
  29. *
  30. * @see MollomResponseException
  31. */
  32. const RESPONSE_ERROR = 511;
  33. /**
  34. * Client communication failure code: Bad request.
  35. *
  36. * Used in case of a too large system time offset from UTC.
  37. *
  38. * @see MollomBadRequestException
  39. * @see Mollom::TIME_OFFSET_MAX
  40. */
  41. const REQUEST_ERROR = 400;
  42. /**
  43. * Client communication failure code: Authentication error.
  44. *
  45. * @see MollomAuthenticationException
  46. */
  47. const AUTH_ERROR = 401;
  48. /**
  49. * Maximum allowed time offset from UTC in seconds allowed for the client's local time.
  50. *
  51. * @see Mollom::handleRequest()
  52. * @see MollomBadRequestException
  53. *
  54. * @var integer
  55. */
  56. const TIME_OFFSET_MAX = 300;
  57. /**
  58. * List of ISO 639-1 language codes supported by Mollom.
  59. *
  60. * If your application has a predefined list of ISO 639-1 languages already,
  61. * intersect your list with this via strtok($langcode, '-').
  62. *
  63. * Should be a constant, but PHP does not support arrays for constants.
  64. */
  65. public static $LANGUAGES_SUPPORTED = array(
  66. 'af',
  67. 'ar',
  68. 'bg',
  69. 'bn',
  70. 'cs',
  71. 'da',
  72. 'de',
  73. 'el',
  74. 'en',
  75. 'es',
  76. 'et',
  77. 'fa',
  78. 'fi',
  79. 'fr',
  80. 'gu',
  81. 'he',
  82. 'hi',
  83. 'hr',
  84. 'hu',
  85. 'id',
  86. 'it',
  87. 'ja',
  88. 'kn',
  89. 'ko',
  90. 'lt',
  91. 'lv',
  92. 'mk',
  93. 'ml',
  94. 'mr',
  95. 'ne',
  96. 'nl',
  97. 'no',
  98. 'pa',
  99. 'pl',
  100. 'pt',
  101. 'ro',
  102. 'ru',
  103. 'sk',
  104. 'sl',
  105. 'so',
  106. 'sq',
  107. 'sv',
  108. 'sw',
  109. 'ta',
  110. 'te',
  111. 'th',
  112. 'tl',
  113. 'tr',
  114. 'uk',
  115. 'ur',
  116. 'vi',
  117. 'zh-cn',
  118. 'zh-tw',
  119. );
  120. /**
  121. * The public Mollom API key to use for request authentication.
  122. *
  123. * @var string
  124. */
  125. public $publicKey = '';
  126. /**
  127. * The private Mollom API key to use for request authentication.
  128. *
  129. * @var string
  130. */
  131. public $privateKey = '';
  132. /**
  133. * OAuth protocol parameter strategy to use.
  134. *
  135. * Either 'header' for HTTP headers (preferred), or 'query' for GET/POST
  136. * request parameters.
  137. *
  138. * @see Mollom::addAuthentication()
  139. *
  140. * @var string
  141. */
  142. public $oAuthStrategy = 'header';
  143. /**
  144. * The Mollom server to communicate with, without protocol.
  145. *
  146. * @var string
  147. */
  148. public $server = 'rest.mollom.com';
  149. /**
  150. * Maximum number of attempts for a request to a Mollom server.
  151. *
  152. * @see Mollom::query()
  153. * @see Mollom::$requestTimeout
  154. *
  155. * @var integer
  156. */
  157. public $requestMaxAttempts = 2;
  158. /**
  159. * Seconds in which a request to a Mollom server times out.
  160. *
  161. * Mollom servers usually respond within a few milliseconds. However, if a
  162. * server is under very high load, or in case of networking issues, a response
  163. * might take longer. A maximum response time of 3 seconds ensures that the
  164. * client waits long enough in edge-cases, but also not too long in case a
  165. * particular server is unreachable.
  166. *
  167. * The timeout applies per request. Mollom::query() will retry a request until
  168. * it reaches Mollom::$requestMaxAttempts. With the default values, a Mollom
  169. * API call has a total timeout of 6 seconds in case of a server failure.
  170. *
  171. * @see Mollom::request()
  172. * @see Mollom::$requestMaxAttempts
  173. *
  174. * @var float
  175. */
  176. public $requestTimeout = 3.0;
  177. /**
  178. * The status code of the last server response, or TRUE if the request succeeded.
  179. *
  180. * If not TRUE, then the value is either one of
  181. * - Mollom::NETWORK_ERROR
  182. * - Mollom::RESPONSE_ERROR
  183. * - Mollom::REQUEST_ERROR
  184. * - Mollom::AUTH_ERROR
  185. * or the actual HTTP status code returned by the server.
  186. *
  187. * @var int|bool|null
  188. */
  189. public $lastResponseCode = NULL;
  190. /**
  191. * The amount of items contained in a list response.
  192. *
  193. * @var int
  194. */
  195. public $listCount = NULL;
  196. /**
  197. * The current offset of a list response.
  198. *
  199. * @var int
  200. */
  201. public $listOffset = 0;
  202. /**
  203. * The total amount of items contained in a list response.
  204. *
  205. * @var int
  206. */
  207. public $listTotal = NULL;
  208. /**
  209. * Flag indicating whether to invoke Mollom::writeLog() in Mollom::query().
  210. *
  211. * @var bool
  212. */
  213. public $writeLog = TRUE;
  214. /**
  215. * A list of logged requests.
  216. *
  217. * @var array
  218. */
  219. public $log = array();
  220. function __construct() {
  221. $this->publicKey = $this->loadConfiguration('publicKey');
  222. $this->privateKey = $this->loadConfiguration('privateKey');
  223. }
  224. /**
  225. * Loads a configuration value from client-side storage.
  226. *
  227. * @param string $name
  228. * The configuration setting name to load, one of:
  229. * - publicKey: The public API key for Mollom authentication.
  230. * - privateKey: The private API key for Mollom authentication.
  231. * - expectedLanguages: List of expected language codes for site content.
  232. *
  233. * @return mixed
  234. * The stored configuration value or NULL if there is none.
  235. *
  236. * @see Mollom::saveConfiguration()
  237. * @see Mollom::deleteConfiguration()
  238. */
  239. abstract protected function loadConfiguration($name);
  240. /**
  241. * Saves a configuration value to client-side storage.
  242. *
  243. * @param string $name
  244. * The configuration setting name to save.
  245. * @param mixed $value
  246. * The value to save.
  247. *
  248. * @see Mollom::loadConfiguration()
  249. * @see Mollom::deleteConfiguration()
  250. */
  251. abstract protected function saveConfiguration($name, $value);
  252. /**
  253. * Deletes a configuration value from client-side storage.
  254. *
  255. * @param string $name
  256. * The configuration setting name to delete.
  257. *
  258. * @see Mollom::loadConfiguration()
  259. * @see Mollom::saveConfiguration()
  260. */
  261. abstract protected function deleteConfiguration($name);
  262. /**
  263. * Returns platform and version information about the Mollom client.
  264. *
  265. * Retrieves platform and Mollom client version information to send along to
  266. * Mollom when verifying keys.
  267. *
  268. * This information is used to speed up support requests and technical
  269. * inquiries. The data may also be aggregated to help the Mollom staff to make
  270. * decisions on new features or the necessity of back-porting improved
  271. * functionality to older versions.
  272. *
  273. * @return array
  274. * An associative array containing:
  275. * - platformName: The name of the platform/distribution; e.g., "Drupal".
  276. * - platformVersion: The version of platform/distribution; e.g., "7.0".
  277. * - clientName: The official Mollom client name; e.g., "Mollom".
  278. * - clientVersion: The version of the Mollom client; e.g., "7.x-1.0".
  279. */
  280. abstract public function getClientInformation();
  281. /**
  282. * Writes log messages to a permanent location/storage.
  283. *
  284. * Not abstract, since clients are not required to write log messages.
  285. * However, all clients should permanently store the log messages, as it
  286. * dramatically improves resolution of support requests filed by users.
  287. * The log may be written and appended to a file (via file_put_contents()),
  288. * syslog (on *nix-based systems), or a database.
  289. *
  290. * @see Mollom::log
  291. */
  292. public function writeLog() {
  293. // After writing log messages, empty the log.
  294. $this->purgeLog();
  295. }
  296. /**
  297. * Purges captured log messages.
  298. *
  299. * @see Mollom::writeLog()
  300. */
  301. final public function purgeLog() {
  302. $this->log = array();
  303. }
  304. /**
  305. * Send or retrieve data from/to Mollom.
  306. *
  307. * @param string $method
  308. * The HTTP method to use; i.e., 'GET', 'POST', or 'PUT'.
  309. * @param string $path
  310. * The REST path/resource to request; e.g., 'site/1a2b3c'.
  311. * @param array $data
  312. * An associative array of query parameters to send with the request.
  313. * @param array $expected
  314. * (optional) An element that is expected in the response, denoted as a list
  315. * of parent element keys to the element and the element key itself; e.g., a
  316. * value of array('content', 'id') expects $response['content']['id'] to
  317. * exist in the response.
  318. *
  319. * @return mixed
  320. * On success, the parsed response body. On failure, the last response code,
  321. * in case it is a known one; otherwise Mollom::NETWORK_ERROR.
  322. *
  323. * @see Mollom::handleRequest()
  324. * @see Mollom::request()
  325. */
  326. public function query($method, $path, array $data = array(), array $expected = array()) {
  327. // Reset list response properties.
  328. $this->listCount = NULL;
  329. $this->listOffset = 0;
  330. $this->listTotal = NULL;
  331. // Send the request to the server.
  332. $server = 'http://' . $this->server;
  333. $max_attempts = $this->requestMaxAttempts;
  334. while ($max_attempts-- > 0) {
  335. try {
  336. $result = $this->handleRequest($method, $server, $path, $data, $expected);
  337. }
  338. catch (MollomBadRequestException $e) {
  339. // Irrecoverable error, so don't try further.
  340. break;
  341. }
  342. catch (MollomAuthenticationException $e) {
  343. // Irrecoverable error, so don't try further.
  344. break;
  345. }
  346. catch (MollomException $e) {
  347. // If the requested resource does not exist, or the request was
  348. // malformed, there is no point in trying further.
  349. if ($e->getCode() >= 400 && $e->getCode() < 500) {
  350. break;
  351. }
  352. }
  353. // Unless we have a positive result, try again.
  354. if ($this->lastResponseCode === TRUE) {
  355. break;
  356. }
  357. }
  358. // Write all captured log messages.
  359. if ($this->writeLog) {
  360. $this->writeLog();
  361. }
  362. // If there is a result and the last request succeeded, return the result to
  363. // the caller.
  364. if (isset($result) && $this->lastResponseCode === TRUE) {
  365. // Generically handle the special case of 'list' responses.
  366. if (isset($result['list']) && is_array($result)) {
  367. // Assign list meta properties to corresponding class properties.
  368. $this->listCount = (int) $result['listCount'];
  369. $this->listOffset = (int) $result['listOffset'];
  370. $this->listTotal = (int) $result['listTotal'];
  371. // If there is only one item, parseXML() is not able to detect it as a
  372. // list and will return a named key for the value. Ensure an indexed
  373. // array is returned.
  374. if (is_array($result['list'])) {
  375. $result['list'] = array_values($result['list']);
  376. }
  377. // In XML, the 'list' element is parsed as a string when the list is
  378. // empty.
  379. else {
  380. $result['list'] = array();
  381. }
  382. }
  383. return $result;
  384. }
  385. // If the last request succeeded but there was a unexpected response, return
  386. // the error code.
  387. if ($this->lastResponseCode === self::RESPONSE_ERROR) {
  388. return $this->lastResponseCode;
  389. }
  390. // Return an authentication error, which may require special client-side
  391. // processing.
  392. if ($this->lastResponseCode === self::AUTH_ERROR) {
  393. return $this->lastResponseCode;
  394. }
  395. // Return a request error, which always requires client-side measures to
  396. // resolve the problem.
  397. if ($this->lastResponseCode >= self::REQUEST_ERROR && $this->lastResponseCode < 500) {
  398. return $this->lastResponseCode;
  399. }
  400. // In case of any kind of HTTP error (0 [invalid-address],
  401. // -1002 [bad URI], etc), return a generic NETWORK_ERROR.
  402. return self::NETWORK_ERROR;
  403. }
  404. /**
  405. * Prepares a HTTP request to a Mollom server and processes the response.
  406. *
  407. * @param string $method
  408. * The HTTP method to use; i.e., 'GET' or 'POST'.
  409. * @param string $server
  410. * The base URL of the server to perform the request against; e.g.,
  411. * 'http://foo.mollom.com'.
  412. * @param string $path
  413. * The REST path/resource to request; e.g., 'site/1a2b3c'.
  414. * @param array $data
  415. * An associative array of query parameters to send with the request.
  416. * @param array $expected
  417. * (optional) An element that is expected in the response, denoted as a list
  418. * of parent element keys to the element and the element key itself; e.g., a
  419. * value of array('content', 'id') expects $response['content']['id'] to
  420. * exist in the response. If the expected element does not exist, a
  421. * MollomResponseException is thrown.
  422. *
  423. * @return array
  424. * An associative array representing the parsed response body on success. On
  425. * any failure, a MollomException is thrown. Additionally,
  426. * Mollom::lastResponseCode is set to TRUE on success, or to the Mollom or
  427. * HTTP status code on failure.
  428. *
  429. * @throws MollomNetworkException
  430. * @throws MollomAuthenticationException
  431. * @throws MollomResponseException
  432. * @throws MollomException
  433. *
  434. * @see Mollom::lastResponseCode
  435. * @see Mollom::query()
  436. * @see Mollom::httpBuildQuery()
  437. * @see Mollom::parseXML()
  438. * @see json_decode()
  439. */
  440. protected function handleRequest($method, $server, $path, $data, $expected = array()) {
  441. $time_start = microtime(TRUE);
  442. if (!empty($this->publicKey) && !empty($this->privateKey)) {
  443. // @todo Move into class property.
  444. $headers['Accept'] = 'application/xml, application/json;q=0.8, */*;q=0.5';
  445. if ($method == 'POST') {
  446. $headers['Content-Type'] = 'application/x-www-form-urlencoded';
  447. }
  448. // Append API version to REST endpoint.
  449. $server .= '/' . self::API_VERSION;
  450. // Add OAuth request parameters.
  451. $query = $this->addAuthentication($method, $server, $path, $data, $headers);
  452. $response = $this->request($method, $server, $path, $query, $headers);
  453. // Determine basic error condition based on HTTP status code.
  454. $response->isError = ($response->code < 200 || $response->code >= 300);
  455. }
  456. else {
  457. $headers = array();
  458. $response = new stdClass;
  459. $response->code = self::AUTH_ERROR;
  460. $response->message = 'Missing API keys.';
  461. $response->isError = TRUE;
  462. $response->body = NULL;
  463. }
  464. $time_stop = microtime(TRUE);
  465. // Parse the response body string into an array.
  466. $response->data = array();
  467. if (isset($response->body) && isset($response->headers['content-type'])) {
  468. if (strstr($response->headers['content-type'], 'application/json')) {
  469. $response->data = json_decode($response->body, TRUE);
  470. }
  471. elseif (strstr($response->headers['content-type'], 'application/xml')) {
  472. try {
  473. $response->elements = new SimpleXmlIterator($response->body);
  474. $response->data = $this->parseXML($response->elements);
  475. }
  476. catch (Exception $e) {
  477. // Invalid XML so just set the data back to blank.
  478. $response->data = array();
  479. }
  480. }
  481. }
  482. // A 'code' in the response has precedence, regardless of a possibly
  483. // positive HTTP status code.
  484. if (isset($response->data['code']) && $response->data['code'] != 200) {
  485. $response->isError = TRUE;
  486. // Replace HTTP status code with 'code' from response.
  487. $response->code = (int) $response->data['code'];
  488. // If there is no HTTP status message, take over 'message' from response,
  489. // if any.
  490. if (!isset($response->message) && isset($response->data['message'])) {
  491. $response->message = $response->data['message'];
  492. }
  493. }
  494. // Verify that the expected (parent) element exists in the response.
  495. // There is no notion of DTD/schema in JSON. A dedicated class for each
  496. // response would be a large overhead. Therefore, response validation is
  497. // limited to checking for one expected element (in a nested array of
  498. // variable depth).
  499. if (!$response->isError && !empty($response->data) && !empty($expected)) {
  500. $ref = &$response->data;
  501. $parent = reset($expected);
  502. while ($parent !== FALSE && is_array($ref) && array_key_exists($parent, $ref)) {
  503. $ref = &$ref[$parent];
  504. $parent = next($expected);
  505. }
  506. // Only if $parent is FALSE we have reached the last expected key.
  507. if ($parent !== FALSE) {
  508. $response->isError = TRUE;
  509. $response->code = self::RESPONSE_ERROR;
  510. $response->message = 'Unexpected server response.';
  511. }
  512. }
  513. $request_info = array(
  514. 'request' => $method . ' ' . $server . '/' . $path,
  515. 'headers' => $headers,
  516. 'data' => $data,
  517. 'response_code' => $response->code,
  518. 'response_message' => $response->message,
  519. 'response' => !empty($response->data) ? $response->data : $response->body,
  520. 'response_time' => $time_stop - $time_start,
  521. );
  522. if ($response->isError) {
  523. if ($response->code <= 0) {
  524. throw new MollomNetworkException('Network error.', self::NETWORK_ERROR, NULL, $this, $request_info);
  525. }
  526. if ($response->code == self::AUTH_ERROR) {
  527. // Check whether authentication failed due to a too large time offset.
  528. if (isset($response->headers['date'])) {
  529. // Parse the 'Date' HTTP header in the response into a UNIX timestamp;
  530. // strtotime() normally performs poorly on date operations, but in
  531. // this case, there is a standardized date format and timezone
  532. // conversion, so it is safe to use.
  533. $offset = abs(time() - strtotime($response->headers['date']));
  534. // The abs() above turns a negative offset into an absolute integer
  535. // value, so the difference is always positive.
  536. if ($offset > self::TIME_OFFSET_MAX) {
  537. throw new MollomBadRequestException(sprintf('Invalid client system time: Too large offset from UTC: %s seconds.', $offset), self::REQUEST_ERROR, NULL, $this, $request_info);
  538. }
  539. }
  540. throw new MollomAuthenticationException('Invalid authentication.', self::AUTH_ERROR, NULL, $this, $request_info);
  541. }
  542. if ($response->code == self::RESPONSE_ERROR || $response->code >= 500) {
  543. throw new MollomResponseException($response->message, $response->code, NULL, $this, $request_info);
  544. }
  545. throw new MollomException($response->message, $response->code, NULL, $this, $request_info);
  546. }
  547. else {
  548. $this->lastResponseCode = TRUE;
  549. // No message is logged in case of success.
  550. $this->log[] = array(
  551. 'severity' => 'debug',
  552. ) + $request_info;
  553. return $response->data;
  554. }
  555. }
  556. /**
  557. * Returns GET/POST request parameters after adding 2-legged OAuth authentication parameters.
  558. *
  559. * All available OAuth libraries for PHP are needlessly over-engineered,
  560. * poorly written and maintained, contain code for server implementations, and
  561. * would be mostly overhead for the simple purpose of 2-legged authentication.
  562. * Therefore, this simple function implements OAuth request signing based on
  563. * latest RFC 5849, using the public API key as client/consumer key and the
  564. * private API key as client secret.
  565. *
  566. * Make sure that your server time is synchronized with the world clocks, and
  567. * that you do not share your private key with anyone else.
  568. *
  569. * @param string $method
  570. * The HTTP method to use; i.e., 'GET' or 'POST'.
  571. * @param string $server
  572. * The base URL of the server to perform the request against; e.g.,
  573. * 'http://foo.mollom.com'.
  574. * @param string $path
  575. * The REST path/resource to request; e.g., 'site/1a2b3c'.
  576. * @param array $data
  577. * An associative array of query parameters to send with the request. Passed
  578. * by reference.
  579. * @param array $headers
  580. * An associative array of HTTP request headers to send along with the
  581. * request. Passed by reference.
  582. *
  583. * @return string
  584. * A string containing encoded request parameters derived of $data to use as
  585. * GET query string or POST body data.
  586. *
  587. * @see http://tools.ietf.org/html/rfc5849
  588. * @see Mollom::oAuthStrategy
  589. */
  590. public function addAuthentication($method, $server, $path, &$data, &$headers) {
  591. $oauth['oauth_consumer_key'] = $this->publicKey;
  592. $oauth['oauth_version'] = '1.0';
  593. // Random string; must be unique across all requests with the same
  594. // timestamp, client credentials, and token combinations. (3.3)
  595. $oauth['oauth_nonce'] = md5(microtime() . mt_rand());
  596. // Number of seconds since January 1, 1970 00:00:00 GMT. (3.3)
  597. $oauth['oauth_timestamp'] = time();
  598. $oauth['oauth_signature_method'] = 'HMAC-SHA1';
  599. // Prepare the request query parameters to return (and pass on to request())
  600. // and the query parameters to include in the OAuth signature base string.
  601. // Note that Mollom::httpBuildQuery() sorts parameters already.
  602. if ($this->oAuthStrategy == 'header') {
  603. $query = $this->httpBuildQuery($data);
  604. $oauth_query = $this->httpBuildQuery($oauth + $data);
  605. }
  606. elseif ($this->oAuthStrategy == 'query') {
  607. $data += $oauth;
  608. $oauth_query = $query = $this->httpBuildQuery($data);
  609. }
  610. // Skip authentication entirely; required to create testing site record when
  611. // running tests.
  612. else {
  613. $query = $this->httpBuildQuery($data);
  614. $oauth_query = '';
  615. }
  616. // Generate the signature. (3.4.1)
  617. // Base string to sign is compound of
  618. // - uppercase HTTP method
  619. // - rawurlencoded lowercase server URI (without default ports 80/443),
  620. // including path in natural case
  621. // - encoded, sorted, and lastly (double-)rawurlencoded request parameters,
  622. // having GET query parameters first, and POST body data parameters last
  623. // (if any)
  624. // delimited by "&". (3.4.1.1)
  625. $base_string = implode('&', array(
  626. $method,
  627. self::rawurlencode($server . '/' . $path),
  628. self::rawurlencode($oauth_query),
  629. ));
  630. // Key is unconditionally compound of client secret and token secret, even
  631. // if empty. (3.4.2)
  632. $key = self::rawurlencode($this->privateKey) . '&' . '';
  633. $oauth['oauth_signature'] = base64_encode(hash_hmac('sha1', $base_string, $key, TRUE));
  634. // Add the OAuth protocol parameters as HTTP header, or append the signature
  635. // to query parameters.
  636. if ($this->oAuthStrategy == 'header') {
  637. foreach ($oauth as $key => $value) {
  638. $oauth[$key] = $key . '="' . self::rawurlencode($value) . '"';
  639. }
  640. // Header parameters are joined and delimited by comma. (3.5.1)
  641. $headers['Authorization'] = 'OAuth ' . implode(', ', $oauth);
  642. }
  643. elseif ($this->oAuthStrategy == 'query') {
  644. $oauth['oauth_signature'] = rawurlencode($oauth['oauth_signature']);
  645. $data['oauth_signature'] = $oauth['oauth_signature'];
  646. $query .= '&oauth_signature=' . $oauth['oauth_signature'];
  647. }
  648. return $query;
  649. }
  650. /**
  651. * Encodes a URL compliant with OAuth RFC 5849.
  652. *
  653. * @param string $value
  654. * The URL string to be encoded.
  655. *
  656. * @return string
  657. * The rawurlencode()d URL, containing string literals for "~" (tildes) and
  658. * " " (spaces). (RFC 5849 3.4.1.3.1 and 3.6)
  659. */
  660. public static function rawurlencode($value) {
  661. return strtr(rawurlencode($value), array('%7E' => '~', '+' => ' '));
  662. }
  663. /**
  664. * Performs a HTTP request to a Mollom server.
  665. *
  666. * @param string $method
  667. * The HTTP method to use; i.e., 'GET', 'POST', or 'PUT'.
  668. * @param string $server
  669. * The base URL of the server to perform the request against; e.g.,
  670. * 'http://foo.mollom.com'.
  671. * @param string $path
  672. * The REST path/resource to request; e.g., 'site/1a2b3c'.
  673. * @param string $query
  674. * (optional) A prepared string of HTTP query parameters to append to $path
  675. * for $method GET, or to use as request body for $method POST.
  676. * @param array $headers
  677. * (optional) An associative array of HTTP request headers to send along
  678. * with the request.
  679. *
  680. * @return object
  681. * An object containing response properties:
  682. * - code: The HTTP status code as integer returned by the Mollom server.
  683. * - message: The HTTP status message string returned by the Mollom server,
  684. * or NULL if there is no message.
  685. * - headers: An associative array containing the HTTP response headers
  686. * returned by the Mollom server. Header name keys are expected to be
  687. * lower-case; i.e., "content-type" instead of "Content-Type".
  688. * - body: The HTTP response body string returned by the Mollom server, or
  689. * NULL if there is none.
  690. *
  691. * @see Mollom::handleRequest()
  692. */
  693. abstract protected function request($method, $server, $path, $query = NULL, array $headers = array());
  694. /**
  695. * Converts a SimpleXMLIterator structure into an associative array.
  696. *
  697. * Used to parse an XML response from Mollom servers into a PHP array. For
  698. * example:
  699. * @code
  700. * $elements = new SimpleXmlIterator($response_body);
  701. * $parsed_response = $this->parseXML($elements);
  702. * @endcode
  703. *
  704. * @param SimpleXMLIterator $sxi
  705. * A SimpleXMLIterator structure of the server response body.
  706. *
  707. * @return array
  708. * An associative, possibly multidimensional array.
  709. */
  710. public static function parseXML(SimpleXMLIterator $sxi) {
  711. $a = array();
  712. $remove = array();
  713. for ($sxi->rewind(); $sxi->valid(); $sxi->next()) {
  714. $key = $sxi->key();
  715. // Recurse into non-scalar values.
  716. if ($sxi->hasChildren()) {
  717. $value = self::parseXML($sxi->current());
  718. }
  719. // Use a simple key/value pair for scalar values.
  720. else {
  721. $value = strval($sxi->current());
  722. }
  723. if (!isset($a[$key])) {
  724. $a[$key] = $value;
  725. }
  726. // Convert already existing keys into indexed keys, retaining other
  727. // existing keys in the array; i.e., two or more XML elements of the
  728. // same name and on the same level.
  729. // Note that this XML to PHP array conversion does not support multiple
  730. // different elements that each appear multiple times.
  731. else {
  732. // First time we reach here, convert the existing keyed item. Do not
  733. // remove $key, so we enter this path again.
  734. if (!isset($remove[$key])) {
  735. $a[] = $a[$key];
  736. // Mark $key for removal.
  737. $remove[$key] = $key;
  738. }
  739. // Add the new item.
  740. $a[] = $value;
  741. }
  742. }
  743. // Lastly, remove named keys that have been converted to indexed keys.
  744. foreach ($remove as $key) {
  745. unset($a[$key]);
  746. }
  747. return $a;
  748. }
  749. /**
  750. * Builds an RFC-compliant, rawurlencoded query string.
  751. *
  752. * PHP did a design decision to only support HTTP query parameters in the form
  753. * of foo[]=1&foo[]=2, primarily for its built-in and automated conversion to
  754. * PHP arrays. Other platforms (including the Mollom backend) do not support
  755. * this syntax and expect multiple parameters to be in the form of
  756. * foo=1&foo=2.
  757. *
  758. * @see http_build_query()
  759. * @see http://en.wikipedia.org/wiki/Query_string
  760. * @see http://tools.ietf.org/html/rfc3986#section-3.4
  761. *
  762. * @param array $query
  763. * The query parameter array to be processed, e.g. $_GET.
  764. * @param string $parent
  765. * Internal use only. Used to build the $query array key for nested items.
  766. *
  767. * @return string
  768. * A rawurlencoded string which can be used as or appended to the URL query
  769. * string.
  770. *
  771. * @see Mollom::httpParseQuery()
  772. */
  773. public static function httpBuildQuery(array $query, $parent = '') {
  774. $params = array();
  775. foreach ($query as $key => $value) {
  776. // For indexed (unnamed) child array keys, use the same parameter name,
  777. // leading to param=foo&param=bar instead of param[]=foo&param[]=bar.
  778. if ($parent && is_int($key)) {
  779. $key = rawurlencode($parent);
  780. }
  781. else {
  782. $key = ($parent ? $parent . '[' . rawurlencode($key) . ']' : rawurlencode($key));
  783. }
  784. // Recurse into children.
  785. if (is_array($value)) {
  786. if (!empty($value)) {
  787. $params[] = self::httpBuildQuery($value, $key);
  788. }
  789. // An empty array means that the parameter accepts a list of values, but
  790. // the list is empty and no value should be assigned, so pass NULL.
  791. else {
  792. $params[] = $key . '=';
  793. }
  794. }
  795. // If a query parameter value is NULL, only append its key, followed by
  796. // "=" (3.4.1.3.2).
  797. elseif (!isset($value)) {
  798. $params[] = $key . '=';
  799. }
  800. else {
  801. $params[] = $key . '=' . rawurlencode($value);
  802. }
  803. }
  804. // Parameters are sorted by name, using ascending byte value ordering. If
  805. // two or more parameters share the same name, they are sorted by their
  806. // value. (3.4.1.3.2)
  807. sort($params, SORT_STRING);
  808. $result = implode('&', $params);
  809. // Prior to PHP 5.3.0, rawurlencode encoded tildes (~) as per RFC 1738.
  810. // Percent-encoded octets corresponding to unreserved characters can be
  811. // decoded at any time. For example, the octet corresponding to the tilde
  812. // ("~") character is often encoded as "%7E" by older URI processing
  813. // implementations; the "%7E" can be replaced by "~" without changing its
  814. // interpretation.
  815. // @see http://php.net/manual/en/function.rawurlencode.php
  816. // @see http://tools.ietf.org/html/rfc3986#section-2.3
  817. $result = str_replace('%7E', '~', $result);
  818. return $result;
  819. }
  820. /**
  821. * Parses an RFC-compliant, rawurlencoded query string.
  822. *
  823. * Mollom clients normally do not need this function, as they do not need to
  824. * process requests from a server - unless a client attempts to implement
  825. * client-side unit testing.
  826. *
  827. * @param string $query
  828. * The query parameter string to process, e.g. $_SERVER['QUERY_STRING']
  829. * (GET) or php://input (POST/PUT).
  830. *
  831. * @return array
  832. * A query parameter array parsed from $query.
  833. *
  834. * @see Mollom::httpBuildQuery()
  835. * @see parse_str()
  836. */
  837. public static function httpParseQuery($query) {
  838. if ($query === '') {
  839. return array();
  840. }
  841. // Explode parameters into arrays to check for duplicate names.
  842. $params = array();
  843. $seen = array();
  844. $duplicate = array();
  845. foreach (explode('&', $query) as $chunk) {
  846. $param = explode('=', $chunk, 2);
  847. if (isset($seen[$param[0]])) {
  848. $duplicate[$param[0]] = TRUE;
  849. }
  850. $seen[$param[0]] = TRUE;
  851. $params[] = $param;
  852. }
  853. // Implode back into a string.
  854. $query = '';
  855. foreach ($params as $param) {
  856. $query .= $param[0];
  857. if (isset($duplicate[$param[0]])) {
  858. $query .= '[]';
  859. }
  860. if (isset($param[1])) {
  861. $query .= '=' . $param[1];
  862. }
  863. $query .= '&';
  864. }
  865. // Parse query string as usual.
  866. parse_str($query, $result);
  867. return $result;
  868. }
  869. /**
  870. * Retrieves GET/HEAD or POST/PUT parameters of an inbound HTTP request.
  871. *
  872. * @return array
  873. * An array containing either GET/HEAD query string parameters or POST/PUT
  874. * post body parameters. Parameter parsing accounts for multiple request
  875. * parameters in non-PHP format; e.g., 'foo=one&foo=bar'.
  876. */
  877. public static function getServerParameters() {
  878. if ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD') {
  879. $data = self::httpParseQuery($_SERVER['QUERY_STRING']);
  880. }
  881. elseif ($_SERVER['REQUEST_METHOD'] == 'POST' || $_SERVER['REQUEST_METHOD'] == 'PUT') {
  882. $data = self::httpParseQuery(file_get_contents('php://input'));
  883. }
  884. return $data;
  885. }
  886. /**
  887. * Retrieves the OAuth Authorization header of an inbound HTTP request.
  888. *
  889. * @return array
  890. * An array containing all key/value pairs extracted out of the
  891. * 'Authorization' HTTP header, if any.
  892. */
  893. public static function getServerAuthentication() {
  894. $header = array();
  895. // PHP as Apache module provides a SAPI function.
  896. // PHP 5.4+ enables getallheaders() also for FastCGI.
  897. if (function_exists('getallheaders')) {
  898. $headers = getallheaders();
  899. if (isset($headers['Authorization'])) {
  900. $input = $headers['Authorization'];
  901. }
  902. }
  903. // PHP as CGI with server/.htaccess configuration (e.g., via mod_rewrite)
  904. // may transfer/forward HTTP request data into server variables.
  905. elseif (isset($_SERVER['HTTP_AUTHORIZATION'])) {
  906. $input = $_SERVER['HTTP_AUTHORIZATION'];
  907. }
  908. // PHP as CGI may provide HTTP request data as environment variables.
  909. elseif (isset($_ENV['HTTP_AUTHORIZATION'])) {
  910. $input = $_ENV['HTTP_AUTHORIZATION'];
  911. }
  912. if (isset($input)) {
  913. preg_match_all('@([^, =]+)="([^"]*)"@', $input, $header);
  914. if (!empty($header[1]) && !empty($header[2])) {
  915. $header = array_combine($header[1], $header[2]);
  916. }
  917. }
  918. return $header;
  919. }
  920. /**
  921. * Retrieves a list of sites accessible to this client.
  922. *
  923. * Used by Mollom resellers only.
  924. *
  925. * @return array
  926. * An array containing site resources, as returned by Mollom::getsite().
  927. */
  928. public function getSites() {
  929. $result = $this->query('GET', 'site', array(), array('list'));
  930. return isset($result['list']) ? $result['list'] : $result;
  931. }
  932. /**
  933. * Retrieves information about a site.
  934. *
  935. * @param string $publicKey
  936. * (optional) The public Mollom API key of the site to retrieve. Defaults to
  937. * the public key of the client.
  938. *
  939. * @return mixed
  940. * On success, an associative array containing:
  941. * - publicKey: The public Mollom API key of the site.
  942. * - privateKey: The private Mollom API key of the site.
  943. * - url: The URL of the site.
  944. * - email: The e-mail address of the primary contact of the site.
  945. * - platformName: (optional) The name of the platform running the site
  946. * (e.g., "Drupal").
  947. * - platformVersion: (optional) The version of the platform running the
  948. * site (e.g., "6.20").
  949. * - clientName: (optional) The name of the Mollom client plugin used
  950. * (e.g., "Mollom").
  951. * - clientVersion: (optional) The version of the Mollom client plugin used
  952. * (e.g., "6.15").
  953. * - expectedLanguages: (optional) An array of language ISO codes, content
  954. * is expected to be submitted in on the site.
  955. * On failure, the error response code returned by the server.
  956. */
  957. public function getSite($publicKey = NULL) {
  958. if (!isset($publicKey)) {
  959. $publicKey = $this->publicKey;
  960. }
  961. $publicKey = rawurlencode($publicKey);
  962. $result = $this->query('GET', 'site/' . $publicKey, array(), array('site', 'publicKey'));
  963. return isset($result['site']) ? $result['site'] : $result;
  964. }
  965. /**
  966. * Creates a new site.
  967. *
  968. * @param array $data
  969. * An associative array of properties for the new site. At least 'url' and
  970. * 'email' are required. See Mollom::getSite() for details.
  971. *
  972. * @return mixed
  973. * On success, the full site information of the created site; see
  974. * Mollom::getSite() for details. On failure, the error response code
  975. * returned by the server. Or FALSE if 'url' or 'email' was not specified.
  976. */
  977. public function createSite(array $data = array()) {
  978. if (empty($data['url']) || empty($data['email'])) {
  979. return FALSE;
  980. }
  981. $result = $this->query('POST', 'site', $data, array('site', 'publicKey'));
  982. return isset($result['site']) ? $result['site'] : $result;
  983. }
  984. /**
  985. * Updates a site.
  986. *
  987. * Note that most Mollom clients want to use Mollom::verifyKeys() only. This
  988. * method is primarily used by Mollom resellers, who are provisioning sites
  989. * and may need to set other site properties.
  990. *
  991. * @param array $data
  992. * (optional) An associative array of properties to set for the site. See
  993. * Mollom::getSite() for details.
  994. * @param string $publicKey
  995. * (optional) The public Mollom API key of the site to update. Defaults to
  996. * the public key of the client.
  997. *
  998. * @return mixed
  999. * On success, the full site information of the created site; see
  1000. * Mollom::getSite() for details. On failure, the error response code
  1001. * returned by the server.
  1002. */
  1003. public function updateSite(array $data = array(), $publicKey = NULL) {
  1004. if (!isset($publicKey)) {
  1005. $publicKey = $this->publicKey;
  1006. }
  1007. $publicKey = rawurlencode($publicKey);
  1008. $result = $this->query('POST', 'site/' . $publicKey, $data, array('site', 'publicKey'));
  1009. return isset($result['site']) ? $result['site'] : $result;
  1010. }
  1011. /**
  1012. * Updates a site to verify API keys and send client information.
  1013. *
  1014. * Mollom API keys are validated in all API calls already. This method should
  1015. * be used when the API keys of a Mollom client are configured for a site. It
  1016. * should be invoked at least once for a site, to send client and version
  1017. * information to Mollom in order to aid with Mollom support requests.
  1018. *
  1019. * @return mixed
  1020. * TRUE on success. On failure, the error response code returned by the
  1021. * server; either Mollom::REQUEST_ERROR, Mollom::AUTH_ERROR or
  1022. * Mollom::NETWORK_ERROR.
  1023. */
  1024. public function verifyKeys() {
  1025. $data = $this->getClientInformation();
  1026. $result = $this->updateSite($data);
  1027. // lastResponseCode will either be TRUE, REQUEST_ERROR, AUTH_ERROR, or
  1028. // NETWORK_ERROR.
  1029. return $this->lastResponseCode === TRUE ? TRUE : $this->lastResponseCode;
  1030. }
  1031. /**
  1032. * Deletes a site.
  1033. *
  1034. * @param string $publicKey
  1035. * The public Mollom API key of the site to delete.
  1036. *
  1037. * @return bool
  1038. * TRUE on success, FALSE otherwise.
  1039. */
  1040. public function deleteSite($publicKey) {
  1041. $publicKey = rawurlencode($publicKey);
  1042. $result = $this->query('POST', 'site/' . $publicKey . '/delete');
  1043. return $this->lastResponseCode === TRUE;
  1044. }
  1045. /**
  1046. * Checks user-submitted content with Mollom.
  1047. *
  1048. * @param array $data
  1049. * An associative array containing any of the keys:
  1050. * - id: The existing content ID of the content, if it or a variant or
  1051. * revision of it has been checked before.
  1052. * - postTitle: The title of the content.
  1053. * - postBody: The body of the content. If the content consists of multiple
  1054. * fields, concatenate them into one postBody string, separated by " \n"
  1055. * (space and line-feed).
  1056. * - authorName: The (real) name of the content author.
  1057. * - authorUrl: The homepage/website URL of the content author.
  1058. * - authorMail: The e-mail address of the content author.
  1059. * - authorIp: The IP address of the content author.
  1060. * - authorId: The local user ID on the client site of the content author.
  1061. * - authorOpenid: An indexed array of Open IDs of the content author.
  1062. * - checks: An indexed array of strings denoting the checks to perform, one
  1063. * or more of: 'spam', 'quality', 'profanity', 'language', 'sentiment'.
  1064. * Defaults to 'spam'.
  1065. * - type: An optional string identifier to request a special content
  1066. * classification behavior. Possible values are:
  1067. * - 'user': Enables classification of 'author*' request parameters as
  1068. * primary content. postTitle and postBody may be left empty without
  1069. * negative impact on the classification result. Use this for checking
  1070. * user registration forms. Optionally pass additional user profile text
  1071. * fields as postBody.
  1072. * - unsure: Integer denoting whether a "unsure" response should be allowed
  1073. * (1) for the 'spam' check (which should lead to CAPTCHA) or not (0).
  1074. * Defaults to 1.
  1075. * - strictness: A string denoting the strictness of Mollom checks to
  1076. * perform; one of 'strict', 'normal', or 'relaxed'. Defaults to 'normal'.
  1077. * - rateLimit: Seconds that must have passed by for the same author to post
  1078. * again. Defaults to 15.
  1079. * - honeypot: The value of a client-side honeypot form element, if
  1080. * non-empty.
  1081. * - stored: Integer denoting whether the content has been stored (1) on the
  1082. * client-side or not (0). Use 0 during form validation, 1 after
  1083. * successful submission. Defaults to 0.
  1084. * - url: The absolute URL to the stored content.
  1085. * - contextUrl: An absolute URL to parent/context content of the stored
  1086. * content; e.g., the URL of the article or forum thread a comment is
  1087. * posted on (not the parent comment that was replied to).
  1088. * - contextTitle: The title of the parent/context content of the stored
  1089. * content; e.g., the title of the article or forum thread a comment is
  1090. * posted on (not the parent comment that was replied to).
  1091. * - contextCreated: The creation date of the parent/context content of the
  1092. * stored content as a unix timestamp in seconds; e.g., the creation date
  1093. * of the article or forum thread a comment is posted on (not the parent
  1094. * comment that was replied to).
  1095. * - trackingImageId: An optional string identifier used to request an
  1096. * image beacon. This is used for form behavior analysis.
  1097. *
  1098. * @return mixed
  1099. * On success, an associative array representing the full content record,
  1100. * containing the additional keys:
  1101. * - spamScore: A floating point value with a precision of 2, ranging
  1102. * between 0.00 and 1.00; whereas 0.00 denotes 100% spam, 0.50 denotes
  1103. * "unsure", and 1.00 denotes ham. Only returned if 'spam' was passed for
  1104. * 'checks'.
  1105. * - spamClassification: The final spam classification; one of 'spam',
  1106. * 'unsure', or 'ham'. Only returned if 'spam' was passed for 'checks'.
  1107. * - profanityScore: A floating point value with a precision of 2, ranging
  1108. * between 0.00 and 1.00; whereas 0.00 denotes 0% profanity and 1.00
  1109. * denotes 100% profanity. Only returned if 'profanity' was passed for
  1110. * 'checks'.
  1111. * - qualityScore: A floating point value with a precision of 2, ranging
  1112. * between 0.00 and 1.00; whereas 0.00 denotes poor quality and 1.00
  1113. * high quality. Only returned if 'quality' was passed for 'checks'.
  1114. * - sentimentScore: A floating point value with a precision of 2, ranging
  1115. * between 0.00 and 1.00; whereas 0.00 denotes bad sentiment and 1.00
  1116. * good sentiment. Only returned if 'sentiment' was passed for 'checks'.
  1117. * - reason: A string denoting the reason for Mollom's classification; e.g.,
  1118. * - rateLimit: Author was seen on Mollom-protected sites within the given
  1119. * 'rateLimit' time-frame.
  1120. * On failure, the error response code returned by the server.
  1121. */
  1122. public function checkContent(array $data = array()) {
  1123. $path = 'content';
  1124. if (!empty($data['id'])) {
  1125. // The ID originates from raw form input. Ensure we hit the right endpoint
  1126. // in case a bogus bot fills in even hidden input fields with random
  1127. // strings, by performing a basic syntax validation.
  1128. if (preg_match('@^[a-z0-9-]+$@i', $data['id'])) {
  1129. $path .= '/' . rawurlencode($data['id']);
  1130. }
  1131. unset($data['id']);
  1132. }
  1133. $result = $this->query('POST', $path, $data, array('content', 'id'));
  1134. // parseXML() can only convert multiple sub-elements into an indexed array.
  1135. if (isset($result['content']['languages']) && is_array($result['content']['languages'])) {
  1136. $result['content']['languages'] = array_values($result['content']['languages']);
  1137. }
  1138. return isset($result['content']) ? $result['content'] : $result;
  1139. }
  1140. /**
  1141. * Retrieves a CAPTCHA resource from Mollom.
  1142. *
  1143. * @param array $data
  1144. * An associative array containing:
  1145. * - type: A string denoting the type of CAPTCHA to create; one of 'image'
  1146. * or 'audio'.
  1147. * and any of the keys:
  1148. * - contentId: The ID of a content resource to link the CAPTCHA to. Allows
  1149. * Mollom to learn when it was unsure.
  1150. * - ssl: An integer denoting whether to create a CAPTCHA URL using HTTPS
  1151. * (1) or not (0). Only available for paid subscriptions.
  1152. *
  1153. * @return mixed
  1154. * On success, an associative array representing the full CAPTCHA record,
  1155. * containing:
  1156. * - id: The ID of the CAPTCHA.
  1157. * - url: The URL of the CAPTCHA.
  1158. * On failure, the error response code returned by the server.
  1159. * Or FALSE if a unknown 'type' was specified.
  1160. */
  1161. public function createCaptcha(array $data = array()) {
  1162. if (!isset($data['type']) || !in_array($data['type'], array('image', 'audio'))) {
  1163. return FALSE;
  1164. }
  1165. $path = 'captcha';
  1166. $result = $this->query('POST', $path, $data, array('captcha', 'id'));
  1167. return isset($result['captcha']) ? $result['captcha'] : $result;
  1168. }
  1169. /**
  1170. * Checks whether a user-submitted solution for a CAPTCHA is correct.
  1171. *
  1172. * @param array $data
  1173. * An associative array containing:
  1174. * - id: The ID of the CAPTCHA to check.
  1175. * - solution: The answer provided by the author.
  1176. * and any of the keys:
  1177. * - authorName: The (real) name of the content author.
  1178. * - authorUrl: The homepage/website URL of the content author.
  1179. * - authorMail: The e-mail address of the content author.
  1180. * - authorIp: The IP address of the content author.
  1181. * - authorId: The local user ID on the client site of the content author.
  1182. * - authorOpenid: An indexed array of Open IDs of the content author.
  1183. * - rateLimit: Seconds that must have passed by for the same author to post
  1184. * again. Defaults to 15.
  1185. * - honeypot: The value of a client-side honeypot form element, if
  1186. * non-empty.
  1187. *
  1188. * @return mixed
  1189. * On success, an associative array representing the full CAPTCHA record,
  1190. * additionally containing:
  1191. * - solved: Whether the provided solution was correct (1) or not (0).
  1192. * - reason: A string denoting the reason for Mollom's classification; e.g.,
  1193. * - rateLimit: Author was seen on Mollom-protected sites within the given
  1194. * 'rateLimit' time-frame.
  1195. * On failure, the error response code returned by the server.
  1196. * Or FALSE if no 'id' was specified.
  1197. */
  1198. public function checkCaptcha(array $data = array()) {
  1199. if (empty($data['id'])) {
  1200. return FALSE;
  1201. }
  1202. // The ID originates from raw form input. Ensure we hit the right endpoint
  1203. // in case a bogus bot fills in even hidden input fields with random
  1204. // strings, by performing a basic syntax validation.
  1205. if (!preg_match('@^[a-z0-9-]+$@i', $data['id'])) {
  1206. return FALSE;
  1207. }
  1208. $path = 'captcha/' . rawurlencode($data['id']);
  1209. unset($data['id']);
  1210. $result = $this->query('POST', $path, $data, array('captcha', 'id'));
  1211. return isset($result['captcha']) ? $result['captcha'] : $result;
  1212. }
  1213. /**
  1214. * Sends feedback to Mollom.
  1215. *
  1216. * @param array $data
  1217. * An associative array containing:
  1218. * - reason: A string denoting the reason for why the content associated
  1219. * with either contentId or captchaId is being reported; one of:
  1220. * - spam: The content is spam, unsolicited advertising.
  1221. * - profanity: The content contains obscene, violent, profane language.
  1222. * - quality: The content is of low quality.
  1223. * - unwanted: The content is unwanted, taunting, off-topic.
  1224. * - type (optional): A string denoting the type of feedback submitted.
  1225. * - moderate: Feedback from the admin moderation process (default).
  1226. * - flag: feedback from end users flagging content as inappropriate
  1227. * - source (optional): A string denoting the user-interface source of the feedback.
  1228. * and at least one of:
  1229. * - contentId: A Mollom content ID associated with the content.
  1230. * - captchaId: A Mollom CAPTCHA ID associated with the content.
  1231. *
  1232. * @return bool
  1233. * TRUE if the feedback was sent successfully, FALSE otherwise.
  1234. */
  1235. public function sendFeedback(array $data) {
  1236. if (empty($data['contentId']) && empty($data['captchaId'])) {
  1237. return FALSE;
  1238. }
  1239. if (empty($data['reason'])) {
  1240. return FALSE;
  1241. }
  1242. $this->query('POST', 'feedback', $data);
  1243. return $this->lastResponseCode === TRUE ? TRUE : FALSE;
  1244. }
  1245. /**
  1246. * Retrieves the blacklist for a site.
  1247. *
  1248. * @param string $publicKey
  1249. * (optional) The public Mollom API key of the site to retrieve the
  1250. * blacklist for. Defaults to the public key of the client.
  1251. *
  1252. * @return mixed
  1253. * An array containing blacklist entries; see Mollom::getBlacklistEntry()
  1254. * for details. On failure, the error response code returned by the server.
  1255. *
  1256. * @todo List parameters.
  1257. */
  1258. public function getBlacklist($publicKey = NULL) {
  1259. if (!isset($publicKey)) {
  1260. $publicKey = $this->publicKey;
  1261. }
  1262. $publicKey = rawurlencode($publicKey);
  1263. $result = $this->query('GET', 'blacklist/' . $publicKey, array(), array('list'));
  1264. return isset($result['list']) ? $result['list'] : $result;
  1265. }
  1266. /**
  1267. * Retrieves a blacklist entry stored for a site.
  1268. *
  1269. * @param string $entryId
  1270. * The ID of the blacklist entry to retrieve.
  1271. * @param string $publicKey
  1272. * (optional) The public Mollom API key of the site to retrieve the
  1273. * blacklist entry for. Defaults to the public key of the client.
  1274. *
  1275. * @return mixed
  1276. * On success, an associative array containing:
  1277. * - id: The ID the of blacklist entry.
  1278. * - created: A timestamp in seconds since the UNIX epoch of when the entry
  1279. * was created.
  1280. * - value: The blacklisted string/value.
  1281. * - reason: A string denoting the reason for why the value is blacklisted;
  1282. * one of 'spam', 'profanity', 'quality', or 'unwanted'. Defaults to
  1283. * 'unwanted'.
  1284. * - context: A string denoting where the entry's value may match; one of
  1285. * 'allFields', 'links', 'authorName', 'authorMail', 'authorIp',
  1286. * 'authorIp', or 'postTitle'. Defaults to 'allFields'.
  1287. * - match: A string denoting how precise the entry's value may match; one
  1288. * of 'exact' or 'contains'. Defaults to 'contains'.
  1289. * - status: An integer denoting whether the entry is enabled (1) or not
  1290. * (0).
  1291. * - note: A custom string explaining the entry. Useful in a multi-moderator
  1292. * scenario.
  1293. * On failure, the error response code returned by the server.
  1294. */
  1295. public function getBlacklistEntry($entryId, $publicKey = NULL) {
  1296. if (!isset($publicKey)) {
  1297. $publicKey = $this->publicKey;
  1298. }
  1299. $path = 'blacklist/' . rawurlencode($publicKey) . '/' . rawurlencode($entryId);
  1300. $result = $this->query('GET', $path, array(), array('entry', 'id'));
  1301. return isset($result['entry']) ? $result['entry'] : $result;
  1302. }
  1303. /**
  1304. * Creates or updates a blacklist entry for a site.
  1305. *
  1306. * @param array $data
  1307. * An associative array describing the blacklist entry to create or update.
  1308. * See return value of Mollom::getBlacklistEntry() for details. To update
  1309. * an existing entry, its ID must be specified in 'id'.
  1310. * @param string $publicKey
  1311. * (optional) The public Mollom API key of the site to save the blacklist
  1312. * entry for. Defaults to the public key of the client.
  1313. *
  1314. * @return mixed
  1315. * On success, the full blacklist entry record of the saved entry; see
  1316. * Mollom::getBlacklistEntry() for details. On failure, the error response
  1317. * code returned by the server.
  1318. */
  1319. public function saveBlacklistEntry(array $data = array(), $publicKey = NULL) {
  1320. if (!isset($publicKey)) {
  1321. $publicKey = $this->publicKey;
  1322. }
  1323. $path = 'blacklist/' . rawurlencode($publicKey);
  1324. if (!empty($data['id'])) {
  1325. $path .= '/' . rawurlencode($data['id']);
  1326. unset($data['id']);
  1327. }
  1328. $result = $this->query('POST', $path, $data, array('entry', 'id'));
  1329. return isset($result['entry']) ? $result['entry'] : $result;
  1330. }
  1331. /**
  1332. * Deletes a blacklist entry from a site.
  1333. *
  1334. * @param string $entryId
  1335. * The ID of the blacklist entry to delete.
  1336. * @param string $publicKey
  1337. * (optional) The public Mollom API key of the site to create the blacklist
  1338. * entry for. Defaults to the public key of the client.
  1339. *
  1340. * @return bool
  1341. * TRUE on success, FALSE otherwise.
  1342. */
  1343. public function deleteBlacklistEntry($entryId, $publicKey = NULL) {
  1344. if (!isset($publicKey)) {
  1345. $publicKey = $this->publicKey;
  1346. }
  1347. $path = 'blacklist/' . rawurlencode($publicKey) . '/' . rawurlencode($entryId) . '/delete';
  1348. $result = $this->query('POST', $path);
  1349. return $this->lastResponseCode === TRUE;
  1350. }
  1351. /**
  1352. * Generates a URL to a tracking image.
  1353. *
  1354. * This image can be used by form behavior analysis to analyze the
  1355. * humanity of the content author.
  1356. *
  1357. * @return array
  1358. * An associative array of tracking information generated.
  1359. * - tracking_url: the url to the tracking image (without any http protocol)
  1360. * - tracking_id: the tracking id generated
  1361. */
  1362. public function getTrackingImage() {
  1363. $public_key = $this->publicKey;
  1364. $ts = time();
  1365. $tracking_id = hash_hmac('sha1', $public_key . $ts, $this->privateKey);
  1366. $result = array(
  1367. 'tracking_url' => '//' . $this->server . '/v1/tracking/' . urlencode($tracking_id) . '.png?public_key=' . $public_key . "&timestamp=" . $ts,
  1368. 'tracking_id' => $tracking_id,
  1369. );
  1370. return $result;
  1371. }
  1372. }
  1373. /**
  1374. * A catchable Mollom exception.
  1375. *
  1376. * The Mollom class internally uses exceptions to handle HTTP request errors
  1377. * within the Mollom::handleRequest() method. All exceptions thrown in the
  1378. * Mollom class and derived classes should be instances of the MollomException
  1379. * class if they pertain to errors that can be catched/handled within the class.
  1380. * Other errors should not use the MollomException class and handled
  1381. * differently.
  1382. *
  1383. * No MollomException is supposed to pile up as a user-facing fatal error. All
  1384. * functions that invoke Mollom::handleRequest() have to catch Mollom
  1385. * exceptions.
  1386. *
  1387. * @see Mollom::query()
  1388. * @see Mollom::handleRequest()
  1389. *
  1390. * @param $message
  1391. * The Exception message to throw.
  1392. * @param $code
  1393. * The Exception code.
  1394. * @param $previous
  1395. * (optional) The previous Exception, if any.
  1396. * @param $instance
  1397. * The Mollom class instance the Exception is thrown in.
  1398. * @param $arguments
  1399. * (optional) A associative array containing information about a performed
  1400. * HTTP request that failed:
  1401. * - request: (string) The HTTP method and URI of the performed request; e.g.,
  1402. * "GET http://server.mollom.com/v1/foo/bar". In case of GET requests, do
  1403. * not add query parameters to the URI; pass them in 'data' instead.
  1404. * - data: (array) An associative array containing HTTP GET/POST/PUT request
  1405. * query parameters that were sent to the server.
  1406. * - response: (mixed) The server response, either as string, or the already
  1407. * parsed response; i.e., an array.
  1408. */
  1409. class MollomException extends Exception {
  1410. /**
  1411. * @var Mollom
  1412. */
  1413. protected $mollom;
  1414. /**
  1415. * The severity of this exception.
  1416. *
  1417. * By default, all exceptions should be logged and appear as errors (unless
  1418. * overridden by a later log entry).
  1419. *
  1420. * @var string
  1421. */
  1422. protected $severity = 'error';
  1423. /**
  1424. * Overrides Exception::__construct().
  1425. */
  1426. function __construct($message = '', $code = 0, Exception $previous = NULL, Mollom $mollom, array $request_info = array()) {
  1427. // Fatal error on PHP <5.3 when passing more arguments to Exception.
  1428. if (version_compare(phpversion(), '5.3') >= 0) {
  1429. parent::__construct($message, $code, $previous);
  1430. }
  1431. else {
  1432. parent::__construct($message, $code);
  1433. }
  1434. $this->mollom = $mollom;
  1435. // Set the error code on the Mollom class.
  1436. $mollom->lastResponseCode = $code;
  1437. // Log the exception.
  1438. // To aid Mollom technical support, include the IP address of the server we
  1439. // tried to reach in case a request fails.
  1440. // PHP's native gethostbyname() is available on all platforms, but its DNS
  1441. // lookup and caching behavior is undocumented and unclear. User comments on
  1442. // php.net mention that it does not have an own cache and also does not use
  1443. // the OS/platform's native DNS name resolver. Due to that, we only use it
  1444. // under error conditions.
  1445. $message = array(
  1446. 'severity' => $this->severity,
  1447. 'message' => 'Error @code: %message (@server-ip)',
  1448. 'arguments' => array(
  1449. '@code' => $code,
  1450. '%message' => $message,
  1451. '@server-ip' => gethostbyname($mollom->server),
  1452. ),
  1453. );
  1454. // Add HTTP request information, if available.
  1455. if (!empty($request_info)) {
  1456. $message += $request_info;
  1457. }
  1458. $mollom->log[] = $message;
  1459. }
  1460. }
  1461. /**
  1462. * Mollom network error exception.
  1463. *
  1464. * Thrown in case a HTTP request results in code <= 0, denoting a low-level
  1465. * communication error.
  1466. */
  1467. class MollomNetworkException extends MollomException {
  1468. /**
  1469. * Overrides MollomException::$severity.
  1470. *
  1471. * The client may be able to recover from this error, so use a warning level.
  1472. */
  1473. protected $severity = 'warning';
  1474. }
  1475. /**
  1476. * Mollom authentication error exception.
  1477. *
  1478. * Thrown in case API keys or other authentication parameters are invalid.
  1479. */
  1480. class MollomAuthenticationException extends MollomException {
  1481. }
  1482. /**
  1483. * Mollom error due to bad client request exception.
  1484. *
  1485. * Thrown in case the local time diverges too much from UTC.
  1486. *
  1487. * @see Mollom::TIME_OFFSET_MAX
  1488. * @see Mollom::REQUEST_ERROR
  1489. * @see Mollom::handleRequest()
  1490. */
  1491. class MollomBadRequestException extends MollomException {
  1492. }
  1493. /**
  1494. * Mollom server response exception.
  1495. *
  1496. * Thrown when a request to a Mollom server succeeds, but the response does not
  1497. * contain an expected element; e.g., a backend configuration or execution
  1498. * error that possibly exists on one server only.
  1499. *
  1500. * @see Mollom::handleRequest()
  1501. */
  1502. class MollomResponseException extends MollomException {
  1503. /**
  1504. * Overrides MollomException::$severity.
  1505. *
  1506. * Might be a client-side error, but more likely a server-side error. The
  1507. * client may be able to recover from this error.
  1508. */
  1509. protected $severity = 'debug';
  1510. }