Added ability to delete the draft revision; [SECURITY] fixed lack of permission check on draft save; renamed messagebox() constructor to MessageBox() (backward compat. maintained)
authorDan
Sun, 11 May 2008 16:58:58 -0400
changeset 550 685e839d934e
parent 549 6894cfd94dfb
child 551 3acd624d4f4f
Added ability to delete the draft revision; [SECURITY] fixed lack of permission check on draft save; renamed messagebox() constructor to MessageBox() (backward compat. maintained)
ajax.php
includes/clientside/static/ajax.js
includes/clientside/static/autocomplete.js
includes/clientside/static/autofill.js
includes/clientside/static/comments.js
includes/clientside/static/editor.js
includes/clientside/static/faders.js
includes/clientside/static/login.js
includes/clientside/static/misc.js
includes/clientside/static/paginate.js
language/english/admin.json
language/english/core.json
--- a/ajax.php	Wed May 07 14:06:16 2008 -0400
+++ b/ajax.php	Sun May 11 16:58:58 2008 -0400
@@ -246,11 +246,11 @@
     case "savepage_json":
       header('Content-type: application/json');
       if ( !isset($_POST['r']) )
-        die('Invalid request [1]');
+        die('Invalid request');
       
       $request = enano_json_decode($_POST['r']);
       if ( !isset($request['src']) || !isset($request['summary']) || !isset($request['minor_edit']) || !isset($request['time']) || !isset($request['draft']) )
-        die('Invalid request [2]<pre>' . htmlspecialchars(print_r($request, true)) . '</pre>');
+        die('Invalid request');
       
       $time = intval($request['time']);
       
@@ -260,35 +260,57 @@
         // The user wants to save a draft version of the page.
         //
         
-        // Delete any draft copies if they exist
-        $q = $db->sql_query('DELETE FROM ' . table_prefix . 'logs WHERE log_type = \'page\' AND action = \'edit\'
-                               AND page_id = \'' . $db->escape($paths->page_id) . '\'
-                               AND namespace = \'' . $db->escape($paths->namespace) . '\'
-                               AND is_draft = 1;');
-        if ( !$q )
-          $db->die_json();
-        
-        $src = RenderMan::preprocess_text($request['src'], false, false);
-        
-        // Save the draft
-        $q = $db->sql_query('INSERT INTO ' . table_prefix . 'logs ( log_type, action, page_id, namespace, author, edit_summary, page_text, is_draft, time_id )
-                               VALUES (
-                                 \'page\',
-                                 \'edit\',
-                                 \'' . $db->escape($paths->page_id) . '\',
-                                 \'' . $db->escape($paths->namespace) . '\',
-                                 \'' . $db->escape($session->username) . '\',
-                                 \'' . $db->escape($request['summary']) . '\',
-                                 \'' . $db->escape($src) . '\',
-                                 1,
-                                 ' . time() . '
-                               );');
-        
-        // Done!
-        $return = array(
-            'mode' => 'success',
-            'is_draft' => true
+        // Validate permissions
+        if ( !$session->get_permissions('edit_page') )
+        {
+          $return = array(
+            'mode' => 'error',
+            'error' => 'access_denied'
           );
+        }
+        else
+        {
+          // Delete any draft copies if they exist
+          $q = $db->sql_query('DELETE FROM ' . table_prefix . 'logs WHERE log_type = \'page\' AND action = \'edit\'
+                                 AND page_id = \'' . $db->escape($paths->page_id) . '\'
+                                 AND namespace = \'' . $db->escape($paths->namespace) . '\'
+                                 AND is_draft = 1;');
+          if ( !$q )
+            $db->die_json();
+          
+          // are we just supposed to delete the draft?
+          if ( $request['src'] === -1 )
+          {
+            $return = array(
+              'mode' => 'success',
+              'is_draft' => 'delete'
+            );
+          }
+          else
+          {
+            $src = RenderMan::preprocess_text($request['src'], false, false);
+            
+            // Save the draft
+            $q = $db->sql_query('INSERT INTO ' . table_prefix . 'logs ( log_type, action, page_id, namespace, author, edit_summary, page_text, is_draft, time_id )
+                                   VALUES (
+                                     \'page\',
+                                     \'edit\',
+                                     \'' . $db->escape($paths->page_id) . '\',
+                                     \'' . $db->escape($paths->namespace) . '\',
+                                     \'' . $db->escape($session->username) . '\',
+                                     \'' . $db->escape($request['summary']) . '\',
+                                     \'' . $db->escape($src) . '\',
+                                     1,
+                                     ' . time() . '
+                                   );');
+            
+            // Done!
+            $return = array(
+                'mode' => 'success',
+                'is_draft' => true
+              );
+          }
+        }
       }
       else
       {
--- a/includes/clientside/static/ajax.js	Wed May 07 14:06:16 2008 -0400
+++ b/includes/clientside/static/ajax.js	Sun May 11 16:58:58 2008 -0400
@@ -133,7 +133,7 @@
   a.id = 'invalidjson_link';
   a.onclick = function()
   {
-    var mb = new messagebox(MB_YESNO | MB_ICONEXCLAMATION, 'Do you really want to view this response as HTML?', 'If the response was changed during transmission to include malicious code, you may be allowing that malicious code to run by viewing the response as HTML. Only do this if you have reviewed the response text and have found no suspicious code in it.');
+    var mb = new MessageBox(MB_YESNO | MB_ICONEXCLAMATION, 'Do you really want to view this response as HTML?', 'If the response was changed during transmission to include malicious code, you may be allowing that malicious code to run by viewing the response as HTML. Only do this if you have reviewed the response text and have found no suspicious code in it.');
     mb.onclick['Yes'] = function()
     {
       var html = $dynano('invalidjson_link').object._resp;
@@ -351,7 +351,7 @@
         {
           miniPromptDestroy(box, true);
           ajaxRenameDoClientTransform(newname);
-          new messagebox( MB_OK|MB_ICONINFORMATION, $lang.get('ajax_rename_success_title'), $lang.get('ajax_rename_success_body', { page_name_new: newname }) );
+          new MessageBox( MB_OK|MB_ICONINFORMATION, $lang.get('ajax_rename_success_title'), $lang.get('ajax_rename_success_body', { page_name_new: newname }) );
           mb_previously_had_darkener = false;
         }
         else
@@ -770,7 +770,7 @@
   inner_html +=      ENANO_THEME_LIST;
   inner_html += '  </select>';
   inner_html += '</label></p>';
-  var chtheme_mb = new messagebox(MB_OKCANCEL|MB_ICONQUESTION, $lang.get('ajax_changestyle_title'), inner_html);
+  var chtheme_mb = new MessageBox(MB_OKCANCEL|MB_ICONQUESTION, $lang.get('ajax_changestyle_title'), inner_html);
   chtheme_mb.onbeforeclick['OK'] = ajaxChangeStyleComplete;
 }
 
@@ -1333,7 +1333,7 @@
 
 function aboutKeepAlive()
 {
-  new messagebox(MB_OK|MB_ICONINFORMATION, $lang.get('user_keepalive_info_title'), $lang.get('user_keepalive_info_body'));
+  new MessageBox(MB_OK|MB_ICONINFORMATION, $lang.get('user_keepalive_info_title'), $lang.get('user_keepalive_info_body'));
 }
 
 function ajaxShowCaptcha(code)
--- a/includes/clientside/static/autocomplete.js	Wed May 07 14:06:16 2008 -0400
+++ b/includes/clientside/static/autocomplete.js	Sun May 11 16:58:58 2008 -0400
@@ -164,7 +164,7 @@
         var response = String(ajax.responseText) + ' ';
         if ( response.substr(0,1) != '{' )
         {
-          new messagebox(MB_OK|MB_ICONSTOP, 'Invalid response', 'Invalid or unexpected JSON response from server:<pre>' + ajax.responseText + '</pre>');
+          new MessageBox(MB_OK|MB_ICONSTOP, 'Invalid response', 'Invalid or unexpected JSON response from server:<pre>' + ajax.responseText + '</pre>');
           return false;
         }
         
--- a/includes/clientside/static/autofill.js	Wed May 07 14:06:16 2008 -0400
+++ b/includes/clientside/static/autofill.js	Sun May 11 16:58:58 2008 -0400
@@ -190,7 +190,7 @@
         var response = String(ajax.responseText) + ' ';
         if ( response.substr(0,1) != '{' )
         {
-          new messagebox(MB_OK|MB_ICONSTOP, 'Invalid response', 'Invalid or unexpected JSON response from server:<pre>' + ajax.responseText + '</pre>');
+          new MessageBox(MB_OK|MB_ICONSTOP, 'Invalid response', 'Invalid or unexpected JSON response from server:<pre>' + ajax.responseText + '</pre>');
           return false;
         }
         if ( $dynano(afobj.field_id).object.value.length < 3 )
--- a/includes/clientside/static/comments.js	Wed May 07 14:06:16 2008 -0400
+++ b/includes/clientside/static/comments.js	Sun May 11 16:58:58 2008 -0400
@@ -53,7 +53,7 @@
           materializeComment(response);
           break;
         case 'error':
-          new messagebox(MB_OK|MB_ICONSTOP, ( response.title ? response.title : 'Error fetching comment data' ), response.error);
+          new MessageBox(MB_OK|MB_ICONSTOP, ( response.title ? response.title : 'Error fetching comment data' ), response.error);
           break;
         default:
           alert(ajax.responseText);
@@ -346,12 +346,12 @@
   }
   if ( subj == '' )
   {
-    new messagebox(MB_OK|MB_ICONSTOP, 'Input validation failed', 'Please enter a subject for your comment.');
+    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 .');
+    new MessageBox(MB_OK|MB_ICONSTOP, 'Input validation failed', 'Please enter some text for the body of your comment .');
     return false;
   }
   var req = {
--- a/includes/clientside/static/editor.js	Wed May 07 14:06:16 2008 -0400
+++ b/includes/clientside/static/editor.js	Sun May 11 16:58:58 2008 -0400
@@ -71,7 +71,8 @@
       tinymce_initted = true;
     }
   }
-}
+};
+
 // Safari doesn't fire the init on demand so call it on page load
 if ( is_Safari )
 {
@@ -587,11 +588,11 @@
   }
 }
 
-function ajaxEditorSave(is_draft)
+function ajaxEditorSave(is_draft, text_override)
 {
   if ( !is_draft )
     ajaxSetEditorLoading();
-  var ta_content = $dynano('ajaxEditArea').getContent();
+  var ta_content = ( text_override ) ? text_override : $dynano('ajaxEditArea').getContent();
   
   if ( !is_draft && ( ta_content == '' || ta_content == '<p></p>' || ta_content == '<p>&nbsp;</p>' ) )
   {
@@ -701,13 +702,27 @@
             document.getElementById('ajaxEditArea').needReset = true;
             var img = $dynano('ajax_edit_savedraft_btn').object.getElementsByTagName('img')[0];
             var lbl = $dynano('ajax_edit_savedraft_btn').object.getElementsByTagName('span')[0];
-            img.src = scriptPath + '/images/mini-info.png';
-            var d = new Date();
-            var m = String(d.getMinutes());
-            if ( m.length < 2 )
-              m = '0' + m;
-            var time = d.getHours() + ':' + m;
-            lbl.innerHTML = $lang.get('editor_msg_draft_saved', { time: time });
+            if ( response.is_draft == 'delete' )
+            {
+              img.src = scriptPath + '/images/editor/savedraft.gif';
+              lbl.innerHTML = $lang.get('editor_btn_savedraft');
+              
+              var dn = $dynano('ajax_edit_draft_notice').object;
+              if ( dn )
+              {
+                dn.parentNode.removeChild(dn);
+              }
+            }
+            else
+            {
+              img.src = scriptPath + '/images/mini-info.png';
+              var d = new Date();
+              var m = String(d.getMinutes());
+              if ( m.length < 2 )
+                m = '0' + m;
+              var time = d.getHours() + ':' + m;
+              lbl.innerHTML = $lang.get('editor_msg_draft_saved', { time: time });
+            }
           }
           else
           {
@@ -736,6 +751,39 @@
     }, true);
 }
 
+// Delete the draft (this is a massive server-side hack)
+function ajaxEditorDeleteDraft()
+{
+  miniPromptMessage({
+      title: $lang.get('editor_msg_confirm_delete_draft_title'),
+      message: $lang.get('editor_msg_confirm_delete_draft_body'),
+      buttons: [
+          {
+            text: $lang.get('editor_btn_delete_draft'),
+            color: 'red',
+            style: {
+              fontWeight: 'bold'
+            },
+            onclick: function() {
+              ajaxEditorDeleteDraftReal();
+              miniPromptDestroy(this);
+            }
+          },
+          {
+            text: $lang.get('etc_cancel'),
+            onclick: function() {
+              miniPromptDestroy(this);
+            }
+          }
+        ]
+    });
+}
+
+function ajaxEditorDeleteDraftReal()
+{
+  return ajaxEditorSave(true, -1);
+}
+
 function ajaxEditorGenPreview()
 {
   ajaxSetEditorLoading();
--- a/includes/clientside/static/faders.js	Wed May 07 14:06:16 2008 -0400
+++ b/includes/clientside/static/faders.js	Sun May 11 16:58:58 2008 -0400
@@ -121,7 +121,7 @@
  * Methods:
  *   destroy: kills the running message box
  * Example:
- *   var my_message = new messagebox(MB_OK|MB_ICONSTOP, 'Error logging in', 'The username and/or password is incorrect. Please check the username and retype your password');
+ *   var my_message = new MessageBox(MB_OK|MB_ICONSTOP, 'Error logging in', 'The username and/or password is incorrect. Please check the username and retype your password');
  *   my_message.onclick['OK'] = function() {
  *       document.getElementById('password').value = '';
  *     };
@@ -135,7 +135,7 @@
 var mb_current_obj;
 var mb_previously_had_darkener = false;
 
-function messagebox(type, title, message)
+function MessageBox(type, title, message)
 {
   var y = getScrollOffset();
   
@@ -360,6 +360,8 @@
   mb_current_obj = this;
 }
 
+var messagebox = MessageBox;
+
 function mb_runFlyIn()
 {
   var mydiv = document.getElementById('messageBox');
@@ -405,7 +407,7 @@
 
 function testMessageBox()
 {
-  mb = new messagebox(MB_OKCANCEL|MB_ICONINFORMATION, 'Javascripted dynamic message boxes', 'This is soooooo coool, now if only document.createElement() worked in IE!<br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text');
+  mb = new MessageBox(MB_OKCANCEL|MB_ICONINFORMATION, 'Javascripted dynamic message boxes', 'This is soooooo coool, now if only document.createElement() worked in IE!<br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text');
   mb.onclick['OK'] = function()
     {
       alert('You clicked OK!');
@@ -762,7 +764,7 @@
 
 function mb_logout()
 {
-  var mb = new messagebox(MB_YESNO|MB_ICONQUESTION, $lang.get('user_logout_confirm_title'), $lang.get('user_logout_confirm_body'));
+  var mb = new MessageBox(MB_YESNO|MB_ICONQUESTION, $lang.get('user_logout_confirm_title'), $lang.get('user_logout_confirm_body'));
   mb.onclick['Yes'] = function()
     {
       window.location = makeUrlNS('Special', 'Logout/' + title);
--- a/includes/clientside/static/login.js	Wed May 07 14:06:16 2008 -0400
+++ b/includes/clientside/static/login.js	Sun May 11 16:58:58 2008 -0400
@@ -93,7 +93,7 @@
   logindata = {};
   
   var title = ( user_level > USER_LEVEL_MEMBER ) ? $lang.get('user_login_ajax_prompt_title_elev') : $lang.get('user_login_ajax_prompt_title');
-  logindata.mb_object = new messagebox(MB_OKCANCEL | MB_ICONLOCK, title, '');
+  logindata.mb_object = new MessageBox(MB_OKCANCEL | MB_ICONLOCK, title, '');
   
   logindata.mb_object.onclick['Cancel'] = function()
   {
@@ -338,7 +338,7 @@
   {
     logindata.mb_object.destroy();
     var error_msg = $lang.get('user_' + ( response.error.toLowerCase() ));
-    new messagebox(MB_ICONSTOP | MB_OK, $lang.get('user_err_login_generic_title'), error_msg);
+    new MessageBox(MB_ICONSTOP | MB_OK, $lang.get('user_err_login_generic_title'), error_msg);
     return false;
   }
   // Main mode switch
--- a/includes/clientside/static/misc.js	Wed May 07 14:06:16 2008 -0400
+++ b/includes/clientside/static/misc.js	Sun May 11 16:58:58 2008 -0400
@@ -436,10 +436,10 @@
 
   for ( var i = 0; i < pid_dirty.length; i++ )
   {
-    var char = pid_dirty[i];
-    if ( char == 'X' )
+    var chr = pid_dirty[i];
+    if ( chr == 'X' )
       continue;
-    var cid = char.charCodeAt(0);
+    var cid = chr.charCodeAt(0);
     cid = cid.toString(16).toUpperCase();
     if ( cid.length < 2 )
     {
@@ -456,9 +456,9 @@
 
   for ( var id in pid_chars )
   {
-    var char = pid_chars[id];
+    var chr = pid_chars[id];
     if ( pid_dirty[id] == 'X' )
-      page_id_cleaned += char;
+      page_id_cleaned += chr;
     else
       page_id_cleaned += pid_dirty[id];
   }
--- a/includes/clientside/static/paginate.js	Wed May 07 14:06:16 2008 -0400
+++ b/includes/clientside/static/paginate.js	Sun May 11 16:58:58 2008 -0400
@@ -319,7 +319,7 @@
   var offset = ( userinput - 1 ) * perpage;
   if ( userinput > max || isNaN(userinput) || userinput < 1 )
   {
-    new messagebox(MB_OK|MB_ICONSTOP, $lang.get('paginate_err_bad_page_title'), $lang.get('paginate_err_bad_page_body', { max: max }));
+    new MessageBox(MB_OK|MB_ICONSTOP, $lang.get('paginate_err_bad_page_title'), $lang.get('paginate_err_bad_page_body', { max: max }));
     return false;
   }
   if ( typeof(formatstring) == 'object' )
--- a/language/english/admin.json	Wed May 07 14:06:16 2008 -0400
+++ b/language/english/admin.json	Sun May 11 16:58:58 2008 -0400
@@ -14,7 +14,7 @@
   categories: [
     'meta', 'adm', 'acl', 'adminusers',
     'acphome', 'acpgc', 'acpup', 'acpft', 'acppl', 'acppm', 'acped', 'acpdb', 'acplm', 'acppg', 'acpum', 'acpug', 'acpcp', 'acpmm', 'acpsl',
-    'acpbc', 'acplo', 'acptm', 'sbedit',
+    'acpbc', 'acplo', 'acptm', 'acpur', 'sbedit',
   ],
   strings: {
     meta: {
@@ -32,6 +32,7 @@
       acptm: 'ACP: Theme manager',
       acppg: 'ACP: Page groups',
       acpum: 'ACP: User management',
+      acpur: 'ACP: User rank management',
       acpug: 'ACP: User group management',
       acpcp: 'ACP: COPPA support',
       acpmm: 'ACP: Mass e-mail',
@@ -67,6 +68,7 @@
       page_user_groups: 'Edit user groups',
       page_coppa: 'COPPA support',
       page_mass_email: 'Mass e-mail',
+      page_user_ranks: 'User ranks and titles',
       
       page_security_log: 'Security log',
       page_ban_control: 'Ban control',
@@ -853,6 +855,8 @@
       btn_send: 'Send message',
       msg_send_takeawhile: 'Please be warned: it may take a LONG time to send this message. <b>Please do not stop the script until the process is finished.</b>',
     },
+    acpur: {
+    },
     acpsl: {
       heading_main: 'System security log',
       col_type: 'Type',
--- a/language/english/core.json	Wed May 07 14:06:16 2008 -0400
+++ b/language/english/core.json	Sun May 11 16:58:58 2008 -0400
@@ -17,7 +17,7 @@
 
 var enano_lang = {
   categories: [
-    'meta', 'page', 'comment', 'onpage', 'etc', 'editor', 'history', 'catedit', 'tags', 'delvote', 'ajax', 'sidebar', 'perm', 'plugin', 'paginate', 'upload', 'tz',
+    'meta', 'page', 'comment', 'onpage', 'etc', 'editor', 'history', 'catedit', 'tags', 'delvote', 'ajax', 'sidebar', 'perm', 'plugin', 'paginate', 'upload', 'tz'
   ],
   strings: {
     meta: {
@@ -47,7 +47,7 @@
       enano_about_lbl_serverplatform: 'Server platform:',
       enano_about_lbl_phpversion: '<a href="http://www.php.net/">PHP</a> version:',
       enano_about_lbl_mysqlversion: '<a href="http://www.mysql.com/">MySQL</a> version:',
-      enano_about_lbl_pgsqlversion: '<a href="http://www.postgresql.org/">PostgreSQL</a> version:',
+      enano_about_lbl_pgsqlversion: '<a href="http://www.postgresql.org/">PostgreSQL</a> version:'
     },
     page: {
       sitedisabled_admin_msg_title: 'The site is currently disabled and thus is only accessible to administrators.',
@@ -180,7 +180,7 @@
                     
       autosuggest_heading: 'Page name matches',
       autosuggest_col_name: 'Page title',
-      autosuggest_col_page_id: 'Page ID',
+      autosuggest_col_page_id: 'Page ID'
     },
     comment: {
       lbl_subject: 'Subject',
@@ -230,7 +230,7 @@
       postform_btn_submit: 'Submit comment',
       
       on_friend_list: 'On your friend list',
-      on_foe_list: 'On your foe list',
+      on_foe_list: 'On your foe list'
     },
     onpage: {
       lbl_pagetools: 'Page tools',
@@ -308,7 +308,7 @@
       filebox_heading_history: 'File history',
       filebox_btn_this_version: 'this ver',
       filebox_btn_revert: 'restore',
-      filebox_btn_current: 'current',
+      filebox_btn_current: 'current'
     },
     editor: {
       err_server: 'There was a problem starting the editor',
@@ -342,7 +342,7 @@
       msg_diff_empty: 'There are no differences between the text in the editor and the current revision of the page.',
       msg_editing_old_revision: 'You are editing an old revision of this page. If you click Save, newer revisions of this page will be undone.',
       msg_have_draft_title: 'A draft copy of this page is available.',
-      msg_have_draft_body: '%author% saved a draft version of this page on %time%. You can <a href="#use_draft" onclick="ajaxEditorUseDraft(); return false;">use the draft copy</a>, or edit the current published version (below). If you edit the published version, the draft copy will remain available, but will not reflect your changes. It is recommended that you edit the draft version instead of editing the published version.',
+      msg_have_draft_body: '%author% saved a draft version of this page on %time%. You can <a href="#use_draft" onclick="ajaxEditorUseDraft(); return false;">use the draft copy</a>, or edit the current published version (below). If you edit the published version, the draft copy will remain available, but will not reflect your changes. It is recommended that you edit the draft version instead of editing the published version. You can also <a href="#delete_draft" onclick="ajaxEditorDeleteDraft(); return false;">discard the draft revision</a>.',
       btn_graphical: 'graphical editor',
       btn_wikitext: 'wikitext editor',
       lbl_edit_summary: 'Brief summary of your changes:',
@@ -363,11 +363,14 @@
       msg_save_success_title: 'Changes saved',
       msg_save_success_body: 'Your changes to this page have been saved. Redirecting...',
       reversion_edit_summary: 'Undid %undo_count% revision(s) by %current_author% to revision %last_rev_id% by %old_author%',
+      msg_confirm_delete_draft_title: 'Delete the draft revision?',
+      msg_confirm_delete_draft_body: 'This will discard the saved draft version of this page.',
+      btn_delete_draft: 'Delete draft',
       
       msg_captcha_pleaseenter: 'Please enter the code shown in the image to the right into the text box. This process helps to ensure that this page is not being edited by an automated bot. If the image to the right is illegible, you can regenerate it by clicking on the image (only works if your browser supports Javascript).',
       msg_captcha_blind: 'If you are visually impaired or otherwise cannot read the text shown to the right, please contact the site management and they will be able to make your requested edits.',
       lbl_field_captcha: 'Visual confirmation',
-      lbl_field_captcha_code: 'Code:',
+      lbl_field_captcha_code: 'Code:'
     },
     history: {
       summary_clearlogs: 'Automatic backup created when logs were purged',
@@ -402,7 +405,7 @@
       log_delete: 'Deleted page',
       log_uploadnew: 'Uploaded new file version',
       lbl_comparingrevisions: 'Comparing revisions:',
-      summary_none_given: 'No edit summary provided.',
+      summary_none_given: 'No edit summary provided.'
     },
     catedit: {
       title: 'Select which categories this page should be included in.',
@@ -410,7 +413,7 @@
       catbox_lbl_categories: 'Categories:',
       catbox_lbl_uncategorized: '(Uncategorized)',
       catbox_link_edit: 'edit categorization',
-      catbox_link_showcategorization: 'show page categorization',
+      catbox_link_showcategorization: 'show page categorization'
     },
     tags: {
       catbox_link: 'show page tags',
@@ -418,14 +421,14 @@
       lbl_no_tags: 'No tags on this page',
       btn_add_tag: '(add a tag)',
       lbl_add_tag: 'Add a tag:',
-      btn_add: '+ Add',
+      btn_add: '+ Add'
     },
     delvote: {
       lbl_votes_one: 'There is one user that thinks this page should be deleted.',
       lbl_votes_plural: 'There are %num_users% users that think this page should be deleted.',
       lbl_users_that_voted: 'Users that voted:',
       btn_deletepage: 'Delete page',
-      btn_resetvotes: 'Reset votes',
+      btn_resetvotes: 'Reset votes'
     },
     ajax: {
       // Client-side messages
@@ -465,7 +468,7 @@
       delvote_already_voted: 'It appears that you have already voted to have this page deleted.',
       delvote_reset_success: 'The number of votes for having this page deleted has been reset to zero.',
       password_success: 'The password for this page has been set.',
-      password_disable_success: 'The password for this page has been disabled.',
+      password_disable_success: 'The password for this page has been disabled.'
     },
     sidebar: {
       title_navigation: 'Navigation',
@@ -490,7 +493,7 @@
       btn_login: 'Log in',
       btn_logout: 'Log out',
       btn_changestyle: 'Change theme',
-      btn_recent_changes: 'Recent edits',
+      btn_recent_changes: 'Recent edits'
     },
     perm: {
       read: 'Read page(s)',
@@ -524,7 +527,7 @@
       html_in_pages: 'Embed unrestricted HTML in pages',
       php_in_pages: 'Embed PHP code in pages',
       custom_user_title: 'Use a custom user title',
-      edit_acl: 'Edit access control lists',
+      edit_acl: 'Edit access control lists'
     },
     plugin: {
       specialadmin_title: 'Runt - the Enano administration panel',
@@ -546,7 +549,7 @@
       specialuserprefs_title: 'User control panel',
       specialuserprefs_desc: 'Provides the page Special:Preferences.',
       specialrecentchanges_title: 'Recent changes interface',
-      specialrecentchanges_desc: 'Provides the page Special:RecentChanges, which is used to view recent modifications to pages on the site.',
+      specialrecentchanges_desc: 'Provides the page Special:RecentChanges, which is used to view recent modifications to pages on the site.'
     },
     paginate: {
       lbl_page: 'Page:',
@@ -556,7 +559,7 @@
       btn_next: 'Next',
       lbl_goto_page: 'Go to page:',
       err_bad_page_title: 'Invalid entry',
-      err_bad_page_body: 'Please enter a page number between 1 and %max%.',
+      err_bad_page_body: 'Please enter a page number between 1 and %max%.'
     },
     upload: {
       err_disabled_site: 'File uploads are disabled this website.',
@@ -584,7 +587,7 @@
       btn_upload: 'Upload file',
       
       err_not_found_title: 'File not found',
-      err_not_found_body: 'The file "%filename%" cannot be found.',
+      err_not_found_body: 'The file "%filename%" cannot be found.'
     },
     tz: {
       // Thanks to phpBB for this timezone data.
@@ -668,7 +671,7 @@
       title_13: '[UTC + 13] Tonga Time, Phoenix Islands Time',
       title_14: '[UTC + 14] Line Island Time',
       // This is a JSON string that lists all the timezones that are defined here.
-      list: '{"n12":-12,"n11":-11,"n10":-10,"n9p5":-9.5,"n9":-9,"n8":-8,"n7":-7,"n6":-6,"n5":-5,"n4":-4,"n3p5":-3.5,"n3":-3,"n2":-2,"n1":-1,"0":0,"1":1,"2":2,"3":3,"3p5":3.5,"4":4,"4p5":4.5,"5":5,"5p5":5.5,"5p75":5.75,"6":6,"6p5":6.5,"7":7,"8":8,"8p75":8.75,"9":9,"9p5":9.5,"10":10,"10p5":10.5,"11":11,"11p5":11.5,"12":12,"12p75":12.75,"13":13,"14":14}',
+      list: '{"n12":-12,"n11":-11,"n10":-10,"n9p5":-9.5,"n9":-9,"n8":-8,"n7":-7,"n6":-6,"n5":-5,"n4":-4,"n3p5":-3.5,"n3":-3,"n2":-2,"n1":-1,"0":0,"1":1,"2":2,"3":3,"3p5":3.5,"4":4,"4p5":4.5,"5":5,"5p5":5.5,"5p75":5.75,"6":6,"6p5":6.5,"7":7,"8":8,"8p75":8.75,"9":9,"9p5":9.5,"10":10,"10p5":10.5,"11":11,"11p5":11.5,"12":12,"12p75":12.75,"13":13,"14":14}'
     },
     etc: {
       redirect_title: 'Redirecting...',
@@ -710,7 +713,7 @@
       unit_gigabytes_short: 'GB',
       unit_terabytes_short: 'TB',
       unit_pixels: 'pixels',
-      unit_pixels_short: 'px',
+      unit_pixels_short: 'px'
     }
   }
 };