Extension:CCM

From MediaWiki.org

Jump to: navigation, search
If you need per-page or partial page access restrictions, you are advised to install an appropriate content management package. MediaWiki was not written to provide per-page access restrictions, and almost all hacks or patches promising to add them will likely have flaws somewhere, which could lead to exposure of confidential data. We are not responsible for anything being leaked, leading to loss of funds or one's job.
For further details, see Security issues with authorization extensions


         

Manual on MediaWiki Extensions
List of MediaWiki Extensions
Crystal Clear action run.png
CCM

Release status: experimental

Implementation  Special page
Description provides a collaborative content management system integrated into mediawiki
Author(s)  Artem Kaznatcheev (DFRussiaTalk)
Last Version  0.0.2 (2008-03-25)
MediaWiki  various
License No license specified
Download from page

check usage (experimental)

The goal of the CCM extension, is to provide a secure file sharing and collaboration tool inside of mediawiki. The basic principle is to have a specialpage that has a relatively secure JavaScript interface with the a part of the filesystem the wiki sits on. Certain users will be allowed to access this page, and inside the page they will be deligated into certain groups which limits the files they can access and how they can edit them. Currently the code is incomplete and unstable. I am developing on MediaWiki 1.10 and I do not recomend using it until it goes into beta.

Contents

[edit] Code

The code consists of 8 main files; CCM.php, CCM_Ajax.php and CCM_body.php are responsible for the server back-end; CCM.js provides the user interface functionality; CCM.css allows customization of the look and feel; dbConfig.sql sets up the needed database structure; and CCM.i18n.php is for future internationalization.

[edit] CCM.php

<?php
/*
 * CCM 0.0.2 - a collaborative content management system for MediaWiki
 * Copyright (C) 2008  Artem Kaznatcheev
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
# Not a valid entry point, skip unless MEDIAWIKI is defined
if (!defined('MEDIAWIKI')) {exit( 1 );}
 
### Bring in Special Page ### 
//main body that contains the special page
$wgAutoloadClasses['CCM'] = dirname(__FILE__) . '/CCM_body.php';
$wgSpecialPages['CCM'] = 'CCM';
$wgHooks['LoadAllMessages'][] = 'CCM::loadMessages';
 
### Define and Create CCMUser ###
/* hook needed to avoid trying to use database calls before 
 * GlobalFunctions.php is loaded */
$wgHooks['AuthPluginSetup'][] = 'efCCMClassLoad'; 
function efCCMClassLoad() {
  global $egCCMUser;
  $egCCMUser = new CCMUser();
}
class CCMUser {
  public $name; //user name
  public $id;   //user id
  public $currentGroup;
  public $groups = array();
  public $privelages = array();
 
  function groupVerify($group_id) {
    foreach ($this->groups as $g) {
        if ($g == $group_id) {
          return $g;
        }
      }
      return $this->currentGroup;
  }
 
  function __construct() {
    global $wgUser, $wgDBprefix;
 
    ### Set basics of $egCCMUser ###
    $this->id = $wgUser->getId();
    $this->name = $wgUser->getName();
 
    $dbr =& wfGetDB( DB_SLAVE ); //load the database object
    $res = $dbr->query("
      SELECT ccm_ug_group_id,ccm_ug_privelage FROM ".$wgDBprefix.
        "ccm_user_groups
        WHERE ccm_ug_user_id=".$this->id.";
    "); //SQL query for groups the user belongs to
 
    while ($row = $dbr->fetchObject( $res ) ){
      //add groups to array of user groups
      $this->groups[] = $row->ccm_ug_group_id;
      //create assosiative array of privelages
      $this->privelages[$row->ccm_ug_group_id] = $row->ccm_ug_privelage;
    }
    if ($this->groups){ 
      //set the current (default) group
      $this->currentGroup = $this->groups[0]; 
    }
 
    $dbr->close(); //close the database
  }
}
 
### Bring in AJAX ###
//file that contains AJAX functions
require_once(dirname(__FILE__) . '/CCM_Ajax.php');
$wgAjaxExportList[] = 'efCCM_Ajax'; //register AJAX function
 
?>

[edit] CCM_Ajax.php

<?php
/*
 * CCM 0.0.2 - a collaborative content management system for MediaWiki
 * Copyright (C) 2008  Artem Kaznatcheev
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
 
/* 
 * This is main AJAX function that is registered with MediaWiki and thus
 * serves as the entry point for AJAX. All AJAX requests for CCM go through
 * it
 */
function efCCM_Ajax($action, $group, $data, $new){
  global $egCCMUser;
  /*this case statement handles all the actions that can be taken from the
   *JavaScript*/
  switch ($action) {
    case "fetch_files":
      $response = efCCMFetchFiles($egCCMUser->groupVerify($group));
      break;
    case "fetch_gmem":
      $response = efCCMFetchGMem($egCCMUser->groupVerify($group));
      break;
    case "fetch_gdis":
      $response = efCCMFetchGDis($egCCMUser->groupVerify($group));
      break;
    case "fetch_users":
      $response = efCCMFetchUsers($egCCMUser->groupVerify($group));
      break;
    case "change_group":
      $response = new AjaxResponse($egCCMUser->groupVerify($group));
      break;
    case "change_priv":
      $response 
        = efCCMChangePriv($egCCMUser->groupVerify($group), $data, $new);
      break;
    case "add_gmem":
      $response = efCCMAddGMem($egCCMUser->groupVerify($group), $data);
      break;
    case "add_gdis":
      $response 
        = efCCMAddGDis($egCCMUser->groupVerify($group), $egCCMUser->id, $data);
      break;
    default: //if action not found, return error
      $response = new AjaxResponse("<error>no such action</error>"); 
      break;
  }
  return $response;
}
 
/*
 * This function returns all the files in the group (to populate the file
 * list)
 */
function efCCMFetchFiles( $group ) {
  if (!$group) { //if not member of group return error and don't do action
    $response = 
      new AjaxResponse("<error>you are not a member of this group</error>"); 
    return $response;
  }
 
  ###Database Operations###
  $dbr =& wfGetDB( DB_SLAVE ); //load the database object
 
  $res = $dbr->select( 
    "ccm_files", //table name
    array("ccm_f_file_id", "ccm_f_file_name"), //array of fields
    'ccm_f_group_id='.(int)$group, //for this group
    __METHOD__ 
  ); //request list of files from the ccm_files table
 
  /*create array of files*/
  while ($row = $dbr->fetchObject( $res ) ) {
    $files[$row->ccm_f_file_id] = $row->ccm_f_file_name;
  }
 
  $dbr->close(); //close the database
 
  ### Response Creation ##
  $out = '<?xml version="1.0" encoding="ISO-8859-1"?>';
  $out .= "<filelist>";
  foreach ($files as $f_id => $f_name) {
    /* FIXME: causes warning when no files are present, fix by initializing
     * ahead of time */
    $out .= "<file id=".$f_id." >";
    $out .=  $f_name;
    $out .=  "</file>";
  }
  $out .= "</filelist>";
 
  $response = new AjaxResponse($out);
  return $response;
}
 
/*
 * This function returns the group members of the current group to the
 * JavaScript
 */
function efCCMFetchGMem( $group ) {
  if (!$group) { //if not member of group return error and don't do action
    $response =
      new AjaxResponse("<error>you are not a member of this group</error>"); 
    return $response;
  }
 
  global $wgDBprefix, $wgUser;
 
  ### Database Operations ###
  $dbr =& wfGetDB( DB_SLAVE ); //load the database object
 
  $res = $dbr->query("
    SELECT ccm_ug_user_id,ccm_ug_privelage FROM "
    .$wgDBprefix."ccm_user_groups
      WHERE ccm_ug_group_id=".$group
  ); //query for members of group
 
  /*create array of members*/
  while ($row = $dbr->fetchObject( $res ) ){
    $u_id = $row->ccm_ug_user_id;
    $u_priv = $row->ccm_ug_privelage;
    $u = $wgUser->newFromId($u_id);
    $u_name = $u->getName();
    $members[$u_id] = $u_name;
    $privelages[$u_id] = $u_priv;
  }
  $dbr->close(); //close the database
 
  ### Response Creation ##
  $out = '<?xml version="1.0" encoding="ISO-8859-1"?>';
  $out .= "<userlist>";
  foreach ($members as $u_id => $u_name){
    $out .= "<user id='".$u_id."'>";
    $out .= "<name>".$u_name."</name>";
    $out .= "<privelage>".$privelages[$u_id]."</privelage>";
    $out .= "</user>";
  }
  $out .= "</userlist>";
 
  $response = new AjaxResponse();
  $response->addText($out);
  return $response;
}
 
/*
 * This function returns the group discussion for the current group to the
 * JavaScript
 */
function efCCMFetchGDis($group) {
  global $wgDBprefix;
 
  if (!$group) { //if not member of group return error and don't do action
    $response =
      new AjaxResponse("<error>you are not a member of this group</error>");
    return $response;
  }
 
  ### Database Operations ###
  $dbr =& wfGetDB( DB_SLAVE ); //load the database object
 
  $res = $dbr->query("
    SELECT ccm_gd_discussion_user_id,ccm_gd_discussion_text FROM "
    .$wgDBprefix."ccm_group_discussion
      WHERE ccm_gd_group_id=".$group."
      ORDER BY ccm_gd_discussion_number
  "); //query for discussions
 
  /*create array of discussion*/
  $i = 0;
  while ($row = $dbr->fetchObject( $res ) ){
    $u_id = $row->ccm_gd_discussion_user_id;
    $text = $row->ccm_gd_discussion_text;
    $disItems[$i] = Array($u_id, $text);
    $i += 1;
  }
 
  $dbr->close(); //close the database
 
  ### Response Creation ##
  $out = '<?xml version="1.0" encoding="ISO-8859-1"?>';
  $out .= '<disItems>';
  foreach ($disItems as $item) {
      $out .= '<discussion user="'.$item[0].'">'.$item[1].'</discussion>';
  }
  $out .= '</disItems>';
 
  $response = new AjaxResponse();
  $response->addText($out);
  return $response;
}
 
/*
 * This function fetches all the users in the database
 */
function efCCMFetchUsers($group) {
  global $egCCMUser;
 
  //if not high enough privelage return error and don't do action
  if ($egCCMUser->privelages[$group] != 3) {
    $response = 
      new AjaxResponse(
        "<error>you are not authorized to fetch users</error>"
      ); 
    return $response;
  }
 
  ### Database Operations ###
  $dbr = wfGetDB( DB_SLAVE );
  $res = $dbr->select( 
    "user", //table name
    array( //array of fields
      "user_id", 
      "user_name", 
      "user_real_name", 
      "user_email"
    ), 
    '', //no WHERE statements
    __METHOD__ 
  ); //request user data from the user table
  if( $dbr->numRows( $res ) > 0 ) {
    /*create array of users*/
    while( $row = $dbr->fetchObject( $res ) ) {
      $users[$row->user_id] = array(
        "name" => $row->user_name,
        "realname" => $row->user_real_name,
        "email" => $row->user_email
      );
      /* instanstiate a $user_groups for each user, otherwise errors come
       * up later for users with no groups */
      $user_groups[$row->user_id] = array();
    }
    $dbr->freeResult( $res );
  }
  //request user data from the user_groups table
  $res = $dbr->select('user_groups', '*', '',__METHOD__ );
  if( $dbr->numRows( $res ) > 0 ) {
    /*create array of user_groups and group_users*/
    while( $row = $dbr->fetchObject( $res ) ) {
      $user_groups[$row->ug_user][] = $row->ug_group;
      $group_users[$row->ug_group][] = $row->ug_user;
    }
    $dbr->freeResult( $res );
  }
  $dbr->close();
 
  ### Response Creation ##
  $out = '<?xml version="1.0" encoding="ISO-8859-1"?>';
  $out .= '<ug>';
 
  /*create a list of users*/
  $out .= '<userlist>';
  foreach ($users as $u_id => $u_fields) {
    $out .= "<user id='".$u_id."'>";
    foreach ($u_fields as $field => $value) {
      $out .= "<".$field.">".$value."</".$field.">";
    }
    $out .= "<usergroups>";
    foreach ($user_groups[$u_id] as $group) {
      $out .= "<group>".$group."</group>";
    }    
    $out .= "</usergroups>";
    $out .= "</user>";
  }
  $out .= '</userlist>';
 
  /*create a list of groups*/
  $out .= '<grouplist>';
  foreach ($group_users as $group => $u_list) {
    $out .= "<group>";
    $out .= "<name>".$group."</name>";
    $out .= "<userlist>";
    foreach ($u_list as $u_id) {
      $out .= "<user id='".$u_id."'>".$users[$u_id]["name"]."</user>";
    }
    $out .= "</userlist>";
    $out .= "</group>";
  }
  $out .= '</grouplist>';
 
  $out .= '</ug>';
 
  $response = new AjaxResponse($out);
  return $response;
}
 
/*
 * This function changes the proper users privelage and does verification
 */
function efCCMChangePriv($group, $user, $newpriv) {
  global $egCCMUser, $wgDBprefix;
 
  if (!$group) { //if not member of group return error and don't do action
    $response =
      new AjaxResponse("<error>you are not a member of this group</error>");
    return $response;
  }
 
  //if not high enough privelage return error and don't do action
  if ($egCCMUser->privelages[$group] != 3) {
    $response = 
      new AjaxResponse(
        "<error>you are not authorized to change privelages</error>"
      ); 
    return $response;
  }
 
  //if newpriv is not an int return error and don't do action
  if (!is_numeric($newpriv)) { 
    $response = 
      new AjaxResponse(
        "<error>the new privelage specified is not of valid type</error>"
      ); 
    return $response;
  }
 
  /* 
   * This statement checks if the newpriv is in the proper range.
   * It allows for a newpriv to be 3, which isn't possible input from 
   * standard JavaScript but should be allowed from else-where. 
   * Mostly this is allowed for code reusability purposes
   */
  //if newpriv is out of range return error and don't do action
  if (((int)$newpriv < 0) || ((int)$newpriv > 3)) {
    $response = 
      new AjaxResponse(
        "<error>the new privelage specified is out of range</error>"
      ); 
    return $response;
  }
 
  ### Database Operations ###
  $dbw =& wfGetDB( DB_MASTER ); //load the database object
 
  $res = $dbw->query("
    SELECT ccm_ug_privelage FROM "
    .$wgDBprefix."ccm_user_groups
      WHERE ccm_ug_user_id=".$user."
        and ccm_ug_group_id=".$group
  ); //query for privelages of group member
 
  //if user being modified is a PI, then do not let user be changed
  if ($dbw->fetchObject( $res )->ccm_ug_privelage == 3) {
    $response = new AjaxResponse("3"); 
    $dbw->close(); //close database to avoid a leak
    return $response;
  }
 
  $res = $dbw->query("
    UPDATE ".$wgDBprefix."ccm_user_groups
      SET ccm_ug_privelage = ".(int)$newpriv."
      WHERE ccm_ug_user_id=".$user."
        and ccm_ug_group_id=".$group
  ); //update the privelage of the specified user
 
  $dbw->close();
 
  $response = new AjaxResponse($newpriv);
  return $response;
}
 
/*
 * This function adds a new user to the group
 */
function efCCMAddGMem($group, $newUser) {
  global $egCCMUser, $wgDBprefix;
 
  if (!$group) {  //if not member of group return error and don't do action
    $response = 
      new AjaxResponse("<error>you are not a member of this group</error>"); 
    return $response;
  }
 
  //if not high enough privelage return error and don't do action
  if ($egCCMUser->privelages[$group] != 3) {
    $response = 
      new AjaxResponse(
        "<error>you are not authorized to add users</error>"
      ); 
    return $response; 
  }
 
  /*check if user is already in group*/
  $dbr =& wfGetDB( DB_SLAVE ); //load the database object
  $res = $dbr->select( 
    "ccm_user_groups", //table name 
    "ccm_ug_user_id", //field to fetch
    'ccm_ug_group_id='.(int)$group, //WHERE group is current group
    __METHOD__ 
  ); //request user data from the user table
 
  while ($row = $dbr->fetchObject($res)) { //check against each user
    //if user already a member return error and don't do action
    if ($row->ccm_ug_user_id == $newUser) { //user already exists
      $response = 
        new AjaxResponse(
          "<error>this user is already a group member</error>"
        );
      return $response; 
    }
  }
 
  $dbr->close(); //close the database
  unset($dbr); //destroy dbr
 
  /*add user to group*/
  $dbw =& wfGetDB( DB_MASTER ); //load the database object
  $res = $dbw->query("
    INSERT INTO ".$wgDBprefix.
      "ccm_user_groups(ccm_ug_user_id,ccm_ug_group_id)
      VALUES (".(int)$newUser.",".(int)$group.")
  "); //insert the specific user-group combo, with default privelages.
  //could switch DB to MyISAM and try to use INSERT DELAYED for optimization
  $dbw->close();
 
  $response = new AjaxResponse("yes");
  return $response;
}
 
/*
 * This function adds to the group discussion
 */
function efCCMAddGDis($group, $user, $data) {
  global $wgDBprefix;
 
  ### Database Operations ###
  $dbw =& wfGetDB( DB_MASTER ); //load the database object
 
  /* adds the data to the database. Use mysql_real_escape_string to avoid
    insertion attacks, as recomeneded by Tbleher*/
  $res = $dbw->query("
    INSERT INTO ".$wgDBprefix.
    "ccm_group_discussion ".
    "(ccm_gd_group_id, ccm_gd_discussion_user_id, ccm_gd_discussion_text)
    VALUES (".$group.", ".$user.', "'.
    mysql_real_escape_string('"','\"', $data).'")
  '); 
 
  $dbw->close();
 
  $response = new AjaxResponse("yes");
  return $response;
 
}
?>

[edit] CCM_body.php

<?php
/*
 * CCM 0.0.2 - a collaborative content management system for MediaWiki
 * Copyright (C) 2008  Artem Kaznatcheev
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
class CCM extends SpecialPage {
 
  function CCM() {
    SpecialPage::SpecialPage("CCM");
    self::loadMessages();
  }
 
  function execute( $par ) {
    global $wgRequest, $wgOut, $wgUser, $wgDBprefix, $egCCMUser;
    $egCCMUser = new CCMUser();
    $this->setHeaders();
 
    # Get request data from, e.g.
    $param = $wgRequest->getText('param');
 
    $dbr =& wfGetDB( DB_SLAVE ); //load the database object
 
    //if the user belongs to no groups, then there is nothing to display
    if (!$egCCMUser->groups) { 
      $wgOut->showErrorPage('error','nogroups');
      return;
    }
 
    ### FIX ME: Needs to be not confined to only this server ###
    $wgOut->addScript(
      '<link rel="stylesheet" type="text/css" href="/testwiki/extensions/CCM/CCM.css" />'
    ); //add CSS
    $wgOut->addScript(
      '<script type="text/javascript" src="/testwiki/extensions/CCM/CCM.js"></script>'
    ); //add JavaScript
 
    /* Expanding on the JavaScript to include all the proper data to
     * manipulate later */
    $output .= '
      <script type="text/javascript">
        CCMData.name = "'.$egCCMUser->name.'"
        CCMData.currentGroup = '.$egCCMUser->groups[0].';
        CCMData.groups = new Object;
    ';
    foreach ($egCCMUser->groups as $g) {
      /*Creating an object for each group*/
      /* create the property in data that stores the object that represents
       * a given group */
      $output .= '
        CCMData.groups.g_'.$g.' = new Object;'; 
      $output .= '
        CCMData.groups.g_'.$g.'.privelage = '.$egCCMUser->privelages[$g].
        ';';
      /*Getting the name of each group*/
      $res = $dbr->query("
        SELECT ccm_g_group_name FROM ".$wgDBprefix."ccm_group
          WHERE ccm_g_group_id=".$g."
      "); //database query for the name of each group
      $row = $dbr->fetchObject( $res );
      //create and set the name property
      $output .= '
        CCMData.groups.g_'.$g.'.name = "'.$row->ccm_g_group_name.'";';
      //create and set the cached property
      $output .= '
        CCMData.groups.g_'.$g.'.cached = false;'; 
    }
 
    $output .= '
      </script>';
 
    $dbr->close(); //close the database     
    $wgOut->addHTML($output); //add output to page
 
    ### Rendering the page ###
    $output = '
      <div id="CCM-body">
        <ul>
        <li class="floatright" onclick="Files_Click();">Files</li>
        <li class="floatright" onclick="Group_Click();">Group</li>
    ';
    foreach ($egCCMUser->groups as $g) {
      //create each placeholder tab to be filled in by JavaScript
      $output .= '<li id="tab_g_'.$g.'" onclick="Change_Group('.$g.
        ')">Loading...</li>'; 
    }
    $output .= "</ul>";
 
    $output .= '
      <div class="CCM-main" id="tab-files">
        <table id="primarytable" style="width:100%;">
          <td valign="top" class="CCM-left">
            <div class="floatright">Up</div>
            Files:
            <div id="CCM-files" class="textdiv"></div>
          </td><td valign="top" >
            File Info:
            <div id="CCM-info" class="textdiv">
              Name:
              <table style="width:100%;">
                <tr>
                  <td style="padding:0; margin:0;">Created by:</td>
                  <td style="padding:0;">At:</td>
                </tr><tr>
                  <td style="padding:0;">Edited by:</td>
                  <td style="padding:0;">At:</td>
                </tr>
              </table>
              Description:
              <div id="CCM-description" style="border: none;">
                File info stuff
              </div>
            </div><ul>
              <li class="floatright">History</li>
              <li class="floatright">Permission</li>
              <li>Download</li>
              <li>Update</li>
            </ul>
          </td>   
        </table>
        <div id="CCM-extra">
          Discussion:
          <div id="CCM-fileDiscussion" class="textdiv">
          </div>
          <div class="floatright">Reply</div>
        </div>
        <br />
      </div>
      <div class="CCM-main" id="tab-group">
          <table id="primarytable" style="width:100%;">
            <td valign="top" id="CCM-memCol" class="CCM-left">
              <div id="CCM-addMember" class="floatright" onclick="addMember_Click();" style="display:none;">Add</div>
              Members:
              <div id="CCM-members" class="textdiv">
              </div>
            </td><td valign="top" id="CCM-gdis" >
              Discussion:
              <div id="CCM-discussion" class="textdiv">
              </div>
              <div class="floatright" id="CCM_gDis_Reply" onclick="Reply_G_Click();">Reply</div>
              <br />
            </td>
          </table>
        </div>
      </div>
    '; //this string represents the physical structure of the page
 
    //add the data to the page
    $output .=  '<script type="text/javascript">renderPage();</script>'; 
 
    $wgOut->addHTML($output); //add output to page
  }
 
 
  function loadMessages() {
    static $messagesLoaded = false;
    global $wgMessageCache;
    if ( $messagesLoaded ) return;
    $messagesLoaded = true;
 
    require( dirname( __FILE__ ) . '/CCM.i18n.php' );
    foreach ( $allMessages as $lang => $langMessages ) {
      $wgMessageCache->addMessages( $langMessages, $lang );
    }
  }
}
?>

[edit] CCM.js

/*
 * CCM 0.0.2 - a collaborative content management system for MediaWiki
 * Copyright (C) 2008  Artem Kaznatcheev
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
 
/**
 * Unobtrusive event handeling from Peter-Paul Kock available at:
 * http://www.quirksmode.org/js/eventSimple.html; Function for adding and
 * removing events available. Used by the dragDrop code mostly
 */
function addEventSimple(obj,evt,fn) {
  if (obj.addEventListener)
    obj.addEventListener(evt,fn,false);
  else if (obj.attachEvent)
    obj.attachEvent('on'+evt,fn);
}
function removeEventSimple(obj,evt,fn) {
  if (obj.removeEventListener)
    obj.removeEventListener(evt,fn,false);
  else if (obj.detachEvent)
    obj.detachEvent('on'+evt,fn);
}
 
/**
 * This object was originally created by Peter-Paul Kock availabe at:
 * http://www.quirksmode.org/js/dragdrop.html code has been modified from
 * original to better suit it's new goal  most significant modification is
 * the removal of keyboard control. Also, the dragBar option was added,
 * where you can define by which element the item is dragged around by (for
 * instance, the title bar). This was done to allow typeable inputs inside
 * the dragged element.
 * To create object, use one of two methods:
 *  dragDrop.initElement("elementID", "dragBarId");
 *  dragDrop.initElement(element, dragBar); 
 *    - can be used for: 
 *      "dragDrop.initElement(document.getElementById("elementID"), "dragBarId");"
 * it is possible to combine the element reference or name and dragBar
 * reference or name in any way. 
 */
dragDrop = {
  initialMouseX: undefined,
  initialMouseY: undefined,
  startX: undefined,
  startY: undefined,
  draggedObject: undefined,
  element: undefined, //the element being moved around
  //the part of the element that has to be clicked on to move it
  dragBar: undefined, 
  initElement: function (obj, bar) {
    if (typeof obj == 'string')
      obj = document.getElementById(obj);
    if (typeof bar == 'string')
      bar = document.getElementById(bar);
    dragDrop.element = obj; //define element
    dragDrop.dragBar = bar; //define dragBar
    //the dragBar is the one that actually should be clicked on
    dragDrop.dragBar.onmousedown = dragDrop.startDragMouse; 
  },
  startDragMouse: function (e) {
    dragDrop.startDrag(this);
    var evt = e || window.event;
    dragDrop.initialMouseX = evt.clientX;
    dragDrop.initialMouseY = evt.clientY;
    addEventSimple(document,'mousemove',dragDrop.dragMouse);
    addEventSimple(document,'mouseup',dragDrop.releaseElement);
    return false;
  },
  startDrag: function (obj) {
    if (dragDrop.draggedObject)
      dragDrop.releaseElement();
    dragDrop.startX = dragDrop.element.offsetLeft;
    dragDrop.startY = dragDrop.element.offsetTop;
    dragDrop.draggedObject = obj;
    dragDrop.element.className += ' dragged';
  },
  dragMouse: function (e) {
    var evt = e || window.event;
    var dX = evt.clientX - dragDrop.initialMouseX;
    var dY = evt.clientY - dragDrop.initialMouseY;
    dragDrop.setPosition(dX,dY);
    return false;
  },
  setPosition: function (dx,dy) {
    dragDrop.element.style.left = dragDrop.startX + dx + 'px';
    dragDrop.element.style.top = dragDrop.startY + dy + 'px';
  },
  releaseElement: function() {
    removeEventSimple(document,'mousemove',dragDrop.dragMouse);
    removeEventSimple(document,'mouseup',dragDrop.releaseElement);
    dragDrop.element.className = dragDrop.element.className.replace(/dragged/,'');
    dragDrop.draggedObject = null;
  }
}
 
/**
 * This is the object used to change all the settings assosiated with
 * memberss and to create the proper output
 */
 memberObject = function(parentElement, i_name, u_id, g_id){
  /*initialize object variables*/
  this.elements = { //elements object
    li: undefined, //the list item in which the user is stored
    drop: undefined //the div that appears below the user
  }
  this.elements.li = parentElement;
  this.name = i_name 
  this.id = u_id; //stores ID of user for the box is created
  //get privelages from CCMData (to avoid having to pass it)
  this.priv = CCMData.groups["g_" + g_id].privelages[u_id]; 
  this.group = g_id; //stores ID of group
 
  if ((this.priv != 3) && (CCMData.currentGroup = this.group)) {
    Out = '   <img id="img_' + this.id + '" ';
    Out += 'onclick="' + this.name + '.dropRender()" ';
    Out += 'src="' + wgScriptPath + '/extensions/CCM/images/downarrow.gif';
    Out += ' ></div>';
    this.elements.li.innerHTML += Out;
  }
 
  /*creates the select box below username to allow changes in privelages*/
  this.dropRender = function() {
    if (!this.elements.drop) {
      Out = '<div>';
 
      /*add the select statement that list all the privelage options in a
       *drop down menu*/
      Out += '<select>';
      //add option, do checking to see if it should be default
      Out += '<option ' + ((this.priv == 0)?'selected="selected"':'') + ' >'
      Out += 'Minimal Privelages</option>';
      //add option, do checking to see if it should be default
      Out += '<option ' + ((this.priv == 1)?'selected="selected"':'') + ' >'
      Out += 'Standard Privelages</option>';
      //add option, do checking to see if it should be default
      Out += '<option ' + ((this.priv == 2)?'selected="selected"':'') + ' >'
      Out += 'Full Privelages</option>';
      /*no option is made for making PIs... since we don't want those
       *created in excess*/
      Out += '</select>'; 
 
      //add the checkmark submit button
      Out += ' <img id="img_submit" src="' + wgScriptPath + '/extensions/CCM/images/checkmark.gif" onclick="' + this.name + '.submitPriv()" />';
 
      Out += '</div>';
      this.elements.li.innerHTML += Out;
      document.getElementById('img_' + this.id).src = wgScriptPath + '/extensions/CCM/images/uparrow.gif';
      this.elements.drop = this.elements.li.lastChild;
    } else {
      /*if drop alread exists and someone clicks on the arrow, that means
       *they want it to dissappear*/
      //remove the div that holds all the dropdown content
      this.elements.li.removeChild(this.elements.drop);
      //remove the reference to the now non-existent drop area
      this.elements.drop = null;
 
      //resets the arrow image
      document.getElementById('img_' + this.id).src = wgScriptPath + '/extensions/CCM/images/downarrow.gif';
    }
  }
 
  /*sends the request to the server to change privelages*/
  this.submitPriv = function() {
     /*lastChild is the image, change the graphic to the loading screen, so
      *the person knows stuff is hapening*/
    this.elements.drop.lastChild.src = wgScriptPath + '/extensions/CCM/images/loading.gif'
    //remove the onclick action to avoid the user clicking again by mistake
    this.elements.drop.lastChild.onclick = "";
 
    //call to server
    sajax_do_call( "efCCM_Ajax", ["change_priv", CCMData.currentGroup, this.id, this.elements.drop.firstChild.selectedIndex], this.responsePriv );
  }
 
  /*a hacky way to get around loss of "this" when function is  passed to
   *another function. Explained at
   *http://w3future.com/html/stories/callbacks.xml*/
  me = this;
 
  /**
   * This function acts on the data sent back by the server after a request
   * to change privelages. Due to the way functions are passed in
   * JavaScript, and the nature of "this" a small work around has to be
   * used. Since this function is sent outside of the object, all
   * references to the parent object should be refered to as "me" as
   * opposed to as "this".
   */
  this.responsePriv = function(response) {
    //change users privelage
    CCMData.groups["g_" + me.group].privelages[me.id] = response.responseText; 
    //changer users privelages inside object
    me.priv = response.responseText; 
 
    me.dropRender(); //removes the drop area
  }
}
/*
 * Object that exists for dealing with the manage screen created by PIs on
 * the group page
 */
addScreen = {
  //property to store reference to the floatingDiv element
  element: undefined,
  //property to store reference to the editable portion of the element
  inner: undefined,
  //property to store an XML DOM that holds the users
  usersDOM: undefined,
  //property to store an XML DOM that holds the groups
  groupsDOM: undefined,
  /*property to store the XML DOM chunks in its heap form, ready for final
   *sort and output*/
  heapDOM: undefined,
  tableBody: undefined, //property to store the HTML DOM of the table body
  /*creates the object, making it visible and rendering the defaul
   *elements of it */
  create: function() {
    if (!addScreen.element) { //checks if items has already been created
      /*creates the structure of the element and the start page*/
      Out = '<div id="CCM-floatDiv">';
      Out += '<div id="CCM-floatDivHead"><b style="padding-left:0.5em;">Add User</b>';
      Out += '<b style="position:absolute;right:0;padding-right:0.5em;" onclick="addScreen.destroy()">x</b></div>';
      Out += '<div id="CCM-AddDiv">';
      Out += '</div>';
      Out += '</div>';
 
      //document.getElementById("CCM-body").innerHTML += Out;
      document.getElementById("globalWrapper").innerHTML += Out;
      //sets the property of element to point to the object
      addScreen.element = document.getElementById("CCM-floatDiv");
      addScreen.inner = document.getElementById("CCM-AddDiv");
      //sets the element as dragable and defines the dragBar
      dragDrop.initElement(addScreen.element, "CCM-floatDivHead");
 
      sajax_do_call("efCCM_Ajax", ["fetch_users", CCMData.currentGroup, null, null], addScreen.populateArrays );
    }
  },
  /* destroys the object and floatingDiv */
  destroy: function() {
    //document.getElementById("CCM-body").removeChild(addScreen.element);
    document.getElementById("globalWrapper").removeChild(addScreen.element);
    addScreen.element = null;
  },
  /* renders all the pages visible inside the element */
  renderPage: function() {
    /*output a table with all the values*/
    Out = "<table><thead>";
    /*output table header*/
    Out += "<tr>";
    Out += "<th>User Name</th>";
    Out += "<th>Real Name</th>";
    Out += "<th>Email</th>";
    Out += "<th>Groups</th>";
    Out += "<th>Actions</th>";
    Out += "</tr>";
    /*output search fields*/
    Out += "<tr>";
    Out += '<td><input type="text" autocomplete="off" onkeyup="addScreen.search(this, \'name\')"/></td>';
    Out += '<td><input type="text" autocomplete="off" onkeyup="addScreen.search(this, \'realname\')"/></td>';
    Out += '<td><input type="text" autocomplete="off" onkeyup="addScreen.search(this, \'email\')"/></td>';
 
    Out += "<td><select>";
    Out += '<option selected="selected" />'; 
    for (i=0;i<addScreen.groupsDOM.childNodes.length;i++) {
      Out += "<option>" + addScreen.groupsDOM.childNodes[i].firstChild.firstChild.nodeValue + "</option>";
    }
    Out += "</select></td>";
    Out += "<td></td>";
    Out += "</tr>";
    Out += "</thead><tfoot></tfoot><tbody></tbody><table>";
 
    addScreen.inner.innerHTML = Out; //add to page
    pickChild = function(parentIndex, leftIndex, rightIndex) {
      maxIndex = addScreen.heapDOM.length;
      pValue = addScreen.heapDOM[parentIndex].firstChild.firstChild.nodeValue.toUpperCase();
      lValue = (leftIndex<maxIndex)? addScreen.heapDOM[leftIndex].firstChild.firstChild.nodeValue.toUpperCase():false;
      rValue = (rightIndex<maxIndex)? addScreen.heapDOM[rightIndex].firstChild.firstChild.nodeValue.toUpperCase():false;
      if (lValue && rValue) {
        if ((pValue > lValue)&&(rValue >= lValue)) return leftIndex;
        else if ((pValue > rValue)&&(lValue >= rValue)) return rightIndex;
        else return parentIndex;
      } else if (lValue) {
        if (pValue > lValue) return leftIndex;
        else return parentIndex;
      } else { return parentIndex; }
    }
    //firstChild.lastChild since we want to be inside the tbody
    addScreen.tableBody = addScreen.inner.firstChild.lastChild
    addScreen.heapRender(addScreen.tableBody, pickChild); 
  },
  /** 
   * This function takes the heap and outputs parts of it as it does the
   * final sorting. This rellies on the heap in addScreen.heapDOM being
   * already built and assumes that it is a proper binary-heap. Custome
   * compare pickChild is to be provided. Arguments are given to pickChild
   * as follows: pickChild(parentIndex, leftIndex, rightIndex). The return
   * must be the child that should replace the parent or the parent if no
   * replacement is to be made. The function should account for the chance
   * that rightChild will be missing.
   */
  heapRender: function(obj, pickChild) {
    /*this function removes and returns the root and starts the bubbleDown
     *for new root*/
    function pullRoot(){
      if (addScreen.heapDOM.length > 2){ //if there is more than one item that fetch leaf
        farLeaf = addScreen.heapDOM.pop();
        root = addScreen.heapDOM[1];
        addScreen.heapDOM[1] = farLeaf;
 
        /* this is the bubble down for new root */
        function bubbleDown(indexNode) {
          indexLeft = indexNode*2;
          indexRight = indexNode*2 + 1;
 
          newIndex = pickChild(indexNode, indexLeft, indexRight);
          //parent should remain as parent, then you are done
          if (newIndex == indexNode) { 
          } else if (newIndex == indexLeft) {
            tempDOM = addScreen.heapDOM[indexLeft];
            addScreen.heapDOM[indexLeft] = addScreen.heapDOM[indexNode];
            addScreen.heapDOM[indexNode] = tempDOM;
            bubbleDown(indexLeft); //recurse on left branch 
          } else if (newIndex == indexRight) {
            tempDOM = addScreen.heapDOM[indexRight];
            addScreen.heapDOM[indexRight] = addScreen.heapDOM[indexNode];
            addScreen.heapDOM[indexNode] = tempDOM;
            bubbleDown(indexRight); //recurse on right branch
          }
        }
 
        bubbleDown(1); //bubble down the root if need be
        return root;
      } else {
        //if only one item left (length 2) then just return the last item
        return addScreen.heapDOM.pop();
      } 
    }
 
    //for each item in array (remember that array[0]=undefined, hence the >1
    while (addScreen.heapDOM.length>1){
      Out = ""; //initialize/clear Out
      //initialize/clear Out, used for creating the inner content of the tr
      OutInner = "";
      user = pullRoot();
      Out += "<tr ";
      for (i=0;i<user.childNodes.length;i++) {
        OutInner += "<td>";
        //if item text, then only one data field in it
        if (user.childNodes[i].firstChild == "[object Text]") {
          if (i==0) u_id = user.getAttribute("id");
          //fetch single field
          OutInner += user.childNodes[i].firstChild.nodeValue;
          //set attribute
          Out += user.childNodes[i].tagName + " = '" + user.childNodes[i].firstChild.nodeValue + "'";
        } else if (user.childNodes[i].firstChild =="[object Element]") {
          for (j=0;j<user.childNodes[i].childNodes.length;j++) {
            //output group
            OutInner += user.childNodes[i].childNodes[j].firstChild.nodeValue;
            //set attribute
            Out += user.childNodes[i].tagName + "_" + j + "='" + user.childNodes[i].childNodes[j].firstChild.nodeValue + "' ";
            //output the comma for all but the last group
            if (user.childNodes[i].childNodes.length > j+1) OutInner += ", ";
          }
        } else {Out += "";} //output a blank if there is no data
        OutInner += "</td>";
      }
      OutInner += "<td><a onclick='addScreen.addMember(" + u_id + ")'><b>Add</b></a></td>";
      //finish the opening tr tag, with all the attributes inside
      Out += ">";
      Out += OutInner;
      Out += "</tr>";
      obj.innerHTML += Out;
    }
  },
  /**
   * sorting using heap sort with custom compare "doBubble" function
   * doBubble function has to return true if node needs to bubble up, or
   * false if it is to remain. Arguments are given to doBubble as follows:
   * doBubble(childNode, parentNode)
   */
  customSort: function(nList, doBubble) {
    heap = new Array();
    bubbleUp = function(indexNode){
      indexParent = Math.floor(indexNode/2);
      if (indexParent != 0) { //check if we bubbled all the way to root
        if (doBubble(heap[indexNode], heap[indexParent])) {
          temp = heap[indexParent];
          heap[indexParent] = heap[indexNode];
          heap[indexNode] = temp;
          bubbleUp(indexParent); //recurse
        }
      }
    }
    heap[0] = undefined;
    for (i=0;i<nList.length;i++) {
      heap[i+1] = nList[i];
      bubbleUp(i+1);
    }
    addScreen.heapDOM = heap; //set the heapDOM for global use later
    addScreen.renderPage(); //start the basic rendering
  },
  /*populate user and group arrays*/
  populateArrays: function(request) {
    result = request.responseText;
 
    /* 
     * Turn string to XML object for easier navigation 
     * IE and Mozilla, Firefox, and Opera handle it differently
     * hence need checking code.
     */
    if (window.ActiveXObject) { //IE
      var xmlResult = new ActiveXObject("Microsoft.XMLDOM");
      xmlResult.async = "false";
      xmlResult.loadXML(result);
    } else { //Mozilla, Firefox and Opera
      var xmlResult = (new DOMParser()).parseFromString(result, "text/xml");
    }
 
    //get ug->userlist
    addScreen.usersDOM = xmlResult.firstChild.firstChild;
    //get ug->grouplist
    addScreen.groupsDOM = xmlResult.firstChild.lastChild;
 
    doBubble = function(childNode, parentNode) {
      return (childNode.firstChild.firstChild.nodeValue.toUpperCase()<parentNode.firstChild.firstChild.nodeValue.toUpperCase());
    }
 
    addScreen.customSort(addScreen.usersDOM.childNodes, doBubble);
  },
  /*this function deals with dynamic search of all fields*/
  search: function(obj, field){
    //the regualar expression for searching the field
    rE = new RegExp("(.)*" + obj.value + "(.)*" , "i");
    for (i=0;i<addScreen.tableBody.childNodes.length;i++) {
      if (rE.test(addScreen.tableBody.childNodes[i].getAttribute(field))) addScreen.tableBody.childNodes[i].style.display = "table-row";
      else addScreen.tableBody.childNodes[i].style.display = "none";
    }
  },
  /*this function is used to addMembers*/
  addMember: function(u_id) {
    sajax_do_call("efCCM_Ajax", ["add_gmem", CCMData.currentGroup, u_id, null], addScreen.destroy);
  }
}
 
/** 
 * This function was mostly written by Dustin Diaz and is available at
 * http://www.dustindiaz.com/getelementsbyclass/; I modified it slightly to
 * better fit my needs. 
 */
function getElementsByClass(searchClass,node,tag) {
  var classElements = new Array();
  if ( node == null ) node = document;
  if ( tag == null ) tag = '*';
  var els = node.getElementsByTagName(tag);
  var elsLen = els.length;
  for (i = 0, j = 0; i < elsLen; i++) {
    if ( els[i].className == searchClass ) {
      classElements[j] = els[i];
      j++;
    }
  }
  return classElements;
}
 
function userDiscussion(user, action){
  var gdis = document.getElementById("CCM-gdis");
  elements = new Array;
  elementClass = "dis_" + user.toString();
  elements = getElementsByClass(elementClass, gdis, "div");
  for (i in elements){
    if (action == "hl") {
      elements[i].id = "discussion-item-hl";
    } else if (action == "clear") {
      elements[i].id = "discussion-item";
    }
  }
}
 
function Group_Click(){
  document.getElementById("tab-files").style.display = "none";
  document.getElementById("tab-group").style.display = "block";
} 
 
function Files_Click(){
  document.getElementById("tab-files").style.display = "block";
  document.getElementById("tab-group").style.display = "none";
}
 
/**
 * This function is to take effect when a user tries to reply to a 
 * discussion
 */ 
function Reply_G_Click(){
  //this function handles the response by just silencing it
  function f (request) { 
  }
  if (input_ready) {
    textBox = document.getElementById("CCM-gDis-in");
    textBody = document.getElementById("CCM-gDis");
    //remove edit area if no text is entered (cancel action)
    if (!textBox.value) {
      textBody.innerHTML = "";
      //reset value to be ready to create text area again
      input_ready = false;
      //warn user if text entered is too short to post
    } else if (textBox.value.length < 10) {
      textBody.innerHTML += "<i>your message is too short</i><br />";
    } else {
      //call to server
      sajax_do_call( "efCCM_Ajax", ["add_gdis", CCMData.currentGroup , textBox.value, null], f);
      renderDiscussion(); //this removes the posting box as well
    }
  } else {
    Out = '<br />';
    Out += '<b>' + CCMData.name + ':</b> (new reply)';
    Out += '<textarea rows="5" id="CCM-gDis-in"></textarea>';
    document.getElementById("CCM-gDis").innerHTML = Out;
    input_ready = true;
  }
}
 
/**
 * This function is fired when a PI tries to add a new member
 */
function addMember_Click() {
  obj = document.getElementById("CCM-addMember");
  //set a new onclick handle (destructor)
  obj.onclick = function onclick(event){
    addScreen.destroy();
  }
  addScreen.create();
}
 
/**
 * This function is responsible for populating the file list
 */
function renderFiles(request) {
  g_id = "g_" + CCMData.currentGroup; //g_id for indexing
  /*turn string to XML object for easier navigation IE and Mozilla,
   *Firefox, and Opera handle it differently hence need checking code*/
  if (window.ActiveXObject) { //IE
    var xmlResult = new ActiveXObject("Microsoft.XMLDOM")
    xmlResult.async = "false";
    xmlResult.loadXML(request.responseText);
  } else { //Mozilla, Firefox and Opera
    var xmlResult = (new DOMParser()).parseFromString(request.responseText, "text/xml"); 
  }
 
  /*set global variables*/
  var filesDOM = xmlResult.getElementsByTagName("file");
  for (i=0;i<filesDOM.length;i++){
    f_id = filesDOM[i].getAttribute('id');
    //create an object (assosiative array) for the specific file
    CCMData.groups[g_id].files[f_id] = new Object;
    //gets the file->text property
    CCMData.groups[g_id].files[f_id].name = filesDOM[i].firstChild.nodeValue;
  }
 
  /*output the file list*/
  out = "<ul>";
  for (f_id in CCMData.groups[g_id].files) {
    out += "<li file_id=" + f_id + " >";
    out += CCMData.groups[g_id].files[f_id].name;
    out += "</li>";
  }
  out += "</ul>";
 
  document.getElementById("CCM-files").innerHTML = out;
}
 
/**
 * This function is responsible for populating the group member list
 */
function renderGMem(request) { 
  g_id = "g_" + CCMData.currentGroup;
 
  result = request.responseText;
 
  /*turn string to XML object for easier navigation IE and Mozilla,
   *Firefox, and Opera handle it differently hence need checking code*/
  if (window.ActiveXObject) { //IE
    var xmlResult = new ActiveXObject("Microsoft.XMLDOM")
    xmlResult.async = "false";
    xmlResult.loadXML(result);
  } else { //Mozilla, Firefox and Opera
    var xmlResult = (new DOMParser()).parseFromString(result, "text/xml"); 
  }
 
  var usersDOM = xmlResult.getElementsByTagName("user");
  for (i=0;i<usersDOM.length;i++){
    u_id = usersDOM[i].getAttribute('id');
    //gets the user->name->text property
    CCMData.groups[g_id].members[u_id] = usersDOM[i].childNodes[0].childNodes[0].nodeValue;
    //gets the user->privelage->text property
    CCMData.groups[g_id].privelages[u_id] = usersDOM[i].childNodes[1].childNodes[0].nodeValue;
  }
 
  wikiurl = location.href.slice(0, location.href.length - 11);
  document.getElementById("CCM-members").innerHTML = "";
  Out = "<ul>";
  Out += "</ul>";
  //add output to page
  document.getElementById("CCM-members").innerHTML += Out;
  //get reference to newly created <ul> element
  ulElement = document.getElementById("CCM-members").lastChild;
  for (id in CCMData.groups[g_id].members) {
    Out = '<li user_id=' + id + ' >'
    Out += "</li>";
    ulElement.innerHTML += Out; //add output to page
    /*get referece to newly created <li> element, needed for memberObject
     *later*/
    liElement = ulElement.lastChild;
    Out = '<a href="' + wikiurl + 'User:' + CCMData.groups[g_id].members[id] 
      + '" onmouseover="userDiscussion(' + id + ', ' + "'hl'" + ')"'
      + '" onmouseout="userDiscussion(' + id + ', ' + "'clear'" + ')">';
    Out += CCMData.groups[g_id].members[id];
    Out += "</a>";
    liElement.innerHTML += Out;
    /*check if user has admin privelages and should thus have admin links
     *render*/
    if (CCMData.groups[g_id].privelage == 3){
      //create and initialize the new memberObject
      memObjArray[id] = new memberObject(liElement, "memObjArray[" + id + "]", id, CCMData.currentGroup);
    }
  }
 
  /*check if user has admin privelages and should thus have admin links
   *render*/
  if (CCMData.groups[g_id].privelage == 3){
    document.getElementById("CCM-addMember").style.display = "block";
  }
}
 
/**
 * This function is responsible for populating the group discussion
 */
function renderGDis(request) {
  g_id = "g_" + CCMData.currentGroup; 
 
  result = request.responseText;
 
  /*Turn string to XML object for easier navigation IE and Mozilla,
   *Firefox, and Opera handle it differently hence need checking code*/
  if (window.ActiveXObject) { //IE
    var xmlResult = new ActiveXObject("Microsoft.XMLDOM")
    xmlResult.async = "false";
    xmlResult.loadXML(result);
  } else { //Mozilla, Firefox and Opera
    var xmlResult = (new DOMParser()).parseFromString(result, "text/xml"); 
  }
 
  var usersDOM = xmlResult.getElementsByTagName("discussion");
  for (i=0;i<usersDOM.length;i++){
    CCMData.groups[g_id].discussion[i] = new Object;
    CCMData.groups[g_id].discussion[i].text = usersDOM[i].childNodes[0].nodeValue;
    u_id = usersDOM[i].getAttribute('user');
    CCMData.groups[g_id].discussion[i].u_id = u_id;
    CCMData.groups[g_id].discussion[i].user = CCMData.groups[g_id].members[u_id];
    //re-renders page if there is an error. 
    /*!!!! Need to expand on this to go from a partial cache !!!!!*/
    if (!CCMData.groups[g_id].members[u_id]) setTimeout("renderDiscussion()", 500);
  }
 
  wikiurl = location.href.slice(0, location.href.length - 11);
  document.getElementById("CCM-discussion").innerHTML = "";
  Out = "";
  //render each individual discussion item
  for (i=0;i<CCMData.groups[g_id].discussion.length;i++) {
    Out += '<div id="discussion-item"';
    if (i==0) Out += 'style="border-top:none;"';
    Out += 'class="dis_' + CCMData.groups[g_id].discussion[i].u_id + '">';
    Out += '<a href="' + wikiurl + 'User:' + CCMData.groups[g_id].discussion[i].user + '">';
    Out += CCMData.groups[g_id].discussion[i].user;
    Out += "</a>:";
    Out += '<div class="discussion-text">' + CCMData.groups[g_id].discussion[i].text + "</div>";
    Out += '</div>';
  }
  Out += '<div id="CCM-gDis">';
  Out += '</div>';
 
  document.getElementById("CCM-discussion").innerHTML += Out; //add output to page
}
 
/**
 * This function allows the user to change groups
 */
function Change_Group(id) {
  function f (request) {
    CCMData.currentGroup = parseInt(request.responseText);
    g_id = "g_" + CCMData.currentGroup;
    //if (CCMData.groups[g_id].cached) renderPage_cache();
    //else renderPage();
    renderPage();
  }
  sajax_do_call( "efCCM_Ajax", ["change_group", id, null, null], f ); //call to server
}
 
/**
 * This function renders the page from existing data instead of making new
 * server requests
 * FIXME: Need to check if the cache is still valid....
 */
function renderPage_cache(){
  g_id = "g_" + CCMData.currentGroup;
  //base url for generating links
  wikiurl = location.href.slice(0, location.href.length - 11);
 
  /*loead members from cache*/
  document.getElementById("CCM-members").innerHTML = "";
  Out = "<ul>";
  for (id in CCMData.groups[g_id].members) {
    Out += '<li><a href="' + wikiurl + 'User:' + CCMData.groups[g_id].members[id] 
      + '" onmouseover="userDiscussion(' + id + ', ' + "'hl'" + ')"'
      + '" onmouseout="userDiscussion(' + id + ', ' + "'clear'" + ')">';
    Out += CCMData.groups[g_id].members[id];
    Out += "</a></li>";
  }
  Out += "</ul>";
  document.getElementById("CCM-members").innerHTML += Out; //add output to page
 
  /*load discussion from cache*/
  document.getElementById("CCM-discussion").innerHTML = "";
  Out = "";
  for (i=0;i<CCMData.groups[g_id].discussion.length;i++) { //render each individual discussion item
    Out += '<div id="discussion-item"';
    if (i==0) Out += 'style="border-top:none;"';
    Out += 'class="dis_' + CCMData.groups[g_id].discussion[i].u_id + '">';
    Out += '<a href="' + wikiurl + 'User:' + CCMData.groups[g_id].discussion[i].user + '">';
    Out += CCMData.groups[g_id].discussion[i].user;
    Out += "</a>";
    Out += '<div class="discussion-text">' + CCMData.groups[g_id].discussion[i].text + "</div>";
    Out += '</div>';
  }
 
  document.getElementById("CCM-discussion").innerHTML += Out; //add output to page
}
 
function renderDiscussion() {
  g_id = "g_" + CCMData.currentGroup;
 
  /*Populate the group discussion*/ 
  CCMData.groups[g_id].discussion = new Array;
  //call to server
  sajax_do_call( "efCCM_Ajax", ["fetch_gdis", CCMData.currentGroup, null, null], renderGDis );
 
  input_ready = false; //since the input is redrawn, it is no longer ready
}
 
function renderPage(){
  for (group in CCMData.groups) {
    //places the names of the groups in their tabs
    document.getElementById("tab_" + group).innerHTML = CCMData.groups[group].name;
  }
 
  g_id = "g_" + CCMData.currentGroup;
 
  /*Populate the file list*/
  CCMData.groups[g_id].files = new Object;
  //call to server
  sajax_do_call( "efCCM_Ajax", ["fetch_files", CCMData.currentGroup, null, null], renderFiles);
 
  /*Populate the group members*/
  CCMData.groups[g_id].members = new Object;
  CCMData.groups[g_id].privelages = new Object;
  //call to server
  sajax_do_call( "efCCM_Ajax", ["fetch_gmem", CCMData.currentGroup, null, null], renderGMem );
 
  /*Populate the group discussion*/ 
  renderDiscussion();
 
  CCMData.groups[g_id].cached = true;
}
 
/*this is the object that holds all the data retrieved from the database at
 *some point*/
var CCMData = new Object;
/*this is the assosiative array (created as an object) that will story all
 *the member objects when they are created*/
var memObjArray = new Object;
/*his variable looks after only opening one textarea at a time*/
var input_ready = false;

[edit] CCM.css

#CCM-body ul{
  list-style: none;
  display:inline;
  padding:0;
  margin:0;
  color:white;
}
#CCM-body li{
  display:inline;
  background: #35064E;
  margin-right: 0.3em;
}
#CCM-body .floatright {
  float: right;
  margin-right: 0;
  margin-left: 0.4em;
  background: #35064E;
  color:white;
}
.textdiv {
  background: white;
  border: 0.1em solid #35064E;
  padding: 0.2em;
}
.CCM-main {
  background: #E2DCE6;
}
#CCM-body table {
  background: none;
}
.CCM-left {
  width: 15em;
}
#CCM-files ul, #CCM-members ul{
  display:block;
  color: black;
}
#CCM-files li, #CCM-members li{
  display:block;
  background: none;
}
#CCM-extra {
  padding:0.3em;
}
#primarytable td {
  padding:0.3em;
}
#discussion-item {
  font-weight: none;
  border-top: 0.1em dashed #EC008C;
}
#discussion-item-hl {
  font-weight: bold;
  border-top: 0.1em dashed #EC008C;
}
.discussion-text {
  margin-left:1em;
}
#tab-group {
  display:none;
}
#CCM-gDis-in {
  margin:1em;
  width:95%;
  border:1px dashed #35064E;
}
#CCM-floatDiv {
  position:absolute;
  top: 10px;
  left: 10px;
  background-color:#E2DCE6;
  z-index: 10;
  border:0.1em solid #35064E;
}
#CCM-floatDivHead {
  background-color: #35064E;
  color: white;
  font-weight: bold;
  width: 100%;
}
#CCM-AddDiv {
  background-color:white;
  margin: 1em;
}
#CCM-AddDiv table {
  background: #35064E;
}
#CCM-AddDiv td,#CCM-AddDiv th {
  background: white;
}
#CCM-AddDiv input {
  z-index: 11;
}
#CCM-members select {
  border: none;
}

[edit] dbConfig.sql

CREATE TABLE IF NOT EXISTS tw_ccm_user_groups (
  ccm_ug_user_id            int(10)   UNSIGNED  NOT NULL,
  ccm_ug_group_id           int(10)   UNSIGNED  NOT NULL,
  ccm_ug_privelage          tinyint   UNSIGNED  NOT NULL DEFAULT '1'
);
 
CREATE TABLE IF NOT EXISTS tw_ccm_group_discussion (
  ccm_gd_group_id           int(10)   UNSIGNED  NOT NULL,
  ccm_gd_discussion_number  int(10)   UNSIGNED  NOT NULL  AUTO_INCREMENT,
  ccm_gd_discussion_user_id int(10)   UNSIGNED  NOT NULL,
  ccm_gd_discussion_text    blob,
  PRIMARY KEY( ccm_gd_group_id, ccm_gd_discussion_number )
)type=MyISAM;
 
CREATE TABLE IF NOT EXISTS tw_ccm_group (
  ccm_g_group_id            int(10)   UNSIGNED  NOT NULL  AUTO_INCREMENT,
  ccm_g_group_name          tinyblob            NOT NULL,
  PRIMARY KEY (ccm_g_group_id)
);
 
CREATE TABLE IF NOT EXISTS tw_ccm_files (
  ccm_f_group_id            int(10)   UNSIGNED  NOT NULL,
  ccm_f_file_id             int(10)   UNSIGNED  NOT NULL  AUTO_INCREMENT, 
  ccm_f_file_name           tinyblob            NOT NULL,
  PRIMARY KEY ( ccm_f_file_id )
);
 
CREATE TABLE IF NOT EXISTS tw_ccm_versions (
  ccm_v_file_id             int(10)   UNSIGNED  NOT NULL, 
  ccm_v_version_id          int(10)   UNSIGNED  NOT NULL  AUTO_INCREMENT, 
  ccm_v_file_realname       tinyblob            NOT NULL,
  ccm_v_editor_id           int(10)   UNSIGNED  NOT NULL,
  ccm_v_edit_time           datetime            NOT NULL,
  PRIMARY KEY (ccm_v_file_id,ccm_v_version_id )
)type=MyISAM;
 
CREATE TABLE IF NOT EXISTS tw_ccm_file_discussion (
  ccm_fd_file_id            int(10)   UNSIGNED  NOT NULL,
  ccm_fd_discussion_number  int(10)   UNSIGNED  NOT NULL  AUTO_INCREMENT,
  ccm_fd_discussion_user_id int(10)   UNSIGNED  NOT NULL,
  ccm_fd_discussion_text    blob,
  ccm_fd_discussion_time    datetime            NOT NULL,
  PRIMARY KEY(ccm_fd_file_id,ccm_fd_discussion_number )
)type=MyISAM;
 
CREATE INDEX ccm_index_ug_user
  ON tw_ccm_user_groups ( ccm_ug_user_id );
CREATE INDEX ccm_index_ug_group
  ON tw_ccm_user_groups ( ccm_ug_group_id );

[edit] CCM.i18n.php

<?php
$allMessages = array(
  'en' => array( 
    'ccm' => 'Collaborative Content Management',
    'nogroups' => 'Error: You belong to zero groups. This page is for users who belong to atleast one group',
    'error' => 'Error'
  )
);
?>

[edit] Changes

[edit] 0.0.2

  • Fixed potential insertion attack bug
  • Improved readability

[edit] Future Improvements

  • Find a better and easier to track/read way of accesing DOM from JavaScript
  • Fix the bugs that appear when using IE
  • Try to break the code up into more chunks if it continues to grow
  • Rewrite parts of the code to use MediaWiki's built in server API
  • Improve the layout and readability of this page