Major revamps to the template parser. Fixed a few security holes that could allow PHP to be injected in untimely places in TPL code. Improved Ux for XSS attempt in tplWikiFormat. Documented many functions. Backported much cleaner parser from 2.0 branch. Beautified a lot of code in the depths of the template class. Pretty much a small-scale Extreme Makeover.
// Comments
var comment_template = false;
var comment_render_track = 0;
function ajaxComments(parms)
{
setAjaxLoading();
var pid = strToPageID(title);
if(!parms)
{
var parms = {
'mode' : 'fetch'
};
}
parms.page_id = pid[0];
parms.namespace = pid[1];
if(comment_template)
parms.have_template = true;
parms = ajaxEscape(toJSONString(parms));
ajaxPost(stdAjaxPrefix+'&_mode=comments', 'data=' + parms, function() {
if(ajax.readyState == 4) {
unsetAjaxLoading();
selectButtonMajor('discussion');
unselectAllButtonsMinor();
// IE compatibility - doing ajax.responseText.substr() doesn't work
var rsptxt = ajax.responseText + '';
if ( rsptxt.substr(0, 1) != '{' )
{
document.getElementById('ajaxEditContainer').innerHTML = '<p>Comment system Javascript runtime: invalid JSON response from server, response text:</p><pre>' + ajax.responseText + '</pre>';
return false;
}
var response = parseJSON(ajax.responseText);
switch(response.mode)
{
case 'fetch':
document.getElementById('ajaxEditContainer').innerHTML = '<div class="wait-box">Rendering '+response.count_total+' comments...</div>';
if(response.template)
comment_template = response.template;
setAjaxLoading();
renderComments(response);
unsetAjaxLoading();
break;
case 'redraw':
redrawComment(response);
break;
case 'annihilate':
annihiliateComment(response.id);
break;
case 'materialize':
alert('Your comment has been posted. If it does not appear right away, it is probably awaiting approval.');
hideCommentForm();
materializeComment(response);
break;
case 'error':
new messagebox(MB_OK|MB_ICONSTOP, ( response.title ? response.title : 'Error fetching comment data' ), response.error);
break;
default:
alert(ajax.responseText);
break;
}
}
});
}
function renderComments(data)
{
var html = '';
// Header
html += '<h3>Article Comments</h3>';
var ns = ( strToPageID(title)[1]=='Article' ) ? 'article' : ( strToPageID(title)[1].toLowerCase() ) + ' page';
// Counters
if ( data.auth_mod_comments )
{
var cnt = ( data.auth_mod_comments ) ? data.count_total : data.count_appr;
if ( cnt == 0 ) cnt = 'no';
var s = ( cnt == 1 ) ? '' : 's';
var is = ( cnt == 1 ) ? 'is' : 'are';
html += "<p id=\"comment_status\">There "+is+" " + cnt + " comment"+s+" on this "+ns+".";
if ( data.count_unappr > 0 )
{
html += ' <span style="color: #D84308">' + data.count_unappr + ' of those are unapproved.</span>';
}
html += '</p>';
}
else
{
var cnt = data.count_appr;
if ( cnt == 0 ) cnt = 'no';
var s = ( cnt == 1 ) ? '' : 's';
var is = ( cnt == 1 ) ? 'is' : 'are';
html += "<p id=\"comment_status\">There "+is+" " + cnt + " comment"+s+" on this "+ns+".";
if ( data.count_unappr > 0 )
{
var s = ( data.count_unappr == 1 ) ? '' : 's';
var is = ( data.count_unappr == 1 ) ? 'is' : 'are';
html += ' However, there '+is+' '+data.count_unappr+' additional comment'+s+' awaiting approval.';
}
html += '</p>';
}
// Comment display
if ( data.count_total > 0 )
{
comment_render_track = 0;
var commentpages = new paginator(data.comments, _render_comment, 0, 10, data);
html += commentpages.html;
}
if ( data.auth_post_comments )
{
// Posting form
html += '<h3>Got something to say?</h3>';
html += '<p>If you have comments or suggestions on this article, you can shout it out here.';
if ( data.approval_needed )
html+=' Before your post will be visible to the public, a moderator will have to approve it.';
html += ' <a id="leave_comment_button" href="#" onclick="displayCommentForm(); return false;">Leave a comment...</a></p>';
html += '<div id="comment_form" style="display: none;">';
html += ' <table border="0">';
html += ' <tr><td>Your name/screen name:</td><td>';
if ( data.user_id > 1 ) html += data.username + '<input id="commentform_name" type="hidden" value="'+data.username+'" size="40" />';
else html += '<input id="commentform_name" type="text" size="40" />';
html += ' </td></tr>';
html += ' <tr><td>Comment subject:</td><td><input id="commentform_subject" type="text" size="40" /></td></tr>';
html += ' <tr><td>Comment:</td><td><textarea id="commentform_message" rows="15" cols="50"></textarea></td></tr>';
if ( !data.logged_in && data.guest_posting == '1' )
{
html += ' <tr><td>Visual confirmation:<br /><small>Please enter the confirmation code seen in the image on the right into the box. If you cannot read the code, please click on the image to generate a new one. This helps to prevent automated bot posting.</small></td><td>';
html += ' <img alt="CAPTCHA image" src="'+makeUrlNS('Special', 'Captcha/' + data.captcha)+'" onclick="this.src=\''+makeUrlNS('Special', 'Captcha/' + data.captcha)+'/\'+Math.floor(Math.random()*10000000);" style="cursor: pointer;" /><br />';
html += ' Confirmation code: <input type="text" size="8" id="commentform_captcha" />';
html += ' <!-- This input is used to track the ID of the CAPTCHA image --> <input type="hidden" id="commentform_captcha_id" value="'+data.captcha+'" />';
html += ' </td></tr>';
}
html += ' <tr><td colspan="2" style="text-align: center;"><input type="button" onclick="submitComment();" value="Submit comment" /></td></tr>';
html += ' </table>';
html += '</div>';
}
document.getElementById('ajaxEditContainer').innerHTML = html;
for ( i = 0; i < data.comments.length; i++ )
{
document.getElementById('comment_source_'+i).value = data.comments[i].comment_source;
}
}
var _render_comment = function(this_comment, data)
{
var i = comment_render_track;
comment_render_track++;
var parser = new templateParser(comment_template);
var tplvars = new Object();
if ( this_comment.approved != '1' && !data.auth_mod_comments )
return '';
tplvars.ID = i;
tplvars.DATETIME = this_comment.time;
tplvars.SUBJECT = this_comment.subject;
tplvars.DATA = this_comment.comment_data;
tplvars.SIGNATURE = this_comment.signature;
if ( this_comment.approved != '1' )
tplvars.SUBJECT += ' <span style="color: #D84308">(Unapproved)</span>';
// Name
tplvars.NAME = this_comment.name;
if ( this_comment.user_id > 1 )
tplvars.NAME = '<a href="' + makeUrlNS('User', this_comment.name) + '">' + this_comment.name + '</a>';
// User level
tplvars.USER_LEVEL = 'Guest';
if ( this_comment.user_level >= data.user_level.member ) tplvars.USER_LEVEL = 'Member';
if ( this_comment.user_level >= data.user_level.mod ) tplvars.USER_LEVEL = 'Moderator';
if ( this_comment.user_level >= data.user_level.admin ) tplvars.USER_LEVEL = 'Administrator';
// Send PM link
tplvars.SEND_PM_LINK=(this_comment.user_id>1 && data.logged_in)?'<a onclick="window.open(this.href); return false;" href="'+ makeUrlNS('Special', 'PrivateMessages/Compose/To/' + ( this_comment.name.replace(/ /g, '_') )) +'">Send private message</a><br />':'';
// Add buddy link
tplvars.ADD_BUDDY_LINK=(this_comment.user_id>1 && data.logged_in && this_comment.is_buddy != 1)?'<a onclick="window.open(this.href); return false;" href="'+ makeUrlNS('Special', 'PrivateMessages/FriendList/Add/' + ( this_comment.name.replace(/ /g, '_') )) +'">Add to buddy list</a><br />':'';
// Edit link
tplvars.EDIT_LINK='<a href="#edit_'+i+'" onclick="editComment(\''+i+'\', this); return false;" id="cmteditlink_'+i+'">edit</a>';
// Delete link
tplvars.DELETE_LINK='<a href="#delete_'+i+'" onclick="deleteComment(\''+i+'\'); return false;">delete</a>';
// Moderation: (Un)approve link
var appr = ( this_comment.approved == 1 ) ? 'Unapprove' : 'Approve';
tplvars.MOD_APPROVE_LINK='<a href="#approve_'+i+'" id="comment_approve_'+i+'" onclick="approveComment(\''+i+'\'); return false;">'+appr+'</a>';
// Moderation: Delete post link
tplvars.MOD_DELETE_LINK='<a href="#mod_del_'+i+'" onclick="deleteComment(\''+i+'\'); return false;">Delete</a>';
var tplbool = new Object();
tplbool.signature = ( this_comment.signature == '' ) ? false : true;
tplbool.can_edit = ( data.auth_edit_comments && ( ( this_comment.user_id == data.user_id && data.logged_in ) || data.auth_mod_comments ) );
tplbool.auth_mod = data.auth_mod_comments;
tplbool.is_friend = ( this_comment.is_buddy == 1 && this_comment.is_friend == 1 );
tplbool.is_foe = ( this_comment.is_buddy == 1 && this_comment.is_friend == 0 );
if ( tplbool.is_friend )
tplvars.USER_LEVEL += '<br /><b>On your friend list</b>';
else if ( tplbool.is_foe )
tplvars.USER_LEVEL += '<br /><b>On your foe list</b>';
parser.assign_vars(tplvars);
parser.assign_bool(tplbool);
return '<div id="comment_holder_' + i + '"><input type="hidden" value="'+this_comment.comment_id+'" /><input type="hidden" id="comment_source_'+i+'" />' + parser.run() + '</div>';
}
function displayCommentForm()
{
document.getElementById('leave_comment_button').style.display = 'none';
document.getElementById('comment_form').style.display = 'block';
}
function hideCommentForm()
{
document.getElementById('leave_comment_button').style.display = 'inline';
document.getElementById('comment_form').style.display = 'none';
}
function editComment(id, link)
{
var ctr = document.getElementById('subject_'+id);
var subj = ( ctr.firstChild ) ? trim(ctr.firstChild.nodeValue) : ''; // If there's a span in there that says 'unapproved', this eliminates it
ctr.innerHTML = '';
var ipt = document.createElement('input');
ipt.id = 'subject_edit_'+id;
ipt.value = subj;
ctr.appendChild(ipt);
var src = document.getElementById('comment_source_'+id).value;
var cmt = document.getElementById('comment_'+id);
cmt.innerHTML = '';
var ta = document.createElement('textarea');
ta.rows = '10';
ta.cols = '40';
ta.value = src;
ta.id = 'comment_edit_'+id;
cmt.appendChild(ta);
link.style.fontWeight = 'bold';
link.innerHTML = 'save';
link.onclick = function() { var id = this.id.substr(this.id.indexOf('_')+1); saveComment(id, this); return false; };
}
function saveComment(id, link)
{
var data = document.getElementById('comment_edit_'+id).value;
var subj = document.getElementById('subject_edit_'+id).value;
var div = document.getElementById('comment_holder_'+id);
var real_id = div.getElementsByTagName('input')[0]['value'];
var req = {
'mode' : 'edit',
'id' : real_id,
'local_id' : id,
'data' : data,
'subj' : subj
};
link.style.fontWeight = 'normal';
link.innerHTML = 'edit';
link.onclick = function() { var id = this.id.substr(this.id.indexOf('_')+1); editComment(id, this); return false; };
ajaxComments(req);
}
function deleteComment(id)
{
if ( !shift )
{
var c = confirm('Do you really want to delete this comment?');
if(!c)
return false;
}
var div = document.getElementById('comment_holder_'+id);
var real_id = div.getElementsByTagName('input')[0]['value'];
var req = {
'mode' : 'delete',
'id' : real_id,
'local_id' : id
};
ajaxComments(req);
}
function submitComment()
{
var name = document.getElementById('commentform_name').value;
var subj = document.getElementById('commentform_subject').value;
var text = document.getElementById('commentform_message').value;
if ( document.getElementById('commentform_captcha') )
{
var captcha_code = document.getElementById('commentform_captcha').value;
var captcha_id = document.getElementById('commentform_captcha_id').value;
}
else
{
var captcha_code = '';
var captcha_id = '';
}
if ( subj == '' )
{
new messagebox(MB_OK|MB_ICONSTOP, 'Input validation failed', 'Please enter a subject for your comment.');
return false;
}
if ( text == '' )
{
new messagebox(MB_OK|MB_ICONSTOP, 'Input validation failed', 'Please enter some text for the body of your comment .');
return false;
}
var req = {
'mode' : 'submit',
'name' : name,
'subj' : subj,
'text' : text,
'captcha_code' : captcha_code,
'captcha_id' : captcha_id
};
ajaxComments(req);
}
function redrawComment(data)
{
if ( data.subj )
{
document.getElementById('subject_' + data.id).innerHTML = data.subj;
}
if ( data.approved && data.approved != '1' )
{
document.getElementById('subject_' + data.id).innerHTML += ' <span style="color: #D84308">(Unapproved)</span>';
}
if ( data.approved && ( typeof(data.approve_updated) == 'string' && data.approve_updated == 'yes' ) )
{
var appr = ( data.approved == '1' ) ? 'Unapprove' : 'Approve';
document.getElementById('comment_approve_'+data.id).innerHTML = appr;
// Update approval status
var p = document.getElementById('comment_status');
var count = p.firstChild.nodeValue.split(' ')[2];
if ( p.firstChild.nextSibling )
{
var span = p.firstChild.nextSibling;
var is = ( data.approved == '1' ) ? -1 : 1;
var n_unapp = parseInt(span.firstChild.nodeValue.split(' ')[0]) + is;
n_unapp = n_unapp + '';
}
else
{
var span = document.createElement('span');
p.innerHTML += ' ';
span.innerHTML = ' ';
span.style.color = '#D84308';
var n_unapp = '1';
p.appendChild(span);
}
span.innerHTML = n_unapp + ' of those are unapproved.';
if ( n_unapp == '0' )
p.removeChild(span);
}
if ( data.text )
{
document.getElementById('comment_' + data.id).innerHTML = data.text;
}
if ( data.src )
{
document.getElementById('comment_source_' + data.id).value = data.src;
}
}
function approveComment(id)
{
var div = document.getElementById('comment_holder_'+id);
var real_id = div.getElementsByTagName('input')[0]['value'];
var req = {
'mode' : 'approve',
'id' : real_id,
'local_id' : id
};
ajaxComments(req);
}
// Does the actual DOM object removal
function annihiliateComment(id) // Did I spell that right?
{
// Approved?
var p = document.getElementById('comment_status');
if(document.getElementById('comment_approve_'+id))
{
var appr = document.getElementById('comment_approve_'+id).firstChild.nodeValue;
if ( p.firstChild.nextSibling && appr == 'Approve' )
{
var span = p.firstChild.nextSibling;
var t = span.firstChild.nodeValue;
var n_unapp = ( parseInt(t.split(' ')[0]) ) - 1;
if ( n_unapp == 0 )
p.removeChild(span);
else
span.firstChild.nodeValue = n_unapp + t.substr(t.indexOf(' '));
}
}
var div = document.getElementById('comment_holder_'+id);
div.parentNode.removeChild(div);
var t = p.firstChild.nodeValue.split(' ');
t[2] = ( parseInt(t[2]) - 1 ) + '';
delete(t.toJSONString);
if ( t[2] == '1' )
{
t[1] = 'is';
t[3] = 'comment';
}
else
{
t[1] = 'are';
t[3] = 'comments';
}
t = implode(' ', t);
p.firstChild.nodeValue = t;
}
function materializeComment(data)
{
// Intelligently get an ID
var i = 0;
var brother;
while ( true )
{
var x = document.getElementById('comment_holder_'+i);
if(!x)
break;
brother = x;
i++;
}
var parser = new templateParser(comment_template);
var tplvars = new Object();
if ( data.approved != '1' && !data.auth_mod_comments )
return false;
tplvars.ID = i;
tplvars.DATETIME = data.time;
tplvars.SUBJECT = data.subject;
tplvars.DATA = data.comment_data;
tplvars.SIGNATURE = data.signature;
tplvars.NAME = data.name;
if ( data.user_id > 1 )
tplvars.NAME = '<a href="' + makeUrlNS('User', data.name) + '">' + data.name + '</a>';
if ( data.approved != '1' )
tplvars.SUBJECT += ' <span style="color: #D84308">(Unapproved)</span>';
// User level
tplvars.USER_LEVEL = 'Guest';
if ( data.user_level >= data.user_level_list.member ) tplvars.USER_LEVEL = 'Member';
if ( data.user_level >= data.user_level_list.mod ) tplvars.USER_LEVEL = 'Moderator';
if ( data.user_level >= data.user_level_list.admin ) tplvars.USER_LEVEL = 'Administrator';
// Send PM link
tplvars.SEND_PM_LINK=(data.user_id>1)?'<a onclick="window.open(this.href); return false;" href="'+ makeUrlNS('Special', 'PrivateMessages/Compose/To/' + ( data.name.replace(/ /g, '_') )) +'">Send private message</a><br />':'';
// Add buddy link
tplvars.ADD_BUDDY_LINK=(data.user_id>1)?'<a onclick="window.open(this.href); return false;" href="'+ makeUrlNS('Special', 'PrivateMessages/FriendList/Add/' + ( data.name.replace(/ /g, '_') )) +'">Add to buddy list</a><br />':'';
// Edit link
tplvars.EDIT_LINK='<a href="#edit_'+i+'" onclick="editComment(\''+i+'\', this); return false;" id="cmteditlink_'+i+'">edit</a>';
// Delete link
tplvars.DELETE_LINK='<a href="#delete_'+i+'" onclick="deleteComment(\''+i+'\'); return false;">delete</a>';
// Moderation: (Un)approve link
var appr = ( data.approved == 1 ) ? 'Unapprove' : 'Approve';
tplvars.MOD_APPROVE_LINK='<a href="#approve_'+i+'" id="comment_approve_'+i+'" onclick="approveComment(\''+i+'\'); return false;">'+appr+'</a>';
// Moderation: Delete post link
tplvars.MOD_DELETE_LINK='<a href="#mod_del_'+i+'" onclick="deleteComment(\''+i+'\'); return false;">Delete</a>';
var tplbool = new Object();
tplbool.signature = ( data.signature == '' ) ? false : true;
tplbool.can_edit = ( data.auth_edit_comments && ( ( data.user_id == data.user_id && data.logged_in ) || data.auth_mod_comments ) );
tplbool.auth_mod = data.auth_mod_comments;
parser.assign_vars(tplvars);
parser.assign_bool(tplbool);
var div = document.createElement('div');
div.id = 'comment_holder_'+i;
div.innerHTML = '<input type="hidden" value="'+data.comment_id+'" /><input type="hidden" id="comment_source_'+i+'" />' + parser.run();
if ( brother )
{
brother.parentNode.insertBefore(div, brother.nextSibling);
}
else
{
// No comments in ajaxEditContainer, insert it after the header
var aec = document.getElementById("ajaxEditContainer");
aec.insertBefore(div, aec.firstChild.nextSibling.nextSibling);
}
document.getElementById('comment_source_'+i).value = data.comment_source;
var p = document.getElementById('comment_status');
var t = p.firstChild.nodeValue.split(' ');
var n = ( isNaN(parseInt(t[2])) ) ? 0 : parseInt(t[2]);
t[2] = ( n + 1 ) + '';
delete(t.toJSONString);
if ( t[2] == '1' )
{
t[1] = 'is';
t[3] = 'comment';
}
else
{
t[1] = 'are';
t[3] = 'comments';
}
t = implode(' ', t);
p.firstChild.nodeValue = t;
if(document.getElementById('comment_approve_'+i))
{
var appr = document.getElementById('comment_approve_'+i).firstChild.nodeValue;
if ( p.firstChild.nextSibling && appr == 'Approve' )
{
var span = p.firstChild.nextSibling;
var t = span.firstChild.nodeValue;
var n_unapp = ( parseInt(t.split(' ')[0]) ) - 1;
if ( n_unapp == 0 )
p.removeChild(span);
else
span.firstChild.nodeValue = n_unapp + t.substr(t.indexOf(' '));
}
else if ( appr == 'Approve' && !p.firstChild.nextSibling )
{
var span = document.createElement('span');
p.innerHTML += ' ';
span.innerHTML = '1 of those are unapproved.';
span.style.color = '#D84308';
var n_unapp = '1';
p.appendChild(span);
}
}
}
function htmlspecialchars(text)
{
text = text.replace(/</g, '<');
text = text.replace(/>/g, '>');
return text;
}
// Equivalent to PHP trim() function
function trim(text)
{
text = text.replace(/^([\s]+)/, '');
text = text.replace(/([\s]+)$/, '');
return text;
}
// Equivalent to PHP implode() function
function implode(chr, arr)
{
if ( typeof ( arr.toJSONString ) == 'function' )
delete(arr.toJSONString);
var ret = '';
var c = 0;
for ( var i in arr )
{
if(i=='toJSONString')continue;
if ( c > 0 )
ret += chr;
ret += arr[i];
c++;
}
return ret;
}
function nl2br(text)
{
var regex = new RegExp(unescape('%0A'), 'g');
return text.replace(regex, '<br />' + unescape('%0A'));
}