AJAX Kommentare

Es gibt zahlreiche Plugins für Wordpress, die dafür sorgen, dass Kommentare von Besuchern per AJAX eingetragen werden. Die meisten davon schaffen es nicht lange mit den aktuellen Versionen von Wordpress kompatibel zu bleiben und diejenigen die es schaffen, sind so überladen mit Schnickschnack und hässlichen Frontendgestaltungen, dass ich soviel Zeit brauche alles nach meinen Wünschen anzupasssen, da kann ich gleich alles von vornerein neu machen.

Zuerst mal brauche ich dafür ein PHP-Skript, dass nicht nur die Formulardaten verarbeitet und bei erfolgreicher Überprüfung ein neues Kommentar anlegt, sondern auch eine Ausgabe erzeugt, die man mit Javascript leicht verarbeiten kann.

Die normale Kommentarverarbeitung in Wordpress 2.8+

Ohne AJAX übernimmt die Datei wp-comments-post.php, die sich im Wordpress-Stammverzeichnis befindet, das Verarbeiten des Kommentarformulars. Kann ein Kommentar, aus welchen Gründen auch immer, nicht eingetragen werden, gibt das Skript über die Wordpress-interne Funktion wp_die() eine kleine HTML-Seite mitsamt Fehlerbeschreibung aus. Sollte nichts gegen das Erstellen des Kommentars sprechen, wird es angelegt und mit der ebenfalls Wordpress-internen Funktion wp_redirect() wird auf die zum Kommentar gehörende Artikelseite weitergeleitet.

Die Vermeidung von Doppelpostings und Flooding wird nicht direkt innerhalb von wp-comment-post.php geregelt, sondern im Zuge der Funktion wp_new_comment(). Die per Filter im Laufe der Funktion angewandten Funktionen wp_allow_comment() und check_comment_flood_db() (beide definiert in wp-includes/comment.php) sind dafür verantwortlich und liefern bei positivem Befund ebenfalls eine HTML-Seite per wp_die(). Aber hier haben die Wordpress-Entwickler bereits Vorarbeit geleistet: Ist eine Konstante namens DOING_AJAX definiert, wird bei Doppelpostings und Flooding stattdessen direkt die() verwendet und die Fehlermeldung als reiner Text ausgegeben.

Los gehts…

Ich nehme mir also die Datei wp-comments-post.php als Vorlage für mein PHP-Skript zur Formularverarbeitung mit AJAX. Dazu erstelle ich eine Kopie der Datei unter dem Namen new-comment.php und speichere diese im Unterverzeichnis ajax meines Themeordners. Falls bei einem zukünftigen Update von Wordpress die wp-comments-post.php verändert wird und ich somit meine new-comment.php neu erstellen muss, markiere ich vorsichtshalber alle geänderten Stellen im Quelltext um die Anpassungen später besser nachvollziehen und wiederholen zu können. Ich habe auf Wordpress.org auch bereits den Vorschlag gemacht, diese Anpassungen von Haus aus in wp-comments-post.php zu integrieren um die Implementierung von AJAX Kommentaren in Zukunft zu vereinfachen.

Damit das Skript innerhalb der Wordpress-Umgebung ausgeführt wird, muss der Pfad zu wp-load.php ziemlich am Anfang der Datei angepasst werden. Direkt davor füge ich define('DOING_AJAX',1); ein. Damit anstatt der HTML-Fehlerseiten reine Textnachrichten ausgegeben werden, ersetze ich alle Vorkommen von wp_die() durch die Funktion die(). Die letzten 3 Zeilen der Datei, die verantwortlich für den Redirect sind, kommentiere ich aus. In der Variablen $comment ist nun das Objekt des eben erstellten Kommentars gespeichert. Nachdem ich den Content des Kommentars gefiltert und das Datum formatiert habe, verwandle ich das Objekt in einen JSON-String und gebe ihn per die() aus. Das PHP-Skript liefert nun also im Fehlerfall eine Textmeldung zurück und im Erfolgsfalls das eben erstellte Kommentar.

Meine new-comment.php sieht jetzt so aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/**-----------------------------------------------------------------------------
 *	start	/wp-comments-post.php
 *	changes start with //## and end with //^^
 */
 
if ( 'POST' != $_SERVER['REQUEST_METHOD'] ) {
	header('Allow: POST');
	header('HTTP/1.1 405 Method Not Allowed');
	header('Content-Type: text/plain');
	exit;
}
 
/** Sets up the WordPress Environment. */
//## require( dirname(__FILE__) . '/wp-load.php' );
define('DOING_AJAX',1);
require  '../../../../wp-load.php' ;
//^^
 
nocache_headers();
 
$comment_post_ID = (int) $_POST['comment_post_ID'];
 
$status = $wpdb->get_row( $wpdb->prepare("SELECT post_status, comment_status FROM $wpdb->posts WHERE ID = %d", $comment_post_ID) );
 
if ( empty($status->comment_status) ) {
	do_action('comment_id_not_found', $comment_post_ID);
	exit;
} elseif ( !comments_open($comment_post_ID) ) {
	do_action('comment_closed', $comment_post_ID);
	//## wp_die( __('Sorry, comments are closed for this item.') );
	die( __('Sorry, comments are closed for this item.') );
	//^^
} elseif ( in_array($status->post_status, array('draft', 'pending') ) ) {
	do_action('comment_on_draft', $comment_post_ID);
	exit;
} else {
	do_action('pre_comment_on_post', $comment_post_ID);
}
 
$comment_author       = ( isset($_POST['author']) )  ? trim(strip_tags($_POST['author'])) : null;
$comment_author_email = ( isset($_POST['email']) )   ? trim($_POST['email']) : null;
$comment_author_url   = ( isset($_POST['url']) )     ? trim($_POST['url']) : null;
$comment_content      = ( isset($_POST['comment']) ) ? trim($_POST['comment']) : null;
 
// If the user is logged in
$user = wp_get_current_user();
if ( $user->ID ) {
	if ( empty( $user->display_name ) )
		$user->display_name=$user->user_login;
	$comment_author       = $wpdb->escape($user->display_name);
	$comment_author_email = $wpdb->escape($user->user_email);
	$comment_author_url   = $wpdb->escape($user->user_url);
	if ( current_user_can('unfiltered_html') ) {
		if ( wp_create_nonce('unfiltered-html-comment_' . $comment_post_ID) != $_POST['_wp_unfiltered_html_comment'] ) {
			kses_remove_filters(); // start with a clean slate
			kses_init_filters(); // set up the filters
		}
	}
} else {
	if ( get_option('comment_registration') || 'private' == $status->post_status )
		//## wp_die( __('Sorry, you must be logged in to post a comment.') );
		die( __('Sorry, you must be logged in to post a comment.') );
		//^^
}
 
$comment_type = '';
 
if ( get_option('require_name_email') && !$user->ID ) {
	if ( 6 > strlen($comment_author_email) || '' == $comment_author )
		//## wp_die( __('Error: please fill the required fields (name, email).') );
		die( __('Error: please fill the required fields (name, email).') );
		//^^
	elseif ( !is_email($comment_author_email))
		//## wp_die( __('Error: please enter a valid email address.') );
		die( __('Error: please enter a valid email address.') );
		//^^
}
 
if ( '' == $comment_content )
	//## wp_die( __('Error: please type a comment.') );
	die( __('Error: please type a comment.') );
	//^^
 
$comment_parent = isset($_POST['comment_parent']) ? absint($_POST['comment_parent']) : 0;
 
$commentdata = compact('comment_post_ID', 'comment_author', 'comment_author_email', 'comment_author_url', 'comment_content', 'comment_type', 'comment_parent', 'user_ID');
 
$comment_id = wp_new_comment( $commentdata );
 
$comment = get_comment($comment_id);
if ( !$user->ID ) {
	$comment_cookie_lifetime = apply_filters('comment_cookie_lifetime', 30000000);
	setcookie('comment_author_' . COOKIEHASH, $comment->comment_author, time() + $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN);
	setcookie('comment_author_email_' . COOKIEHASH, $comment->comment_author_email, time() + $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN);
	setcookie('comment_author_url_' . COOKIEHASH, esc_url($comment->comment_author_url), time() + $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN);
}
 
//##
// $location = empty($_POST['redirect_to']) ? get_comment_link($comment_id) : $_POST['redirect_to'] . '#comment-' . $comment_id;
// $location = apply_filters('comment_post_redirect', $location, $comment);
// wp_redirect($location);
$comment->comment_content = apply_filters('comment_text',$comment->comment_content);
$comment->comment_date = date('l, j. F Y G:i',strtotime($comment->comment_date));
die(json_encode($comment));
//^^
 
/**-----------------------------------------------------------------------------
 *	end	/wp-comments-post.php
 */

Jetzt da ich eine Datei habe, die meine AJAX-Anfrage verarbeiten und beantworten kann, kann ich das zugehörige Javascript basteln. Ein div wird beim Formular eingefügt um den Ladestatus oder eine Fehlermeldung anzuzeigen. Der Rest ist ein simpler jQuery.ajax()-Aufruf, der beim success-Event den zurückgelieferten Text als Fehlermeldung in besagtem div anzeigt oder, sollte der Text mit einem { beginnen, ihn als JSON-Objekt des neuen Kommentars behandelt. Aus den Kommentardaten lasse ich ein HTML-Konstrukt erstellen, das dem Kommentarlisteneintrag meines Templates entspricht, und füge es in dann in die Liste ein. Sollte es auf der Seite noch keine Kommentare geben, wird der gesamte notwendige Bereich mitsamt Überschrift erstellt und eingefügt. Den HTML-Teil und das Einreihen in das DOM sollte man natürlich an sein jeweiliges Theme anpassen. Bei mir sieht das dann so aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
jQuery(document).ready( function() {
 
	// ajax comments
	if( jQuery('#commentform').length>0 ) {
		var ac = {
			form: jQuery('#commentform'),
			response: jQuery('<div id="response" />').hide()
		};
		ac.form.before(ac.response).submit(function(){
			jQuery.ajax({
				type: 'POST',
				url: TPLDIR+'ajax/new-comment.php',
				dataType: 'text',
				data: ac.form.serialize(),
				beforeSend: function() {
					ac.response.empty().removeClass('error success').addClass('loading').slideDown('fast');
				},
				success: function(r){
					if( r.substr(0,1)!='{' )
						ac.response.empty().removeClass('loading').addClass('error').text(r);
					else {
						ac.form.find('textarea').val('')
						ac.response.empty().removeClass('loading').addClass('success').text('Dein Kommentar wurde erfolgreich eingetragen.');
						c = eval('('+r+')');
						var comment_count = jQuery('ol.commentlist li').length;
						var comment_html = '<li id="comment-'+c.comment_ID+'" class="comment'+(c.user_id>0?' user':'')+(comment_count==0?' first':'')+'">'
							+'<div class="comment-count">'+(parseInt(comment_count)+1)+'</div>'
							+'<div class="comment-author">'+(c.comment_author_url!=''?'<a href="'+c.comment_author_url+'">'+c.comment_author+'</a>':c.comment_author)+'</div>'
							+'<div class="comment-date" title="'+c.comment_date+'">vor einer Sekunde'+(c.comment_approved==0?' (noch nicht freigeschaltet)':'')+'</div>'
							+'<div class="entry">'+c.comment_content+'</div>'
							+'</li>';
						if( comment_count == 0 )
							jQuery('<div class="comments"><h3 class="post-title">Bisher eine Reaktion</h3><ol class="commentlist">'+comment_html+'</ol></div>').hide().insertBefore('#respond').slideDown('medium');
						else {
							jQuery('#comments h3').text('Bisher '+(parseInt(comment_count)+1)+' Reaktionen')
							jQuery(comment_html).hide().appendTo('ol.commentlist').slideDown('medium');
						}
					}
					jQuery('#submit').blur();
				},
				error: function(XMLHttpRequest, textStatus, errorThrown){
					ac.response.empty().removeClass('loading').addClass('error').text("Fehler: Die AJAX-Anfrage ist fehlgeschlagen ("+textStatus+")");
					jQuery('#submit').blur();
				}
			});
			return false;
		});
	}
 
});

Damit alles noch akzeptabel aussieht, braucht es noch eine Loading-Animation, die ich mir bei Ajaxload.info erstelle, und eine Priese CSS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*----- ajax comments -----*/
 
#response {
	height:3em;
}
 
#response.loading {
	background: transparent url('images/loading.gif') top left no-repeat;
}
 
#response.success {
	color:#222222;
}
 
#response.error {
	color:#aa1010;
}

Fertig.

Die Kommentare werden nun per AJAX eingetragen, es werden Ladestatus, Fehler- bzw. Erfolgsmeldungen am Formular angezeigt und das neue Kommentar wird direkt in die Kommentarliste eingefügt. Verschachtelte Kommentare habe ich in diesem Skript nicht beachtet, da ich sie bisher nicht nutze. Da aber im JSON-Objekt des Kommentars auch die ID des Elternkommentars enthalten ist, dürfte es ein Leichtes sein, die entsprechende Stelle im DOM zu finden und es dort einzufügen.