/[drupal]/contributions/modules/akismet/akismet.module
ViewVC logotype

Contents of /contributions/modules/akismet/akismet.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.28 - (show annotations) (download) (as text)
Sun Jul 13 07:48:00 2008 UTC (16 months, 1 week ago) by drewish
Branch: MAIN
CVS Tags: HEAD
Changes since 1.27: +4 -4 lines
File MIME type: text/x-php
#274536 (alastair) Calls to url() need to be updated for D6.
1 <?php
2 // $Id: akismet.module,v 1.27 2008/07/13 07:44:26 drewish Exp $
3
4 /**
5 * Akismet Drupal and Module versions.
6 */
7 define('AKISMET_DRUPAL_VERSION', '6');
8 define('AKISMET_MODULE_VERSION', '2.0');
9 define('AKISMET_MODULE_HOMEURL', 'http://www.drupal.org/project/akismet');
10 define('AKISMET_MODULE_USERAGENT', 'Drupal/'. AKISMET_DRUPAL_VERSION .' | akismet.module/'. AKISMET_MODULE_VERSION);
11
12
13 /**
14 * Akismet API constants.
15 */
16 define('AKISMET_API_HOST', 'rest.akismet.com');
17 define('AKISMET_API_PORT', 80);
18 define('AKISMET_API_VERSION', '1.1');
19 define('AKISMET_API_USERAGENT', 'Drupal/'. AKISMET_DRUPAL_VERSION .' | akismet.module/'. AKISMET_MODULE_VERSION);
20 define('AKISMET_API_RESULT_ERROR', -1);
21 define('AKISMET_API_RESULT_SUCCESS', 0);
22 define('AKISMET_API_RESULT_IS_SPAM', 1);
23 define('AKISMET_API_RESULT_IS_HAM', 2);
24
25
26 /**
27 * Implementation of hook_help().
28 */
29 function akismet_help($path, $arg) {
30 switch ($path) {
31 case 'admin/help#akismet':
32 $output = t('<p>In order to use the <a href="!akismet">Akismet Service</a>, you need a <a href="!wpapikey">WordPress.com API key</a>. If you don\'t have one already, you can get it by simply signing up for a free account at <a href="!wordpress-com">wordpress.com</a>. Please, consult the <a href="!akismet-faq">Akismet FAQ</a> for further information.</p>
33 <p>The <em>akismet module</em> may automatically check for spam posted in content (nodes and/or comments) by any user, except node or comment administrators respectively. It is also possible, from the <a href="!access-control">access control</a> panel, to grant <em>%no-check-perm</em> permission to <em>user roles</em> of your choice.</p>
34 <p>Content marked as <em>spam</em> is still saved into database so it can be reviewed by content administrators. There is <a href="!akismet-settings">an option</a> that allows you to specify how long this information will be kept in the database. <em>Spam</em> older than a specified age will be automatically removed. Requires crontab.</p>
35 <p>Automatic spam detection can be enabled or disabled by content type and/or comments. In addition to this, the <em>akismet module</em> makes it easy for <em>content administrators</em> to manually <em>publish</em>/<em>unpublish</em> content and <em>mark</em>/<em>unmark</em> content as spam, from links available at the bottom of content.</p>
36 <p></p>',
37 array(
38 '!akismet' => url('http://akismet.com'),
39 '!wpapikey' => url('http://wordpress.com/api-keys/'),
40 '!wordpress-com' => url('http://wordpress.com'),
41 '!akismet-faq' => url('http://akismet.com/faq/'),
42 '!akismet-settings' => url('admin/settings/akismet'),
43 '!access-control' => url('admin/access'),
44 '%no-check-perm' => t('post with no akismet checking')
45 ));
46 return $output;
47 case 'admin/help/akismet':
48 case 'admin/settings/akismet':
49 $output = t('<p>The <a href="!akismet-module-home">akismet module</a> for <a href="!drupal">Drupal</a> allows you to use the <a href="!akismet">Akismet Service</a> to protect your site from being spammed.</p>',
50 array(
51 '!akismet-module-home' => url(AKISMET_MODULE_HOMEURL),
52 '!drupal' => url('http://drupal.org'),
53 '!akismet' => url('http://akismet.com')
54 ));
55 $output .= t('<p>Akismet has caught <strong>@count spam</strong> for you since %since.</p>', array('@count' => akismet_get_spam_counter(), '%since' => akismet_get_counting_since()));
56 return $output;
57 case 'admin/content/akismet/nodes/unpublished':
58 $output = t('Below is the list of <strong>unpublished nodes</strong> awaiting for moderation.');
59 $output .= ' '. t('Click on the titles to see the content of the nodes or the author\'s name to view the author\'s user information. You may also wish to click on the headers to order the nodes upon your needs.');
60 break;
61 case 'admin/content/akismet/nodes/published':
62 $output = t('Below is the list of <strong>published nodes</strong>.');
63 $output .= ' '. t('Click on the titles to see the content of the nodes or the author\'s name to view the author\'s user information. You may also wish to click on the headers to order the nodes upon your needs.');
64 break;
65 case 'admin/content/akismet/nodes': // spam
66 $output = t('Below is the list of <strong>nodes marked as spam</strong> awaiting for moderation.');
67 $output .= ' '. t('Click on the titles to see the content of the nodes or the author\'s name to view the author\'s user information. You may also wish to click on the headers to order the nodes upon your needs.');
68 break;
69 case 'admin/content/akismet/comments/unpublished':
70 $output = t('Below is the list of <strong>unpublished comments</strong> awaiting for moderation.');
71 $output .= ' '. t('Click on the subjects to see the comments or the author\'s name to view the author\'s user information. You may also wish to click on the headers to order the comments upon your needs.');
72 break;
73 case 'admin/content/akismet/comments/published':
74 $output = t('Below is the list of <strong>published comments</strong>.');
75 $output .= ' '. t('Click on the subjects to see the comments or the author\'s name to view the author\'s user information. You may also wish to click on the headers to order the comments upon your needs.');
76 break;
77 case 'admin/content/akismet/comments': // spam
78 $output = t('Below is the list of <strong>comments marked as spam</strong> awaiting for moderation.');
79 $output .= ' '. t('Click on the subjects to see the comments or the author\'s name to view the author\'s user information. You may also wish to click on the headers to order the comments upon your needs.');
80 break;
81 }
82 if (arg(0) == 'admin' && arg(1) == 'content '&& arg(2) == 'akismet' && !isset($_POST) && !empty($output)) {
83 $output .= '<br />'. t('<strong>Note:</strong> To interact fully with the <a href="!akismet">Akismet Service</a> you really should try putting data back into the system as well as just taking it out. If it is at all possible, please use the submit <em>ham</em> operation rather than simply publishing content that was identified as spam (false positives). This is necessary in order to let Akismet learn from its mistakes. Thank you.', array('!akismet' => url('http://akismet.com')));
84 }
85 }
86
87 // Check run-time requirements and status information
88 function akismet_requirements($phase) {
89 $t = get_t();
90
91 if ($phase == 'runtime') {
92 if (variable_get('akismet_wpapikey', '') == '') {
93 $requirements['akismet_key'] = array(
94 'title' => $t('Akismet API key'),
95 'value' => $t('Not present'),
96 'description' => $t("Akismet spam protection requires a <a href='!wpapikey'>WordPress.com API key</a> to function. Obtain a key by signing up for a free account at <a href='!wordpress-com'>WordPress.com</a>, then enter the key on the <a href='!akismet-settings'>Akismet settings page</a>.",
97 array(
98 '!wpapikey' => url('http://wordpress.com/api-keys/'),
99 '!wordpress-com' => url('http://wordpress.com'),
100 '!akismet-settings' => url('admin/settings/akismet'),
101 )),
102 'severity' => REQUIREMENT_ERROR,
103 );
104 return $requirements;
105 }
106 }
107 }
108
109 /**
110 * Implementation of hook_perm().
111 */
112 function akismet_perm() {
113 $perms = array('administer akismet settings');
114
115 foreach (node_get_types('names') as $type => $name) {
116 $perms[] = 'moderate spam in nodes of type '. $name;
117 }
118 $perms[] = 'moderate spam in comments';
119 $perms[] = 'post with no akismet checking';
120
121 return $perms;
122 }
123
124 /**
125 * Implementation of hook_cron().
126 */
127 function akismet_cron() {
128 require_once('./'. drupal_get_path('module', 'akismet') . '/akismet_cron.inc');
129 register_shutdown_function('akismet_cron_shutdown');
130 }
131
132 function _akismet_moderator_types_count($types = array()) {
133 if (empty($types)) {
134 $types = akismet_get_moderator_types();
135 }
136 return count($types);
137 }
138
139 function _akismet_is_moderator($moderator_types = array(), $type = '') {
140 if (empty($moderator_types)) {
141 $moderator_types = akismet_get_moderator_types();
142 }
143 if (_akismet_moderator_types_count($moderator_types) > 0 && (empty($type) || isset($moderator_types[$type]))) {
144 return TRUE;
145 }
146 else {
147 return FALSE;
148 }
149 }
150
151 function _akismet_is_node_moderator($moderator_types = array()) {
152 if (empty($moderator_types)) {
153 $moderator_types = akismet_get_moderator_types();
154 }
155 if (_akismet_is_moderator($moderator_types) && (!_akismet_is_moderator($moderator_types, $type = 'comments') || _akismet_moderator_types_count($moderator_types) > 1)) {
156 return TRUE;
157 }
158 else {
159 return FALSE;
160 }
161 }
162
163 function _akismet_is_moderator_type($type, $types = array()) {
164 if (empty($types)) {
165 $types = akismet_get_moderator_types();
166 }
167 if (_akismet_moderator_types_count($types) > 0 && isset($types[$type])) {
168 return TRUE;
169 }
170 else {
171 return FALSE;
172 }
173 }
174
175 /**
176 * Implementation of hook_menu().
177 */
178 function akismet_menu() {
179 $items = array();
180
181 $items['admin/settings/akismet'] = array(
182 'title' => t('Akismet'),
183 'description' => t('Use the Akismet Service to protect your site from spam.'),
184 'page callback' => 'drupal_get_form',
185 'page arguments' => array('akismet_settings'),
186 'access arguments' => array('administer akismet settings'),
187 'file' => 'akismet.admin.inc',
188 );
189
190 $moderator_types = akismet_get_moderator_types();
191 if (_akismet_is_moderator($moderator_types)) {
192 $items['admin/content/akismet'] = array(
193 'title' => t('Akismet moderation queue'),
194 'description' => t('Manage the Akismet spam queue, appropving or deleting content in need of moderation.'),
195 'page callback' => 'akismet_callback_queue',
196 'access callback' => '_akismet_is_moderator',
197 'access arguments' => array($moderator_types),
198 'file' => 'akismet.admin.inc',
199 );
200 $items['admin/content/akismet/overview'] = array(
201 'title' => t('Overview'),
202 'type' => MENU_DEFAULT_LOCAL_TASK,
203 'weight' => 0,
204 'file' => 'akismet.admin.inc',
205 );
206 if (_akismet_is_node_moderator($moderator_types)) {
207 $items['admin/content/akismet/nodes'] = array(
208 'title' => t('Nodes'),
209 'page callback' => 'akismet_callback_queue',
210 'page arguments' => array('nodes'),
211 'access callback' => '_akismet_is_node_moderator',
212 'access arguments' => array($moderator_types),
213 'type' => MENU_LOCAL_TASK,
214 'weight' => 1,
215 'file' => 'akismet.admin.inc',
216 );
217 $items['admin/content/akismet/nodes/spam'] = array(
218 'title' => t('Spam'),
219 'page arguments' => array('nodes'),
220 'type' => MENU_DEFAULT_LOCAL_TASK,
221 'weight' => 0,
222 'file' => 'akismet.admin.inc',
223 );
224 $items['admin/content/akismet/nodes/unpublished'] = array(
225 'title' => t('Unpublished nodes'),
226 'page arguments' => array('nodes', 'unpublished'),
227 'type' => MENU_LOCAL_TASK,
228 'weight' => 1,
229 'file' => 'akismet.admin.inc',
230 );
231 $items['admin/content/akismet/nodes/published'] = array(
232 'title' => t('Published nodes'),
233 'callback arguments' => array('nodes', 'published'),
234 'type' => MENU_LOCAL_TASK,
235 'weight' => 2,
236 'file' => 'akismet.admin.inc',
237 );
238 }
239 if (_akismet_is_moderator($moderator_types, 'comments')) {
240 $items['admin/content/akismet/comments'] = array(
241 'title' => t('Comments'),
242 'page callback' => 'akismet_callback_queue',
243 'page arguments' => array('comments'),
244 'access callback' => '_akismet_is_moderator',
245 'access arguments' => array($moderator_types, 'comments'),
246 'type' => MENU_LOCAL_TASK,
247 'weight' => 2,
248 'file' => 'akismet.admin.inc',
249 );
250 $items['admin/content/akismet/comments/spam'] = array(
251 'title' => t('Spam'),
252 'page arguments' => array('comments'),
253 'type' => MENU_DEFAULT_LOCAL_TASK,
254 'weight' => 0,
255 'file' => 'akismet.admin.inc',
256 );
257 $items['admin/content/akismet/comments/unpublished'] = array(
258 'title' => t('Unpublished comments'),
259 'page arguments' => array('comments', 'unpublished'),
260 'type' => MENU_LOCAL_TASK,
261 'weight' => 1,
262 'file' => 'akismet.admin.inc',
263 );
264 $items['admin/content/akismet/comments/published'] = array(
265 'title' => t('Published comments'),
266 'page arguments' => array('comments', 'published'),
267 'type' => MENU_LOCAL_TASK,
268 'weight' => 2,
269 'file' => 'akismet.admin.inc',
270 );
271 }
272 }
273 else {
274 $item = array(
275 'title' => 'switch content status',
276 'page callback' => 'akismet_page',
277 'page arguments' => array(0, 1, 2, 3),
278 'load arguments' => array('%map', '%index'),
279 'access callback' => 'akismet_access_callback',
280 'access argument' => array(0, 1, 2, 3),
281 );
282 foreach (array('publish', 'unpublish', 'submit-spam', 'submit-ham') as $op) {
283 $items['akismet/%akismet/%/'. $op] = $item;
284 }
285 }
286
287 return $items;
288 }
289
290 function akismet_load($arg, &$map, $index) {
291 if (!is_numeric($map[2])) {
292 // Node and comment ids are always numeric!
293 return FALSE;
294 }
295 $content_type = $map[1];
296 if ($content_type == 'node') {
297 if (!$map[2] = node_load($map[2])) {
298 return FALSE;
299 }
300 }
301 if ($content_type == 'comment' && module_exists('comment')) {
302 if (!$map[2] = comment_load($map[2])) {
303 return FALSE;
304 }
305 }
306 $op == $map[3];
307 if ($op == 'publish' || $op == 'unpublish') {
308 $map[0] = 'akismet_callback_set_published_status';
309 }
310 else if ($op == 'submit-spam' || $op == 'submit-ham') {
311 $map[0] = 'akismet_callback_set_spam_status';
312 }
313 return $map[$index];
314 }
315
316 function akismet_access_callback($callback, $content_type, $object, $op) {
317 if ($content_type == 'node' && !node_access($map[2])) {
318 return FALSE;
319 }
320 if (function_exists($callback && !akismet_is_spam_moderator(akismet_content_get_moderator_type($content_type, $object)))) {
321 return FALSE;
322 }
323 // Is there a comment access check we need to run? If yes, then do the same as above.
324 return TRUE;
325 }
326
327 function akismet_page($callback, $content_type, $object, $op) {
328 if (function_exists($callback)) {
329 return $callback($content_type, $object, $op);
330 }
331 drupal_not_found();
332 }
333
334 /**
335 * Implementation of hook_link().
336 */
337 function akismet_link($type, $content = 0, $main = 0) {
338 $links = array();
339 if ($type == 'node' && akismet_is_spam_moderator($content->type)) {
340 if (variable_get('akismet_node_publish_links', 0)) {
341 if ($content->status) {
342 $links['akismet_node_unpublish'] = array('title' => t('Unpublish'), 'href' => 'akismet/node/'. $content->nid .'/unpublish');
343 }
344 else {
345 $links['akismet_node_publish'] = array('title' => t('Publish'), 'href' => 'akismet/node/'. $content->nid .'/publish');
346 }
347 }
348 if (variable_get('akismet_node_spam_links', 0)) {
349 if (akismet_content_is_spam('node', $content->nid)) {
350 $links['akismet_node_ham'] = array('title' => (variable_get('akismet_connection_enabled', 1) ? t('Submit ham') : t('Mark as ham')), 'href' => 'akismet/node/'. $content->nid .'/submit-ham');
351 }
352 else {
353 $links['akismet_node_spam'] = array('title' => (variable_get('akismet_connection_enabled', 1) ? t('Submit spam') : t('Mark as spam')), 'href' => 'akismet/node/'. $content->nid .'/submit-spam');
354 }
355 }
356 }
357 else if ($type == 'comment' && akismet_is_spam_moderator('comments')) {
358 if (variable_get('akismet_comment_publish_links', 1)) {
359 if ($content->status == COMMENT_PUBLISHED) {
360 $links['akismet_comment_unpublish'] = array('title' => t('Unpublish'), 'href' => 'akismet/comment/'. $content->cid .'/unpublish');
361 }
362 else if ($content->status == COMMENT_NOT_PUBLISHED) {
363 $links['akismet_comment_publish'] = array('title' => t('Publish'), 'href' => 'akismet/comment/'. $content->cid .'/publish');
364 }
365 }
366 if (variable_get('akismet_comment_spam_links', 1)) {
367 if (akismet_content_is_spam('comment', $content->cid)) {
368 $links['akismet_comment_ham'] = array('title' => (variable_get('akismet_connection_enabled', 1) ? t('Submit ham') : t('Mark as ham')), 'href' => 'akismet/comment/'. $content->cid .'/submit-ham');
369 }
370 else {
371 $links['akismet_comment_spam'] = array('title' => (variable_get('akismet_connection_enabled', 1) ? t('Submit spam') : t('Mark as spam')), 'href' => 'akismet/comment/'. $content->cid .'/submit-spam');
372 }
373 }
374 }
375 return $links;
376 }
377
378 /**
379 * Menu callback; publish/unpublish content.
380 *
381 * @param string Content type; it can be 'node' or 'comment'.
382 * @param integer Content ID; can be either a nid or a cid.
383 * @param string Operation; it can be 'publish' or 'unpublish'.
384 */
385 function akismet_callback_set_published_status($content_type, $object, $op) {
386 // Load the content (existence has been checked in hook_menu).
387 $content = akismet_content_load($content_type, $object);
388
389 if ($content_type == 'node') {
390 $is_published = ($content->status ? TRUE : FALSE);
391 }
392 else { // comment
393 $is_published = ($content->status == COMMENT_PUBLISHED ? TRUE : FALSE);
394 }
395
396 if ($op == 'publish' && !$is_published) {
397 akismet_content_publish_operation($content_type, $content, 'publish');
398 }
399 else if ($op == 'unpublish' && $is_published) {
400 akismet_content_publish_operation($content_type, $content, 'unpublish');
401 }
402
403 if ($content_type == 'node') {
404 drupal_goto('node/'. $content->nid);
405 }
406 else { // comment
407 drupal_goto('node/'. $content->nid, NULL, 'comment-'. $content->cid);
408 }
409 }
410
411 /**
412 * Menu callback; mark/unmark content as spam.
413 *
414 * When content is marked as spam, it is also unpublished (if necessary) and vice-versa.
415 *
416 * @param string Content type; it can be 'node' or 'comment'.
417 * @param integer Content ID; can be either a nid or a cid.
418 * @param string Operation; it can be 'submit-spam' or 'submit-ham'.
419 */
420 function akismet_callback_set_spam_status($content_type, $object, $op) {
421 $is_spam = akismet_content_is_spam($content_type, $object);
422
423 // Load the content (existence has been checked in hook_menu).
424 $content = akismet_content_load($content_type, $object);
425
426 if ($content_type == 'node') {
427 $is_published = ($content->status ? TRUE : FALSE);
428 }
429 else { // comment
430 $is_published = ($content->status == COMMENT_PUBLISHED ? TRUE : FALSE);
431 }
432
433 // insert or remove the spam marker (publishing/unpublishing if necessary).
434 if ($op == 'submit-spam') {
435 if (!$is_spam) {
436 akismet_content_spam_operation($content_type, $content, 'submit-spam');
437 }
438 if ($is_published) {
439 akismet_content_publish_operation($content_type, $content, 'unpublish');
440 }
441 }
442 else if ($op == 'submit-ham') {
443 if ($is_spam) {
444 akismet_content_spam_operation($content_type, $content, 'submit-ham');
445 }
446 if (!$is_published) {
447 akismet_content_publish_operation($content_type, $content, 'publish');
448 }
449 }
450
451 if ($content_type == 'node') {
452 drupal_goto('node/'. $content->nid);
453 }
454 else { // comment
455 drupal_goto('node/'. $content->nid, NULL, 'comment-'. $content->cid);
456 }
457 }
458
459 /**
460 * Implementation of hook_nodeapi().
461 */
462 function akismet_nodeapi(&$node, $op, $teaser, $page) {
463 switch ($op) {
464 case 'insert':
465 case 'update':
466 // If Akismet connections are not enabled, we have nothing else to do here.
467 if (!variable_get('akismet_connection_enabled', 1)) {
468 akismet_notify_moderators('node', $node, ($node->status ? TRUE : FALSE), FALSE);
469 break;
470 }
471
472 // Also quit asap, if current user has administration permission
473 // or permission to post without spam checking.
474 if (akismet_is_spam_moderator($node->type) || user_access('post with no akismet checking')) {
475 akismet_notify_moderators('node', $node, ($node->status ? TRUE : FALSE), FALSE);
476 break;
477 }
478
479 // Now, check if it's about a node type that we have not been explicitly requested to check.
480 $check_nodetypes = variable_get('akismet_check_nodetypes', array());
481 if (!is_array($check_nodetypes) || !isset($check_nodetypes[$node->type]) || !$check_nodetypes[$node->type]) {
482 akismet_notify_moderators('node', $node, ($node->status ? TRUE : FALSE), FALSE);
483 break;
484 }
485
486 // Ok, let's send a query to Akismet.
487 $akismet_api_result = akismet_api_cmd_comment_check(akismet_prepare_comment_data('node', $node));
488 if ($akismet_api_result == AKISMET_API_RESULT_IS_HAM) {
489 akismet_notify_moderators('node', $node, ($node->status ? TRUE : FALSE), FALSE);
490 }
491 else {
492 if ($akismet_api_result == AKISMET_API_RESULT_IS_SPAM) {
493 // Oops! Akismet is telling us we got spammed, let's mark the comment as such.
494 akismet_content_spam_operation('node', $node, 'submit-spam', FALSE);
495 // Increment Akismet spam counter
496 variable_set('akismet_counter_spam', akismet_get_spam_counter() + 1);
497
498 akismet_notify_moderators('node', $node, FALSE, TRUE);
499 }
500 else {
501 akismet_notify_moderators('node', $node, FALSE, FALSE);
502 }
503
504 // Unpublish the node, if necessary.
505 if ($node->status) {
506 akismet_content_publish_operation('node', $node, 'unpublish', FALSE);
507 }
508
509 // Since users won't see their content published, show them a polite explanation on why.
510 $content_type_name = node_get_types('name', $node);
511 drupal_set_message(t('Your %content-type-name has been queued for moderation by site administrators and will be published after approval.', array('%content-type-name' => $content_type_name)));
512
513 // Record the event to watchdog.
514 if ($akismet_api_result == AKISMET_API_RESULT_ERROR) {
515 watchdog('content', 'Akismet service seems to be down, %content-type-name queued for manual approval: %title', array('%content-type-name' => $content_type_name, '%title' => $node->title), WATCHDOG_WARNING, l(t('view'), 'node/'. $node->nid));
516 }
517 else {
518 watchdog('content', 'Spam detected by Akismet in %content-type-name: %title', array('%content-type-name' => $content_type_name, '%title' => $node->title), WATCHDOG_WARNING, l(t('view'), 'node/'. $node->nid));
519 // If requested to, generate a delay so the spammer has to wait for a while.
520 if (($seconds = variable_get('akismet_antispambot_delay', 60)) > 0) {
521 sleep($seconds);
522 }
523 }
524 }
525 break;
526 case 'delete':
527 db_query('DELETE FROM {akismet_spam_marks} WHERE content_type = \'node\' AND content_id = %d', $node->nid);
528 break;
529 }
530 }
531
532 /**
533 * Implementation of hook_comment().
534 */
535 function akismet_comment(&$comment, $op) {
536 switch ($op) {
537 case 'insert':
538 case 'update':
539 if (!variable_get('akismet_check_comments', 0) || akismet_is_spam_moderator('comments') || !variable_get('akismet_connection_enabled', 1)) {
540 akismet_notify_moderators('comment', $comment, ($comment->status == COMMENT_PUBLISHED ? TRUE : FALSE), FALSE);
541 }
542 break;
543 case 'delete':
544 db_query('DELETE FROM {akismet_spam_marks} WHERE content_type = \'comment\' AND content_id = %d', $comment->cid);
545 break;
546 }
547 }
548
549 /**
550 * Implementation of hook_form_alter().
551 */
552 function akismet_form_alter(&$form, &$form_state, $form_id) {
553 // Hook into comment edit/reply form.
554 if ($form_id == 'comment_form' && variable_get('akismet_check_comments', 1)) {
555 // ...only if current user is not moderator.
556 if (!akismet_is_spam_moderator('comments')) {
557 // ...also check if Akismet connections are enabled.
558 if (variable_get('akismet_connection_enabled', 1)) {
559 // This is the simple hook method, *if* we already have the $cid.
560 $form['#submit'][] = '_akismet_comment_form_submit';
561 if (!isset($form['cid']) || !isset($form['cid']['#value']) || !is_numeric($form['cid']['#value'])) {
562 // This is a bit more complex, because the user is creating a new comment, so
563 // how can we get the $cid? See comments below, within our own submit callback.
564 $form['#comment_form_param1'] = $form['#submit'];
565 }
566 }
567 // Inject anti-spambot code, if requested to.
568 if (akismet_is_anti_spambot_enabled()) {
569 $form['#validate'][] = '_akismet_comment_form_validate';
570 }
571 }
572 }
573 // Hook into node edit form.
574 else if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) {
575 // ...only if current user is not moderator.
576 if (!akismet_is_spam_moderator('comments')) {
577 // ...also check if it's about a node type that we have been explicitly requested to check.
578 $check_nodetypes = variable_get('akismet_check_nodetypes', array());
579 $node_type = $form['type']['#value'];
580 if (is_array($check_nodetypes) && isset($check_nodetypes[$node_type]) && $check_nodetypes[$node_type]) {
581 // Inject anti-spambot code, if requested to.
582 if (akismet_is_anti_spambot_enabled()) {
583 $form['#validate'][] = '_akismet_node_form_validate';
584 }
585 }
586 }
587 }
588 }
589
590 /**
591 * Node form validate callback; check for spambots.
592 */
593 function _akismet_node_form_validate($form, &$form_state) {
594 // Quit if there have already been errors in the form.
595 if (form_get_errors()) {
596 return;
597 }
598
599 // Ok, let's build a quick query to see if we can catch a spambot.
600 global $user;
601 $antispambot_rules = akismet_get_anti_spambot_rules();
602 $sql_where = array();
603 $sql_args = array();
604 if ($antispambot_rules['ip']) {
605 $sql_where[] = 's.hostname = \'%s\'';
606 $sql_args[] = ip_address();
607 }
608 if ($antispambot_rules['body'] && !empty($form_state['values']['body'])) {
609 $sql_where[] = 'r.body = \'%s\'';
610 $sql_args[] = $form_state['values']['body'];
611 }
612 if ($antispambot_rules['mail'] && !empty($user->mail)) {
613 $sql_where[] = 's.mail = \'%s\'';
614 $sql_args[] = $user->mail;
615 }
616
617 if (count($sql_where) > 0) {
618 if ($antispambot_rules['body'] || $antispambot_rules['mail']) {
619 $sql_stmt = 'SELECT 1 FROM {node} n INNER JOIN {node_revisions} r ON r.nid = n.nid INNER JOIN {akismet_spam_marks} s ON s.content_type = \'node\' AND s.content_id = n.nid WHERE (%cond)';
620 }
621 else {
622 $sql_stmt = 'SELECT 1 FROM {akismet_spam_marks} s WHERE s.content_type = \'node\' AND (%cond)';
623 }
624 $sql_stmt = str_replace('%cond', implode(' OR ', $sql_where), $sql_stmt);
625 if (db_result(db_query($sql_stmt, $sql_args, 0, 1))) {
626 akismet_anti_spambot_action(array(
627 t('SQL') => _akismet_translate_query($sql_stmt, $sql_args),
628 t('E-mail') => (isset($user->mail) ? $user->mail : ''),
629 t('Body') => $form_state['values']['body']
630 ));
631 }
632 }
633 }
634
635 /**
636 * Comment form validate callback; check for spambots.
637 */
638 function _akismet_comment_form_validate($form, &$form_state) {
639 // Quit if there have already been errors in the form.
640 if (form_get_errors()) {
641 return;
642 }
643
644 // Ok, let's build a quick query to see if we can catch a spambot.
645 $antispambot_rules = akismet_get_anti_spambot_rules();
646 $sql_where = array();
647 $sql_args = array();
648 if ($antispambot_rules['ip']) {
649 $sql_where[] = 's.hostname = \'%s\'';
650 $sql_args[] = ip_address();
651 }
652 if ($antispambot_rules['body'] && !empty($form_state['values']['comment'])) {
653 $sql_where[] = 'c.comment = \'%s\'';
654 $sql_args[] = $form_state['values']['comment'];
655 }
656 if ($antispambot_rules['mail'] && !empty($form_state['values']['mail'])) {
657 $sql_where[] = 's.mail = \'%s\'';
658 $sql_args[] = $$form_state['values']['mail'];
659 }
660
661 if (count($sql_where) > 0) {
662 if ($antispambot_rules['body'] || $antispambot_rules['mail']) {
663 $sql_stmt = 'SELECT 1 FROM {comments} c INNER JOIN {akismet_spam_marks} s ON s.content_type = \'comment\' AND s.content_id = c.cid WHERE (%cond)';
664 }
665 else {
666 $sql_stmt = 'SELECT 1 FROM {akismet_spam_marks} s WHERE s.content_type = \'comment\' AND (%cond)';
667 }
668 $sql_stmt = str_replace('%cond', implode(' OR ', $sql_where), $sql_stmt);
669 if (db_result(db_query($sql_stmt, $sql_args, 0, 1))) {
670 akismet_anti_spambot_action(array(
671 t('SQL') => _akismet_translate_query($sql_stmt, $sql_args),
672 t('E-mail') => $form_state['values']['mail'],
673 t('Comment') => $form_state['values']['comment']
674 ));
675 }
676 }
677 }
678
679 /**
680 * Comment form submit callback; check for spam.
681 */
682 function _akismet_comment_form_submit($form, &$form_state, $original_submit_callback = NULL) {
683 // Our default destination. It doesn't need to override the original.
684 $goto = NULL;
685
686 // If the comment is being edited, then there's no problem to get the $cid.
687 // In this case, the original #submit callback has already been called.
688 if (isset($form_state['values']['cid'])) {
689 $cid = $form_state['values']['cid'];
690 }
691 // However, if the comment is being created, we'll try to get the $cid from the
692 // return value of the original #submit callback. It's an array that customizes
693 // the URL the user should be sent when the form is submitted. It contains the
694 // $cid in the last argument, in the form of "comment-$cid", the hash of the URL.
695 else {
696 // Invoke the previous submit callbacks and capture their return values to try
697 // to get the $cid from there.
698
699 // The first critical part with this approach is that we have to emmulate the
700 // $form argument that form.inc::drupal_submit_form() expects. At this point in
701 // time, this function just uses the '#submit' element, but that could change in
702 // the future. We have to keep an eye here, or think about a completely different
703 // approach. Hopefully, this one will remain stable during the 4.x lifecycle.
704 $form = array('#submit' => $original_submit_callback);
705
706 // The second critical part is that we expect to find the $cid in the 3rd element
707 // of the $goto array, as described above.
708 $goto = drupal_submit_form($form_id, $form);
709 if (is_array($goto) && isset($goto[2]) && preg_match('#^comment-([0-9]+)$#', $goto[2], $match)) {
710 $cid = $match[1];
711 }
712 }
713
714 // Once we have a $cid, we can (try to) load the comment with all relevant
715 // information that we need to make the Akismet request to check for spam.
716 if ($cid) {
717 $comment = akismet_content_load('comment', $cid);
718 // If we got a comment, send query to Akismet.
719 if ($comment) {
720 $akismet_api_result = akismet_api_cmd_comment_check(akismet_prepare_comment_data('comment', $comment));
721 if ($akismet_api_result == AKISMET_API_RESULT_IS_HAM) {
722 akismet_notify_moderators('comment', $comment, ($comment->status == COMMENT_PUBLISHED ? TRUE : FALSE), FALSE);
723 }
724 else {
725 if ($akismet_api_result == AKISMET_API_RESULT_IS_SPAM) {
726 // Oops! Akismet is telling us we got spammed, let's mark the comment as such.
727 akismet_content_spam_operation('comment', $comment, 'submit-spam', FALSE);
728 // Increment Akismet spam counter
729 variable_set('akismet_counter_spam', akismet_get_spam_counter() + 1);
730
731 akismet_notify_moderators('comment', $comment, FALSE, TRUE);
732 }
733 else {
734 akismet_notify_moderators('comment', $comment, FALSE, FALSE);
735 }
736
737 // Unpublish the comment, if necessary.
738 if ($comment->status == COMMENT_PUBLISHED) {
739 akismet_content_publish_operation('comment', $comment, 'unpublish', FALSE);
740 }
741
742 // Since users won't see their replies published, show them a polite explanation on why.
743 drupal_set_message(t('Your comment has been queued for moderation by site administrators and will be published after approval.'));
744
745 // Record the event to watchdog.
746 if ($akismet_api_result == AKISMET_API_RESULT_ERROR) {
747 watchdog('content', 'Akismet service seems to be down, comment queued for manual approval: %subject', array('%subject' => $comment->subject), WATCHDOG_WARNING, l(t('view'), 'node/'. $comment->nid, NULL, NULL, 'comment-'. $comment->cid));
748 }
749 else {
750 watchdog('content', 'Spam detected by Akismet in comment: %subject', array('%subject' => $comment->subject), WATCHDOG_WARNING, l(t('view'), 'node/'. $comment->nid, NULL, NULL, 'comment-'. $comment->cid));
751 // If requested to, generate a delay so the spammer has to wait for a while.
752 if (($seconds = variable_get('akismet_antispambot_delay', 60)) > 0) {
753 sleep($seconds);
754 }
755 }
756 }
757 }
758 }
759
760 // Return NULL or the destination returned by the original #submit callback.
761 return $goto;
762 }
763
764 /**
765 * Get anti-spambot rules.
766 *
767 * @return array
768 */
769 function akismet_get_anti_spambot_rules() {
770 static $antispambot_rules = FALSE;
771 if (!$antispambot_rules) {
772 $antispambot_rules = array();
773 $options = variable_get('akismet_antispambot_rules', array());
774 if (is_array($options)) {
775 foreach ($options as $key => $value) {
776 if (is_string($key)) {
777 $antispambot_rules[$key] = ($key === $value ? TRUE : FALSE);
778 }
779 }
780 }
781 }
782 return $antispambot_rules;
783 }
784
785 /**
786 * Check if anti-spambot options are enabled.
787 *
788 * @return boolean TRUE if enabled; FALSE otherwise.
789 */
790 function akismet_is_anti_spambot_enabled() {
791 $antispambot_rules = akismet_get_anti_spambot_rules();
792 return (count($antispambot_rules) > 0 ? TRUE : FALSE);
793 }
794
795 /**
796 * Perform an anti-spambot action based on module settings.
797 *
798 * @param array Extra data, used here to enhance the logged information, for debugging purposes.
799 */
800 function akismet_anti_spambot_action($debug_info) {
801 $antispambot_action = variable_get('akismet_antispambot_action', '503');
802
803 // First action is generate a delay, if requested to.
804 if (($seconds = variable_get('akismet_antispambot_delay', 60)) > 0) {
805 sleep($seconds);
806 }
807
808 // If no other action was set, we're done.
809 if ($antispambot_action == 'none') {
810 return;
811 }
812
813 $items = array();
814 foreach ($debug_info as $label => $value) {
815 $items[] = '<strong>'. check_plain($label) .'</strong>: '. check_plain($value);
816 }
817
818 // From here on, the request is killed using different methods.
819 if ($antispambot_action == '403d') {
820 drupal_access_denied();
821 $message = t('Spambot detected (action: 403 Forbidden).');
822 }
823 else if ($antispambot_action == '403') {
824 @header('HTTP/1.0 403 Forbidden');
825 print t('Access denied');
826 $message = t('Spambot detected (action: 403 Forbidden).');
827 }
828 else { // 503
829 @header('HTTP/1.0 503 Service unavailable');
830 print t('Service unavailable');
831 $message = t('Spambot detected (action: 503 Service unavailable).');
832 }
833
834 watchdog('akismet', '%message<p>Additional information:</p>%items', array('%message' => $message, '%items' => theme('item_list', $items)));
835
836 module_invoke_all('exit');
837 exit;
838 }
839
840 /**
841 * Expand query for debugging purposes.
842 *
843 * @param string SQL statement.
844 * @param mixed array or variable list of arguments.
845 */
846 function _akismet_translate_query($query) {
847 $args = func_get_args();
848 array_shift($args);
849 $query = db_prefix_tables($query);
850 if (isset($args[0]) && is_array($args[0])) { // 'All arguments in one array' syntax
851 $args = $args[0];
852 }
853 _db_query_callback($args, TRUE);
854 $query = preg_replace_callback(DB_QUERY_REGEXP, '_db_query_callback', $query);
855 return $query;
856 }
857
858 /**
859 * Check if specified content is marked as spam.
860 *
861 * @param string Content type; can be either 'node' or 'comment'.
862 * @param integer Content ID; can be either a nid or a cid.
863 * @return boolean TRUE if content is marked as spam; FALSE otherwise.
864 */
865 function akismet_content_is_spam($content_type, $content_id) {
866 return db_result(db_query('SELECT 1 FROM {akismet_spam_marks} WHERE content_type = \'%s\' AND content_id = %d', $content_type, $content_id));
867 }
868
869 /**
870 * Get moderator type required for specified content.
871 *
872 * @param string Content type; can be either 'node' or 'comment'.
873 * @param integer Content ID; can be either a nid or a cid.
874 * @return string Moderator Type or empty string if content is not found.
875 */
876 function akismet_content_get_moderator_type($content_type, $content_id) {
877 if ($content_type == 'node') {
878 $moderator_type = db_result(db_query('SELECT type FROM {node} WHERE nid = %d', $content_id));
879 if (!$moderator_type) {
880 $moderator_type = '';
881 }
882 }
883 else if ($content_type == 'comment') {
884 $moderator_type = (db_result(db_query('SELECT 1 FROM {comments} WHERE cid = %d', $content_id)) ? 'comments' : '');
885 }
886 else {
887 $moderator_type = '';
888 }
889 return $moderator_type;
890 }
891
892 /**
893 * Get the types the current user is allowed to moderate.
894 *
895 * @param object The account to check; use current user if not given.
896 * @return array Moderator Types.
897 */
898 function akismet_get_moderator_types($account = NULL) {
899 global $user;
900 static $node_types = FALSE;
901 static $moderator_types = array();
902
903 if (is_null($account)) {
904 $account = $user;
905 }
906
907 if ($node_types === FALSE) {
908 $node_types = node_get_types('names');
909 }
910
911 if (!isset($moderator_types[$account->uid])) {
912 if (user_access('administer nodes', $account)) {
913 foreach ($node_types as $type => $name) {
914 $moderator_types[$account->uid][$type] = $name;
915 }
916 }
917 else {
918 foreach ($node_types as $type => $name) {
919 if (user_access('moderate spam in nodes of type '. $node_types[$type], $account)) {
920 $moderator_types[$account->uid][$type] = $name;
921 }
922 }
923 }
924
925 if (user_access('administer comments', $account) || user_access('moderate spam in comments', $account)) {
926 $moderator_types[$account->uid]['comments'] = t('comments');
927 }
928 }
929
930 return $moderator_types[$account->uid];
931 }
932
933 /**
934 * Is current user spam moderator?
935 *
936 * @param string Moderator Type (comments, node type or NULL).
937 * @param object The account to check; use current user if not given.
938 * @return boolean TRUE if current user is moderator of specified type; FALSE otherwise.
939 */
940 function akismet_is_spam_moderator($moderator_type = NULL, $account = NULL) {
941 global $user;
942 if (is_null($account)) {
943 $account = $user;
944 }
945 $moderator_types = akismet_get_moderator_types($account);
946 if (is_null($moderator_type)) {
947 return (count($moderator_types) > 0 ? TRUE : FALSE);
948 }
949 return isset($moderator_types[$moderator_type]);
950 }
951
952 /**
953 * Notify moderators of new/updated content, only content needing approval or nothing at all.
954 *
955 * @param string Content type; can be either 'node' or 'comment'.
956 * @param object Content object.
957 * @param boolean TRUE if content is in published status.
958 * @param boolean TRUE if content has been marked as spam.
959 */
960 function akismet_notify_moderators($content_type, $content, $is_published, $is_spam) {
961 global $user, $base_url;
962
963 // Proceed only if e-mail notifications are enabled.
964 if (!variable_get('akismet_email_enabled', 0)) {
965 return;
966 }
967
968 // Make sure we have an object.
969 $content = (object)$content;
970
971 // Compute the related moderator permission.
972 if ($content_type == 'comment') {
973 $moderator_permission = 'moderate spam in comments';
974 $administer_permission = 'administer comments';
975 }
976 else {
977 $moderator_types = akismet_get_moderator_types($account);
978 $moderator_permission = 'moderate spam in nodes of type '. $moderator_types[$content->type];
979 $administer_permission = 'administer nodes';
980 }
981
982 // Obtain list of moderators of the specified content type.
983 $sql = 'SELECT u.uid, u.name, u.mail, m.email_for'.
984 ' FROM {permission} p'.
985 ' INNER JOIN {users_roles} r ON r.rid = p.rid'.
986 ' INNER JOIN {users} u ON u.uid = r.uid OR u.uid = 1'.
987 ' LEFT JOIN {akismet_moderator} m ON m.uid = u.uid'.
988 ' WHERE p.perm LIKE \'%%%s%%\''.
989 ' OR p.perm LIKE \'%%%s%%\'';
990 $result = db_query($sql, $moderator_permission, $administer_permission);
991 $moderators = array();
992 while ($u = db_fetch_object($result)) {
993 if ($u->uid != $user->uid) {
994 $moderators[$u->uid] = array(
995 'name' => $u->name,
996 'email_to' => $u->mail,
997 'email_for' => (!is_null($u->email_for) ? $u->email_for : 'approval')
998 );
999 }
1000 }
1001
1002 // Extract unique email addresses and ignore those who have requested to not get e-mail notifications.
1003 $unique_emails = array();
1004 foreach ($moderators as $uid => $moderator) {
1005 if ($moderator['email_for'] == 'all' || ($moderator['email_for'] == 'approval' && !$is_published)) {
1006 if (!isset($unique_emails[$moderator['email_to']])) {
1007 $unique_emails[$moderator['email_to']] = $uid;
1008 }
1009 }
1010 }
1011 if (count($unique_emails) <= 0) {
1012 return;
1013 }
1014
1015 // If this is about a comment, try to load the node.
1016 // Also, prepare arguments for notification message.
1017 $site_name = variable_get('site_name', t('Drupal'));
1018 if ($content_type == 'comment') {
1019 if (!($node = akismet_content_load('node', $content->nid))) {
1020 watchdog('akismet', 'An error has ocurred while trying to notify moderators about a comment. The associated node could not be loaded.', array(), WATCHDOG_NOTICE, l(t('view'), 'node/'. $content->nid, NULL, NULL, 'comment-'. $content->cid));
1021 return;
1022 }
1023 $message_args = array(
1024 '@title-label' => t('Subject'),
1025 '@content-title' => $content->subject,
1026 '@content-type' => t('comment'),
1027 '!content-link' => url('node/'. $content->nid, array('fragment' => 'comment-'. $content->cid, 'absolute' => TRUE))
1028 );
1029 }
1030 else {
1031 $message_args = array(
1032 '@title-label' => t('Title'),
1033 '@content-title' => $content->title,
1034 '@content-type' => $moderator_types[$content->type],
1035 '!content-link' => url('node/'. $content->nid, array ('absolute' => TRUE))
1036 );
1037 }
1038 $message_args['@content-status'] = ($is_published ? t('published') : t('not published')) . ($is_spam ? ' ('. t('marked as spam') .')' : '');
1039 $message_args['@site-name'] = $site_name;
1040 $message_args['!site-link'] = $base_url . base_path();
1041 $message_args['@type'] = $moderator_types[$content->type];
1042
1043 $message_title = t('[@site-name] moderator notification - Posted @content-type \'@content-title\'', $message_args);
1044
1045 $message_body = t(<<<EOT
1046 Hello @user-name,
1047
1048 You can use the following information to review this @content-type:
1049
1050 @title-label: @content-title
1051 URL: !content-link
1052 Status: @content-status
1053
1054 Please, do not reply to this e-mail. It is an automated notification you are receiving because you are a moderator at @site-name. If you no longer wish to receive such notifications, you can change your moderator settings in your user profile.
1055
1056 Thank you
1057
1058 !site-link
1059 EOT
1060 , $message_args);
1061
1062 // Log the notification to watchdog.
1063 watchdog('akismet', 'Trying to notify the following recipients: %emails', array('%emails' => implode(', ', array_keys($unique_emails))));
1064
1065 // Send e-mails.
1066 foreach ($unique_emails as $email_to => $uid) {
1067 drupal_mail(
1068 'akismet_moderator_notification',
1069 $email_to,
1070 $message_title,
1071 str_replace('@user-name', check_plain($moderators[$uid]['name']), $message_body)
1072 );
1073 }
1074 }
1075
1076 /**
1077 * Implementation of hook_user().
1078 */
1079 function akismet_user($op, &$edit, &$account, $category = NULL) {
1080 $moderator_email_for_options = array(
1081 'all' => t('All new (or updated) content'),
1082 'approval' => t('Only content needing approval'),
1083 'never' => t('Never')
1084 );
1085 switch ($op) {
1086 case 'form':
1087 if ($category == 'account'