includes/clientside/static/comments.js
author Dan
Fri, 05 Oct 2007 01:57:00 -0400
changeset 162 e1a22031b5bd
parent 108 1c7f59df9474
child 212 30b857a6b811
permissions -rw-r--r--
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, '&lt;');
  text = text.replace(/>/g, '&gt;');
  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'));
}