diff --git includes/LICENSE.txt includes/LICENSE.txt new file mode 100644 index 0000000..10a0692 --- /dev/null +++ includes/LICENSE.txt @@ -0,0 +1,276 @@ +GNU GENERAL PUBLIC LICENSE + + Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The licenses for most software are designed to take away your freedom to +share and change it. By contrast, the GNU General Public License is +intended to guarantee your freedom to share and change free software--to +make sure the software is free for all its users. This General Public License +applies to most of the Free Software Foundation's software and to any other +program whose authors commit to using it. (Some other Free Software +Foundation software is covered by the GNU Library General Public License +instead.) You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the +freedom to distribute copies of free software (and charge for this service if +you wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of the +software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for +a fee, you must give the recipients all the rights that you have. You must make +sure that they, too, receive or can get the source code. And you must show +them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If the +software is modified by someone else and passed on, we want its recipients +to know that what they have is not the original, so that any problems +introduced by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will individually +obtain patent licenses, in effect making the program proprietary. To prevent +this, we have made it clear that any patent must be licensed for everyone's +free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND + MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms +of this General Public License. The "Program", below, refers to any such +program or work, and a "work based on the Program" means either the +Program or any derivative work under copyright law: that is to say, a work +containing the Program or a portion of it, either verbatim or with +modifications and/or translated into another language. (Hereinafter, translation +is included without limitation in the term "modification".) Each licensee is +addressed as "you". + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running the Program is +not restricted, and the output from the Program is covered only if its contents +constitute a work based on the Program (independent of having been made +by running the Program). Whether that is true depends on what the Program +does. + +1. You may copy and distribute verbatim copies of the Program's source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and +disclaimer of warranty; keep intact all the notices that refer to this License +and to the absence of any warranty; and give any other recipients of the +Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, +thus forming a work based on the Program, and copy and distribute such +modifications or work under the terms of Section 1 above, provided that you +also meet all of these conditions: + +a) You must cause the modified files to carry prominent notices stating that +you changed the files and the date of any change. + +b) You must cause any work that you distribute or publish, that in whole or in +part contains or is derived from the Program or any part thereof, to be +licensed as a whole at no charge to all third parties under the terms of this +License. + +c) If the modified program normally reads commands interactively when run, +you must cause it, when started running for such interactive use in the most +ordinary way, to print or display an announcement including an appropriate +copyright notice and a notice that there is no warranty (or else, saying that +you provide a warranty) and that users may redistribute the program under +these conditions, and telling the user how to view a copy of this License. +(Exception: if the Program itself is interactive but does not normally print such +an announcement, your work based on the Program is not required to print +an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be +reasonably considered independent and separate works in themselves, then +this License, and its terms, do not apply to those sections when you distribute +them as separate works. But when you distribute the same sections as part +of a whole which is a work based on the Program, the distribution of the +whole must be on the terms of this License, whose permissions for other +licensees extend to the entire whole, and thus to each and every part +regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to +work written entirely by you; rather, the intent is to exercise the right to +control the distribution of derivative or collective works based on the +Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of a +storage or distribution medium does not bring the other work under the scope +of this License. + +3. You may copy and distribute the Program (or a work based on it, under +Section 2) in object code or executable form under the terms of Sections 1 +and 2 above provided that you also do one of the following: + +a) Accompany it with the complete corresponding machine-readable source +code, which must be distributed under the terms of Sections 1 and 2 above +on a medium customarily used for software interchange; or, + +b) Accompany it with a written offer, valid for at least three years, to give +any third party, for a charge no more than your cost of physically performing +source distribution, a complete machine-readable copy of the corresponding +source code, to be distributed under the terms of Sections 1 and 2 above on +a medium customarily used for software interchange; or, + +c) Accompany it with the information you received as to the offer to distribute +corresponding source code. (This alternative is allowed only for +noncommercial distribution and only if you received the program in object +code or executable form with such an offer, in accord with Subsection b +above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source code +means all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation and +installation of the executable. However, as a special exception, the source +code distributed need not include anything that is normally distributed (in +either source or binary form) with the major components (compiler, kernel, +and so on) of the operating system on which the executable runs, unless that +component itself accompanies the executable. + +If distribution of executable or object code is made by offering access to +copy from a designated place, then offering equivalent access to copy the +source code from the same place counts as distribution of the source code, +even though third parties are not compelled to copy the source along with the +object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as +expressly provided under this License. Any attempt otherwise to copy, +modify, sublicense or distribute the Program is void, and will automatically +terminate your rights under this License. However, parties who have received +copies, or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. +However, nothing else grants you permission to modify or distribute the +Program or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the +Program (or any work based on the Program), you indicate your acceptance +of this License to do so, and all its terms and conditions for copying, +distributing or modifying the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the original +licensor to copy, distribute or modify the Program subject to these terms and +conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), conditions +are imposed on you (whether by court order, agreement or otherwise) that +contradict the conditions of this License, they do not excuse you from the +conditions of this License. If you cannot distribute so as to satisfy +simultaneously your obligations under this License and any other pertinent +obligations, then as a consequence you may not distribute the Program at all. +For example, if a patent license would not permit royalty-free redistribution +of the Program by all those who receive copies directly or indirectly through +you, then the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or +other property right claims or to contest validity of any such claims; this +section has the sole purpose of protecting the integrity of the free software +distribution system, which is implemented by public license practices. Many +people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Program under this License may add an explicit +geographical distribution limitation excluding those countries, so that +distribution is permitted only in or among countries not thus excluded. In such +case, this License incorporates the limitation as if written in the body of this +License. + +9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will be +similar in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that +version or of any later version published by the Free Software Foundation. If +the Program does not specify a version number of this License, you may +choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make +exceptions for this. Our decision will be guided by the two goals of +preserving the free status of all derivatives of our free software and of +promoting the sharing and reuse of software generally. + + NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT +PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT +WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL +NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR +AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR +ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE +LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, +SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE +PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES +SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN +IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF +THE POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS diff --git includes/README.txt includes/README.txt new file mode 100644 index 0000000..d8837ae --- /dev/null +++ includes/README.txt @@ -0,0 +1,24 @@ + +-- SUMMARY -- + +A generic Mollom client PHP class. + +To submit bug reports and feature suggestions, or to track changes: + http://drupal.org/project/issues/mollom + + +-- REQUIREMENTS -- + +* PHP 5 + + +-- USAGE -- + +* @todo + + +-- LICENSE -- + +The Mollom class is released under the GNU General Public License, Version 2. +See LICENSE.txt. + diff --git includes/mollom.class.inc includes/mollom.class.inc new file mode 100644 index 0000000..d190672 --- /dev/null +++ includes/mollom.class.inc @@ -0,0 +1,1283 @@ += 0) { + parent::__construct($message, $code, $previous); + } + else { + parent::__construct($message, $code); + } + + // Set the error code on the Mollom class. + $mollom->lastResponseCode = $code; + + // Log the exception. + $message = array( + 'type' => $this->severity, + 'message' => 'Error @code: %message', + 'arguments' => array( + '@code' => $code, + '%message' => $message, + ), + ); + // Add HTTP request information, if available. + if (!empty($arguments)) { + $message += array( + 'request' => $arguments['request'], + 'data' => $arguments['data'], + 'response' => $arguments['response'], + ); + } + $mollom->log[] = $message; + } +} + +/** + * Mollom network error exception. + * + * Thrown in case a HTTP request results in code <= 0, denoting a low-level + * communication error. + */ +class MollomNetworkException extends MollomException { + protected $severity = 'error'; +} + +/** + * Mollom authentication error exception. + * + * Thrown in case API keys or other authentication parameters are invalid. + */ +class MollomAuthenticationException extends MollomException { + protected $severity = 'error'; +} + +/** + * Mollom server refresh exception. + * + * Thrown when a Mollom server asks the client to update the server list. + */ +class MollomRefreshException extends MollomException { + protected $severity = 'debug'; +} + +/** + * Mollom server redirect exception. + * + * Thrown when a Mollom server asks the client to use next server in the server + * list. + */ +class MollomRedirectException extends MollomException { + protected $severity = 'debug'; +} + +/** + * Mollom server response exception. + * + * Thrown when a request to a Mollom server succeeds, but the response does not + * contain an expected element; e.g., a backend configuration or execution + * error that possibly exists on one server only. + * + * @see Mollom::request() + */ +class MollomResponseException extends MollomException { + protected $severity = 'debug'; +} + +/** + * The base class for Mollom client implementations. + */ +abstract class Mollom { + /** + * The Mollom API version, used in HTTP requests. + */ + const API_VERSION = 'v1'; + + /** + * Network communication failure code: No servers could be reached. + * + * @see MollomNetworkException + */ + const NETWORK_ERROR = 900; + + /** + * Server communication failure code: Unexpected server response. + * + * Using the 5xx HTTP status code range, but not re-using an existing HTTP + * code to prevent bogus bug reports. 511 is the closest comparable code + * 501 (Not Implemented) plus 10. + * + * @see MollomResponseException + */ + const RESPONSE_ERROR = 511; + + /** + * Server communication failure code: Authentication error. + * + * @see MollomAuthenticationException + */ + const AUTH_ERROR = 1000; + + /** + * Server communication failure code: Client is asked to update the server list. + * + * @see MollomRefreshException + */ + const REFRESH_ERROR = 1100; + + /** + * Server communication failure code: Client is asked to try next server. + * + * @see MollomRedirectException + */ + const REDIRECT_ERROR = 1200; + + /** + * The public Mollom API key to use for request authentication. + * + * @var string + */ + public $publicKey = ''; + + /** + * The private Mollom API key to use for request authentication. + * + * @var string + */ + public $privateKey = ''; + + /** + * The list of Mollom servers to communicate with, as returned by Mollom. + * + * @var array + * + * @see Mollom::getServers() + */ + public $servers = array(); + + /** + * A hard-coded list of Mollom servers to fetch the server list from. + * + * @var array + */ + public $serversInit = array('http://rest.mollom.com'); + + /** + * The status code of the last response or TRUE if it succeeded. + * + * @var int|bool|null + */ + public $lastResponseCode = NULL; + + /** + * A list of logged requests. + * + * @var array + */ + public $log = array(); + + function __construct() { + $this->publicKey = $this->loadConfiguration('publicKey'); + $this->privateKey = $this->loadConfiguration('privateKey'); + } + + /** + * Loads a configuration value from client-side storage. + * + * @param string $name + * The configuration setting name to load, one of: + * - publicKey: The public API key for Mollom authentication. + * - privateKey: The private API key for Mollom authentication. + * - servers: An indexed array of Mollom server addresses. + * + * @return mixed + * The stored configuration value or NULL if there is none. + * + * @see Mollom::saveConfiguration() + * @see Mollom::deleteConfiguration() + */ + abstract public function loadConfiguration($name); + + /** + * Saves a configuration value to client-side storage. + * + * @param string $name + * The configuration setting name to save. + * @param mixed $value + * The value to save. + * + * @see Mollom::loadConfiguration() + * @see Mollom::deleteConfiguration() + */ + abstract public function saveConfiguration($name, $value); + + /** + * Deletes a configuration value from client-side storage. + * + * @param string $name + * The configuration setting name to delete. + * + * @see Mollom::loadConfiguration() + * @see Mollom::saveConfiguration() + */ + abstract public function deleteConfiguration($name); + + /** + * Returns platform and version information about the Mollom client. + * + * Retrieves platform and Mollom client version information to send along to + * Mollom when verifying keys. + * + * This information is used to speed up support requests and technical + * inquiries. The data may also be aggregated to help the Mollom staff to make + * decisions on new features or the necessity of back-porting improved + * functionality to older versions. + * + * @return array + * An associative array containing: + * - platformName: The name of the platform/distribution; e.g., "Drupal". + * - platformVersion: The version of platform/distribution; e.g., "7.0". + * - clientName: The official Mollom client name; e.g., "Mollom". + * - clientVersion: The version of the Mollom client; e.g., "7.x-1.0". + */ + abstract public function getClientInformation(); + + /** + * Writes log messages to a permanent location/storage. + * + * Not abstract, since clients are not required to write log messages. + * However, all clients should permanently store the log messages, as it + * dramatically improves resolution of support requests filed by users. + * The log may be written and appended to a file (via file_put_contents()), + * syslog (on *nix-based systems), or a database. + * + * @see Mollom::log + */ + public function writeLog() { + // After writing log messages, empty the log. + $this->purgeLog(); + } + + /** + * Purges captured log messages. + * + * @see Mollom::writeLog() + */ + final public function purgeLog() { + $this->log = array(); + } + + /** + * Returns the current request time as UNIX timestamp. + * + * @return integer + */ + public function getRequestTime() { + return $_SERVER['REQUEST_TIME']; + } + + /** + * Generates authentication parameters for communication with Mollom servers. + * + * This function generates an array with all information required to + * authenticate against Mollom. To prevent forged requests where you are + * impersonated, each request is signed with a hash based on a private + * key and a timestamp. + * + * Both the client and the server share the secret key used to create + * the authentication hash. They both hash a timestamp with the secret + * key, and if the hashes match, the authenticity of the message is + * validated. + * + * To avoid someone intercepting a (hash, timestamp)-pair and using it + * to impersonate a client, Mollom rejects any request where the timestamp + * is more than 15 minutes off. + * + * Make sure your server's time is synchronized with the world clocks, + * and that you don't share your private key with anyone else. + * + * @return array + * An associative array containing HMAC authentication request parameters + * to be sent to Mollom. + * + * @throws MollomAuthenticationException + */ + public function getAuthentication() { + if (empty($this->publicKey) || empty($this->privateKey)) { + throw new MollomAuthenticationException('Missing API keys.', self::AUTH_ERROR, NULL, $this); + } + + // Generate a timestamp according to the dateTime format. + // @see http://www.w3.org/TR/xmlschema-2/#dateTime + $time = gmdate("Y-m-d\TH:i:s.\\0\\0\\0O", $this->getRequestTime()); + + // Generate a random number. + $nonce = md5(mt_rand()); + + // Calculate a HMAC-SHA1 according to RFC 2104. + // @see http://www.ietf.org/rfc/rfc2104.txt + $request_data = $time . ':' . $nonce . ':' . $this->privateKey; + $hash = base64_encode(hash_hmac('sha1', $request_data, $this->privateKey, TRUE)); + + // Return HMAC authentication parameters as a keyed array. + $data['publicKey'] = $this->publicKey; + $data['time'] = $time; + $data['hash'] = $hash; + $data['nonce'] = $nonce; + + return $data; + } + + /** + * Fetches Mollom servers from local configuration or retrieves a new list. + * + * @return + * An indexed array of Mollom servers, which are also assigned to + * $this->servers. + * + * @see Mollom::refreshServers() + */ + public function getServers() { + // If there is no server list yet, consult the local configuration. + if (empty($this->servers)) { + $servers = $this->loadConfiguration('servers'); + // Use the local configuration value, if any. + if (!empty($servers) && is_array($servers)) { + $this->servers = $servers; + } + // Otherwise, retrieve a new server list from Mollom. + else { + $this->servers = $this->refreshServers(); + if ($this->servers) { + $this->saveConfiguration('servers', $this->servers); + $this->log[] = array( + 'type' => 'debug', + 'message' => 'Refreshed servers: %servers', + 'arguments' => array( + '%servers' => implode(', ', $this->servers), + ), + ); + } + } + } + return $this->servers; + } + + /** + * Returns a new server list retrieved from Mollom. + * + * The server list returned from Mollom should be stored as configuration + * value on the client-side. This method should only be called when the + * configuration value is not yet or no longer available, or when a Mollom + * server specifically asks the client to refresh its server list. + * + * @return array + * An indexed array of server URLs retrieved from Mollom. + * + * @see Mollom::getServers() + * @see Mollom::query() + * @see MollomRefreshException + * @see Mollom::REFRESH_ERROR + */ + protected function refreshServers() { + // refreshServers() cannot use query() as we need to prevent infinite + // recursion. In addition, we handle returned error codes differently here, + // since REDIRECT_ERROR, REFRESH_ERROR, and any other communication error + // requires us to skip to the next server in order to retrieve a new server + // list. We only ever abort, if we get an AUTH_ERROR, in which case there + // is a configuration error (i.e., invalid API keys). + $servers = array(); + try { + $data = $this->getAuthentication(); + } + catch (MollomAuthenticationException $e) { + return $servers; + } + + $path = 'site/' . $this->publicKey; + $expected = array('site', 'servers'); + foreach ($this->serversInit as $server) { + try { + $result = $this->request('GET', $server . '/' . self::API_VERSION, $path, $data, $expected); + } + catch (MollomAuthenticationException $e) { + // Bogus configuration. Stop trying, since all servers will fail. + break; + } + catch (MollomException $e) { + // On any other error, skip to the next server. + continue; + } + + if (isset($result) && $this->lastResponseCode === TRUE) { + $servers = $result['site']['servers']; + break; + } + } + return $servers; + } + + /** + * Retrieve or send data from/to Mollom servers. + * + * @param string $method + * The HTTP method to use; i.e., 'GET', 'POST', or 'PUT'. + * @param string $path + * The REST path/resource to request; e.g., 'site/1a2b3c'. + * @param array $data + * An associative array of query parameters to send with the request. + * @param array $expected + * (optional) An element that is expected in the response, denoted as a list + * of parent element keys to the element and the element key itself; e.g., a + * value of array('site', 'servers') expects $response['site']['servers'] to + * exist in the response. + * + * @return mixed + * On success, the parsed response body. On failure, the last response code, + * in case it is a known one; otherwise Mollom::NETWORK_ERROR. + * + * @see Mollom::request() + */ + protected function query($method, $path, array $data = array(), array $expected = array()) { + // Retrieve server list. + // If we get no list, we will have no servers to iterate over and only the + // error logic remains. + $this->getServers(); + + // Unconditionally add HMAC request authentication parameters. + try { + $data += $this->getAuthentication(); + } + catch (MollomAuthenticationException $e) { + // MollomAuthenticationException sets the appropriate error code. Merely + // catch the exception, move on to error logic below. + // Prevent any requests from being performed in case we have servers + // loaded from configuration or due to some other edge-case (scripting). + $this->servers = array(); + } + + // Initialize refresh variable. + $refresh = FALSE; + // Send the request to the first server; if that fails, try the other + // servers in the list. + // @todo The Mollom instance "sticks" now, as it's statically cached. Thus, + // the array cursor in $this->servers is retained across multiple queries. + // I.e., subsequent queries will continue to communicate with the + // "current" server, which might not be the first. + // PRO: In a scenario that uses the statically cached class instance + // (low-level scripts and possibly subsequent form submissions in the + // future) and in which a server redirects, subsequent queries will + // continue to use the current/second/next server in the list instead of + // restarting on the first; i.e., potentially less "wasted" requests. + // CON: Higher chance to reach the end of the server list, unless we allow + // to iterate two times over the server list until we consider a request + // to fail. + while ($server = current($this->servers)) { + try { + $result = $this->request($method, $server, $path, $data, $expected); + } + catch (MollomRefreshException $e) { + // Prevent infinite loops. + if (!$refresh) { + $refresh = TRUE; + + // In any case, the current server list is no longer valid. + $this->servers = array(); + $this->deleteConfiguration('servers'); + + // Retrieve a fresh list of Mollom servers. + $this->servers = $this->getServers(); + // If API keys are invalid, we won't be able to get a new server list. + // To reach this, we must have had a server list (and therefore + // valid keys) before, so we do not immediately return, but trigger + // the fallback mode instead. + if (empty($this->servers)) { + break; + } + } + } + catch (MollomRedirectException $e) { + // Try the next server in the list. + $next = next($this->servers); + + // @todo $next may be FALSE, confusing users looking into logs. + $this->log[] = array( + 'type' => 'debug', + 'message' => 'Server %server redirected to %next.', + 'arguments' => array( + '%server' => $server, + '%next' => $next, + ), + ); + continue; + } + catch (MollomAuthenticationException $e) { + // This is an irrecoverable error, so don't try other servers. + break; + } + catch (MollomException $e) { + // If the resource does not exist, there is no point in trying another + // server. + if ($e->getCode() == 404) { + break; + } + // On any other known error, try the next server. + next($this->servers); + continue; + } + + // Unless we have a positive result, continue to next server. + if ($this->lastResponseCode === TRUE) { + break; + } + else { + next($this->servers); + } + } + + // In case all servers failed, reset the server list to enforce retrieval of + // a new list the next time. + if (current($this->servers) === FALSE) { + $this->servers = array(); + $this->deleteConfiguration('servers'); + + $this->log[] = array( + 'type' => 'error', + 'message' => 'All servers unreachable or returning errors. The server list was emptied: %servers', + 'arguments' => array( + '%servers' => implode(', ', $this->servers ? $this->servers : $this->serversInit), + ), + ); + } + + // Write all captured log messages. + $this->writeLog(); + + // If there is a result (only possible with a server list) and the last + // request succeeded, return the result to the caller. + if (isset($result) && $this->lastResponseCode === TRUE) { + return $result; + } + // If the last request succeeded but there was a unexpected response, return + // the error code. + if ($this->lastResponseCode == self::RESPONSE_ERROR) { + return $this->lastResponseCode; + } + // Return an authentication error, which may require special client-side + // processing. + if ($this->lastResponseCode == self::AUTH_ERROR) { + return $this->lastResponseCode; + } + // Return a not found error, which always needs to be handled by the calling + // code. + if ($this->lastResponseCode == 404) { + return $this->lastResponseCode; + } + + // In case of any kind of HTTP error (404, 0 [invalid-address], + // -1002 [bad URI], etc), return a generic NETWORK_ERROR. + return self::NETWORK_ERROR; + } + + /** + * Performs a HTTP request to a Mollom server. + * + * @param string $method + * The HTTP method to use; i.e., 'GET', 'POST', or 'PUT'. + * @param string $server + * The base URL of the server to perform the request against; e.g., + * 'http://foo.mollom.com'. + * @param string $path + * The REST path/resource to request; e.g., 'site/1a2b3c'. + * @param array $data + * An associative array of query parameters to send with the request. + * @param array $expected + * (optional) An element that is expected in the response, denoted as a list + * of parent element keys to the element and the element key itself; e.g., a + * value of array('site', 'servers') expects $response['site']['servers'] to + * exist in the response. + * + * @throws MollomNetworkException + * @throws MollomAuthenticationException + * @throws MollomRedirectException + * @throws MollomRefreshException + * @throws MollomException + * + * @see Mollom::query() + * @see Mollom::httpBuildQuery() + * @see Mollom::httpParseQuery() + * @see Mollom::parseXML() + * @see json_decode() + */ + abstract protected function request($method, $server, $path, array $data, array $expected = array()); + + /** + * Converts a SimpleXMLIterator structure into an associative array. + * + * Used to parse an XML response from Mollom servers into a PHP array. For + * example: + * @code + * $elements = new SimpleXmlIterator($response_body); + * $parsed_response = $this->parseXML($elements); + * @endcode + * + * @param SimpleXMLIterator $sxi + * A SimpleXMLIterator structure of the server response body. + * + * @return array + * An associative, possibly multidimensional array. + */ + public static function parseXML(SimpleXMLIterator $sxi) { + $a = array(); + $remove = array(); + for ($sxi->rewind(); $sxi->valid(); $sxi->next()) { + $key = $sxi->key(); + + // Recurse into non-scalar values. + if ($sxi->hasChildren()) { + $value = self::parseXML($sxi->current()); + } + // Use a simple key/value pair for scalar values. + else { + $value = strval($sxi->current()); + } + + if (!isset($a[$key])) { + $a[$key] = $value; + } + // Convert already existing keys into indexed keys, retaining other + // existing keys in the array; i.e., two or more XML elements of the + // same name and on the same level. + // Note that this XML to PHP array conversion does not support multiple + // different elements that each appear multiple times. + else { + // First time we reach here, convert the existing keyed item. Do not + // remove $key, so we enter this path again. + if (!isset($remove[$key])) { + $a[] = $a[$key]; + // Mark $key for removal. + $remove[$key] = $key; + } + // Add the new item. + $a[] = $value; + } + } + // Lastly, remove named keys that have been converted to indexed keys. + foreach ($remove as $key) { + unset($a[$key]); + } + return $a; + } + + /** + * Determines whether a nested array with variable depth contains all of the requested keys. + * + * @param array $array + * The array with variable depth that may contain the value to check for. + * @param array $expected + * A list of parent keys of the value, starting with the outermost key. + * + * @return bool + * TRUE if all the parent keys exist, FALSE otherwise. + * + * @see Mollom::request() + */ + public static function arrayKeyExists(array $array, array $expected) { + $ref = &$array; + foreach ($expected as $parent) { + if (is_array($ref) && array_key_exists($parent, $ref)) { + $ref = &$ref[$parent]; + } + else { + return FALSE; + } + } + return TRUE; + } + + /** + * Builds an RFC-compliant, rawurlencoded query string. + * + * PHP did a design decision to only support HTTP query parameters in the form + * of foo[]=1&foo[]=2, primarily for its built-in and automated conversion to + * PHP arrays. Other platforms (including the Mollom backend) do not support + * this syntax and expect multiple parameters to be in the form of + * foo=1&foo=2. + * + * @see http_build_query() + * @see http://en.wikipedia.org/wiki/Query_string + * @see http://tools.ietf.org/html/rfc3986#section-3.4 + * + * @param array $query + * The query parameter array to be processed, e.g. $_GET. + * @param string $parent + * Internal use only. Used to build the $query array key for nested items. + * + * @return string + * A rawurlencoded string which can be used as or appended to the URL query + * string. + * + * @see Mollom::httpParseQuery() + */ + public static function httpBuildQuery(array $query, $parent = '') { + $params = array(); + + foreach ($query as $key => $value) { + // For indexed (unnamed) child array keys, use the same parameter name, + // leading to param=foo¶m=bar instead of param[]=foo¶m[]=bar. + if ($parent && is_int($key)) { + $key = rawurlencode($parent); + } + else { + $key = ($parent ? $parent . '[' . rawurlencode($key) . ']' : rawurlencode($key)); + } + + // Recurse into children. + if (is_array($value)) { + $params[] = self::httpBuildQuery($value, $key); + } + // If a query parameter value is NULL, only append its key. + elseif (!isset($value)) { + $params[] = $key; + } + else { + // For better readability of paths in query strings, we decode slashes. + $params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value)); + } + } + + return implode('&', $params); + } + + /** + * Parses an RFC-compliant, rawurlencoded query string. + * + * Mollom clients normally do not need this function, as they do not need to + * process requests from a server - unless a client attempts to implement + * client-side unit testing. + * + * @param string $query + * The query parameter string to process, e.g. $_SERVER['QUERY_STRING'] + * (GET) or php://input (POST/PUT). + * + * @return array + * A query parameter array parsed from $query. + * + * @see Mollom::httpBuildQuery() + * @see parse_str() + */ + public static function httpParseQuery($query) { + if ($query === '') { + return array(); + } + // Explode parameters into arrays to check for duplicate names. + $params = array(); + $seen = array(); + $duplicate = array(); + foreach (explode('&', $query) as $chunk) { + $param = explode('=', $chunk, 2); + if (isset($seen[$param[0]])) { + $duplicate[$param[0]] = TRUE; + } + $seen[$param[0]] = TRUE; + $params[] = $param; + } + // Implode back into a string. + $query = ''; + foreach ($params as $param) { + $query .= $param[0]; + if (isset($duplicate[$param[0]])) { + $query .= '[]'; + } + if (isset($param[1])) { + $query .= '=' . $param[1]; + } + $query .= '&'; + } + // Parse query string as usual. + parse_str($query, $result); + return $result; + } + + /** + * Retrieves a list of sites accessible to this client. + * + * Used by Mollom resellers only. + * + * @return array + * An array containing site resources, as returned by Mollom::getsite(). + */ + public function getSites() { + $result = $this->query('GET', 'site', array(), array('list')); + // In XML, 'list' is a string when blacklist is empty. + // @todo Move into query(). + if (isset($result['list'])) { + // parseXML() can only convert multiple sub-elements into an indexed array. + if (is_array($result['list'])) { + $result['list'] = array_values($result['list']); + return $result['list']; + } + return array(); + } + return $result; + } + + /** + * Retrieves information about a site. + * + * @param string $publicKey + * (optional) The public Mollom API key of the site to retrieve. Defaults to + * the public key of the client. + * + * @return mixed + * On success, an associative array containing: + * - publicKey: The public Mollom API key of the site. + * - privateKey: The private Mollom API key of the site. + * - url: The URL of the site. + * - email: The e-mail address of the primary contact of the site. + * - languages: (optional) An array of language ISO codes, content is + * expected to submitted in on the site. + * - platformName: (optional) The name of the platform running the site + * (e.g., "Drupal"). + * - platformVersion: (optional) The version of the platform running the + * site (e.g., "6.20"). + * - clientName: (optional) The name of the Mollom client plugin used + * (e.g., "Mollom"). + * - clientVersion: (optional) The version of the Mollom client plugin used + * (e.g., "6.15"). + * On failure, the error response code returned by the server. + */ + public function getSite($publicKey = NULL) { + if (!isset($publicKey)) { + $publicKey = $this->publicKey; + } + $result = $this->query('GET', 'site/' . $publicKey, array(), array('site')); + return isset($result['site']) ? $result['site'] : $result; + } + + /** + * Creates a new site. + * + * @param array $data + * An associative array of properties for the new site. At least 'url' and + * 'email' are required. See Mollom::getSite() for details. + * + * @return mixed + * On success, the full site information of the created site; see + * Mollom::getSite() for details. On failure, the error response code + * returned by the server. Or FALSE if 'url' or 'email' was not specified. + */ + public function createSite(array $data = array()) { + if (empty($data['url']) || empty($data['email'])) { + return FALSE; + } + $result = $this->query('POST', 'site', $data, array('site')); + return isset($result['site']) ? $result['site'] : $result; + } + + /** + * Updates a site to verify API keys and send client information. + * + * @return mixed + * TRUE on success. On failure, the error response code returned by the + * server; one of Mollom::AUTH_ERROR or Mollom::NETWORK_ERROR. + */ + public function verifyKey() { + $data = $this->getClientInformation(); + $result = $this->query('POST', 'site/' . $this->publicKey, $data, array('site')); + // If the public key could not be found, make sure to return an + // authentication error. + if ($this->lastResponseCode === 404) { + return self::AUTH_ERROR; + } + // lastResponseCode will either be TRUE, AUTH_ERROR, or NETWORK_ERROR. + return $this->lastResponseCode === TRUE ? TRUE : $this->lastResponseCode; + } + + /** + * Deletes a site. + * + * @param string $publicKey + * The public Mollom API key of the site to delete. + * + * @return bool + * TRUE on success, FALSE otherwise. + */ + public function deleteSite($publicKey) { + $result = $this->query('POST', 'site/' . $publicKey . '/delete'); + return $this->lastResponseCode === TRUE; + } + + /** + * Checks user-submitted content with Mollom. + * + * @param array $data + * An associative array containing any of the keys: + * - id: The existing content ID of the content, if it or a variant or + * revision of it has been checked before. + * - postTitle: The title of the content. + * - postBody: The body of the content. If the content consists of multiple + * fields, concatenate them into one postBody string, separated by " \n" + * (space and line-feed). + * - authorName: The (real) name of the content author. + * - authorUrl: The homepage/website URL of the content author. + * - authorMail: The e-mail address of the content author. + * - authorIp: The IP address of the content author. + * - authorId: The local user ID on the client site of the content author. + * - authorOpenid: An indexed array of Open IDs of the content author. + * - checks: An indexed array of strings denoting the checks to perform, one + * or more of: 'spam', 'quality', 'profanity', 'language', 'sentiment'. + * Defaults to 'spam'. + * - unsure: Integer denoting whether a "unsure" response should be allowed + * (1) for the 'spam' check (which should lead to CAPTCHA) or not (0). + * Defaults to 1. + * - strictness: A string denoting the strictness of Mollom checks to + * perform; one of 'strict', 'normal', or 'relaxed'. Defaults to 'normal'. + * - rateLimit: Seconds that must have passed by for the same author to post + * again. Defaults to 15. + * - honeypot: The value of a client-side honeypot form element, if + * non-empty. + * - ttl: Time-to-live in seconds for the content. Should be set to a + * reasonable small amount of seconds during form validation, until form + * validation passed. Defaults to -1 (forever). + * + * @return mixed + * On success, an associative array representing the full content record, + * containing the additional keys: + * - spamScore: A floating point value with a precision of 2, ranging + * between 0.00 and 1.00; whereas 0.00 denotes 100% spam, 0.50 denotes + * "unsure", and 1.00 denotes ham. Only returned if 'spam' was passed for + * 'checks'. + * - spamClassification: The final spam classification; one of 'spam', + * 'unsure', or 'ham'. Only returned if 'spam' was passed for 'checks'. + * - profanityScore: A floating point value with a precision of 2, ranging + * between 0.00 and 1.00; whereas 0.00 denotes 0% profanity and 1.00 + * denotes 100% profanity. Only returned if 'profanity' was passed for + * 'checks'. + * - qualityScore: A floating point value with a precision of 2, ranging + * between 0.00 and 1.00; whereas 0.00 denotes poor quality and 1.00 + * high quality. Only returned if 'quality' was passed for 'checks'. + * - sentimentScore: A floating point value with a precision of 2, ranging + * between 0.00 and 1.00; whereas 0.00 denotes bad sentiment and 1.00 + * good sentiment. Only returned if 'sentiment' was passed for 'checks'. + * - reason: A string denoting the reason for Mollom's classification; e.g., + * - rateLimit: Author was seen on Mollom-protected sites within the given + * 'rateLimit' time-frame. + * On failure, the error response code returned by the server. + */ + public function checkContent(array $data = array()) { + $path = 'content'; + if (!empty($data['id'])) { + $path .= '/' . $data['id']; + } + $result = $this->query('POST', $path, $data, array('content', 'id')); + + // parseXML() can only convert multiple sub-elements into an indexed array. + if (isset($result['content']['languages']) && is_array($result['content']['languages'])) { + $result['content']['languages'] = array_values($result['content']['languages']); + } + + return isset($result['content']) ? $result['content'] : $result; + } + + /** + * Retrieves a CAPTCHA resource from Mollom. + * + * @param array $data + * An associative array containing: + * - type: A string denoting the type of CAPTCHA to create; one of 'image' + * or 'audio'. + * and any of the keys: + * - contentId: The ID of a content resource to link the CAPTCHA to. Allows + * Mollom to learn when it was unsure. + * - ssl: An integer denoting whether to create a CAPTCHA URL using HTTPS + * (1) or not (0). Only available for paid subscriptions. + * + * @return mixed + * On success, an associative array representing the full CAPTCHA record, + * containing: + * - id: The ID of the CAPTCHA. + * - url: The URL of the CAPTCHA. + * On failure, the error response code returned by the server. + * + * @todo Rename to createCaptcha(). + */ + public function getCaptcha(array $data = array()) { + if (!isset($data['type']) || !in_array($data['type'], array('image', 'audio'))) { + // @todo Public method should not throw a MollomException. + throw new MollomException('Unknown CAPTCHA type.', 0, NULL, $this); + } + $path = 'captcha'; + $result = $this->query('POST', $path, $data, array('captcha', 'id')); + + return isset($result['captcha']) ? $result['captcha'] : $result; + } + + /** + * Checks whether a user-submitted solution for a CAPTCHA is correct. + * + * @param array $data + * An associative array containing: + * - id: The ID of the CAPTCHA to check. + * - solution: The answer provided by the author. + * and any of the keys: + * - authorName: The (real) name of the content author. + * - authorUrl: The homepage/website URL of the content author. + * - authorMail: The e-mail address of the content author. + * - authorIp: The IP address of the content author. + * - authorId: The local user ID on the client site of the content author. + * - authorOpenid: An indexed array of Open IDs of the content author. + * - rateLimit: Seconds that must have passed by for the same author to post + * again. Defaults to 15. + * - honeypot: The value of a client-side honeypot form element, if + * non-empty. + * + * @return mixed + * On success, an associative array representing the full CAPTCHA record, + * additionally containing: + * - solved: Whether the provided solution was correct (1) or not (0). + * - reason: A string denoting the reason for Mollom's classification; e.g., + * - rateLimit: Author was seen on Mollom-protected sites within the given + * 'rateLimit' time-frame. + * On failure, the error response code returned by the server. + * + * @todo Rename to createCaptcha(). + */ + public function checkCaptcha(array $data = array()) { + if (empty($data['id'])) { + // @todo Public method should not throw a MollomException. + throw new MollomException('Missing CAPTCHA ID.', 0, NULL, $this); + } + $result = $this->query('POST', 'captcha/' . $data['id'], $data, array('captcha', 'id')); + + return isset($result['captcha']) ? $result['captcha'] : $result; + } + + /** + * Sends feedback to Mollom. + * + * @param array $data + * An associative array containing: + * - reason: A string denoting the reason for why the content associated + * with either contentId or captchaId is being reported; one of: + * - spam: The content is spam, unsolicited advertising. + * - profanity: The content contains obscene, violent, profane language. + * - quality: The content is of low quality. + * - unwanted: The content is unwanted, taunting, off-topic. + * and at least one of: + * - contentId: A Mollom content ID associated with the content. + * - captchaId: A Mollom CAPTCHA ID associated with the content. + * + * @return bool + * TRUE if the feedback was sent successfully, FALSE otherwise. + */ + public function sendFeedback(array $data) { + if (empty($data['contentId']) && empty($data['captchaId'])) { + // @todo Public method should not throw a MollomException. + throw new MollomException('Missing resource ID.', 0, NULL, $this); + } + if (empty($data['reason'])) { + // @todo Public method should not throw a MollomException. + throw new MollomException('Missing feedback reason.', 0, NULL, $this); + } + $this->query('POST', 'feedback', $data); + return $this->lastResponseCode === TRUE ? TRUE : FALSE; + } + + /** + * Retrieves the blacklist for a site. + * + * @param string $publicKey + * (optional) The public Mollom API key of the site to retrieve the + * blacklist for. Defaults to the public key of the client. + * + * @return mixed + * An array containing blacklist entries; see Mollom::getBlacklistEntry() + * for details. On failure, the error response code returned by the server. + * + * @todo List parameters. + */ + public function getBlacklist($publicKey = NULL) { + if (!isset($publicKey)) { + $publicKey = $this->publicKey; + } + $result = $this->query('GET', 'blacklist/' . $publicKey, array(), array('list')); + // In XML, 'list' is a string when blacklist is empty. + // @todo Move into query(). + if (isset($result['list'])) { + // parseXML() can only convert multiple sub-elements into an indexed array. + if (is_array($result['list'])) { + $result['list'] = array_values($result['list']); + return $result['list']; + } + return array(); + } + return $result; + } + + /** + * Retrieves a blacklist entry stored for a site. + * + * @param string $entryId + * The ID of the blacklist entry to retrieve. + * @param string $publicKey + * (optional) The public Mollom API key of the site to retrieve the + * blacklist entry for. Defaults to the public key of the client. + * + * @return mixed + * On success, an associative array containing: + * - id: The ID the of blacklist entry. + * - created: A timestamp in seconds since the UNIX epoch of when the entry + * was created. + * - value: The blacklisted string/value. + * - reason: A string denoting the reason for why the term is blacklisted; + * one of 'spam', 'profanity', 'quality', or 'unwanted'. Defaults to + * 'unwanted'. + * - context: A string denoting where the entry's value may match; one of + * 'allFields', 'ip', 'email', 'links', 'authorIp', 'title'. Defaults to + * 'allFields'. + * - match: A string denoting how precise the entry's value may match; one + * of 'exact' or 'contains'. Defaults to 'contains'. + * - status: An integer denoting whether the entry is enabled (1) or not + * (0). + * - note: A custom string explaining the entry. Useful when disabling + * entries in a multi-moderator scenario. + * - lastMatch: A timestamp in seconds since the UNIX epoch of when the entry + * was last matched in a content. + * - matchCount: An integer denoting how many times the entry was matched in + * content. + * On failure, the error response code returned by the server. + */ + public function getBlacklistEntry($entryId, $publicKey = NULL) { + if (!isset($publicKey)) { + $publicKey = $this->publicKey; + } + $result = $this->query('GET', 'blacklist/' . $publicKey . '/' . $entryId, array(), array('entry', 'id')); + return isset($result['entry']) ? $result['entry'] : $result; + } + + /** + * Creates a new blacklist entry for a site. + * + * @param array $data + * An associative array specifying the blacklist entry to create. See return + * value of Mollom::getBlacklistEntry() for details. + * @param string $publicKey + * (optional) The public Mollom API key of the site to create the blacklist + * entry for. Defaults to the public key of the client. + * + * @return mixed + * On success, the full blacklist entry record of the created entry; see + * Mollom::getBlacklistEntry() for details. On failure, the error response + * code returned by the server. + * + * @todo Combine into saveBlacklistEntry(). + */ + public function createBlacklistEntry(array $data = array(), $publicKey = NULL) { + if (!isset($publicKey)) { + $publicKey = $this->publicKey; + } + $result = $this->query('POST', 'blacklist/' . $publicKey, $data, array('entry', 'id')); + return isset($result['entry']) ? $result['entry'] : $result; + } + + public function updateBlacklistEntry(array $data = array(), $publicKey = NULL) { + if (empty($data['id'])) { + // @todo Public method should not throw a MollomException. + throw new MollomException('Missing blacklist entry ID.', 0, NULL, $this); + } + if (!isset($publicKey)) { + $publicKey = $this->publicKey; + } + $result = $this->query('POST', 'blacklist/' . $publicKey . '/' . $data['id'], $data, array('entry', 'id')); + + return isset($result['entry']) ? $result['entry'] : $result; + } + + /** + * Deletes a blacklist entry from a site. + * + * @param string $entryId + * The ID of the blacklist entry to delete. + * @param string $publicKey + * (optional) The public Mollom API key of the site to create the blacklist + * entry for. Defaults to the public key of the client. + * + * @return bool + * TRUE on success, FALSE otherwise. + */ + public function deleteBlacklistEntry($entryId, $publicKey = NULL) { + if (!isset($publicKey)) { + $publicKey = $this->publicKey; + } + $result = $this->query('POST', 'blacklist/' . $publicKey . '/' . $entryId . '/delete'); + return $this->lastResponseCode === TRUE; + } +} + diff --git mollom.admin.inc mollom.admin.inc index b0e96be..42b4cea 100644 --- mollom.admin.inc +++ mollom.admin.inc @@ -444,12 +444,13 @@ function mollom_admin_blacklist_form($form, &$form_state, $type = 'spam') { $form['blacklist'] = array(); // Do not retrieve the current blacklist when submitting the form. - $blacklist = (empty($form_state['input']) ? mollom('mollom.listBlacklistText') : array()); + $blacklist = (empty($form_state['input']) ? mollom()->getBlacklist() : array()); if (is_array($blacklist)) { - foreach ($blacklist as $id => $entry) { + foreach ($blacklist as $entry) { if ($entry['reason'] != $type) { continue; } + $id = $entry['id']; // #class property is internally used by // theme_mollom_admin_blacklist_form(). $row = array( @@ -461,21 +462,17 @@ function mollom_admin_blacklist_form($form, &$form_state, $type = 'spam') { '#markup' => check_plain($matches[$entry['match']]), '#class' => 'mollom-blacklist-match value-' . check_plain($entry['match']), ), - 'text' => array( - '#markup' => check_plain($entry['text']), - '#class' => 'mollom-blacklist-text', + 'value' => array( + '#markup' => check_plain($entry['value']), + '#class' => 'mollom-blacklist-value', ), ); $row['actions']['delete'] = array( '#type' => 'link', '#title' => t('delete'), - '#href' => 'admin/config/content/mollom/blacklist/delete', + '#href' => 'admin/config/content/mollom/blacklist/delete/' . $entry['id'], '#options' => array( - 'query' => array( - 'text' => $entry['text'], - 'context' => $entry['context'], - 'reason' => $entry['reason'], - ) + drupal_get_destination(), + 'query' => drupal_get_destination(), ), ); $form['blacklist'][$id] = $row; @@ -501,14 +498,14 @@ function mollom_admin_blacklist_form($form, &$form_state, $type = 'spam') { '#required' => TRUE, '#id' => 'mollom-blacklist-filter-match', ); - $form['entry']['text'] = array( + $form['entry']['value'] = array( '#type' => 'textfield', - '#title' => t('Text'), + '#title' => t('Value'), '#title_display' => 'invisible', '#size' => 40, '#required' => TRUE, '#maxlength' => 64, - '#id' => 'mollom-blacklist-filter-text', + '#id' => 'mollom-blacklist-filter-value', '#attributes' => array( 'autocomplete' => 'off', ), @@ -534,31 +531,31 @@ function mollom_admin_blacklist_form($form, &$form_state, $type = 'spam') { */ function mollom_admin_blacklist_form_submit($form, &$form_state) { $data = array( - 'text' => $form_state['values']['entry']['text'], + 'value' => $form_state['values']['entry']['value'], 'context' => $form_state['values']['entry']['context'], 'match' => $form_state['values']['entry']['match'], 'reason' => $form_state['values']['entry']['reason'], ); - $result = mollom('mollom.addBlacklistText', $data); + $result = mollom()->createBlacklistEntry($data); $args = array( - '@text' => $data['text'], + '@value' => $data['value'], '@context' => $data['context'], '@match' => $data['match'], '@reason' => $data['reason'], ); - if ($result === TRUE) { + if (!empty($result['id'])) { drupal_set_message(t('The entry was added to the blacklist.')); _mollom_watchdog(array( - 'Added @text (@context, @match) to @reason blacklist.' => $args, + 'Added @value (@context, @match) to @reason blacklist.' => $args, 'Data:
@data' => array('@data' => $data), 'Result:
@result' => array('@result' => $result), )); } else { - drupal_set_message(t('An error occurred upon trying to add the text to the blacklist.'), 'error'); + drupal_set_message(t('An error occurred upon trying to add the value to the blacklist.'), 'error'); _mollom_watchdog(array( - 'Failed to add @text (@context, @match) to @reason blacklist.' => $args, + 'Failed to add @value (@context, @match) to @reason blacklist.' => $args, 'Data:
@data' => array('@data' => $data), 'Result:
@result' => array('@result' => $result), ), WATCHDOG_ERROR); @@ -573,7 +570,7 @@ function theme_mollom_admin_blacklist_form($variables) { $header = array( t('Context'), t('Matches'), - t('Text'), + t('Value'), '', ); $rows = array(); @@ -581,7 +578,7 @@ function theme_mollom_admin_blacklist_form($variables) { $rows[] = array( drupal_render($form['entry']['context']), drupal_render($form['entry']['match']), - drupal_render($form['entry']['text']), + drupal_render($form['entry']['value']), drupal_render($form['entry']['actions']), ); @@ -596,8 +593,8 @@ function theme_mollom_admin_blacklist_form($variables) { 'class' => $form['blacklist'][$id]['match']['#class'], ), array( - 'data' => drupal_render($form['blacklist'][$id]['text']), - 'class' => $form['blacklist'][$id]['text']['#class'], + 'data' => drupal_render($form['blacklist'][$id]['value']), + 'class' => $form['blacklist'][$id]['value']['#class'], ), drupal_render($form['blacklist'][$id]['actions']), ); @@ -625,23 +622,16 @@ function theme_mollom_admin_blacklist_form($variables) { * @ingroup forms * @see mollom_admin_blacklist_delete_submit() */ -function mollom_admin_blacklist_delete($form, &$form_state) { - $form['text'] = array( - '#type' => 'value', - '#value' => $_GET['text'], - ); - $form['context'] = array( +function mollom_admin_blacklist_delete($form, &$form_state, $entryId) { + $entry = mollom()->getBlacklistEntry($entryId); + $form['entry'] = array( '#type' => 'value', - '#value' => $_GET['context'], - ); - $form['reason'] = array( - '#type' => 'value', - '#value' => $_GET['reason'], + '#value' => $entry, ); return confirm_form( $form, - t('Are you sure you want to delete %text from the blacklist?', array('%text' => $_GET['text'])), + t('Are you sure you want to delete %value from the blacklist?', array('%value' => $entry['value'])), 'admin/config/content/mollom/blacklist', t('This action cannot be undone.'), t('Delete'), t('Cancel') @@ -652,31 +642,26 @@ function mollom_admin_blacklist_delete($form, &$form_state) { * Form submit handler to delete an entry from the blacklist. */ function mollom_admin_blacklist_delete_submit($form, &$form_state) { - $data = array( - 'text' => $form_state['values']['text'], - 'context' => $form_state['values']['context'], - 'reason' => $form_state['values']['reason'], - ); - $result = mollom('mollom.removeBlacklistText', $data); + $result = mollom()->deleteBlacklistEntry($form_state['values']['entry']['id']); $args = array( - '@text' => $data['text'], - '@context' => $data['context'], - '@reason' => $data['reason'], + '@value' => $form_state['values']['entry']['value'], + '@context' => $form_state['values']['entry']['context'], + '@reason' => $form_state['values']['entry']['reason'], ); if ($result === TRUE) { drupal_set_message(t('The entry was removed from the blacklist.')); _mollom_watchdog(array( - 'Removed @text (@context) from @reason blacklist.' => $args, - 'Data:
@data' => array('@data' => $data), + 'Removed @value (@context) from @reason blacklist.' => $args, + 'Data:
@data' => array('@data' => $form_state['values']['entry']), 'Result:
@result' => array('@result' => $result), )); } else { drupal_set_message(t('An error occurred upon trying to remove the item from the blacklist.'), 'error'); _mollom_watchdog(array( - 'Failed to removed @text (%context) from @reason blacklist.' => $args, - 'Data:
@data' => array('@data' => $data), + 'Failed to removed @value (%context) from @reason blacklist.' => $args, + 'Data:
@data' => array('@data' => $form_state['values']['entry']), 'Result:
@result' => array('@result' => $result), ), WATCHDOG_ERROR); } @@ -692,6 +677,9 @@ function mollom_admin_blacklist_delete_submit($form, &$form_state) { * mollom.verifyKey would invalidate the keys and throw an error; hence, * _mollom_fallback() would invoke form_set_error(), effectively preventing this * form from submitting. + * + * @todo Implement proper form validation now that mollom() no longer triggers + * the fallback mode. */ function mollom_admin_settings($form, &$form_state) { // Output a positive status message, since users keep on asking whether diff --git mollom.admin.js mollom.admin.js index dff1bf9..051015a 100644 --- mollom.admin.js +++ mollom.admin.js @@ -8,12 +8,12 @@ Drupal.behaviors.mollomBlacklistFilter = { var self = this; $('#mollom-blacklist', context).once('mollom-blacklist-filter', function () { // Prepare a list of all entries to optimize performance. Each key is a - // blacklisted text and each value is an object containing the + // blacklisted value and each value is an object containing the // corresponding table row, context, and match. self.entries = {}; - $(this).find('tr:has(.mollom-blacklist-text)').each(function () { + $(this).find('tr:has(.mollom-blacklist-value)').each(function () { var $row = $(this); - self.entries[$row.find('.mollom-blacklist-text').text()] = { + self.entries[$row.find('.mollom-blacklist-value').text()] = { context: $row.children('.mollom-blacklist-context').attr('class').match(/value-(\w+)/)[1], match: $row.children('.mollom-blacklist-match').attr('class').match(/value-(\w+)/)[1], row: $row.get(0) @@ -21,7 +21,7 @@ Drupal.behaviors.mollomBlacklistFilter = { }); // Attach the instant text filtering behavior. - var $filterText = $('#mollom-blacklist-filter-text', context); + var $filterText = $('#mollom-blacklist-filter-value', context); var $filterContext = $('#mollom-blacklist-filter-context', context); var $filterMatch = $('#mollom-blacklist-filter-match', context); @@ -32,7 +32,7 @@ Drupal.behaviors.mollomBlacklistFilter = { var search = { // Blacklist entries are stored in lowercase, so to get any filter // results, the entered text must be converted to lowercase, too. - text: $filterText.val().toLowerCase(), + value: $filterText.val().toLowerCase(), context: $filterContext.val(), match: $filterMatch.val() }; @@ -53,15 +53,15 @@ Drupal.behaviors.mollomBlacklistFilter = { // Likewise, we directly apply the 'display' style, since // jQuery.fn.hide() and jQuery.fn.show() call into jQuery.fn.animate(), // which is useless for this purpose. - for (text in self.entries) { - visible = (search.text.length == 0 || text.indexOf(search.text) != -1); - visible = visible && (search.context.length == 0 || self.entries[text].context == search.context); - visible = visible && (search.match.length == 0 || self.entries[text].match == search.match); + for (value in self.entries) { + visible = (search.value.length == 0 || value.indexOf(search.value) != -1); + visible = visible && (search.context.length == 0 || self.entries[value].context == search.context); + visible = visible && (search.match.length == 0 || self.entries[value].match == search.match); if (visible) { - self.entries[text].row.style.display = ''; + self.entries[value].row.style.display = ''; } else { - self.entries[text].row.style.display = 'none'; + self.entries[value].row.style.display = 'none'; } } }; diff --git mollom.drupal.inc mollom.drupal.inc new file mode 100644 index 0000000..ee8336c --- /dev/null +++ mollom.drupal.inc @@ -0,0 +1,326 @@ + 'mollom_public_key', + 'privateKey' => 'mollom_private_key', + 'servers' => 'mollom_servers', + ); + + /** + * Implements Mollom::loadConfiguration(). + */ + public function loadConfiguration($name) { + $name = $this->configuration_map[$name]; + return variable_get($name); + } + + /** + * Implements Mollom::saveConfiguration(). + */ + public function saveConfiguration($name, $value) { + $name = $this->configuration_map[$name]; + return variable_set($name, $value); + } + + /** + * Implements Mollom::deleteConfiguration(). + */ + public function deleteConfiguration($name) { + $name = $this->configuration_map[$name]; + return variable_del($name); + } + + /** + * Implements Mollom::getClientInformation(). + */ + public function getClientInformation() { + if ($cache = cache_get('mollom_version')) { + return $cache->data; + } + + // Retrieve Drupal distribution and installation profile information. + $profile = drupal_get_profile(); + $profile_info = system_get_info('module', $profile) + array( + 'distribution_name' => 'Drupal', + 'version' => VERSION, + ); + + // Retrieve Mollom module information. + $mollom_info = system_get_info('module', 'mollom'); + if (empty($mollom_info['version'])) { + // Manually build a module version string for repository checkouts. + $mollom_info['version'] = DRUPAL_CORE_COMPATIBILITY . '-1.x-dev'; + } + + $data = array( + 'platformName' => $profile_info['distribution_name'], + 'platformVersion' => $profile_info['version'], + 'clientName' => $mollom_info['name'], + 'clientVersion' => $mollom_info['version'], + ); + cache_set('mollom_version', $data); + + return $data; + } + + /** + * Overrides Mollom::writeLog(). + */ + function writeLog() { + $messages = array(); + foreach ($this->log as $i => $entry) { + $entry += array('arguments' => array()); + $message = array( + $entry['message'] => $entry['arguments'], + ); + if (isset($entry['data'])) { + $message['Request: @request
@parameters'] = array( + '@request' => $entry['request'], + '@parameters' => $entry['data'], + ); + unset($entry['request'], $entry['data']); + } + if (isset($entry['response'])) { + $message['Response:
@response'] = array('@response' => $entry['response']); + } + $messages[] = $message; + + // Translate log messages for debugging without watchdog. + // @todo Move into mollom.unit.inc implementation. + /* + $output = array(); + foreach ($message as $text => $args) { + foreach ($args as &$arg) { + if (is_array($arg)) { + $arg = var_export($arg, TRUE); + } + } + $output[] = strtr($text, $args); + } + $this->log[$i]['message'] = implode("\n", $output); + unset($this->log[$i]['arguments']); + drupal_set_message(implode('
@data' => array('@data' => $data), @@ -1611,16 +1578,11 @@ function mollom_validate_analysis(&$form, &$form_state) { $form['mollom']['captcha']['#access'] = TRUE; $form['mollom']['captcha']['#required'] = TRUE; - $captcha_data = array( - 'session_id' => $result['session_id'], - ); - $captcha = mollom_get_captcha('image', $captcha_data); - + $captcha = mollom_get_captcha($form_state); // If we get a response, add the image CAPTCHA to the form element. - if (isset($captcha['response']['session_id']) && !empty($captcha['markup'])) { - $form_state['mollom']['response']['session_id'] = $captcha['response']['session_id']; - $form['mollom']['session_id']['#value'] = $captcha['response']['session_id']; - $form['mollom']['captcha']['#field_prefix'] = $captcha['markup']; + if (!empty($captcha)) { + $form['mollom']['captchaId']['#value'] = $form_state['mollom']['response']['captcha']['id']; + $form['mollom']['captcha']['#field_prefix'] = $captcha; } } break; @@ -1678,32 +1640,32 @@ function mollom_validate_captcha(&$form, &$form_state) { return; } $data = array( - 'session_id' => $form_state['mollom']['response']['session_id'], - 'captcha_result' => $form_state['values']['mollom']['captcha'], - 'author_ip' => $all_data['author_ip'], + 'id' => $form_state['mollom']['response']['captcha']['id'], + 'solution' => $form_state['values']['mollom']['captcha'], + 'authorIp' => $all_data['authorIp'], ); - if (isset($all_data['author_id'])) { - $data['author_id'] = $all_data['author_id']; + if (isset($all_data['authorId'])) { + $data['authorId'] = $all_data['authorId']; } if (isset($all_data['honeypot'])) { $data['honeypot'] = $all_data['honeypot']; } - $result = mollom('mollom.checkCaptcha', $data); + $result = mollom()->checkCaptcha($data); // Use all available data properties for log messages below. $data += $all_data; // Invoke fallback behavior upon a server error; communication errors are // handled by mollom() already. A server error may happen in case of an // expired or invalid session_id. - if ($result === MOLLOM_ERROR) { + if (!is_array($result) || !isset($result['id'])) { return _mollom_fallback(); } // Store the response for #submit handlers. $form_state['mollom']['response']['captcha'] = $result; - $form['mollom']['session_id']['#value'] = $form_state['mollom']['response']['session_id']; + $form['mollom']['captchaId']['#value'] = $form_state['mollom']['response']['captcha']['id']; - if ($result === TRUE) { + if (!empty($result['solved'])) { $form_state['mollom']['passed_captcha'] = TRUE; $form['mollom']['captcha']['#access'] = FALSE; @@ -1786,11 +1748,20 @@ function mollom_form_submit($form, &$form_state) { // the mapped post_id. $values = mollom_form_get_values($form_state, array(), $form_state['mollom']['mapping']); // We only consider non-empty and non-zero values as valid entity ids. - if (!empty($values['post_id'])) { + if (!empty($values['postId'])) { // Save the Mollom session data. - $data = (object) $form_state['mollom']['response']; + $response = array(); + if (isset($form_state['mollom']['response']['content'])) { + $response += $form_state['mollom']['response']['content']; + $response['contentId'] = $form_state['mollom']['response']['content']['id']; + } + if (isset($form_state['mollom']['response']['captcha'])) { + $response += $form_state['mollom']['response']['captcha']; + $response['captchaId'] = $form_state['mollom']['response']['captcha']['id']; + } + $data = (object) $response; $data->entity = $form_state['mollom']['entity']; - $data->id = $values['post_id']; + $data->id = $values['postId']; $data->form_id = $form_state['mollom']['form_id']; // Set the moderation flag for forms accepting bad posts. $data->moderate = $form_state['mollom']['require_moderation']; @@ -1804,141 +1775,29 @@ function mollom_form_submit($form, &$form_state) { */ /** - * Call a remote procedure at the Mollom server. + * Instantiates a new Mollom client. * - * This function automatically adds the information required to authenticate - * against Mollom. - * - * @todo Currently, this function's return value mixes actual values and - * error values. We should rewrite the error handling so that calling - * functions can properly handle error situations. - */ -function mollom($method, $data = array()) { - module_load_include('inc', 'mollom'); - $messages = array(); - - // Initialize refresh variable. - $refresh = FALSE; - - // Enable testing mode. - if (variable_get('mollom_testing_mode', 0)) { - $data['testing'] = TRUE; - } - - // Retrieve the list of Mollom servers from the database. - $servers = variable_get('mollom_servers', array()); - - if (empty($servers)) { - // Retrieve a new list of servers. - $servers = _mollom_retrieve_server_list(); - // If API keys are invalid, a XML-RPC error code is returned. - if (!is_array($servers)) { - return $servers; + * @param $class + * (optional) The name of a Mollom client implementation class to instantiate. + * Overrides the 'mollom_class' configuration variable. Debug use only. + */ +function mollom($class = NULL) { + $instance = &drupal_static(__FUNCTION__); + if (!isset($class)) { + // @todo Testing mode configuration is not covered by tests. + if (variable_get('mollom_testing_mode', 0)) { + $class = 'MollomDrupalTest'; } - - $messages[] = array( - 'Refreshed servers: %servers' => array('%servers' => implode(', ', $servers)), - ); - - // Store the list of servers in the database. - variable_set('mollom_servers', $servers); - } - - if (is_array($servers)) { - // Send the request to the first server; if that fails, try the other - // servers in the list. - reset($servers); - while ($server = current($servers)) { - $result = xmlrpc($server . '/' . MOLLOM_API_VERSION, array( - $method => array($data + _mollom_authentication()), - )); - - if ($result === FALSE && ($error = xmlrpc_error())) { - if ($error->code === MOLLOM_REFRESH) { - // Avoid endless loops. - if (!$refresh) { - $refresh = TRUE; - - // Retrieve a new list of valid Mollom servers. - $servers = _mollom_retrieve_server_list(); - // If API keys are invalid, the XML-RPC error code is returned. - // To reach this, we must have had a server list (and therefore - // valid keys) before, so we do not immediately return (like above), - // but instead trigger the fallback mode. - if (!is_array($servers)) { - break; - } - - // Reset the list of servers to restart from the first server. - reset($servers); - - // Update the server list. - variable_set('mollom_servers', $servers); - - $messages[] = array( - 'Refreshed servers: %servers' => array('%servers' => implode(', ', $servers)), - ); - } - } - elseif ($error->code === MOLLOM_REDIRECT) { - // Try the next server in the list. - $next = next($servers); - - $messages[] = array( - 'Server %server redirected to: %next.' => array('%server' => $server, '%next' => $next), - ); - } - else { - $messages[] = array( - 'Error @errno from %server for %method: %message' => array( - '@errno' => $error->code, - '%server' => $server, - '%method' => $method, - '%message' => $error->message, - ), - 'Data:
@data' => array('@data' => $data), - ); - - // Instantly return upon a 'real' error. - if ($error->code === MOLLOM_ERROR) { - _mollom_watchdog_multiple($messages, WATCHDOG_ERROR); - return MOLLOM_ERROR; - } - // Otherwise, try the next server. - next($servers); - } - } - else { - _mollom_watchdog_multiple($messages, WATCHDOG_DEBUG); - return $result; - } + else { + $class = variable_get('mollom_class', 'MollomDrupal'); } } - - // If none of the servers worked, activate the fallback mechanism. - // @todo mollom() can be invoked outside of form processing. _mollom_fallback() - // unconditionally invokes form_set_error(), which always displays the - // fallback error message. Ideally, we would pass a $verbose argument to - // _mollom_fallback(), but for that, we'd have to know here already. - // Consequently, mollom() would need that $verbose argument. In the end, we - // likely want to either embed the fallback handling into form processing, - // or introduce a new helper function that is invoked instead of mollom() - // during form processing. - if ($method != 'mollom.verifyKey') { - _mollom_fallback(); + // If there is no instance yet or if it is not of the desired class, create a + // new one. + if (!isset($instance) || !($instance instanceof $class)) { + $instance = new $class(); } - - // If everything failed, we reset the server list to force Mollom to request - // a new list. - variable_del('mollom_servers'); - - // Report this error. - $messages[] = array( - 'All servers unreachable or returning errors. The server list was emptied.' => array(), - ); - _mollom_watchdog_multiple($messages, WATCHDOG_ERROR); - - return NETWORK_ERROR; + return $instance; } /** @@ -2025,65 +1884,41 @@ function _mollom_watchdog_multiple($messages, $severity) { } /** - * Returns version information to send with mollom.verifyKey. - * - * Retrieves platform and module version information for mollom.verifyKey, which - * is normally invoked on Mollom's administration pages only. - * - * This information is solely used to speed up support requests and technical - * inquiries. The data may also be aggregated to help the Mollom staff to make - * decisions on new features or the necessity of back-porting improved - * functionality to older versions. - * - * @return - * An array containing: - * - platform_name: The name of the Drupal distribution; i.e., "Drupal". - * - platform_version: The version of Drupal; e.g., "7.0". - * - client_name: The module name; i.e., "Mollom". - * - client_version: The version of the module; e.g., "7.x-1.0". - * - * @see _mollom_status() - */ -function _mollom_get_version() { - if ($cache = cache_get('mollom_version')) { - return $cache->data; - } - - // Retrieve Drupal distribution and installation profile information. - $profile = drupal_get_profile(); - $profile_info = system_get_info('module', $profile) + array( - 'distribution_name' => 'Drupal', - 'version' => VERSION, - ); - - // Retrieve Mollom module information. - $mollom_info = system_get_info('module', 'mollom'); - if (empty($mollom_info['version'])) { - // Manually build a module version string for repository checkouts. - $mollom_info['version'] = DRUPAL_CORE_COMPATIBILITY . '-1.x-dev'; - } - - $data = array( - 'platform_name' => $profile_info['distribution_name'], - 'platform_version' => $profile_info['version'], - 'client_name' => $mollom_info['name'], - 'client_version' => $mollom_info['version'], - ); - cache_set('mollom_version', $data); - - return $data; -} - -/** * Send feedback to Mollom. - */ -function _mollom_send_feedback($session_id, $feedback = 'spam') { - $result = mollom('mollom.sendFeedback', array( - 'session_id' => $session_id, - 'feedback' => $feedback, + * + * @param $data + * A Mollom data record containing one or both of: + * - contentId: The content ID to send feedback for. + * - captchaId: The CAPTCHA ID to send feedback for. + * @param $reason + * The feedback to send, one of 'spam', 'profanity', 'quality', 'unwanted', + * 'approve', 'delete'. + */ +function _mollom_send_feedback($data, $reason = 'spam') { + if (!empty($data->captchaId)) { + $params['captchaId'] = $data->captchaId; + $resource = 'CAPTCHA'; + $id = $data->captchaId; + } + // In case we also have a contentId, also pass that, and override $resource + // and $id for the log message. + if (!empty($data->contentId)) { + $params['contentId'] = $data->contentId; + $resource = 'content'; + $id = $data->contentId; + } + if (!isset($id)) { + return FALSE; + } + $result = mollom()->sendFeedback($params + array( + 'reason' => $reason, )); _mollom_watchdog(array( - 'Reported %feedback for session id %session.' => array('%session' => $session_id, '%feedback' => $feedback), + 'Reported %feedback for @resource %id.' => array( + '%feedback' => $reason, + '@resource' => $resource, + '%id' => $id, + ), )); return $result; } @@ -2116,8 +1951,8 @@ function mollom_get_statistics($refresh = FALSE) { )); foreach ($statistics as $statistic) { - $result = mollom('mollom.getStatistics', array('type' => $statistic)); - if ($result === NETWORK_ERROR || $result === MOLLOM_ERROR) { + $result = mollom()->getStatistics(array('type' => $statistic)); + if ($result === Mollom::NETWORK_ERROR || $result === Mollom::AUTH_ERROR) { // If there was an error, stop fetching statistics and store FALSE // in the cache. This will help prevent from making unnecessary // requests to Mollom if the service is down or the server cannot @@ -2174,66 +2009,69 @@ function mollom_field_extra_fields() { /** * Get the HTML markup for a Mollom CAPTCHA. * - * @param $type - * The CAPTCHA type to retrieve, e.g. 'image' or 'audio'. - * @param $data - * An optional array of parameters to send to Mollom when requesting the - * CAPTCHA. + * @param $form_state + * The current state of a form. * * @return - * An array with the following key/value pairs: - * - 'data': An array of parameters sent to Mollom when requesting the - * CAPTCHA. - * - 'response': An array with the response from Mollom. - * - 'markup': The markup of the CAPTCHA HTML. - */ -function mollom_get_captcha($type, array $data = array()) { - $data += array( - 'author_ip' => ip_address(), - 'ssl' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on', - ); + * The markup of the CAPTCHA HTML. + */ +function mollom_get_captcha(&$form_state) { + $key = 'captcha_url_' . $form_state['mollom']['captcha_type']; + if (empty($form_state['mollom']['response'][$key])) { + $data = array( + 'type' => $form_state['mollom']['captcha_type'], + 'authorIp' => ip_address(), + 'ssl' => (int) (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'), + ); + if (!empty($form_state['mollom']['response']['content']['id'])) { + $data['contentId'] = $form_state['mollom']['response']['content']['id']; + } + $result = mollom()->getCaptcha($data); + + if (isset($result['url'])) { + $url = $result['url']; + $form_state['mollom']['response'][$key] = $url; + $form_state['mollom']['response']['captcha']['id'] = $result['id']; + } + else { + return ''; + } + } + else { + $url = $form_state['mollom']['response'][$key]; + } // @todo Convert these to actual theme functions? $output = ''; - switch ($type) { + switch ($form_state['mollom']['captcha_type']) { case 'audio': - $response = mollom('mollom.getAudioCaptcha', $data); - if ($response) { - $source = url(base_path() . drupal_get_path('module', 'mollom') . '/mollom-captcha-player.swf', array( - 'query' => array('url' => $response['url']), - 'external' => TRUE, - )); - $output = ''; - - $output = '' . $output . ''; - $output .= ' (' . t('verify using image') . ')'; - } + $source = url(base_path() . drupal_get_path('module', 'mollom') . '/mollom-captcha-player.swf', array( + 'query' => array('url' => $url), + 'external' => TRUE, + )); + $output = ''; + + $output = '' . $output . ''; + $output .= ' (' . t('verify using image') . ')'; break; case 'image': - $response = mollom('mollom.getImageCaptcha', $data); - if ($response) { - $captcha = theme('image', array('path' => url($response['url']), 'alt' => t('Type the characters you see in this picture.'), 'getsize' => FALSE)); - $output = '' . $captcha . ''; - $output .= ' (' . t('verify using audio') . ')'; - } + $captcha = theme('image', array('path' => $url, 'alt' => t('Type the characters you see in this picture.'), 'getsize' => FALSE)); + $output = '' . $captcha . ''; + $output .= ' (' . t('verify using audio') . ')'; break; } - return array( - 'data' => $data, - 'response' => $response, - 'markup' => $output, - ); + return $output; } /** @@ -2256,11 +2094,19 @@ function mollom_mail_alter(&$message) { * Add the 'Report as innapropriate' link to an e-mail message. */ function mollom_mail_add_report_link(&$message) { - if (!empty($GLOBALS['mollom']['response']['session_id'])) { - $mollom = $GLOBALS['mollom']; + $mollom = $GLOBALS['mollom']; + if (!empty($mollom['response']['content']['id']) || !empty($mollom['response']['captcha']['id'])) { $data = (object) $mollom['response']; - $data->entity = 'session'; - $data->id = $mollom['response']['session_id']; + if (!empty($mollom['response']['content']['id'])) { + $data->entity = 'mollom_content'; + $data->id = $data->content['id']; + $data->contentId = $data->content['id']; + } + else { + $data->entity = 'mollom_captcha'; + $data->id = $data->captcha['id']; + $data->captchaId = $data->captcha['id']; + } $data->form_id = $mollom['form_id']; mollom_data_save($data); $report_link = t('Report as inappropriate: @link', array( diff --git mollom.pages.inc mollom.pages.inc index 1880092..15bce77 100644 --- mollom.pages.inc +++ mollom.pages.inc @@ -12,18 +12,22 @@ * The new CAPTCHA type to retrieve, e.g. 'image' or 'audio'. * @param $form_build_id * The internal form build id of the form to update the CAPTCHA for. - * @param $mollom_session_id - * The last known Mollom session id contained in the form. + * @param $captchaId + * The last known Mollom CAPTCHA ID contained in the form. * * @return * A JSON array containing: * - content: The HTML markup for the new CAPTCHA. - * - session_id: The Mollom session id for the new CAPTCHA. + * - captchaId: The ID for the new CAPTCHA. * * @todo Add error handling. */ -function mollom_captcha_js($type, $form_build_id, $mollom_session_id) { - $captcha = mollom_get_captcha($type, array('session_id' => $mollom_session_id)); +function mollom_captcha_js($type, $form_build_id, $captchaId) { + $dummy_state['mollom'] = array( + 'captcha_type' => $type, + ); + $dummy_state['mollom']['response']['captcha']['captchaId'] = $captchaId; + $captcha = mollom_get_captcha($dummy_state); // Update cached session id in the cached $form_state. // We rely on native form caching of Form API to store our Mollom session @@ -36,20 +40,21 @@ function mollom_captcha_js($type, $form_build_id, $mollom_session_id) { // id. Therefore, we need to update the session id in the cached $form_state. // @todo Replace the entire CAPTCHA switch/refresh with new AJAX framework // functionality. - if (!empty($captcha['response']['session_id'])) { + if (!empty($dummy_state['mollom']['response']['session_id'])) { if ($cache = cache_get('form_state_' . $form_build_id, 'cache_form')) { $form_state = $cache->data; - $form_state['mollom']['response']['session_id'] = $captcha['response']['session_id']; + $form_state['mollom']['response']['captcha']['captchaId'] = $dummy_state['mollom']['response']['captcha']['captchaId']; + // @todo Double-check $cid. cache_set('form_state_' . $form_build_id, $form_state, 'cache_form', REQUEST_TIME + 21600); - // After successfully updating the cache, replace the original session id. - $mollom_session_id = $captcha['response']['session_id']; + // After successfully updating the cache, replace the original ID. + $captchaId = $form_state['mollom']['response']['captcha']['captchaId']; } } // Return new content and new session_id via JSON. $data = array( - 'content' => $captcha['markup'], - 'session_id' => $mollom_session_id, + 'content' => $captcha, + 'captchaId' => $captchaId, ); drupal_json_output($data); drupal_exit(); @@ -61,10 +66,9 @@ function mollom_captcha_js($type, $form_build_id, $mollom_session_id) { * @param $entity * The entity type of the data to report, e.g. 'node' or 'comment'. * @param $id - * The entity id the data belongs to. If 'session' is passed as $entity, then - * $id is assumed to be a Mollom session_id, as returned by Mollom servers, - * which should only be used to report session data that was not stored for an - * entity in the database (such as contact form submissions). + * The entity id the data belongs to. + * + * @see mollom_report_access() */ function mollom_report_form($form, &$form_state, $entity, $id) { $form['entity'] = array( @@ -99,17 +103,11 @@ function mollom_report_form_submit($form, &$form_state) { $id = $form_state['values']['id']; // Load the Mollom session data. - if ($entity == 'session') { - $data = new stdClass; - $data->session_id = $id; - } - else { - $data = mollom_data_load($entity, $id); - } + $data = mollom_data_load($entity, $id); // Send feedback to Mollom, if we have session data. - if (!empty($data->session_id) && !empty($form_state['values']['mollom']['feedback'])) { - if (_mollom_send_feedback($data->session_id, $form_state['values']['mollom']['feedback'])) { + if ((!empty($data->contentId) || !empty($data->captchaId)) && !empty($form_state['values']['mollom']['feedback'])) { + if (_mollom_send_feedback($data, $form_state['values']['mollom']['feedback'])) { drupal_set_message(t('The content was successfully reported as inappropriate.')); } } diff --git tests/mollom.class.test tests/mollom.class.test new file mode 100644 index 0000000..ed65fe7 --- /dev/null +++ tests/mollom.class.test @@ -0,0 +1,218 @@ + 'Mollom class', + 'description' => 'Tests Mollom class functionality.', + 'group' => 'Mollom', + ); + } + + function setUp() { + parent::setUp(); + // DrupalUnitTestCase does not autoload classes for whatever reason. + module_load_include('inc', 'mollom'); + } + + /** + * Asserts that two values belonging to the same variable are equal. + * + * Checks to see whether two values, which belong to the same variable name or + * identifier, are equal and logs a readable assertion message. + * + * @param $name + * A name or identifier to use in the assertion message. + * @param $first + * The first value to check. + * @param $second + * The second value to check. + * + * @return + * TRUE if the assertion succeeded, FALSE otherwise. + * + * @see MollomWebTestCase::assertNotSame() + * + * @todo D8: Move into core. This improved assertEqual() did not get into D7, + * since the function signature differs and it's plenty of work to manually + * update all assertEqual() invocations throughout all tests. + */ + protected function assertSame($name, $first, $second) { + $message = t("@name: @first is equal to @second.", array( + '@name' => $name, + '@first' => var_export($first, TRUE), + '@second' => var_export($second, TRUE), + )); + $this->assertEqual($first, $second, $message); + } + + /** + * Asserts that two values belonging to the same variable are not equal. + * + * Checks to see whether two values, which belong to the same variable name or + * identifier, are not equal and logs a readable assertion message. + * + * @param $name + * A name or identifier to use in the assertion message. + * @param $first + * The first value to check. + * @param $second + * The second value to check. + * + * @return + * TRUE if the assertion succeeded, FALSE otherwise. + * + * @see MollomWebTestCase::assertSame() + */ + protected function assertNotSame($name, $first, $second) { + $message = t("@name: @first is not equal to @second.", array( + '@name' => $name, + '@first' => var_export($first, TRUE), + '@second' => var_export($second, TRUE), + )); + $this->assertNotEqual($first, $second, $message); + } + + /** + * Tests Mollom::httpBuildQuery(). + */ + function testHttpBuildQuery() { + $input = array('foo' => 1, 'bar' => 2); + $expected = 'foo=1&bar=2'; + $this->assertSame(var_export($input, TRUE), Mollom::httpBuildQuery($input), $expected); + + $input = array('checks' => array('foo' => 'spam', 'bar' => 'profanity')); + $expected = 'checks[foo]=spam&checks[bar]=profanity'; + $this->assertSame(var_export($input, TRUE), Mollom::httpBuildQuery($input), $expected); + + $input = array('checks' => array('spam', 'profanity')); + $expected = 'checks=spam&checks=profanity'; + $this->assertSame(var_export($input, TRUE), Mollom::httpBuildQuery($input), $expected); + + $input = array('checks' => array(array('spam'), array('profanity'))); + $expected = 'checks=spam&checks=profanity'; + $this->assertSame(var_export($input, TRUE), Mollom::httpBuildQuery($input), $expected); + + $input = array('checks' => array('spam', '')); + $expected = 'checks=spam&checks='; + $this->assertSame(var_export($input, TRUE), Mollom::httpBuildQuery($input), $expected); + + $input = array('checks' => 'spam'); + $expected = 'checks=spam'; + $this->assertSame(var_export($input, TRUE), Mollom::httpBuildQuery($input), $expected); + } + + /** + * Tests Mollom::httpParseQuery(). + */ + function testHttpParseQuery() { + $input = 'foo=1&bar=2'; + $expected = array('foo' => 1, 'bar' => 2); + $this->assertSame($input, Mollom::httpParseQuery($input), $expected); + + $input = 'checks=spam&checks=profanity'; + $expected = array('checks' => array('spam', 'profanity')); + $this->assertSame($input, Mollom::httpParseQuery($input), $expected); + + // Mollom::httpParseQuery() does not attempt to work transparently. Thus, + // multiple parameter names containing brackets itself (regular PHP syntax) + // will lead to an "unexpected" result. Although it wouldn't be hard to add + // support for this, there's currently no need for it. + $input = 'checks[]=spam&checks[]=profanity'; + $expected = array('checks' => array(array('spam'), array('profanity'))); + $this->assertSame($input, Mollom::httpParseQuery($input), $expected); + + $input = 'checks=spam&checks='; + $expected = array('checks' => array('spam', '')); + $this->assertSame($input, Mollom::httpParseQuery($input), $expected); + + $input = 'checks=spam&checks'; + $expected = array('checks' => array('spam', '')); + $this->assertSame($input, Mollom::httpParseQuery($input), $expected); + + $input = 'checks=spam&'; + $expected = array('checks' => 'spam'); + $this->assertSame($input, Mollom::httpParseQuery($input), $expected); + + $input = 'checks=spam'; + $expected = array('checks' => 'spam'); + $this->assertSame($input, Mollom::httpParseQuery($input), $expected); + } + + /** + * Tests Mollom::parseXML(). + */ + function testParseXML() { + $header = ''; + + $input = $header . <<
0
+ en
+ de
+ 0
+ @data', array('%entity' => $entity, '@id' => $id, '@data' => var_export($data, TRUE)))); - if (isset($session_id)) { - $this->assertSame(t('Stored session id'), $data->session_id, $session_id); + $this->assertTrue($data->id, t('Mollom session data for %entity @id exists:
@data', array( + '%entity' => $entity, + '@id' => $id, + '@data' => var_export($data, TRUE), + ))); + if (isset($response_id)) { + $this->assertSame(t('Stored session id'), $data->$response_type, $response_id); } return $data; } @@ -584,12 +668,16 @@ class MollomWebTestCase extends DrupalWebTestCase { * * @see MollomWebTestCase::resetServerRecords() * @see mollom_test_xmlrpc() + * + * @todo Needs update. May directly use the short name suffixes of testing + * server API functions; i.e., 'content', 'captcha', 'blacklist', etc. */ protected function getServerRecord($method = 'mollom.checkContent') { // Map the XML-RPC method name to the corresponding function callback name. drupal_load('module', 'mollom_test'); $method_function_map = mollom_test_xmlrpc(); $function = $method_function_map[$method]; + $function = strtr($function, array('xmlrpc_' => '')); // Retrieve last recorded values. $storage = variable_get($function, array()); @@ -614,6 +702,7 @@ class MollomWebTestCase extends DrupalWebTestCase { drupal_load('module', 'mollom_test'); $method_function_map = mollom_test_xmlrpc(); $function = $method_function_map[$method]; + $function = strtr($function, array('xmlrpc_' => '')); // Delete the variable. variable_del($function); @@ -630,7 +719,7 @@ class MollomWebTestCase extends DrupalWebTestCase { * * @see DrupalWebTestCase->drupalGet() * @see MollomWebTestCase->assertMollomWatchdogMessages() - * @see MollomWebTestCase->assertSessionID() + * @see MollomWebTestCase->assertResponseID() */ protected function drupalGet($path, array $options = array(), array $headers = array()) { $output = parent::drupalGet($path, $options, $headers); @@ -649,7 +738,7 @@ class MollomWebTestCase extends DrupalWebTestCase { * negate the watchdog message severity assertion. * * @see MollomWebTestCase->assertMollomWatchdogMessages() - * @see MollomWebTestCase->assertSessionID() + * @see MollomWebTestCase->assertResponseID() * @see DrupalWebTestCase->drupalPost() */ protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) { @@ -731,10 +820,6 @@ class MollomInstallationTestCase extends MollomWebTestCase { } function setUp() { - // Re-initialize stored session_id and watchdog messages. - $this->resetSessionID(); - $this->messages = array(); - $this->disableDefaultSetup = TRUE; parent::setUp(array('comment')); @@ -834,11 +919,14 @@ class MollomInstallationTestCase extends MollomWebTestCase { $this->assertText(t('The Mollom servers could not be contacted. Please make sure that your web server can make outgoing HTTP requests.')); $this->assertNoText($this->fallback_message, t('Fallback message not found.')); + // Create a testing site on backend to have some API keys. + $this->createKeys(); + // Verify that valid keys work. $this->drupalGet('admin/config/content/mollom/settings', array('watchdog' => WATCHDOG_EMERGENCY)); $edit = array( - 'mollom_public_key' => MOLLOM_TEST_PUBLIC_KEY, - 'mollom_private_key' => MOLLOM_TEST_PRIVATE_KEY, + 'mollom_public_key' => $this->public_key, + 'mollom_private_key' => $this->private_key, 'mollom_testing_mode' => 1, ); $this->drupalPost(NULL, $edit, t('Save configuration')); @@ -881,153 +969,305 @@ class MollomResponseTestCase extends MollomWebTestCase { } /** + * Tests Site API. + */ + function testSiteAPI() { + $mollom = mollom(); + $info = $mollom->getClientInformation(); + + // Create a new site. + $site = array( + 'url' => 'example.com', + 'email' => 'mollom@example.com', + ); + $result = $mollom->createSite($site); + $this->assertMollomWatchdogMessages(); + $this->assertTrue(!empty($result['publicKey']), 'publicKey found.'); + $this->assertTrue(!empty($result['privateKey']), 'privateKey found.'); + $this->assertSame('url', $result['url'], $site['url']); + $this->assertSame('email', $result['email'], $site['email']); + $this->assertTrue(!isset($result['platformName']), 'platformName not found.'); + $this->assertTrue(!isset($result['platformVersion']), 'platformVersion not found.'); + $this->assertTrue(!isset($result['clientName']), 'clientName not found.'); + $this->assertTrue(!isset($result['clientVersion']), 'clientVersion not found.'); + $this->assertTrue(!empty($result['servers']) && is_array($result['servers']), 'Server list found.'); + + $site = $result; + $mollom->publicKey = $site['publicKey']; + $mollom->privateKey = $site['privateKey']; + + // Verify that getSite() response equals the createSite() response. + $result = $mollom->getSite(); + $this->assertMollomWatchdogMessages(); + $this->assertSame('publicKey', $result['publicKey'], $site['publicKey']); + $this->assertSame('privateKey', $result['privateKey'], $site['privateKey']); + $this->assertSame('url', $result['url'], $site['url']); + $this->assertSame('email', $result['email'], $site['email']); + $this->assertTrue(!isset($result['platformName']), 'platformName not found.'); + $this->assertTrue(!isset($result['platformVersion']), 'platformVersion not found.'); + $this->assertTrue(!isset($result['clientName']), 'clientName not found.'); + $this->assertTrue(!isset($result['clientVersion']), 'clientVersion not found.'); + $this->assertSame('servers', $result['servers'], $site['servers']); + + // Test that verifying keys updates client information. + $result = $mollom->verifyKey(); + $this->assertMollomWatchdogMessages(); + $this->assertIdentical($result, TRUE, 'Site was updated.'); + + $result = $mollom->getSite(); + $this->assertMollomWatchdogMessages(); + $this->assertSame('publicKey', $result['publicKey'], $site['publicKey']); + $this->assertSame('privateKey', $result['privateKey'], $site['privateKey']); + $this->assertSame('url', $result['url'], $site['url']); + $this->assertSame('email', $result['email'], $site['email']); + $this->assertSame('platformName', $result['platformName'], $info['platformName']); + $this->assertSame('platformVersion', $result['platformVersion'], $info['platformVersion']); + $this->assertSame('clientName', $result['clientName'], $info['clientName']); + $this->assertSame('clientVersion', $result['clientVersion'], $info['clientVersion']); + $this->assertSame('servers', $result['servers'], $site['servers']); + + // Verify that the site is listed. + // FIXME: Site listing not supported by backend yet. + /* + $result = $mollom->getSites(); + $this->assertMollomWatchdogMessages(); + $found = FALSE; + foreach ($result as $record) { + if ($record['publicKey'] == $site['publicKey']) { + $found = TRUE; + } + } + $this->assertTrue($found, 'Site record was found in site list.'); + */ + + // Verify that the site can be deleted. + $result = $mollom->deleteSite($site['publicKey']); + $this->assertMollomWatchdogMessages(); + $this->assertIdentical($result, TRUE, 'Site was deleted.'); + + // Verify that the site no longer appears in site list. + $mollom->publicKey = $this->public_key; + $mollom->privateKey = $this->private_key; + // FIXME: Site listing not supported by backend yet. + /* + $result = $mollom->getSites(); + $this->assertMollomWatchdogMessages(); + $found = FALSE; + foreach ($result as $record) { + if ($record['publicKey'] == $site['publicKey']) { + $found = TRUE; + } + } + $this->assertFalse($found, 'Deleted site no longer exists.'); + */ + + // Verify that retrieving the deleted site yields a 404. + $result = $mollom->getSite($site['publicKey']); + $this->assertMollomWatchdogMessages(WATCHDOG_EMERGENCY); + $this->assertEqual($result, 404, 'Attempt to get deleted site throws 404.'); + + // Verify that authentication fails. + $mollom->publicKey = $site['publicKey']; + $mollom->privateKey = $site['privateKey']; + $result = $mollom->getSite(); + $this->assertMollomWatchdogMessages(WATCHDOG_EMERGENCY); + $this->assertEqual($mollom->lastResponseCode, 404, 'Attempt to authenticate with deleted site keys returns 404.'); + + // Restore keys for tearDown(). + $mollom->publicKey = $this->public_key; + $mollom->privateKey = $this->private_key; + } + + /** * Tests mollom.checkContent(). */ function testCheckContent() { + $mollom = mollom(); $data = array( - 'author_name' => $this->admin_user->name, - 'author_mail' => $this->admin_user->mail, - 'author_id' => $this->admin_user->uid, - 'author_ip' => ip_address(), + 'authorName' => $this->admin_user->name, + 'authorMail' => $this->admin_user->mail, + 'authorId' => $this->admin_user->uid, + 'authorIp' => ip_address(), ); // Ensure proper response for 'ham' submissions. // By default (i.e., omitting 'checks') we expect spam and quality checking // only. - $data['post_body'] = 'ham'; - $result = mollom('mollom.checkContent', $data); + $data['postBody'] = 'ham'; + $result = $mollom->checkContent($data); $this->assertMollomWatchdogMessages(); - $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_HAM); - $this->assertSame('quality', $result['quality'], 1); - $this->assertTrue(!isset($result['profanity']), 'profanity not returned.'); - $session_id = $this->assertSessionID($result['session_id']); + $this->assertSame('spamScore', $result['spamScore'], 0.0); + $this->assertSame('spamClassification', $result['spamClassification'], 'ham'); + $this->assertSame('qualityScore', $result['qualityScore'], 1.0); + $this->assertTrue(!isset($result['profanityScore']), 'profanityScore not returned.'); + $data['id'] = $this->assertResponseID('contentId', $result['id']); // Ensure proper response for 'spam' submissions, re-using session_id. - $data['post_body'] = 'spam'; - $data['session_id'] = $session_id; - $result = mollom('mollom.checkContent', $data); + $data['postBody'] = 'spam'; + $result = $mollom->checkContent($data); $this->assertMollomWatchdogMessages(); - $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_SPAM); - $this->assertSame('quality', $result['quality'], 0); - $this->assertTrue(!isset($result['profanity']), 'profanity not returned.'); - $session_id = $this->assertSessionID($result['session_id']); + $this->assertSame('spamScore', $result['spamScore'], 1.0); + $this->assertSame('spamClassification', $result['spamClassification'], 'spam'); + $this->assertSame('qualityScore', $result['qualityScore'], 0.0); + $this->assertTrue(!isset($result['profanityScore']), 'profanityScore not returned.'); + $data['id'] = $this->assertResponseID('contentId', $result['id']); // Ensure proper response for 'unsure' submissions, re-using session_id. - $data['post_body'] = 'unsure'; - $data['session_id'] = $session_id; - $result = mollom('mollom.checkContent', $data); + $data['postBody'] = 'unsure'; + $result = $mollom->checkContent($data); $this->assertMollomWatchdogMessages(); - $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_UNSURE); - $this->assertSame('quality', $result['quality'], 0.5); - $this->assertTrue(!isset($result['profanity']), 'profanity not returned.'); - $session_id = $this->assertSessionID($result['session_id']); + $this->assertSame('spamScore', $result['spamScore'], 0.5); + $this->assertSame('spamClassification', $result['spamClassification'], 'unsure'); + $this->assertSame('qualityScore', $result['qualityScore'], 0.5); + $this->assertTrue(!isset($result['profanityScore']), 'profanityScore not returned.'); + $data['id'] = $this->assertResponseID('contentId', $result['id']); // Additionally enable profanity checking. - $data['post_body'] = 'spam profanity'; - $data['checks'] = 'spam,quality,profanity'; - $data['session_id'] = $session_id; - $result = mollom('mollom.checkContent', $data); + $data['postBody'] = 'spam profanity'; + $data['checks'] = array('spam', 'quality', 'profanity'); + $result = $mollom->checkContent($data); $this->assertMollomWatchdogMessages(); - $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_SPAM); - $this->assertSame('quality', $result['quality'], 0); - $this->assertSame('profanity', $result['profanity'], 1); - $session_id = $this->assertSessionID($result['session_id']); + $this->assertSame('spamScore', $result['spamScore'], 1.0); + $this->assertSame('spamClassification', $result['spamClassification'], 'spam'); + $this->assertSame('qualityScore', $result['qualityScore'], 0.0); + $this->assertSame('profanityScore', $result['profanityScore'], 1.0); + $data['id'] = $this->assertResponseID('contentId', $result['id']); // Change the string to contain profanity only. - $data['post_body'] = 'profanity'; - $data['checks'] = 'spam,quality,profanity'; - $data['session_id'] = $session_id; - $result = mollom('mollom.checkContent', $data); + $data['postBody'] = 'profanity'; + $data['checks'] = array('spam', 'quality', 'profanity'); + $result = $mollom->checkContent($data); $this->assertMollomWatchdogMessages(); - $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_UNSURE); - $this->assertSame('quality', $result['quality'], 0); - $this->assertSame('profanity', $result['profanity'], 1); - $session_id = $this->assertSessionID($result['session_id']); + $this->assertSame('spamScore', $result['spamScore'], 0.5); + $this->assertSame('spamClassification', $result['spamClassification'], 'unsure'); + $this->assertSame('qualityScore', $result['qualityScore'], 0.0); + $this->assertSame('profanityScore', $result['profanityScore'], 1.0); + $data['id'] = $this->assertResponseID('contentId', $result['id']); // Disable spam checking, only do profanity checking. - $data['post_body'] = 'spam profanity'; - $data['checks'] = 'profanity'; - $data['session_id'] = $session_id; - $result = mollom('mollom.checkContent', $data); + $data['postBody'] = 'spam profanity'; + $data['checks'] = array('profanity'); + $result = $mollom->checkContent($data); $this->assertMollomWatchdogMessages(); - $this->assertTrue(!isset($result['spam']), 'spam not returned.'); - $this->assertTrue(!isset($result['quality']), 'quality not returned.'); - $this->assertSame('profanity', $result['profanity'], 1); - $session_id = $this->assertSessionID($result['session_id']); + $this->assertTrue(!isset($result['spamScore']), 'spam not returned.'); + $this->assertTrue(!isset($result['spamClassification']), 'spamClassification not returned.'); + $this->assertTrue(!isset($result['qualityScore']), 'qualityScore not returned.'); + $this->assertSame('profanityScore', $result['profanityScore'], 1.0); + $data['id'] = $this->assertResponseID('contentId', $result['id']); // Pass arbitrary string to profanity checking. - $data['post_body'] = $this->randomString(12); - $data['session_id'] = $session_id; - $result = mollom('mollom.checkContent', $data); + $data['postBody'] = $this->randomString(12); + $result = $mollom->checkContent($data); $this->assertMollomWatchdogMessages(); - $this->assertTrue(!isset($result['spam']), 'spam not returned.'); - $this->assertTrue(!isset($result['quality']), 'quality not returned.'); - $this->assertSame('profanity', $result['profanity'], 0); - $session_id = $this->assertSessionID($result['session_id']); + $this->assertTrue(!isset($result['spamScore']), 'spam not returned.'); + $this->assertTrue(!isset($result['spamClassification']), 'spamClassification not returned.'); + $this->assertTrue(!isset($result['qualityScore']), 'qualityScore not returned.'); + $this->assertSame('profanityScore', $result['profanityScore'], 0.0); + $data['id'] = $this->assertResponseID('contentId', $result['id']); } /** * Tests results of mollom.checkContent() across requests for a single session. */ function testCheckContentSession() { + $mollom = mollom(); $data = array( - 'author_name' => $this->admin_user->name, - 'author_mail' => $this->admin_user->mail, - 'author_id' => $this->admin_user->uid, - 'author_ip' => ip_address(), + 'authorName' => $this->admin_user->name, + 'authorMail' => $this->admin_user->mail, + 'authorId' => $this->admin_user->uid, + 'authorIp' => ip_address(), ); // Sequence: Post unsure spam, correct CAPTCHA, change post into spam, // expect it to be ham (due to correct CAPTCHA). - $data['post_body'] = 'unsure'; - $result = mollom('mollom.checkContent', $data); + $data['postBody'] = 'unsure'; + $result = $mollom->checkContent($data); $this->assertMollomWatchdogMessages(); - $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_UNSURE); - $data['session_id'] = $this->assertSessionID($result['session_id']); + $this->assertSame('spamScore', $result['spamScore'], 0.5); + $this->assertSame('spamClassification', $result['spamClassification'], 'unsure'); + $data['id'] = $contentId = $this->assertResponseID('contentId', $result['id']); $captcha_data = array( - 'session_id' => $data['session_id'], - 'author_ip' => $data['author_ip'], + 'type' => 'image', + 'contentId' => $contentId, + 'authorIp' => $data['authorIp'], ); - $result = mollom('mollom.getImageCaptcha', $captcha_data); + $result = $mollom->getCaptcha($captcha_data); $this->assertMollomWatchdogMessages(); - $data['session_id'] = $this->assertSessionID($result['session_id']); + $data['captchaId'] = $captchaId = $this->assertResponseID('captchaId', $result['id']); $captcha_data = array( - 'session_id' => $data['session_id'], - 'author_ip' => $data['author_ip'], - 'author_id' => $data['author_id'], - 'captcha_result' => 'correct', + 'id' => $captchaId, + 'contentId' => $contentId, + 'authorIp' => $data['authorIp'], + 'authorId' => $data['authorId'], + 'solution' => 'correct', ); - $result = mollom('mollom.checkCaptcha', $captcha_data); + $result = $mollom->checkCaptcha($captcha_data); $this->assertMollomWatchdogMessages(); - $this->assertIdentical($result, TRUE, t('CAPTCHA response was correct.')); + $this->assertSame('solved', $result['solved'], 1); - $data['post_body'] = 'spam'; - $result = mollom('mollom.checkContent', $data); + $data['postBody'] = 'spam'; + $result = $mollom->checkContent($data); $this->assertMollomWatchdogMessages(); - $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_HAM); - $data['session_id'] = $this->assertSessionID($result['session_id']); + $this->assertSame('spamScore', $result['spamScore'], 0.0); + $this->assertSame('spamClassification', $result['spamClassification'], 'ham'); + $data['contentId'] = $this->assertResponseID('contentId', $result['id']); + } + + /** + * Tests the language detection functionality at the API level. + */ + function testCheckContentLanguage() { + // Note that Mollom supports more languages than those tested. + $tests = array( + 'Some text in lang-en.' => array( + 'en' => 1.0, + ), + 'Etwas lang-de and some lang-en, too.' => array( + 'en' => 0.5, + 'de' => 0.5, + ), + 'Unknown language' => array( + 'zxx' => 1.0, + ), + 'lang-en lang-de lang-es lang-ru lang-it' => array( + LANGUAGE_NONE => 1.0, + ), + ); + + $mollom = mollom(); + foreach ($tests as $string => $expected) { + $result = $mollom->checkContent(array( + 'checks' => 'language', + 'postBody' => $string, + )); + // Parse result values. + $actual = array(); + foreach ($result['languages'] as $language) { + $actual[$language['languageCode']] = $language['confidence']; + } + $this->assertSame($string, $actual, $expected); + } } /** * Tests mollom.getImageCaptcha(). */ function testGetImageCaptcha() { + $mollom = mollom(); // Ensure we get no SSL URL by default. $data = array( - 'author_ip' => ip_address(), + 'type' => 'image', + 'authorIp' => ip_address(), ); - $result = mollom('mollom.getImageCaptcha', $data); + $result = $mollom->getCaptcha($data); $this->assertMollomWatchdogMessages(); $this->assertTrue(strpos($result['url'], 'http://') === 0, t('CAPTCHA URL uses HTTP protocol.')); // Ensure we get a SSL URL when passing the 'ssl' parameter. - $data = array( - 'author_ip' => ip_address(), - 'ssl' => TRUE, - ); - $result = mollom('mollom.getImageCaptcha', $data); + $data['ssl'] = TRUE; + $result = $mollom->getCaptcha($data); $this->assertMollomWatchdogMessages(); $this->assertTrue(strpos($result['url'], 'https://') === 0, t('CAPTCHA URL uses HTTPS protocol.')); } @@ -1036,22 +1276,24 @@ class MollomResponseTestCase extends MollomWebTestCase { * Tests mollom.checkCaptcha(). */ function testCheckCaptcha() { + $mollom = mollom(); // Ensure we can send an 'author_id'. // Verifying no severe watchdog messages is sufficient, as unsupported // parameters would trigger a XML-RPC error. $uid = rand(); $data = array( - 'author_ip' => ip_address(), - 'author_id' => $uid, + 'type' => 'image', + 'authorIp' => ip_address(), + 'authorId' => $uid, ); - $result = mollom('mollom.getImageCaptcha', $data); + $result = $mollom->getCaptcha($data); $this->assertMollomWatchdogMessages(); + $data['id'] = $this->assertResponseID('captchaId', $result['id']); $data += array( - 'session_id' => $result['session_id'], - 'captcha_result' => 'correct', + 'solution' => 'correct', ); - $result = mollom('mollom.checkCaptcha', $data); + $result = $mollom->checkCaptcha($data); $this->assertMollomWatchdogMessages(); } } @@ -1060,6 +1302,9 @@ class MollomResponseTestCase extends MollomWebTestCase { * Tests low-level communication with local fake Mollom server. */ class MollomResponseLocalTestCase extends MollomResponseTestCase { + // Re-route Mollom communication to this testing site. + protected $mollomClass = 'MollomDrupalTestLocal'; + public static function getInfo() { return array( 'name' => 'Server responses (local)', @@ -1067,13 +1312,6 @@ class MollomResponseLocalTestCase extends MollomResponseTestCase { 'group' => 'Mollom', ); } - - function setUp() { - // Enable testing server implementation. - parent::setUp(array('mollom_test')); - // Re-route Mollom communication to this testing site. - variable_set('mollom_servers', array($GLOBALS['base_url'] . '/xmlrpc.php?version=')); - } } class MollomAccessTestCase extends MollomWebTestCase { @@ -1155,6 +1393,7 @@ class MollomAccessTestCase extends MollomWebTestCase { $this->web_user = $this->drupalCreateUser(array('edit own comments')); $this->drupalLogin($this->web_user); $edit = array( + 'subject' => 'ham', 'comment_body[und][0][value]' => 'ham', ); $this->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview')); @@ -1170,6 +1409,7 @@ class MollomAccessTestCase extends MollomWebTestCase { $this->clickLink('edit'); $edit = array( + 'subject' => 'spam', 'comment_body[und][0][value]' => 'spam', ); $this->drupalPost(NULL, $edit, t('Preview')); @@ -1178,6 +1418,7 @@ class MollomAccessTestCase extends MollomWebTestCase { $this->drupalPost(NULL, array(), t('Save')); $this->assertNoText($this->spam_message); $this->assertText('node body'); + $this->assertText($edit['comment_body[und][0][value]']); // Log in back the regular user and try to edit the comment containing spam. $this->drupalLogin($this->web_user); @@ -1194,6 +1435,9 @@ class MollomAccessTestCase extends MollomWebTestCase { } class MollomFallbackTestCase extends MollomWebTestCase { + // Re-route Mollom communication to this testing site. + protected $mollomClass = 'MollomDrupalTestLocal'; + public static function getInfo() { return array( 'name' => 'Fallback behavior', @@ -1202,11 +1446,6 @@ class MollomFallbackTestCase extends MollomWebTestCase { ); } - function setUp() { - // Enable testing server implementation. - parent::setUp(array('mollom_test')); - } - /** * Make sure that "request new password" submissions can be blocked when * the Mollom servers are unreachable. @@ -1274,8 +1513,7 @@ class MollomFallbackTestCase extends MollomWebTestCase { variable_set('mollom_servers', array( 'http://fake-host-1', 'http://fake-host-2', - $GLOBALS['base_url'] . '/xmlrpc.php?version=', - 'http://xmlrpc1.mollom.com', // The real server. + $GLOBALS['base_url'] . '/mollom-test/rest/' . Mollom::API_VERSION, // A real server. 'http://fake-host-3', )); @@ -1291,6 +1529,8 @@ class MollomFallbackTestCase extends MollomWebTestCase { } class MollomServerListRecoveryTestCase extends MollomWebTestCase { + protected $profile = 'testing'; + public static function getInfo() { return array( 'name' => 'Server list recovery', @@ -1316,54 +1556,22 @@ class MollomServerListRecoveryTestCase extends MollomWebTestCase { ); foreach ($list as $servers) { - // Call mollom.verifyKey with an invalid server list. The expected behavior - // is that the first call fails, but that the second call succeeds because - // the server list is automatically reset or recovered by the Mollom module. variable_set('mollom_servers', $servers); + // Reset static, since the Mollom instance is statically cached. + drupal_static_reset('mollom'); - $key_is_valid = mollom('mollom.verifyKey'); - $this->assertIdentical($key_is_valid, NETWORK_ERROR, t('The Mollom servers could not be contacted.')); + // Verify that Mollom::query() fails with an invalid server list and + // appropriate messages are logged. + $mollom = mollom(); + $key_is_valid = $mollom->verifyKey(); $this->assertMollomWatchdogMessages(WATCHDOG_EMERGENCY); + $this->assertIdentical($key_is_valid, Mollom::NETWORK_ERROR, t('The Mollom servers could not be contacted.')); - $key_is_valid = mollom('mollom.verifyKey'); - $this->assertIdentical($key_is_valid, TRUE, t('The Mollom servers could be contacted.')); + // Verify that Mollom::query() automatically empties the server list and + // a directly following, subsequent request attempt succeeds. + $key_is_valid = $mollom->verifyKey(); $this->assertMollomWatchdogMessages(); - } - } -} - -class MollomLanguageDetectionTestCase extends MollomWebTestCase { - public static function getInfo() { - return array( - 'name' => 'Language detection', - 'description' => 'Tests language detection functionality.', - 'group' => 'Mollom', - ); - } - - /** - * Test the language detection functionality at the API level without using a web interface. - */ - function testLanguageDetectionAPI() { - // Note that Mollom supports more languages than those tested. - $strings = array( - 'en' => "Hi, this is a test of the language detection code to see if it works well.", - 'nl' => "Hallo, dit is een test van de taaldetectiecode om te controleren of het werkt.", - 'fr' => "Bonjour, ceci est un test du detecteur langue automatique pour voir ci ça marche bien.", - 'de' => "Bedecke deinen Himmel, Zeus, Mit Wolkendunst Und übe, dem Knaben gleich, der Disteln köpft, An Eichen dich und Bergeshöhn.", - 'ko' => "'엄마야 누나야 강변살자. 뜰에는 반짝이는 금모래 빛. 뒷문 밖에는 갈잎의 노래", - 'ru' => "Холуй трясется. Раб хохочет. Палач свою секиру точит. Тиран кромсает каплуна. Сверкает зимняя луна.", - 'hu' => "Földszintiek mászófámról pillantva fejjel lefelé ti lógtok bele nézőim az űrbe ki tudja így kölcsönös kíváncsiak a helyes felelet kié", - 'el' => "Σαν να 'χουνε την όψη της αιώνες οργωμένη. Κάτι άναρχο κι ατέλειωτο στο πρόσωπό της μένει.", - 'ja' => "吹くからに秋の草木のしをるれば", - 'th' => "ทั่วประเทศ ประมาณ ๔๐,๐๐๐ แห่ง ชาวไทยนับตั้งแต่ครั้งอดีตมีวิถี ชีวิตผูกพันกับพุทธศาสนาอย่างใกล้ชิด แสดงออกมาเป็น ขนบธรรมเนียมประเพณี", - 'zh' => "螽斯羽,诜诜兮。宜尔子孙,振振兮", - ); - - foreach ($strings as $language => $text) { - $result = mollom('mollom.detectLanguage', array('text' => $text)); - $this->assertEqual($result[0]['language'], $language, t('A language code was specified and they match.')); - $this->assertTrue($result[0]['confidence'] > 0, t('A confidence value was specified and it is greater than 0.')); + $this->assertIdentical($key_is_valid, TRUE, t('The Mollom servers could be contacted.')); } } } @@ -1393,141 +1601,137 @@ class MollomBlacklistTestCase extends MollomWebTestCase { } /** - * Test the URL blacklist functionality at the API level without using a web interface. + * Test the blacklist functionality at the API level without using a web interface. */ - function testUrlBlacklistAPI() { + function testBlacklistAPI() { + $mollom = mollom(); // Remove any stale blacklist entries from test runs that did not finish. - $blacklist = mollom('mollom.listBlacklistURL'); + $blacklist = $mollom->getBlacklist(); foreach ($blacklist as $entry) { if (REQUEST_TIME - strtotime($entry['created']) > 86400) { - mollom('mollom.removeBlacklistURL', array('url' => $entry['url'])); + $mollom->deleteBlacklistEntry($entry['id']); } } + $this->assertMollomWatchdogMessages(); // Blacklist a URL. - $domain = $this->randomName() . '.com'; - $result = mollom('mollom.addBlacklistURL', array('url' => 'http://' . $domain)); - $this->assertTrue($result, t('The URL was blacklisted.')); - - // Check whether posts containing the blacklisted URL are properly blocked. - $result = mollom('mollom.checkContent', array( - 'post_body' => "When the exact URL is present, the post should get blocked: http://{$domain}", + $domain = drupal_strtolower($this->randomName()) . '.com'; + $entry = $mollom->createBlacklistEntry(array( + 'value' => $domain, + 'context' => 'allFields', + 'reason' => 'spam', + 'match' => 'contains', )); - $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Exact URL match was blocked.')); + $this->assertMollomWatchdogMessages(); + $this->assertTrue($entry['id'], t('The URL was blacklisted.')); - $result = mollom('mollom.checkContent', array( - 'post_body' => "When the URL is expanded in the back, the post should get blocked: http://{$domain}/oh-my", + // Check whether posts containing the blacklisted URL are properly blocked. + $result = mollom()->checkContent(array( + 'postBody' => "When the exact URL is present, the post should get blocked: http://{$domain}", )); - $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Partial URL match was blocked.')); + $this->assertMollomWatchdogMessages(); + $this->assertSame('spamScore', $result['spamScore'], 1.0); + $this->assertEqual($result['spamClassification'], 'spam', t('Exact URL match was blocked.')); - $result = mollom('mollom.checkContent', array( - 'post_body' => "When the URL is expanded in the front, the post should get blocked: http://www.{$domain}", + $result = mollom()->checkContent(array( + 'postBody' => "When the URL is expanded in the back, the post should get blocked: http://{$domain}/oh-my", )); - $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('URL with www-prefix was blocked.')); + $this->assertMollomWatchdogMessages(); + $this->assertSame('spamScore', $result['spamScore'], 1.0); + $this->assertEqual($result['spamClassification'], 'spam', t('Partial URL match was blocked.')); - $result = mollom('mollom.checkContent', array( - 'post_body' => "When the URL has a different schema, the post should get blocked: ftp://www.{$domain}", + $result = mollom()->checkContent(array( + 'postBody' => "When the URL is expanded in the front, the post should get blocked: http://www.{$domain}", )); - $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('URL with different schema was blocked.')); + $this->assertMollomWatchdogMessages(); + $this->assertSame('spamScore', $result['spamScore'], 1.0); + $this->assertEqual($result['spamClassification'], 'spam', t('URL with www-prefix was blocked.')); - // @todo Not implemented yet. - /* - $result = mollom('mollom.checkContent', array( - 'post_body' => "When the domain appears on its own, the post should get blocked: www.{$domain}", + $result = mollom()->checkContent(array( + 'postBody' => "When the URL has a different schema, the post should get blocked: ftp://www.{$domain}", )); - $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Plain domain name with www-prefix was blocked.')); - */ - - $result = mollom('mollom.removeBlacklistURL', array('url' => 'http://' . $domain)); - $this->assertTrue($result, t('The blacklisted URL was removed.')); - } + $this->assertMollomWatchdogMessages(); + $this->assertSame('spamScore', $result['spamScore'], 1.0); + $this->assertEqual($result['spamClassification'], 'spam', t('URL with different schema was blocked.')); - /** - * Test the text blacklist functionality at the API level without using a web interface. - */ - function testTextBlacklistAPI() { - // Remove any stale blacklist entries from test runs that did not finish. - $blacklist = mollom('mollom.listBlacklistText'); - foreach ($blacklist as $entry) { - if (REQUEST_TIME - strtotime($entry['created']) > 86400) { - mollom('mollom.removeBlacklistText', array( - 'text' => $entry['text'], - 'context' => $entry['context'], - 'reason' => $entry['reason'], - )); - } - } + $result = $mollom->deleteBlacklistEntry($entry['id']); + $this->assertMollomWatchdogMessages(); + $this->assertIdentical($result, TRUE, t('The blacklisted URL was removed.')); // Blacklist a word. // @todo As of now, only non-numeric, lower-case text seems to be supported. $term = drupal_strtolower(preg_replace('/[^a-zA-Z]/', '', $this->randomName())); - $data = array( - 'text' => $term, + $entry = $mollom->createBlacklistEntry(array( + 'value' => $term, 'context' => 'everything', 'reason' => 'spam', 'match' => 'contains', - ); - $result = mollom('mollom.addBlacklistText', $data); - $this->assertIdentical($result, TRUE, t('The text was blacklisted.')); + )); + $this->assertMollomWatchdogMessages(); + $this->assertTrue($entry['id'], t('The text was blacklisted.')); // Check whether posts containing the blacklisted word are properly blocked. $data = array( - 'post_body' => $term, + 'postBody' => $term, ); - $result = mollom('mollom.checkContent', $data); - $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Identical match was blocked.')); + $result = mollom()->checkContent($data); + $this->assertMollomWatchdogMessages(); + $this->assertSame('spamScore', $result['spamScore'], 1.0); + $this->assertEqual($result['spamClassification'], 'spam', t('Identical match was blocked.')); $data = array( - 'post_body' => "When the term is present, the post should get blocked: " . $term, + 'postBody' => "When the term is present, the post should get blocked: " . $term, ); - $result = mollom('mollom.checkContent', $data); - $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Exact match was blocked.')); + $result = mollom()->checkContent($data); + $this->assertMollomWatchdogMessages(); + $this->assertSame('spamScore', $result['spamScore'], 1.0); + $this->assertEqual($result['spamClassification'], 'spam', t('Exact match was blocked.')); $data = array( - 'post_body' => "When match is 'contains', the word can be surrounded by other text: abc" . $term . "def", + 'postBody' => "When match is 'contains', the word can be surrounded by other text: abc" . $term . "def", ); - $result = mollom('mollom.checkContent', $data); - $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Partial match was blocked.')); + $result = mollom()->checkContent($data); + $this->assertMollomWatchdogMessages(); + $this->assertSame('spamScore', $result['spamScore'], 1.0); + $this->assertEqual($result['spamClassification'], 'spam', t('Partial match was blocked.')); // Update the blacklist entry to match the term only exactly. - $data = array( - 'text' => $term, + $entry = $mollom->updateBlacklistEntry(array( + 'id' => $entry['id'], + 'value' => $term, 'context' => 'everything', 'reason' => 'spam', 'match' => 'exact', - ); - $result = mollom('mollom.addBlacklistText', $data); - $this->assertTrue($result, t('The text was blacklisted.')); + )); + $this->assertMollomWatchdogMessages(); + $this->assertTrue($entry['id'], t('The blacklist entry was updated.')); $data = array( - 'post_body' => "When match is 'exact', it has to be exact: " . $term, + 'postBody' => "When match is 'exact', it has to be exact: " . $term, ); - $result = mollom('mollom.checkContent', $data); - $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Exact match was blocked.')); + $result = mollom()->checkContent($data); + $this->assertMollomWatchdogMessages(); + $this->assertSame('spamScore', $result['spamScore'], 1.0); + $this->assertEqual($result['spamClassification'], 'spam', t('Exact match was blocked.')); $data = array( - 'post_body' => "When match is 'exact', it has to be exact: abc{$term}def", + 'postBody' => "When match is 'exact', it has to be exact: abc{$term}def", ); - $result = mollom('mollom.checkContent', $data); - $this->assertNotEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Partial match was not blocked.')); + $result = mollom()->checkContent($data); + $this->assertMollomWatchdogMessages(); + $this->assertSame('spamScore', $result['spamScore'], 0.5); + $this->assertEqual($result['spamClassification'], 'unsure', t('Partial match was not blocked.')); - $data = array( - 'text' => $term, - 'context' => 'everything', - 'reason' => 'spam', - ); - $result = mollom('mollom.removeBlacklistText', $data); - $this->assertTrue($result, t('The blacklisted text was removed.')); + $result = $mollom->deleteBlacklistEntry($entry['id']); + $this->assertMollomWatchdogMessages(); + $this->assertIdentical($result, TRUE, t('The blacklisted text was removed.')); // Try to remove a non-existing entry. - $data = array( - 'text' => $term, - 'context' => 'everything', - 'reason' => 'spam', - ); - $result = mollom('mollom.removeBlacklistText', $data); + // @todo Ensure that the ID does not exist. + $result = $mollom->deleteBlacklistEntry(999); $this->assertMollomWatchdogMessages(WATCHDOG_EMERGENCY); $this->assertNotIdentical($result, TRUE, t('Error response for a non-existing blacklist text found.')); + $this->assertSame('Response code', $mollom->lastResponseCode, 404); } /** @@ -1549,7 +1753,7 @@ class MollomBlacklistTestCase extends MollomWebTestCase { $this->drupalGet('admin/config/content/mollom/blacklist'); $text = $this->randomName(); $edit = array( - 'entry[text]' => $text, + 'entry[value]' => $text, 'entry[context]' => 'everything', 'entry[match]' => 'contains', ); @@ -1572,7 +1776,7 @@ class MollomBlacklistTestCase extends MollomWebTestCase { $this->drupalGet('admin/config/content/mollom/blacklist/profanity'); $text = $this->randomName(); $edit = array( - 'entry[text]' => $text, + 'entry[value]' => $text, 'entry[context]' => 'everything', 'entry[match]' => 'contains', ); @@ -1685,7 +1889,7 @@ class MollomProfanityTestCase extends MollomWebTestCase { $this->drupalPost(NULL, $edit, t('Save')); $this->assertText($this->profanity_message); $this->assertNoText(t('Your comment has been posted.')); - $session_id = $this->assertSessionIDInForm(); + $contentId = $this->assertResponseIDInForm('contentId'); $edit["comment_body[$langcode][0][value]"] = 'not profane ham'; $this->drupalPost(NULL, $edit, t('Save')); @@ -1693,10 +1897,11 @@ class MollomProfanityTestCase extends MollomWebTestCase { $this->assertText(t('Your comment has been posted.')); $this->assertRaw('
' . $edit["comment_body[$langcode][0][value]"] . '
', t('Comment previously containing profanity was found.')); $cid = db_query('SELECT cid FROM {comment} WHERE subject = :subject ORDER BY created DESC', array(':subject' => $edit['subject']))->fetchField(); - $this->assertMollomData('comment', $cid, $session_id); + $this->assertMollomData('comment', $cid, 'contentId', $contentId); // Sequence: Post unsure spam (not profanity), post profanity along with // correct CAPTCHA, and expect that to be discarded. + $this->resetResponseID(); $this->web_user = $this->drupalCreateUser(); $this->drupalLogin($this->web_user); $edit = array( @@ -1711,7 +1916,8 @@ class MollomProfanityTestCase extends MollomWebTestCase { $this->assertCaptchaField(); $this->assertNoText($this->profanity_message); $this->assertNoText(t('Your comment has been posted.')); - $session_id = $this->assertSessionIDInForm(); + $contentId = $this->assertResponseIDInForm('contentId'); + $captchaId = $this->assertResponseIDInForm('captchaId'); $edit["comment_body[$langcode][0][value]"] = 'unsure profanity'; $this->postCorrectCaptcha(NULL, $edit, t('Save')); @@ -1725,6 +1931,9 @@ class MollomProfanityTestCase extends MollomWebTestCase { * Tests Mollom form configuration functionality. */ class MollomFormConfigurationTestCase extends MollomWebTestCase { + // Re-route Mollom communication to this testing site. + protected $mollomClass = 'MollomDrupalTestLocal'; + public static function getInfo() { return array( 'name' => 'Form administration', @@ -1734,9 +1943,7 @@ class MollomFormConfigurationTestCase extends MollomWebTestCase { } function setUp() { - parent::setUp(array('mollom_test')); - // Re-route Mollom communication to this testing site. - variable_set('mollom_servers', array($GLOBALS['base_url'] . '/xmlrpc.php?version=')); + parent::setUp(); $this->drupalLogin($this->admin_user); } @@ -2191,7 +2398,7 @@ class MollomNodeFormTestCase extends MollomWebTestCase { // Login and submit a node. $this->drupalLogin($this->web_user); $this->drupalGet('node/add/article'); - $session_id = $this->assertSessionIDInForm(); + $captchaId = $this->assertResponseIDInForm('captchaId'); $edit = array( 'title' => 'spam', 'mollom[captcha]' => 'correct', @@ -2199,7 +2406,7 @@ class MollomNodeFormTestCase extends MollomWebTestCase { $this->drupalPost(NULL, $edit, t('Save')); $this->node = $this->drupalGetNodeByTitle($edit['title']); $this->assertUrl('node/' . $this->node->nid); - $this->assertMollomData('node', $this->node->nid, $session_id); + $this->assertMollomData('node', $this->node->nid, 'captchaId', $captchaId); } /** @@ -2280,21 +2487,21 @@ class MollomCommentFormTestCase extends MollomWebTestCase { $this->drupalLogin($this->web_user); $this->drupalGet('comment/reply/' . $this->node->nid); $this->assertCaptchaField(); - $this->assertSessionIDInForm(); + $this->assertResponseIDInForm('captchaId'); $this->assertNoPrivacyLink(); // Try to submit an incorrect answer for the CAPTCHA, without value for // required field. $this->postIncorrectCaptcha(NULL, array(), t('Preview')); $this->assertText(t('Comment field is required.')); - $this->assertSessionIDInForm(); + $this->assertResponseIDInForm('captchaId'); $this->assertNoPrivacyLink(); // Try to submit a correct answer for the CAPTCHA, still without required // field value. $this->postCorrectCaptcha(NULL, array(), t('Preview')); $this->assertText(t('Comment field is required.')); - $session_id = $this->assertSessionIDInForm(); + $captchaId = $this->assertResponseIDInForm('captchaId'); $this->assertNoPrivacyLink(); // Finally, we should be able to submit a comment. @@ -2302,13 +2509,14 @@ class MollomCommentFormTestCase extends MollomWebTestCase { $this->assertText(t('Your comment has been posted.')); $this->assertRaw('spam
', t('Spam comment could be posted with correct CAPTCHA.')); $cid = db_query('SELECT cid FROM {comment} WHERE subject = :subject ORDER BY created DESC', array(':subject' => 'spam'))->fetchField(); - $this->assertMollomData('comment', $cid, $session_id); + $this->assertMollomData('comment', $cid, 'captchaId', $captchaId); // Verify we can solve the CAPTCHA directly. + $this->resetResponseID(); $value = 'some more spam'; $this->drupalGet('comment/reply/' . $this->node->nid); $this->assertCaptchaField(); - $session_id = $this->assertSessionIDInForm(); + $captchaId = $this->assertResponseIDInForm('captchaId'); $edit = array( 'comment_body[und][0][value]' => $value, 'mollom[captcha]' => 'correct', @@ -2316,7 +2524,7 @@ class MollomCommentFormTestCase extends MollomWebTestCase { $this->drupalPost(NULL, $edit, t('Save')); $this->assertText(t('Your comment has been posted.')); $cid = db_query('SELECT cid FROM {comment} WHERE subject = :subject ORDER BY created DESC', array(':subject' => $value))->fetchField(); - $this->assertMollomData('comment', $cid, $session_id); + $this->assertMollomData('comment', $cid, 'captchaId', $captchaId); } /** @@ -2340,7 +2548,7 @@ class MollomCommentFormTestCase extends MollomWebTestCase { ); $this->drupalPost(NULL, $edit, t('Save')); $this->assertCaptchaField(); - $session_id = $this->assertSessionIDInForm(); + $contentId = $this->assertResponseIDInForm('contentId'); $this->assertPrivacyLink(); // Try to submit the form by solving the CAPTCHA incorrectly. At this point, @@ -2348,34 +2556,34 @@ class MollomCommentFormTestCase extends MollomWebTestCase { // the comment is still neither ham or spam. $this->postIncorrectCaptcha(NULL, array(), t('Save')); $this->assertCaptchaField(); - $session_id = $this->assertSessionIDInForm(); + $captchaId = $this->assertResponseIDInForm('captchaId'); $this->assertPrivacyLink(); // Correctly solving the CAPTCHA should accept the form submission. $this->postCorrectCaptcha(NULL, array(), t('Save')); $this->assertRaw('' . $edit['comment_body[und][0][value]'] . '
', t('A comment that may contain spam was found.')); $cid = db_query('SELECT cid FROM {comment} WHERE subject = :subject ORDER BY created DESC', array(':subject' => $edit['comment_body[und][0][value]']))->fetchField(); - $this->assertMollomData('comment', $cid, $session_id); + $this->assertMollomData('comment', $cid, 'contentId', $contentId); // Try to save a new 'spam' comment; it should be discarded, with no CAPTCHA // appearing on the page. - $this->resetSessionID(); + $this->resetResponseID(); $this->drupalGet('comment/reply/' . $this->node->nid); $this->assertPrivacyLink(); $original_number_of_comments = $this->getCommentCount($this->node->nid); $this->assertSpamSubmit(NULL, array('comment_body[und][0][value]'), array(), t('Save')); - $session_id = $this->assertSessionIDInForm(); + $contentId = $this->assertResponseIDInForm('contentId'); $this->assertCommentCount($this->node->nid, $original_number_of_comments); $this->assertPrivacyLink(); // Try to save again; it should be discarded, with no CAPTCHA. $this->assertSpamSubmit(NULL, array('comment_body[und][0][value]'), array(), t('Save')); - $session_id = $this->assertSessionIDInForm(); + $contentId = $this->assertResponseIDInForm('contentId'); $this->assertCommentCount($this->node->nid, $original_number_of_comments); $this->assertPrivacyLink(); // Save a new 'ham' comment. - $this->resetSessionID(); + $this->resetResponseID(); $this->drupalGet('comment/reply/' . $this->node->nid); $this->assertPrivacyLink(); $original_number_of_comments = $this->getCommentCount($this->node->nid); @@ -2503,16 +2711,16 @@ class MollomContactFormTestCase extends MollomWebTestCase { $this->assertText($success); $report_link = $this->parseMollomMailReportLink(); $this->assertTrue($report_link, t('Report to Mollom link found in e-mail.')); - $this->assertEqual($report_link['entity'], 'session', t('Report link in e-mail uses entity type "session".')); - $this->assertMollomData($report_link['entity'], $report_link['session_id']); + $this->assertEqual($report_link['entity'], 'mollom_content', t('Report link in e-mail uses entity type "session".')); + $this->assertMollomData($report_link['entity'], $report_link['id']); // Submit an 'unsure' message. This should be accepted only after the // CAPTCHA has been solved. $this->assertUnsureSubmit($url, array('subject', 'message'), array(), $button, $success); $report_link = $this->parseMollomMailReportLink(); $this->assertTrue($report_link, t('Report to Mollom link found in e-mail.')); - $this->assertEqual($report_link['entity'], 'session', t('Report link in e-mail uses entity type "session".')); - $this->assertMollomData($report_link['entity'], $report_link['session_id']); + $this->assertEqual($report_link['entity'], 'mollom_content', t('Report link in e-mail uses entity type "session".')); + $this->assertMollomData($report_link['entity'], $report_link['id']); // Report the mail to Mollom. $this->drupalGet($report_link['url']); @@ -2541,7 +2749,7 @@ class MollomContactFormTestCase extends MollomWebTestCase { $found = array( 'url' => $matches[0], 'entity' => $matches[1], - 'session_id' => $matches[2], + 'id' => $matches[2], 'mail' => $email, ); } @@ -2569,7 +2777,7 @@ class MollomResellerTestCase extends MollomWebTestCase { // Create 3 test sites: for ($i = 1; $i <= 3; $i++) { - $keys[] = mollom('mollom.createSite', array( + $keys[] = mollom()->createSite(array( 'url' => 'http://example.com/site-'. $i, 'mail' => 'mail@example.com', 'status' => 0, @@ -2583,7 +2791,7 @@ class MollomResellerTestCase extends MollomWebTestCase { $sites = mollom('mollom.listSites'); foreach ($sites as $site) { // Retrieve the site information: - $details = mollom('mollom.getSite', array('client_key' => $site)); + $details = mollom()->getSite(array('client_key' => $site)); $this->assertEqual($details['mail'], 'mail@example.com', t('The original information is correctly retrieved from Mollom.')); $this->assertEqual($details['status'], 0, t('The original information is correctly retrieved from Mollom.')); @@ -2593,8 +2801,8 @@ class MollomResellerTestCase extends MollomWebTestCase { // valid sites in case someone messed up their Mollom settings! if ($details['mail'] == 'mail@example.com' || $details['mail'] == 'root@example.com') { // Update the information on the site and verify that it was updated. - mollom('mollom.updateSite', array('client_key' => $site, 'mail' => 'root@example.com')); - $details = mollom('mollom.getSite', array('client_key' => $site)); + mollom()->updateSite(array('client_key' => $site, 'mail' => 'root@example.com')); + $details = mollom()->getSite(array('client_key' => $site)); $this->assertEqual($details['mail'], 'root@example.com', t('The updated information is correctly retrieved from Mollom.')); // Verify that the existing information did not change (partial updates). @@ -2602,7 +2810,7 @@ class MollomResellerTestCase extends MollomWebTestCase { $this->assertEqual($details['testing'], 1, t('The original information is correctly retrieved from Mollom.')); // Delete the test site: - mollom('mollom.deleteSite', array('client_key' => $site)); + mollom()->deleteSite(array('client_key' => $site)); } else { $this->fail(t('We tried to delete a non-test site.')); @@ -2613,7 +2821,7 @@ class MollomResellerTestCase extends MollomWebTestCase { $this->assertMollomWatchdogMessages(); // Retrieve information about a non-existing site: - $details = mollom('mollom.getSite', array('client_key' => 'bogus')); + $details = mollom()->getSite(array('client_key' => 'bogus')); $this->assertEqual(xmlrpc_errno(), TRUE, t('Retrieving information from a non-existing site returned an XML-RPC error.')); $this->assertMollomWatchdogMessages(WATCHDOG_EMERGENCY); @@ -2627,6 +2835,9 @@ class MollomResellerTestCase extends MollomWebTestCase { * Tests form value processing. */ class MollomDataTestCase extends MollomWebTestCase { + // Re-route Mollom communication to this testing site. + protected $mollomClass = 'MollomDrupalTestLocal'; + public static function getInfo() { return array( 'name' => 'Data processing', @@ -2635,13 +2846,6 @@ class MollomDataTestCase extends MollomWebTestCase { ); } - function setUp() { - // Enable testing server implementation. - parent::setUp(array('mollom_test')); - // Re-route Mollom communication to this testing site. - variable_set('mollom_servers', array($GLOBALS['base_url'] . '/xmlrpc.php?version=')); - } - /** * Test mollom_form_get_values(). */ @@ -2693,20 +2897,20 @@ class MollomDataTestCase extends MollomWebTestCase { $form_state = array('values' => $values, 'buttons' => array()); $data = mollom_form_get_values($form_state, $fields, $form_info['mapping']); - $this->assertSame('post_title', $data['post_title'], $values['subject']); + $this->assertSame('postTitle', $data['postTitle'], $values['subject']); $body = array( $values['message'], $values['parent']['child'], $values['field_checked'][0]['value'], $values['field_checked'][1]['value'], ); - $this->assertSame('post_body', $data['post_body'], implode(" \n", $body)); - $this->assertSame('author_name', $data['author_name'], $values['name']); - $this->assertSame('author_mail', $data['author_mail'], $values['mail']); - $this->assertFalse(isset($data['author_url']), t('author_url: Undefined.')); - $this->assertFalse(isset($data['author_openid']), t('author_openid: Undefined.')); - $this->assertFalse(isset($data['author_id']), t('author_id: Undefined.')); - $this->assertSame('author_ip', $data['author_ip'], ip_address()); + $this->assertSame('postBody', $data['postBody'], implode(" \n", $body)); + $this->assertSame('authorName', $data['authorName'], $values['name']); + $this->assertSame('authorMail', $data['authorMail'], $values['mail']); + $this->assertFalse(isset($data['authorUrl']), t('authorUrl: Undefined.')); + $this->assertFalse(isset($data['authorOpenid']), t('authorOpenid: Undefined.')); + $this->assertFalse(isset($data['authorId']), t('authorId: Undefined.')); + $this->assertSame('authorIp', $data['authorIp'], ip_address()); // Verify submitted form values for an registered user. $user = $this->admin_user; @@ -2718,15 +2922,15 @@ class MollomDataTestCase extends MollomWebTestCase { $form_state = array('values' => $values, 'buttons' => array()); $data = mollom_form_get_values($form_state, $fields, $form_info['mapping']); - $this->assertSame('post_title', $data['post_title'], $values['subject']); - $this->assertSame('post_body', $data['post_body'], $values['message']); - $this->assertSame('author_name', $data['author_name'], $this->admin_user->name); - $this->assertSame('author_mail', $data['author_mail'], $this->admin_user->mail); - $this->assertFalse(isset($data['author_url']), t('author_url: Undefined.')); + $this->assertSame('postTitle', $data['postTitle'], $values['subject']); + $this->assertSame('postBody', $data['postBody'], $values['message']); + $this->assertSame('authorName', $data['authorName'], $this->admin_user->name); + $this->assertSame('authorMail', $data['authorMail'], $this->admin_user->mail); + $this->assertFalse(isset($data['authorUrl']), t('authorUrl: Undefined.')); // @todo Test this. - $this->assertFalse(isset($data['author_openid']), t('author_openid: Undefined.')); - $this->assertSame('author_id', $data['author_id'], $this->admin_user->uid); - $this->assertSame('author_ip', $data['author_ip'], ip_address()); + $this->assertFalse(isset($data['authorOpenid']), t('authorOpenid: Undefined.')); + $this->assertSame('authorId', $data['authorId'], $this->admin_user->uid); + $this->assertSame('authorIp', $data['authorIp'], ip_address()); // Verify that invalid UTF-8 is detected. $values = array( @@ -2746,7 +2950,7 @@ class MollomDataTestCase extends MollomWebTestCase { } /** - * Test that form button values are not contained in post_body sent to Mollom. + * Test that form button values are not contained in postBody sent to Mollom. */ function testFormButtonValues() { $this->drupalLogin($this->admin_user); @@ -2761,7 +2965,7 @@ class MollomDataTestCase extends MollomWebTestCase { ); $this->drupalPost('mollom-test/form', $edit, 'Submit'); $data = $this->getServerRecord(); - $this->assertFalse(preg_match('@Submit|Add@', $data['post_body']), 'Button values not found in post body.'); + $this->assertFalse(preg_match('@Submit|Add@', $data['postBody']), 'Button values not found in post body.'); } /** @@ -2798,11 +3002,11 @@ class MollomDataTestCase extends MollomWebTestCase { // Verify that submitted data equals post data. $data = $this->getServerRecord(); - $this->assertSame('post_title', $data['post_title'], $edit['subject']); - $this->assertSame('post_body', $data['post_body'], $edit['comment_body[und][0][value]']); - $this->assertSame('author_name', $data['author_name'], $this->web_user->name); - $this->assertSame('author_mail', $data['author_mail'], $this->web_user->mail); - $this->assertSame('author_id', $data['author_id'], $this->web_user->uid); + $this->assertSame('postTitle', $data['postTitle'], $edit['subject']); + $this->assertSame('postBody', $data['postBody'], $edit['comment_body[und][0][value]']); + $this->assertSame('authorName', $data['authorName'], $this->web_user->name); + $this->assertSame('authorMail', $data['authorMail'], $this->web_user->mail); + $this->assertSame('authorId', $data['authorId'], $this->web_user->uid); $this->assertSame('strictness', $data['strictness'], 'normal'); $this->PostCorrectCaptcha(NULL, array(), t('Save')); @@ -2811,7 +3015,7 @@ class MollomDataTestCase extends MollomWebTestCase { // Verify that submitted data equals post data. $data = $this->getServerRecord('mollom.checkCaptcha'); - $this->assertSame('author_id', $data['author_id'], $this->web_user->uid); + $this->assertSame('authorId', $data['authorId'], $this->web_user->uid); // Allow anonymous users to post comments without approval. $this->drupalLogin($this->admin_user); @@ -2846,12 +3050,12 @@ class MollomDataTestCase extends MollomWebTestCase { // Verify that submitted data equals post data. $data = $this->getServerRecord(); - $this->assertSame('post_title', $data['post_title'], $edit['subject']); - $this->assertSame('post_body', $data['post_body'], $edit['comment_body[und][0][value]']); - $this->assertSame('author_name', $data['author_name'], $edit['name']); - $this->assertSame('author_mail', $data['author_mail'], $edit['mail']); - $this->assertSame('author_url', $data['author_url'], $edit['homepage']); - $this->assertFalse(isset($data['author_id']), t('author_id: Undefined.')); + $this->assertSame('postTitle', $data['postTitle'], $edit['subject']); + $this->assertSame('postBody', $data['postBody'], $edit['comment_body[und][0][value]']); + $this->assertSame('authorName', $data['authorName'], $edit['name']); + $this->assertSame('authorMail', $data['authorMail'], $edit['mail']); + $this->assertSame('authorUrl', $data['authorUrl'], $edit['homepage']); + $this->assertFalse(isset($data['authorId']), t('authorId: Undefined.')); $this->PostCorrectCaptcha(NULL, array(), t('Save')); $comment = db_query('SELECT * FROM {comment} WHERE subject = :subject', array(':subject' => $edit['subject']))->fetchObject(); @@ -2859,7 +3063,7 @@ class MollomDataTestCase extends MollomWebTestCase { // Verify that submitted data equals post data. $data = $this->getServerRecord('mollom.checkCaptcha'); - $this->assertFalse(isset($data['author_id']), t('author_id: Undefined.')); + $this->assertFalse(isset($data['authorId']), t('authorId: Undefined.')); // Log in admin user and edit comment containing spam. $this->resetServerRecords(); @@ -2975,15 +3179,18 @@ class MollomDataTestCase extends MollomWebTestCase { $edit['title'] = 'unsure'; $this->drupalPost(NULL, $edit, 'Submit'); $this->assertCaptchaField(); + $contentId = $this->assertResponseIDInForm('contentId'); + $captchaId = $this->assertResponseIDInForm('captchaId'); $this->postCorrectCaptcha(NULL, array(), 'Submit', 'Successful form submission.'); $new_data = $this->assertMollomData('mollom_test', $mid); // Verify that only session data was updated. $this->assertSame('entity', $data->entity, $new_data->entity); $this->assertSame('id', $data->id, $new_data->id); - $this->assertNotSame('session_id', $data->session_id, $new_data->session_id); + $this->assertNotSame('contentId', $data->contentId, $new_data->contentId); + $this->assertNotSame('captchaId', $data->captchaId, $new_data->captchaId); $this->assertSame('form_id', $data->form_id, $new_data->form_id); - $this->assertSame('quality', $data->quality, $new_data->quality); + $this->assertSame('qualityScore', $data->qualityScore, $new_data->qualityScore); $count = db_query("SELECT COUNT(1) FROM {mollom}")->fetchField(); $this->assertEqual($count, 1, t('Stored data in {mollom} was updated.')); } @@ -2997,12 +3204,12 @@ class MollomDataTestCase extends MollomWebTestCase { // Verify that we additionally sent version data. $data = $this->getServerRecord('mollom.verifyKey'); - $info = _mollom_get_version(); - $this->assertTrue(!empty($info['platform_name']), t('Version information found.')); - $this->assertSame('platform_name', $data['platform_name'], $info['platform_name']); - $this->assertSame('platform_version', $data['platform_version'], $info['platform_version']); - $this->assertSame('client_name', $data['client_name'], $info['client_name']); - $this->assertSame('client_version', $data['client_version'], $info['client_version']); + $info = mollom()->getClientInformation(); + $this->assertTrue(!empty($info['platformName']), t('Version information found.')); + $this->assertSame('platformName', $data['platformName'], $info['platformName']); + $this->assertSame('platformVersion', $data['platformVersion'], $info['platformVersion']); + $this->assertSame('clientName', $data['clientName'], $info['clientName']); + $this->assertSame('clientVersion', $data['clientVersion'], $info['clientVersion']); } } @@ -3033,28 +3240,28 @@ class MollomDataCRUDTestCase extends MollomWebTestCase { 'entity' => 'type1', 'id' => 123, 'form_id' => 'type1_form', - 'session_id' => 'type1-session-id', + 'contentId' => 1, ); mollom_data_save($data1); - $this->assertMollomData($data1->entity, $data1->id, $data1->session_id); + $this->assertMollomData($data1->entity, $data1->id, 'contentId', $data1->contentId); // Create a second data record; same ID, different entity type. $data2 = (object) array( 'entity' => 'type2', 'id' => 123, 'form_id' => 'type2_form', - 'session_id' => 'type2-session-id', + 'contentId' => 2, ); mollom_data_save($data2); - $this->assertMollomData($data2->entity, $data2->id, $data2->session_id); + $this->assertMollomData($data2->entity, $data2->id, 'contentId', $data2->contentId); // Update the first data record. - $data1->session_id = 'new-session-id-type1'; + $data1->contentId = 3; mollom_data_save($data1); // Verify that both records are correct. - $this->assertMollomData($data1->entity, $data1->id, $data1->session_id); - $this->assertMollomData($data2->entity, $data2->id, $data2->session_id); + $this->assertMollomData($data1->entity, $data1->id, 'contentId', $data1->contentId); + $this->assertMollomData($data2->entity, $data2->id, 'contentId', $data2->contentId); } /** @@ -3066,7 +3273,7 @@ class MollomDataCRUDTestCase extends MollomWebTestCase { 'entity' => 'type1', 'id' => 123, 'form_id' => 'type1_form', - 'session_id' => 'type1-session-id', + 'contentId' => 1, ); mollom_data_save($data1); @@ -3075,28 +3282,25 @@ class MollomDataCRUDTestCase extends MollomWebTestCase { 'entity' => 'type2', 'id' => 123, 'form_id' => 'type2_form', - 'session_id' => 'type2-session-id', + 'contentId' => 2, ); mollom_data_save($data2); // Verify that both records exist. - $this->assertMollomData($data1->entity, $data1->id, $data1->session_id); - $this->assertMollomData($data2->entity, $data2->id, $data2->session_id); + $this->assertMollomData($data1->entity, $data1->id, 'contentId', $data1->contentId); + $this->assertMollomData($data2->entity, $data2->id, 'contentId', $data2->contentId); // Delete the first data record. mollom_data_delete($data1->entity, $data1->id); // Verify that only the second record remained and was not changed. $this->assertNoMollomData($data1->entity, $data1->id); - $this->assertMollomData($data2->entity, $data2->id, $data2->session_id); + $this->assertMollomData($data2->entity, $data2->id, 'contentId', $data2->contentId); } } /** * Tests text analysis functionality. - * - * @todo Verify that no button captions appear in the data that is sent for - * analyis; i.e., no "Add" string for mollom_test_form. */ class MollomAnalysisTestCase extends MollomWebTestCase { protected $profile = 'testing'; @@ -3151,8 +3355,9 @@ class MollomAnalysisTestCase extends MollomWebTestCase { $data = $this->assertMollomData('mollom_test', $mid); $record = mollom_test_load($mid); $this->assertEqual($record['status'], 0, t('Unpublished test post found.')); - $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM); - $this->assertSame('profanity', $data->profanity, 1); + $this->assertSame('spamScore', $data->spamScore, 1.0); + $this->assertSame('spamClassification', $data->spamClassification, 'spam'); + $this->assertSame('profanityScore', $data->profanityScore, 1); $this->assertSame('moderate', $data->moderate, 1); // Verify that editing the post does neither change the session data, nor @@ -3166,8 +3371,9 @@ class MollomAnalysisTestCase extends MollomWebTestCase { $data = $this->assertMollomData('mollom_test', $mid); $record = mollom_test_load($mid); $this->assertEqual($record['status'], 0, t('Unpublished test post found.')); - $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM); - $this->assertSame('profanity', $data->profanity, 1); + $this->assertSame('spamScore', $data->spamScore, 1.0); + $this->assertSame('spamClassification', $data->spamClassification, 'spam'); + $this->assertSame('profanityScore', $data->profanityScore, 1); $this->assertSame('moderate', $data->moderate, 1); // Verify that publishing the post changes the session data accordingly. @@ -3180,17 +3386,18 @@ class MollomAnalysisTestCase extends MollomWebTestCase { $data = $this->assertMollomData('mollom_test', $mid); $record = mollom_test_load($mid); $this->assertEqual($record['status'], 1, t('Published test post found.')); - $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM); - $this->assertSame('profanity', $data->profanity, 1); + $this->assertSame('spamScore', $data->spamScore, 1.0); + $this->assertSame('spamClassification', $data->spamClassification, 'spam'); + $this->assertSame('profanityScore', $data->profanityScore, 1); $this->assertSame('moderate', $data->moderate, 0); // Verify that neither ham or unsure spam posts, nor non-profane posts are // marked for moderation. $this->drupalLogout(); $expectations = array( - 'ham' => array('spam' => MOLLOM_ANALYSIS_HAM, 'profanity' => 0), - 'unsure' => array('spam' => MOLLOM_ANALYSIS_UNSURE, 'profanity' => 0), - $this->randomString() => array('spam' => MOLLOM_ANALYSIS_UNSURE, 'profanity' => 0), + 'ham' => array('spamScore' => 0.0, 'spamClassification' => 'ham', 'profanityScore' => 0), + 'unsure' => array('spamScore' => 0.5, 'spamClassification' => 'unsure', 'profanityScore' => 0), + $this->randomString() => array('spamScore' => 0.5, 'spamClassification' => 'unsure', 'profanityScore' => 0), ); foreach ($expectations as $body => $expected) { $edit = array( @@ -3198,15 +3405,16 @@ class MollomAnalysisTestCase extends MollomWebTestCase { 'body' => $body, ); $this->drupalPost('mollom-test/form', $edit, 'Submit'); - if ($expected['spam'] == MOLLOM_ANALYSIS_UNSURE) { + if ($expected['spamClassification'] == 'unsure') { $this->postCorrectCaptcha(NULL, array(), 'Submit'); } $mid = $this->assertTestSubmitData(); $data = $this->assertMollomData('mollom_test', $mid); $record = mollom_test_load($mid); $this->assertEqual($record['status'], 1, t('Published test post %body found.', array('%body' => $body))); - $this->assertSame('spam', $data->spam, $expected['spam']); - $this->assertSame('profanity', $data->profanity, $expected['profanity']); + $this->assertSame('spamScore', $data->spamScore, $expected['spamScore']); + $this->assertSame('spamClassification', $data->spamClassification, $expected['spamClassification']); + $this->assertSame('profanityScore', $data->profanityScore, $expected['profanityScore']); $this->assertSame('moderate', $data->moderate, 0); } } diff --git tests/mollom_test.install tests/mollom_test.install index b4bea98..8c3e9a1 100644 --- tests/mollom_test.install +++ tests/mollom_test.install @@ -42,3 +42,12 @@ function mollom_test_schema() { return $schema; } +/** + * Implements hook_uninstall(). + */ +function mollom_test_uninstall() { + db_delete('variable') + ->condition('name', db_like('mollom_test_') . '%', 'LIKE') + ->execute(); +} + diff --git tests/mollom_test.module tests/mollom_test.module index d8d9192..1473294 100644 --- tests/mollom_test.module +++ tests/mollom_test.module @@ -3,6 +3,16 @@ /** * @file * Testing functionality for Mollom module. + * + * @todo Extract testing server into a new mollom_test_server.module. The + * mollom_test.module serves as good example for how to implement Mollom + * support in a Drupal module, but 90% of it pertain to the testing server + * now, so it's hard to explain people what they should look at. + */ + +/** + * @defgroup mollom_test_xmlrpc Mollom XML-RPC fake server functions + * @{ */ /** @@ -12,28 +22,504 @@ function mollom_test_xmlrpc() { return array( // $data contains a variable amount of properties, so we cannot specify a // signature. - 'mollom.getServerList' => 'mollom_test_get_server_list', - 'mollom.verifyKey' => 'mollom_test_verify_key', - 'mollom.checkContent' => 'mollom_test_check_content', - 'mollom.getImageCaptcha' => 'mollom_test_get_captcha', - 'mollom.checkCaptcha' => 'mollom_test_check_captcha', - 'mollom.sendFeedback' => 'mollom_test_send_feedback', + 'mollom.getServerList' => 'mollom_test_xmlrpc_get_server_list', + 'mollom.verifyKey' => 'mollom_test_xmlrpc_verify_key', + 'mollom.checkContent' => 'mollom_test_xmlrpc_check_content', + 'mollom.getImageCaptcha' => 'mollom_test_xmlrpc_get_captcha', + 'mollom.checkCaptcha' => 'mollom_test_xmlrpc_check_captcha', + 'mollom.sendFeedback' => 'mollom_test_xmlrpc_send_feedback', ); } /** + * Converts camelCase request/response parameters to lowercase with underscores. + * + * @todo Recurse into multi-dimensional arrays. + */ +function mollom_test_xmlrpc_convert_params(array $data = array()) { + foreach ($data as $key => $value) { + // Convert CamelCase to lowercase with underscores. + $new_key = strtolower(preg_replace('@(?<=[a-z])([A-Z])@', '_$1', $key)); + $data[$new_key] = $value; + unset($data[$key]); + } + return $data; +} + +/** * XML-RPC callback for mollom.getServerList to retrieve new server list. */ +function mollom_test_xmlrpc_get_server_list($data) { + $servers = mollom_test_get_server_list($data); + foreach ($servers as $key => $url) { + $servers[$key] .= '/xmlrpc.php?version='; + } + return $servers; +} + +/** + * XML-RPC callback for mollom.verifyKey to validate API keys. + */ +function mollom_test_xmlrpc_verify_key($data) { + $valid = mollom_test_verify_key($data); + if ($valid) { + return TRUE; + } + xmlrpc_error(Mollom::AUTH_ERROR); +} + +/** + * XML-RPC callback for mollom.checkContent to perform textual analysis. + */ +function mollom_test_xmlrpc_check_content($data) { + return mollom_test_check_content($data); +} + +/** + * XML-RPC callback for mollom.getImageCaptcha to fetch a CATPCHA image. + */ +function mollom_test_xmlrpc_get_captcha($data) { + return mollom_test_get_captcha($data); +} + +/** + * XML-RPC callback for mollom.checkCaptcha to validate a CAPTCHA response. + */ +function mollom_test_xmlrpc_check_captcha($data) { + if (isset($data['captcha_result'])) { + $data['solution'] = $data['captcha_result']; + } + + return mollom_test_check_captcha($data); +} + +/** + * XML-RPC callback for mollom.sendFeedback to send feedback for a moderated post. + */ +function mollom_test_xmlrpc_send_feedback($data) { + $result = mollom_test_send_feedback($data); + if ($result) { + return TRUE; + } + xmlrpc_error(Mollom::AUTH_ERROR); +} + +/** + * @} End of "defgroup mollom_test_xmlrpc". + */ + +/** + * @defgroup mollom_test_rest Mollom REST fake server functions + * @{ + */ + +/** + * Implements hook_menu() for REST API endpoints. + */ +function mollom_test_rest() { + $path = 'mollom-test/rest/v1'; + $base_args = count(explode('/', $path)) - 1; + // @todo Consider to use a generic page callback, passing arg(3), the resource + // type, and optionally arg(4), the resource, as argument. This would allow + // us to use PHP Exceptions to throw different status codes and errors. Make + // that page callback dynamically switch the delivery callback (for JSON). + $base = array( + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + 'delivery callback' => 'mollom_test_rest_deliver', + ); + + $items[$path . '/site'] = $base + array( + 'page callback' => 'mollom_test_rest_site', + ); + $items[$path . '/content'] = $base + array( + 'page callback' => 'mollom_test_rest_content', + ); + $items[$path . '/captcha'] = $base + array( + 'page callback' => 'mollom_test_rest_captcha', + ); + $items[$path . '/feedback'] = $base + array( + 'page callback' => 'mollom_test_rest_send_feedback', + ); + $items[$path . '/blacklist/%'] = $base + array( + 'page callback' => 'mollom_test_rest_blacklist', + 'page arguments' => array($base_args + 2), + ); + // @todo Whitelist endpoints. + + return $items; +} + +/** + * Returns HTTP request query parameters for the current request. + * + * @see Mollom::httpBuildQuery() + * @see http://php.net/manual/en/wrappers.php.php + */ +function mollom_test_rest_get_parameters() { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD') { + $data = Mollom::httpParseQuery($_SERVER['QUERY_STRING']); + } + elseif ($_SERVER['REQUEST_METHOD'] == 'POST' || $_SERVER['REQUEST_METHOD'] == 'PUT') { + $data = Mollom::httpParseQuery(file_get_contents('php://input')); + } + return $data; +} + +/** + * Delivery callback for REST API endpoints. + */ +function mollom_test_rest_deliver($page_callback_result) { + #drupal_add_http_header('Content-Type', 'application/xml; charset=utf-8'); + drupal_add_http_header('Content-Type', 'application/xml'); + + $xml = new DOMDocument('1.0', 'utf-8'); + $element = $xml->createElement('response'); + + // Append status response parameters. + // @todo Add support for custom codes (redirect/refresh) + error messages. + $code = 200; + if (!is_array($page_callback_result) && $page_callback_result !== TRUE) { + switch ($page_callback_result) { + case MENU_NOT_FOUND: + $code = 404; + $message = 'Not found'; + break; + + case Mollom::AUTH_ERROR: + $code = 1000; + $message = 'Authentication failure'; + break; + + default: + $code = 400; + $message = 'Bad request'; + break; + } + } + $status = array( + 'code' => $code, + ); + if (isset($message)) { + $status['message'] = $message; + } + mollom_test_rest_add_xml($xml, $element, $status); + + // Append other response parameters. + if (is_array($page_callback_result)) { + mollom_test_rest_add_xml($xml, $element, $page_callback_result); + } + + $xml->appendChild($element); + print $xml->saveXML(); + + // Perform end-of-request tasks. + drupal_page_footer(); +} + +function mollom_test_rest_add_xml(DOMDocument $doc, DOMNode $parent, $data, $key = NULL) { + if (is_scalar($data)) { + // Mollom REST API always uses integers instead of Booleans due to varying + // implementations of JSON protocol across client platforms/frameworks. + if (is_bool($data)) { + $data = (int) $data; + } + + $element = $doc->createTextNode($data); + $parent->appendChild($element); + } + else { + foreach ($data as $property => $value) { + $key = (is_numeric($property) ? 'item' : $property); + $element = $doc->createElement($key); + $parent->appendChild($element); + mollom_test_rest_add_xml($doc, $element, $value, $key); + } + } +} + +/** + * REST callback for CRUD site operations. + * + * @param $publicKey + * (optional) The public key of a site. + * @param $delete + * (optional) Whether to delete the site with $publicKey. + */ +function mollom_test_rest_site($publicKey = NULL, $delete = FALSE) { + $data = mollom_test_rest_get_parameters(); + + // Prepare server list. + $servers = mollom_test_get_server_list($data); + foreach ($servers as $key => &$url) { + $url .= '/mollom-test/rest/' . Mollom::API_VERSION; + } + + $bin = 'mollom_test_site'; + $sites = variable_get($bin, array()); + + // Check whether publicKey exists. + if (isset($publicKey)) { + if (!isset($sites[$publicKey])) { + return MENU_NOT_FOUND; + } + // Validate authentication. + $time = $data['time']; + $nonce = $data['nonce']; + $request_data = $time . ':' . $nonce . ':' . $sites[$publicKey]['privateKey']; + $hash = base64_encode(hash_hmac('sha1', $request_data, $sites[$publicKey]['privateKey'], TRUE)); + if ($hash !== $data['hash']) { + return Mollom::AUTH_ERROR; + } + } + + if ($_SERVER['REQUEST_METHOD'] == 'GET') { + // Return existing site. + if (isset($publicKey)) { + $response = $sites[$publicKey]; + } + // Return list of existing sites. + else { + $response = array( + 'list' => array_values($sites), + 'listCount' => count($sites), + 'listOffset' => 0, + 'listTotal' => count($sites), + ); + return $response; + } + } + else { + // Update site. + if (isset($publicKey) && !$delete) { + // Remove authentication parameters. + unset($data['publicKey'], $data['time'], $data['hash'], $data['nonce']); + + $storage = variable_get('mollom_test_verify_key', array()); + $storage[] = $data; + variable_set('mollom_test_verify_key', $storage); + + $sites[$publicKey] = $data + $sites[$publicKey]; + variable_set($bin, $sites); + $response = $sites[$publicKey]; + } + // Create new site. + // Authentication is ignored in this case. + elseif (!$delete) { + // Remove authentication parameters. + unset($data['publicKey'], $data['time'], $data['hash'], $data['nonce']); + + $data['publicKey'] = $publicKey = md5(rand() . REQUEST_TIME); + $data['privateKey'] = $privateKey = md5(rand() . REQUEST_TIME); + // Apply default values. + $data += array( + 'url' => '', + 'email' => '', + 'languages' => array(), + 'subscriptionType' => 0, // Mollom Free. + // Client version info is not defined by default. + /* + 'platformName' => '', + 'platformVersion' => '', + 'clientName' => '', + 'clientVersion' => '', + */ + 'servers' => $servers, + ); + $sites[$publicKey] = $data; + variable_set($bin, $sites); + $response = $data; + } + // Delete site. + else { + unset($sites[$publicKey]); + variable_set($bin, $sites); + return TRUE; + } + } + return array('site' => $response); +} + +/** + * REST callback for mollom.checkContent to perform textual analysis. + */ +function mollom_test_rest_content($contentId = NULL) { + $data = mollom_test_rest_get_parameters(); + if ($_SERVER['REQUEST_METHOD'] == 'GET') { + // @todo List/read content. + if (empty($contentId)) { + return FALSE; + } + return FALSE; + } + else { + // Update existing content (includes sending feedback). + // In case the 'moderated' parameter was passed, the call equals the old + // mollom.sendFeedback and we only check whether the parameter value is + // correct. + if (isset($data['moderated'])) { + $valid = is_string($data['moderated']); + $valid = $valid && in_array($data['moderated'], array('spam', 'profanity', 'low-quality', 'unwanted', 'approve', 'escalate', 'delete', 'ignore')); + return $valid; + } + // Content ID in request parameters must match the one in path. + if (isset($data['id']) && $data['id'] != $contentId) { + return FALSE; + } + } + + // Default POST: Create or update content and check it. + return array('content' => mollom_test_check_content($data)); +} + +/** + * REST callback for mollom.getCaptcha to fetch a CAPTCHA. + */ +function mollom_test_rest_captcha($captchaId = NULL) { + $data = mollom_test_rest_get_parameters(); + if ($_SERVER['REQUEST_METHOD'] == 'GET') { + // There is no GET /captcha[/{captchaId}]. + return FALSE; + } + else { + // CAPTCHA ID in request parameters must match the one in path. + if (isset($data['id']) && $data['id'] != $captchaId) { + return FALSE; + } + // Verify CAPTCHA. + if (isset($data['id'])) { + return array('captcha' => mollom_test_check_captcha($data)); + } + } + // Create a new CAPTCHA resource. + return array('captcha' => mollom_test_get_captcha($data)); +} + +/** + * REST callback for Blacklist API. + * + * @param $public_key + * The public key of a site. + * + * @todo Abstract actual functionality like other REST handlers. + */ +function mollom_test_rest_blacklist($public_key, $entryId = NULL, $delete = FALSE) { + if (empty($public_key)) { + return FALSE; + } + $data = mollom_test_rest_get_parameters(); + // Remove authentication parameters. + unset($data['publicKey'], $data['time'], $data['hash'], $data['nonce']); + + // Prepare text value. + if (isset($data['value'])) { + $data['value'] = drupal_strtolower(trim($data['value'])); + } + + $bin = 'mollom_test_blacklist_' . $public_key; + $entries = variable_get($bin, array()); + + if ($_SERVER['REQUEST_METHOD'] == 'GET') { + // List blacklist entries. + if (empty($entryId)) { + $response = array(); + // Remove deleted entries (== FALSE). + $entries = array_filter($entries); + $response['list'] = $entries; + // @todo Not required yet. + $response['listCount'] = count($entries); + $response['listOffset'] = 0; + $response['listTotal'] = count($entries); + return $response; + } + // Read a single entry. + else { + // Check whether the entry exists and was not deleted. + if (!empty($entries[$entryId])) { + return array('entry' => $entries[$entryId]); + } + else { + return MENU_NOT_FOUND; + } + } + } + else { + // Update an existing entry. + if (isset($entryId)) { + // Entry ID must match. + if (isset($data['id']) && $data['id'] != $entryId) { + return FALSE; + } + // Check that the entry was not deleted. + if (empty($entries[$entryId])) { + return MENU_NOT_FOUND; + } + // Entry ID cannot be updated. + unset($data['id']); + $entries[$entryId] = $data; + variable_set($bin, $entries); + $response = $data; + $response['id'] = $entryId; + return array('entry' => $response); + } + // Create a new entry. + elseif (!$delete) { + $entryId = max(array_keys($entries)) + 1; + $data['id'] = $entryId; + $entries[$entryId] = $data; + variable_set($bin, $entries); + + $response = $data; + return array('entry' => $response); + } + // Delete an existing entry. + else { + // Check that the entry was not deleted already. + if (!empty($entries[$entryId])) { + $entries[$entryId] = FALSE; + variable_set($bin, $entries); + return TRUE; + } + else { + return MENU_NOT_FOUND; + } + } + } +} + +/** + * REST callback for mollom.sendFeedback to send feedback for a moderated post. + */ +function mollom_test_rest_send_feedback() { + $data = mollom_test_rest_get_parameters(); + // A resource ID is required. + if (empty($data['contentId']) && empty($data['captchaId'])) { + return 400; + } + + // The feedback is valid if the supplied reason is one of the supported + // strings. Otherwise, it's a bad request. + $result = mollom_test_send_feedback($data); + return $result ? TRUE : 400; +} + +/** + * @} End of "defgroup mollom_test_rest". + */ + +/** + * API callback for mollom.getServerList to retrieve new server list. + */ function mollom_test_get_server_list($data) { $storage = variable_get(__FUNCTION__, array()); $storage[] = $data; variable_set(__FUNCTION__, $storage); - return array($GLOBALS['base_url'] . '/xmlrpc.php?version='); + return array($GLOBALS['base_url'], $GLOBALS['base_url']); } /** - * XML-RPC callback for mollom.verifyKey to validate API keys. + * API callback for mollom.verifyKey to validate API keys. */ function mollom_test_verify_key($data) { $storage = variable_get(__FUNCTION__, array()); @@ -44,167 +530,232 @@ function mollom_test_verify_key($data) { module_load_include('php', 'simpletest', 'drupal_web_test_case'); module_load_include('test', 'mollom', 'tests/mollom'); - if ($data['public_key'] === MOLLOM_TEST_PUBLIC_KEY) { - return TRUE; - } - xmlrpc_error(MOLLOM_ERROR); + return $data['public_key'] === MOLLOM_TEST_PUBLIC_KEY; } /** - * XML-RPC callback for mollom.checkContent to perform textual analysis. + * API callback for mollom.checkContent to perform textual analysis. * * @todo Add support for 'redirect' and 'refresh' values. */ function mollom_test_check_content($data) { - $storage = variable_get(__FUNCTION__, array()); - $storage[] = $data; - variable_set(__FUNCTION__, $storage); - $response = array(); + // If only a single value for checks is passed, it is a string. + if (isset($data['checks']) && is_string($data['checks'])) { + $data['checks'] = array($data['checks']); + } + + // Fetch blacklist. + $blacklist = variable_get('mollom_test_blacklist_' . $data['publicKey'], array()); + + $post = implode('\n', array_intersect_key($data, array('postTitle' => 1, 'postBody' => 1))); + // Spam filter: Check post_title and post_body for ham, spam, or unsure. - if (!isset($data['checks']) || strpos($data['checks'], 'spam') !== FALSE) { + if (!isset($data['checks']) || in_array('spam', $data['checks'])) { $spam = FALSE; $ham = FALSE; - foreach (array('post_title', 'post_body') as $key) { - if (!isset($data[$key])) { - continue; - } - // 'spam' always has precedence. - if (strpos($data[$key], 'spam') !== FALSE) { - $spam = TRUE; - } - // Otherwise, check for 'ham'. - elseif (strpos($data[$key], 'ham') !== FALSE) { - $ham = TRUE; - } - // Lastly, take a forced 'unsure' into account. - elseif (strpos($data[$key], 'unsure') !== FALSE) { - $spam = TRUE; - $ham = TRUE; - } + // 'spam' always has precedence. + if (strpos($post, 'spam') !== FALSE) { + $spam = TRUE; } + // Otherwise, check for 'ham'. + elseif (strpos($post, 'ham') !== FALSE) { + $ham = TRUE; + } + // Lastly, take a forced 'unsure' into account. + elseif (strpos($post, 'unsure') !== FALSE) { + $spam = TRUE; + $ham = TRUE; + } + // Check blacklist. + if ($matches = mollom_test_check_content_blacklist($post, $blacklist, 'spam')) { + $spam = TRUE; + $ham = FALSE; + $response['blacklistSpam'] = $matches; + } + if ($spam && $ham) { - $response['spam'] = MOLLOM_ANALYSIS_UNSURE; - $quality = 0.5; + $response['spamScore'] = 0.5; + $response['spamClassification'] = 'unsure'; + $qualityScore = 0.5; } elseif ($spam) { - $response['spam'] = MOLLOM_ANALYSIS_SPAM; - $quality = 0; + $response['spamScore'] = 1.0; + $response['spamClassification'] = 'spam'; + $qualityScore = 0.0; } elseif ($ham) { - $response['spam'] = MOLLOM_ANALYSIS_HAM; - $quality = 1; + $response['spamScore'] = 0.0; + $response['spamClassification'] = 'ham'; + $qualityScore = 1.0; } else { - $response['spam'] = MOLLOM_ANALYSIS_UNSURE; - $quality = NULL; + $response['spamScore'] = 0.5; + $response['spamClassification'] = 'unsure'; + $qualityScore = NULL; } // In case a previous spam check was unsure and a CAPTCHA was solved, the // result is supposed to be ham. $captcha_sessions = variable_get('mollom_test_check_captcha_sessions', array()); - if (!empty($data['session_id']) && !empty($captcha_sessions[$data['session_id']])) { - $response['spam'] = MOLLOM_ANALYSIS_HAM; + if (!empty($data['captchaId']) && !empty($captcha_sessions[$data['captchaId']])) { + $response['spamScore'] = 0.0; + $response['spamClassification'] = 'ham'; } } // Quality filter. - if (!isset($data['checks']) || strpos($data['checks'], 'quality') !== FALSE) { - if (isset($quality)) { - $response['quality'] = $quality; + if (!isset($data['checks']) || in_array('quality', $data['checks'])) { + if (isset($qualityScore)) { + $response['qualityScore'] = $qualityScore; } - // @todo No idea how quality is calculated during testing without spam - // results above. else { - $response['quality'] = 0; + $response['qualityScore'] = 0; } } // Profanity filter. - if (isset($data['checks']) && strpos($data['checks'], 'profanity') !== FALSE) { - $profanity = 0.0; - foreach (array('post_title', 'post_body') as $key) { - if (isset($data[$key]) && strpos($data[$key], 'profanity') !== FALSE) { - $profanity = 1.0; - } + if (isset($data['checks']) && in_array('profanity', $data['checks'])) { + $profanityScore = 0.0; + if (strpos($post, 'profanity') !== FALSE) { + $profanityScore = 1.0; + } + // Check blacklist. + if ($matches = mollom_test_check_content_blacklist($post, $blacklist, 'profanity')) { + $profanityScore = 1.0; + $response['blacklistProfanity'] = $matches; } - $response['profanity'] = $profanity; + $response['profanityScore'] = $profanityScore; } - if (!empty($data['session_id'])) { - $response['session_id'] = $data['session_id']; - } - else { - drupal_session_start(); - $response['session_id'] = session_id(); + // Language detection. + if (isset($data['checks']) && in_array('language', $data['checks'])) { + $languages = array(); + preg_match_all('@\blang-(..)\b@', $post, $matches); + if (empty($matches[1])) { + $languages[] = array( + 'languageCode' => 'zxx', + 'confidence' => 1.0, + ); + } + elseif (count($matches[1]) > 3) { + $languages[] = array( + 'languageCode' => LANGUAGE_NONE, + 'confidence' => 1.0, + ); + } + else { + $confidence = 1 / count($matches[1]); + foreach ($matches[1] as $language) { + $languages[] = array( + 'languageCode' => $language, + 'confidence' => $confidence, + ); + } + } + $response['languages'] = $languages; + $response['langDebug'] = $matches; } + $storage = variable_get(__FUNCTION__, array()); + $contentId = (!empty($data['id']) ? (int) $data['id'] : max(array_keys($storage)) + 1); + $storage[$contentId] = $data; + $response['id'] = $contentId; + variable_set(__FUNCTION__, $storage); + return $response; } /** - * XML-RPC callback for mollom.getImageCaptcha to fetch a CATPCHA image. + * Checks a string against blacklisted terms. */ -function mollom_test_get_captcha($data) { - $storage = variable_get(__FUNCTION__, array()); - $storage[] = $data; - variable_set(__FUNCTION__, $storage); +function mollom_test_check_content_blacklist($string, $blacklist, $reason) { + $terms = array(); + foreach ($blacklist as $entry) { + if ($entry['reason'] == $reason) { + $term = preg_quote($entry['value']); + if ($entry['match'] == 'exact') { + $term = '\b' . $term . '\b'; + } + $terms[] = $term; + } + } + if (!empty($terms)) { + $terms = '/(' . implode('|', $terms) . ')/'; + preg_match_all($terms, strtolower($string), $matches); + return $matches[1]; + } + return array(); +} - drupal_session_start(); +/** + * API callback for mollom.getImageCaptcha to fetch a CATPCHA image. + */ +function mollom_test_get_captcha($data) { + $response = array(); // Return a HTTPS URL if 'ssl' parameter was passed. $base_url = $GLOBALS['base_url']; if (!empty($data['ssl'])) { $base_url = str_replace('http', 'https', $base_url); } + $response['url'] = $base_url . '/' . drupal_get_path('module', 'mollom') . '/images/powered-by-mollom-2.gif'; - return array( - 'session_id' => !empty($data['session_id']) ? $data['session_id'] : session_id(), - 'url' => $base_url . '/' . drupal_get_path('module', 'mollom') . '/images/powered-by-mollom-2.gif', - ); + $storage = variable_get(__FUNCTION__, array()); + $captchaId = (!empty($data['id']) ? (int) $data['id'] : max(array_keys($storage)) + 1); + $storage[$captchaId] = $data; + $response['id'] = $captchaId; + variable_set(__FUNCTION__, $storage); + + return $response; } /** - * XML-RPC callback for mollom.checkCaptcha to validate a CAPTCHA response. + * API callback for mollom.checkCaptcha to validate a CAPTCHA response. * * @todo Add support for 'redirect' and 'refresh' values. */ function mollom_test_check_captcha($data) { - $storage = variable_get(__FUNCTION__, array()); - $storage[] = $data; - variable_set(__FUNCTION__, $storage); + $response = array(); - if ($data['captcha_result'] == 'correct') { - $result = TRUE; + if (isset($data['solution']) && $data['solution'] == 'correct') { + $response['solved'] = TRUE; } - if ($data['captcha_result'] == 'incorrect') { - $result = FALSE; + else { + $response['solved'] = FALSE; + $response['reason'] = ''; } + + $storage = variable_get(__FUNCTION__, array()); + $captchaId = (!empty($data['id']) ? (int) $data['id'] : max(array_keys($storage)) + 1); + $storage[$captchaId] = $data; + $response['id'] = $captchaId; + variable_set(__FUNCTION__, $storage); + $captcha_sessions = variable_get('mollom_test_check_captcha_sessions', array()); - $captcha_sessions[$data['session_id']] = $result; + $captcha_sessions[$captchaId] = $response['solved']; variable_set('mollom_test_check_captcha_sessions', $captcha_sessions); - return $result; + return $response; } /** - * XML-RPC callback for mollom.sendFeedback to send feedback for a moderated post. + * API callback for mollom.sendFeedback to send feedback for a moderated post. */ function mollom_test_send_feedback($data) { $storage = variable_get(__FUNCTION__, array()); $storage[] = $data; variable_set(__FUNCTION__, $storage); - if (in_array($data['feedback'], array('spam', 'profanity', 'low-quality', 'unwanted', 'ham'))) { - return TRUE; - } - xmlrpc_error(MOLLOM_ERROR); + return in_array($data['reason'], array('spam', 'profanity', 'quality', 'unwanted', 'approve', 'delete')); } /** * Implements hook_menu(). */ function mollom_test_menu() { + $items = mollom_test_rest(); + $items['mollom-test/form'] = array( 'title' => 'Mollom test form', 'page callback' => 'drupal_get_form',