mollom.drupal.inc 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. <?php
  2. /**
  3. * @file
  4. * Mollom client class for Drupal.
  5. */
  6. /**
  7. * Drupal Mollom client implementation.
  8. */
  9. class MollomDrupal extends Mollom {
  10. /**
  11. * Overrides the connection timeout based on module configuration.
  12. *
  13. * @see Mollom::__construct().
  14. */
  15. public function __construct() {
  16. // Set any configured endpoint that may be different from the default.
  17. parent::__construct();
  18. $configured_server = $this->loadConfiguration('server');
  19. if (!empty($configured_server)) {
  20. $this->server = $configured_server;
  21. }
  22. $this->requestTimeout = variable_get('mollom_connection_timeout', 3);
  23. }
  24. /**
  25. * Mapping of configuration names to Drupal variables.
  26. *
  27. * @see Mollom::loadConfiguration()
  28. */
  29. public $configuration_map = array(
  30. 'publicKey' => 'mollom_public_key',
  31. 'privateKey' => 'mollom_private_key',
  32. 'expectedLanguages' => 'mollom_languages_expected',
  33. 'server' => 'mollom_api_endpoint',
  34. );
  35. /**
  36. * Implements Mollom::loadConfiguration().
  37. */
  38. public function loadConfiguration($name) {
  39. $name = $this->configuration_map[$name];
  40. return variable_get($name);
  41. }
  42. /**
  43. * Implements Mollom::saveConfiguration().
  44. */
  45. public function saveConfiguration($name, $value) {
  46. // Set local variable.
  47. if (property_exists('MollomDrupal', $name)) {
  48. $this->{$name} = $value;
  49. }
  50. // Persist in Drupal.
  51. $name = $this->configuration_map[$name];
  52. return variable_set($name, $value);
  53. }
  54. /**
  55. * Implements Mollom::deleteConfiguration().
  56. */
  57. public function deleteConfiguration($name) {
  58. $name = $this->configuration_map[$name];
  59. return variable_del($name);
  60. }
  61. /**
  62. * Implements Mollom::getClientInformation().
  63. */
  64. public function getClientInformation() {
  65. // Retrieve Drupal distribution and installation profile information.
  66. $profile = drupal_get_profile();
  67. $profile_info = system_get_info('module', $profile) + array(
  68. 'distribution_name' => 'Drupal',
  69. 'version' => VERSION,
  70. );
  71. // Retrieve Mollom module information.
  72. $mollom_info = system_get_info('module', 'mollom');
  73. if (empty($mollom_info['version'])) {
  74. // Manually build a module version string for repository checkouts.
  75. $mollom_info['version'] = DRUPAL_CORE_COMPATIBILITY . '-2.x-dev';
  76. }
  77. $data = array(
  78. 'platformName' => $profile_info['distribution_name'],
  79. 'platformVersion' => $profile_info['version'],
  80. 'clientName' => $mollom_info['name'],
  81. 'clientVersion' => $mollom_info['version'],
  82. );
  83. return $data;
  84. }
  85. /**
  86. * Overrides Mollom::writeLog().
  87. */
  88. function writeLog() {
  89. foreach ($this->log as $entry) {
  90. $entry['Request: ' . $entry['request']] = !empty($entry['data']) ? $entry['data'] : NULL;
  91. unset($entry['request'], $entry['data']);
  92. $entry['Request headers:'] = $entry['headers'];
  93. unset($entry['headers']);
  94. $entry['Response: ' . $entry['response_code'] . ' ' . $entry['response_message'] . ' (' . number_format($entry['response_time'], 3) . 's)'] = $entry['response'];
  95. unset($entry['response'], $entry['response_code'], $entry['response_message'], $entry['response_time']);
  96. // The client class contains the logic for recovering from certain errors,
  97. // and log messages are only written after that happened. Therefore, we
  98. // can normalize the severity of all log entries to the overall success or
  99. // failure of the attempted request.
  100. // @see Mollom::query()
  101. mollom_log($entry, $this->lastResponseCode === TRUE ? NULL : WATCHDOG_ERROR);
  102. }
  103. // After writing log messages, empty the log.
  104. $this->purgeLog();
  105. }
  106. /**
  107. * Implements Mollom::request().
  108. */
  109. protected function request($method, $server, $path, $query = NULL, array $headers = array()) {
  110. $request = array(
  111. 'method' => $method,
  112. 'headers' => $headers,
  113. 'timeout' => $this->requestTimeout,
  114. );
  115. if (isset($query)) {
  116. if ($method == 'GET') {
  117. $path .= '?' . $query;
  118. }
  119. elseif ($method == 'POST') {
  120. $request['data'] = $query;
  121. }
  122. }
  123. $dhr = drupal_http_request($server . '/' . $path, $request);
  124. // @todo Core: Ensure that $dhr->code is an integer.
  125. $dhr->code = (int) $dhr->code;
  126. // @todo Core: Any other code than 200 is interpreted as error.
  127. if ($dhr->code >= 200 && $dhr->code < 300) {
  128. unset($dhr->error);
  129. }
  130. // @todo Core: data property is not assigned if there is no response body.
  131. if (!isset($dhr->data)) {
  132. $dhr->data = NULL;
  133. }
  134. // @todo Core: Timeout produces a bogus non-negative status code.
  135. // @see http://drupal.org/node/1246376
  136. if ($dhr->code === 1) {
  137. $dhr->code = -1;
  138. }
  139. $response = (object) array(
  140. 'code' => $dhr->code,
  141. 'message' => isset($dhr->error) ? $dhr->error : NULL,
  142. 'headers' => isset($dhr->headers) ? $dhr->headers : array(),
  143. 'body' => $dhr->data,
  144. );
  145. return $response;
  146. }
  147. /**
  148. * Retrieves GET/HEAD or POST/PUT parameters of an inbound request.
  149. *
  150. * @return array
  151. * An array containing either GET/HEAD query string parameters or POST/PUT
  152. * post body parameters. Parameter parsing accounts for multiple request
  153. * parameters in non-PHP format; e.g., 'foo=one&foo=bar'.
  154. */
  155. public static function getServerParameters() {
  156. $data = parent::getServerParameters();
  157. if ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD') {
  158. // Remove $_GET['q'].
  159. unset($data['q']);
  160. }
  161. return $data;
  162. }
  163. }
  164. /**
  165. * Drupal Mollom client implementation using testing API servers.
  166. */
  167. class MollomDrupalTest extends MollomDrupal {
  168. /**
  169. * Overrides Mollom::$server.
  170. */
  171. public $server = 'dev.mollom.com';
  172. /**
  173. * Flag indicating whether to verify and automatically create testing API keys upon class instantiation.
  174. *
  175. * @var bool
  176. */
  177. public $createKeys;
  178. /**
  179. * Overrides Mollom::__construct().
  180. *
  181. * This class accounts for multiple scenarios:
  182. * - Straight low-level requests against the testing API from a custom script,
  183. * caring for API keys on its own.
  184. * - Whenever the testing mode is enabled (either through the module's
  185. * settings page or by changing the mollom_testing_mode system variable),
  186. * the client requires valid testing API keys to perform any calls. Testing
  187. * API keys are different to production API keys, need to be created first,
  188. * and may vanish at any time (whenever the testing API server is
  189. * redeployed). Since they are different, the class stores them in different
  190. * system variables. Since they can vanish at any time, the class verifies
  191. * the keys upon every instantiation, and automatically creates new testing
  192. * API keys if necessary.
  193. * - Some automated unit tests attempt to verify that authentication errors
  194. * are handled correctly by the class' error handling. The automatic
  195. * creation and recovery of testing API keys would break those assertions,
  196. * so said tests can disable the behavior by preemptively setting
  197. * $createKeys or the 'mollom_testing_create_keys' system variable to FALSE,
  198. * and manually create testing API keys (once).
  199. */
  200. function __construct() {
  201. // Some tests are verifying the production behavior of e.g. setting up API
  202. // keys, in which testing mode is NOT enabled and the test creates fake
  203. // "production" API keys on the local fake server on its own. This special
  204. // override must only be possible when executing tests.
  205. // @todo Add global test_info as condition?
  206. if (module_exists('mollom_test_server') && !variable_get('mollom_testing_mode', 0)) {
  207. // Disable authentication error auto-recovery.
  208. $this->createKeys = FALSE;
  209. }
  210. else {
  211. // Do not destroy production variables when testing mode is enabled.
  212. $this->configuration_map['publicKey'] = 'mollom_test_public_key';
  213. $this->configuration_map['privateKey'] = 'mollom_test_private_key';
  214. $this->configuration_map['server'] = 'mollom_test_api_endpoint';
  215. }
  216. // Load and set publicKey and privateKey configuration values.
  217. parent::__construct();
  218. // Unless pre-set, determine whether API keys should be auto-created.
  219. if (!isset($this->createKeys)) {
  220. $this->createKeys = (bool) variable_get('mollom_testing_create_keys', TRUE);
  221. }
  222. // Testing can require additional time.
  223. $this->requestTimeout = variable_get('mollom_connection_timeout', 3) + 10;
  224. }
  225. /**
  226. * Overrides Mollom::handleRequest().
  227. *
  228. * Automatically tries to generate new API keys in case of a 401 or 404 error.
  229. * Intentionally reacts on 401 or 404 errors only, since any other error code
  230. * can mean that either the Testing API is down or that the client site is not
  231. * able to perform outgoing HTTP requests in general.
  232. */
  233. protected function handleRequest($method, $server, $path, $data, $expected = array()) {
  234. try {
  235. $response = parent::handleRequest($method, $server, $path, $data, $expected);
  236. }
  237. catch (MollomException $e) {
  238. $is_auth_error = $e->getCode() == self::AUTH_ERROR || ($e->getCode() == 404 && strpos($path, 'site') === 0);
  239. $current_public_key = $this->publicKey;
  240. if ($this->createKeys && $is_auth_error && $this->createKeys()) {
  241. $this->saveKeys();
  242. // Avoid to needlessly hit the previous/invalid public key again.
  243. // Mollom::handleRequest() will sign the new request correctly.
  244. // If it was empty, Mollom::handleRequest() returned an AUTH_ERROR
  245. // without issuing a request.
  246. if ($path == 'site/') {
  247. $path = 'site/' . $this->publicKey;
  248. }
  249. elseif (!empty($current_public_key)) {
  250. $path = str_replace($current_public_key, $this->publicKey, $path);
  251. }
  252. $response = parent::handleRequest($method, $server, $path, $data, $expected);
  253. }
  254. else {
  255. throw $e;
  256. }
  257. }
  258. return $response;
  259. }
  260. /**
  261. * Creates new testing API keys.
  262. *
  263. * @todo Move site properties into $data argument (Drupal-specific values),
  264. * rename to createTestingSite(), and move into Mollom class?
  265. */
  266. public function createKeys() {
  267. // Do not attempt to create API keys repeatedly.
  268. $this->createKeys = FALSE;
  269. // Without any API keys, the client does not even attempt to perform a
  270. // request. Set dummy API keys to overcome that sanity check.
  271. $this->publicKey = 'public';
  272. $this->privateKey = 'private';
  273. // Skip authorization for creating testing API keys.
  274. $oAuthStrategy = $this->oAuthStrategy;
  275. $this->oAuthStrategy = '';
  276. $result = $this->createSite(array(
  277. 'url' => $GLOBALS['base_url'],
  278. 'email' => variable_get('site_mail', 'mollom-drupal-test@example.com'),
  279. ));
  280. $this->oAuthStrategy = $oAuthStrategy;
  281. // Set class properties.
  282. if (is_array($result) && !empty($result['publicKey']) && !empty($result['privateKey'])) {
  283. $this->publicKey = $result['publicKey'];
  284. $this->privateKey = $result['privateKey'];
  285. return TRUE;
  286. }
  287. else {
  288. $this->publicKey = $this->privateKey = '';
  289. return FALSE;
  290. }
  291. }
  292. /**
  293. * Saves API keys to local configuration store.
  294. */
  295. public function saveKeys() {
  296. $this->saveConfiguration('publicKey', $this->publicKey);
  297. $this->saveConfiguration('privateKey', $this->privateKey);
  298. }
  299. /**
  300. * Deletes API keys from local configuration store.
  301. */
  302. public function deleteKeys() {
  303. $this->deleteConfiguration('publicKey');
  304. $this->deleteConfiguration('privateKey');
  305. }
  306. }
  307. /**
  308. * Drupal Mollom client implementation using local dummy/fake REST server.
  309. */
  310. class MollomDrupalTestLocal extends MollomDrupalTest {
  311. /**
  312. * Overrides MollomDrupalTest::__construct().
  313. */
  314. function __construct() {
  315. // Replace server/endpoint with our local fake server.
  316. list(, $server) = explode('://', $GLOBALS['base_url'], 2);
  317. $this->server = $server . '/mollom-test/rest';
  318. // Do not destroy production server variables when testing mode is enabled.
  319. $this->configuration_map['server'] = 'mollom_test_local_api_endpoint';
  320. parent::__construct();
  321. }
  322. /**
  323. * Overrides MollomDrupal::saveKeys().
  324. */
  325. public function saveKeys() {
  326. parent::saveKeys();
  327. // Ensure that the site exists on the local fake server. Not required for
  328. // remote REST testing API, because our testing API keys persist there.
  329. // @see mollom_test_server_rest_site()
  330. $bin = 'mollom_test_server_site';
  331. $sites = variable_get($bin, array());
  332. if (!isset($sites[$this->publicKey])) {
  333. // Apply default values.
  334. $sites[$this->publicKey] = array(
  335. 'publicKey' => $this->publicKey,
  336. 'privateKey' => $this->privateKey,
  337. 'url' => '',
  338. 'email' => '',
  339. );
  340. variable_set($bin, $sites);
  341. }
  342. }
  343. /**
  344. * Overrides MollomDrupal::request().
  345. *
  346. * Passes-through SimpleTest assertion HTTP headers from child-child-site and
  347. * triggers errors to make them appear in parent site (where tests are ran).
  348. *
  349. * @todo Remove when in core.
  350. * @see http://drupal.org/node/875342
  351. */
  352. protected function request($method, $server, $path, $query = NULL, array $headers = array()) {
  353. $response = parent::request($method, $server, $path, $query, $headers);
  354. $keys = preg_grep('@^x-drupal-assertion-@', array_keys($response->headers));
  355. foreach ($keys as $key) {
  356. $header = $response->headers[$key];
  357. $header = unserialize(urldecode($header));
  358. $message = strtr('%type: !message in %function (line %line of %file).', array(
  359. '%type' => $header[1],
  360. '!message' => $header[0],
  361. '%function' => $header[2]['function'],
  362. '%line' => $header[2]['line'],
  363. '%file' => $header[2]['file'],
  364. ));
  365. trigger_error($message, E_USER_ERROR);
  366. }
  367. return $response;
  368. }
  369. }
  370. /**
  371. * Drupal Mollom client implementation using an invalid server.
  372. */
  373. class MollomDrupalTestInvalid extends MollomDrupalTest {
  374. /**
  375. * Overrides MollomDrupalTest::$createKeys.
  376. *
  377. * Do not attempt to verify API keys against invalid server.
  378. */
  379. public $createKeys = FALSE;
  380. private $currentAttempt = 0;
  381. private $originalServer;
  382. /**
  383. * Overrides MollomDrupalTest::__construct().
  384. */
  385. function __construct() {
  386. parent::__construct();
  387. $this->originalServer = $this->server;
  388. $this->configuration_map['server'] = 'mollom_test_invalid_api_endpoint';
  389. $this->saveConfiguration('server', 'fake-host');
  390. $this->server = 'fake-host';
  391. }
  392. /**
  393. * Overrides Mollom::query().
  394. */
  395. public function query($method, $path, array $data = array(), array $expected = array()) {
  396. $this->currentAttempt = 0;
  397. return parent::query($method, $path, $data, $expected);
  398. }
  399. /**
  400. * Overrides Mollom::handleRequest().
  401. *
  402. * Mollom::$server is replaced with an invalid server, so all requests will
  403. * result in a network error. However, if the 'mollom_testing_server_failover'
  404. * variable is set to TRUE, then the last request attempt will succeed.
  405. */
  406. protected function handleRequest($method, $server, $path, $data, $expected = array()) {
  407. $this->currentAttempt++;
  408. if (variable_get('mollom_testing_server_failover', FALSE) && $this->currentAttempt == $this->requestMaxAttempts) {
  409. // Prior to PHP 5.3, there is no late static binding, so there is no way
  410. // to access the original value of MollomDrupalTest::$server.
  411. $server = strtr($server, array($this->server => $this->originalServer));
  412. }
  413. return parent::handleRequest($method, $server, $path, $data, $expected);
  414. }
  415. }