r62769 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r62768‎ | r62769 | r62770 >
Date:14:16, 21 February 2010
Author:questpc
Status:deferred
Tags:
Comment:
Initial commit of EditConflict extension
Modified paths:
  • /trunk/extensions/EditConflict (added) (history)
  • /trunk/extensions/EditConflict/COPYING (added) (history)
  • /trunk/extensions/EditConflict/CurrentEdits.php (added) (history)
  • /trunk/extensions/EditConflict/EditConflict.php (added) (history)
  • /trunk/extensions/EditConflict/EditConflict_i18n.php (added) (history)
  • /trunk/extensions/EditConflict/INSTALL (added) (history)
  • /trunk/extensions/EditConflict/README (added) (history)
  • /trunk/extensions/EditConflict/notify.css (added) (history)
  • /trunk/extensions/EditConflict/notify.js (added) (history)
  • /trunk/extensions/EditConflict/tables.src (added) (history)

Diff [purge]

Index: trunk/extensions/EditConflict/notify.css
@@ -0,0 +1,9 @@
 2+/*
 3+ * Group-level based edit page access for MediaWiki. Monitors current edit sessions.
 4+ * Version 0.4.2
 5+ *
 6+ */
 7+
 8+div#editconflict_notify { text-align:left; font-size:9pt; }
 9+div#editconflict_notify span.closelink { cursor:pointer; text-align:left; font-style:normal; }
 10+div#editconflict_popup { position:absolute; top:0px; left:0px; text-align:center; padding:5px; border:1px solid black; background:IndianRed; color:white; width:auto; height:auto; font-size:9pt; font-weight:bold; visibility:hidden; z-index:1000; }
\ No newline at end of file
Property changes on: trunk/extensions/EditConflict/notify.css
___________________________________________________________________
Name: svn:eol-style
111 + native
Index: trunk/extensions/EditConflict/tables.src
@@ -0,0 +1,48 @@
 2+ -- make sure exactly same tables are defined in maintenance/tables.sql if you want to use mediawiki config/index.php
 3+CREATE TABLE /*$wgDBprefix*/ec_edit_conflict (
 4+ -- revision id of destination article (also used in UI as unique ID)
 5+ -- it is required because the same page can have more than one conflict caused by the same user
 6+ ns_user_rev_id int unsigned NOT NULL,
 7+
 8+ -- A source page which content was copied
 9+ page_namespace int NOT NULL,
 10+ -- The rest of the title, as text.
 11+ -- Spaces are transformed into underscores in title storage.
 12+ page_title varchar(255) binary NOT NULL,
 13+
 14+ -- Timestamp of destination revision
 15+ page_touched binary(14) NOT NULL default '',
 16+
 17+ -- user whose editing has caused an conflict
 18+ user_name varchar(255) binary NOT NULL default '',
 19+
 20+ PRIMARY KEY ns_user_rev_id (ns_user_rev_id),
 21+ INDEX page (page_namespace, page_title),
 22+ INDEX page_touched (page_touched),
 23+ INDEX user_name (user_name)
 24+) /*$wgDBTableOptions*/;
 25+
 26+CREATE TABLE /*$wgDBprefix*/ec_current_edits (
 27+ -- unique edit id for ajax operations
 28+ edit_id int unsigned NOT NULL auto_increment,
 29+ -- edited page namespace
 30+ page_namespace int NOT NULL,
 31+ -- The rest of the title, as text.
 32+ -- Spaces are transformed into underscores in title storage.
 33+ page_title varchar(255) binary NOT NULL,
 34+
 35+ -- timestamp of start of the edit (used to timeout the edit)
 36+ start_time binary(14) NOT NULL default '',
 37+ -- timestamp of last ajax check of the edit (used to timeout the edit)
 38+ edit_time binary(14) NOT NULL default '',
 39+
 40+ -- the user who edits the page
 41+ user_name varchar(255) binary NOT NULL default '',
 42+
 43+ PRIMARY KEY edit_id (edit_id),
 44+ UNIQUE INDEX user_page (user_name(50), page_namespace, page_title(150)),
 45+ INDEX page (page_namespace, page_title(200)),
 46+ INDEX start_time (start_time),
 47+ INDEX edit_time (edit_time),
 48+ INDEX user_name (user_name)
 49+) /*$wgDBTableOptions*/;
Index: trunk/extensions/EditConflict/CurrentEdits.php
@@ -0,0 +1,291 @@
 2+<?php
 3+/*
 4+ * Group-level based edit page access for MediaWiki. Monitors current edit sessions.
 5+ * Version 0.4.2
 6+ *
 7+ */
 8+
 9+if ( !defined( 'MEDIAWIKI' ) ) {
 10+ die( "This file is part of the EditConflict extension. It is not a valid entry point.\n" );
 11+}
 12+
 13+define( 'EC_DEFAULT_ORDER_KEY', 'page' );
 14+
 15+
 16+class ec_CurrentEdits extends SpecialPage {
 17+
 18+ var $db;
 19+
 20+ public function __construct() {
 21+ parent::__construct( 'CurrentEdits', 'delete' );
 22+ wfLoadExtensionMessages('EditConflict');
 23+ }
 24+
 25+ public function execute( $par ) {
 26+ global $wgOut, $wgRequest;
 27+ global $wgUser;
 28+ if ( !$wgUser->isAllowed( 'delete' ) ) {
 29+ $wgOut->permissionRequired('delete');
 30+ return;
 31+ }
 32+ $skin = $wgUser->getSkin();
 33+ $this->setHeaders();
 34+ $this->db = wfGetDB( DB_SLAVE );
 35+ if ( ( $result = $this->checkTables( $this->db ) ) !== true ) {
 36+ $wgOut->addHTML( $result );
 37+ return;
 38+ }
 39+ # normal processing
 40+ list( $limit, $offset ) = wfCheckLimits();
 41+ $cmd = $wgRequest->getVal( 'action' );
 42+ $order = $wgRequest->getVal( 'order' );
 43+ # null means default order (by page name)
 44+ if ( !in_array( $order, array( null, 'user', 'time' ) ) ) {
 45+ $order = null;
 46+ }
 47+ if ( $cmd == null ) {
 48+ $cel = new ec_CurrentEditsList( $order );
 49+ $cel->doQuery( $offset, $limit );
 50+ } elseif ( $cmd == 'delete' ) {
 51+ if ( ( $edit_id = $wgRequest->getVal( 'id' ) ) !== null ) {
 52+ $this->deleteSession( $edit_id );
 53+ if ( $order == null ) {
 54+ $wgOut->redirect( $this->getTitle()->getFullURL() );
 55+ } else {
 56+ $wgOut->redirect( $this->getTitle()->getFullURL( 'order=' . $order ) );
 57+ }
 58+ }
 59+ }
 60+ }
 61+
 62+ private function checkTables() {
 63+ $sql_tables = array(
 64+ 'ec_edit_conflict',
 65+ 'ec_current_edits'
 66+ );
 67+ // check whether the tables were initialized
 68+ $tablesFound = 0;
 69+ $result = true;
 70+ foreach ( $sql_tables as $table ) {
 71+ $tname = str_replace( "`", "'", $this->db->tableName( $table ) );
 72+ $res = $this->db->query( "SHOW TABLE STATUS LIKE $tname" );
 73+ if ( $this->db->numRows( $res ) > 0 ) {
 74+ $tablesFound++;
 75+ }
 76+ }
 77+ if ( $tablesFound == 0 ) {
 78+ # no tables were found, initialize the DB completely
 79+ $r = $this->db->sourceFile( EditConflict::$ExtDir . "/tables.src" );
 80+ if ( $r === true ) {
 81+ $result = 'Tables were initialized.<br />Please <a href="#" onclick="window.location.reload()">reload</a> this page to view future page edits.';
 82+ } else {
 83+ $result = $r;
 84+ }
 85+ } else {
 86+ if ( $tablesFound != count( $sql_tables ) ) {
 87+ # some tables are missing, serious DB error
 88+ $result = "Some of the extension database tables are missing.<br />Please restore from backup or drop extension tables, then reload this page.";
 89+ }
 90+ }
 91+ return $result;
 92+ }
 93+
 94+ private function deleteSession( $edit_id ) {
 95+ $this->db->delete( 'ec_current_edits', array( 'edit_id'=>$edit_id ), __METHOD__ );
 96+ }
 97+}
 98+
 99+if ( !class_exists( 'ec_QueryPage' ) ) {
 100+ abstract class ec_QueryPage extends QueryPage {
 101+
 102+ static $skin = null;
 103+
 104+ public function __construct() {
 105+ global $wgUser;
 106+ if ( self::$skin == NULL ) {
 107+ self::$skin = $wgUser->getSkin();
 108+ }
 109+ }
 110+
 111+ function doQuery( $offset, $limit, $shownavigation=true ) {
 112+ global $wgUser, $wgOut, $wgLang, $wgContLang;
 113+
 114+ $res = $this->getIntervalResults( $offset, $limit );
 115+ $num = count($res);
 116+
 117+ $sk = $wgUser->getSkin();
 118+ $sname = $this->getName();
 119+
 120+ if($shownavigation) {
 121+ $wgOut->addHTML( $this->getPageHeader() );
 122+
 123+ // if list is empty, display a warning
 124+ if( $num == 0 ) {
 125+ $wgOut->addHTML( '<p>' . wfMsgHTML('specialpage-empty') . '</p>' );
 126+ return;
 127+ }
 128+
 129+ $top = wfShowingResults( $offset, $num );
 130+ $wgOut->addHTML( "<p>{$top}\n" );
 131+
 132+ // often disable 'next' link when we reach the end
 133+ $atend = $num < $limit;
 134+
 135+ $sl = wfViewPrevNext( $offset, $limit ,
 136+ $wgContLang->specialPage( $sname ),
 137+ wfArrayToCGI( $this->linkParameters() ), $atend );
 138+ $wgOut->addHTML( "<br />{$sl}</p>\n" );
 139+ }
 140+ if ( $num > 0 ) {
 141+ $s = array();
 142+ if ( ! $this->listoutput )
 143+ $s[] = $this->openList( $offset );
 144+
 145+ foreach ($res as $r) {
 146+ $format = $this->formatResult( $sk, $r );
 147+ if ( $format ) {
 148+ $s[] = $this->listoutput ? $format : "<li>{$format}</li>\n";
 149+ }
 150+ }
 151+
 152+ if ( ! $this->listoutput )
 153+ $s[] = $this->closeList();
 154+ $str = $this->listoutput ? $wgContLang->listToText( $s ) : implode( '', $s );
 155+ $wgOut->addHTML( $str );
 156+ }
 157+ if($shownavigation) {
 158+ $wgOut->addHTML( "<p>{$sl}</p>\n" );
 159+ }
 160+ return $num;
 161+ }
 162+
 163+ function getName() {
 164+ return "CurrentEdits";
 165+ }
 166+
 167+ function isExpensive() {
 168+ return false; // disables caching
 169+ }
 170+
 171+ function isSyndicated() {
 172+ return false;
 173+ }
 174+
 175+ }
 176+}
 177+
 178+class ec_CurrentEditsList extends ec_QueryPage {
 179+
 180+ var $order;
 181+ var $order_key;
 182+ var $order_string;
 183+ var $order_strings = array (
 184+ 'page'=>'page_namespace, page_title, user_name, start_time',
 185+ 'user'=>'user_name, page_namespace, page_title, start_time',
 186+ 'time'=>'start_time, page_namespace, page_title, user_name'
 187+ );
 188+ var $order_queries = array ();
 189+
 190+ public function __construct( $order ) {
 191+ global $wgUser;
 192+ parent::__construct();
 193+ $skin = $wgUser->getSkin();
 194+ $this->order = $order;
 195+ $this->order_key = ($order === null) ? 'page' : $order;
 196+ $this->order_string = &$this->order_strings[ $this->order_key ];
 197+ foreach ( $this->order_strings as $order_key => &$order_val ) {
 198+ # default order requires no pass of order GET param
 199+ if ( $order_key == EC_DEFAULT_ORDER_KEY ) {
 200+ $action = '';
 201+ } else {
 202+ $action = 'order=' . $order_key;
 203+ }
 204+ $msg = wfMsg( 'ec_order_' . $order_key );
 205+ if ( $order_key == $this->order_key ) {
 206+ # do not link the selected order
 207+ $this->order_queries[ $order_key ] = $msg;
 208+ } else {
 209+ # link all other orders
 210+ $this->order_queries[ $order_key ] = $skin->makeKnownLinkObj( $this->getTitle(), $msg , $action );
 211+ }
 212+ }
 213+ }
 214+
 215+ function sec2hours( $seconds ) {
 216+ $ss = $seconds;
 217+ $hh = $mm = 0;
 218+ if ( $ss > 59 ) {
 219+ $mm = floor( $ss / 60 );
 220+ $ss = fmod( $ss, 60 );
 221+ if ( $mm > 59 ) {
 222+ $hh = floor( $mm / 60 );
 223+ $mm = fmod( $mm, 60 );
 224+ }
 225+ }
 226+ return sprintf( wfMsg( 'ec_time_sprintf' ), $hh, $mm, $ss );
 227+ }
 228+
 229+ function getEditingTime( $start_timestamp ) {
 230+ $start_seconds = wfTimestamp( TS_UNIX, $start_timestamp );
 231+ $current_seconds = wfTimestamp( TS_UNIX, time() );
 232+ return $this->sec2hours( floatval( $current_seconds ) - floatval( $start_seconds ) );
 233+ }
 234+
 235+ function getIntervalResults( $offset, $limit ) {
 236+ $result = array();
 237+ $db = & wfGetDB( DB_SLAVE );
 238+ EditConflict::deleteExpiredData( $db );
 239+ $res = $db->select(
 240+ 'ec_current_edits',
 241+ array( 'edit_id', 'page_namespace as ns', 'page_title as title', 'start_time', 'user_name' ),
 242+ '',
 243+ __METHOD__,
 244+ array( 'ORDER BY'=>$this->order_string,
 245+ 'OFFSET'=>intval( $offset ),
 246+ 'LIMIT'=>intval( $limit ) ) );
 247+ while( $row = $db->fetchObject( $res ) ) {
 248+ $result[] = $row;
 249+ }
 250+ return $result;
 251+ }
 252+
 253+ function formatResult( $skin, $result ) {
 254+ global $wgLang, $wgContLang;
 255+ $title = Title::makeTitle( $result->ns, $result->title );
 256+ $title_link = $skin->makeKnownLinkObj( $title );
 257+ $user = User::newFromName( $result->user_name );
 258+ if ( $user instanceof User ) {
 259+ $user_weight = EditConflict::getGroupWeight( $user );
 260+ } else {
 261+ $user_weight = -1;
 262+ }
 263+ if ( $user instanceof User ) {
 264+ $user_page = $user->getUserPage();
 265+ $user_page_link = $skin->makeKnownLinkObj( $user_page );
 266+ } else {
 267+ $user_page_link = htmlspecialchars( $result->user_name );
 268+ }
 269+ $start_time = $wgLang->timeAndDate( $result->start_time, true );
 270+ $editing_time = $this->getEditingTime( $result->start_time );
 271+ $action = 'action=delete&id=' . $result->edit_id;
 272+ if ( $this->order_key != EC_DEFAULT_ORDER_KEY ) {
 273+ $action .= '&order=' . $this->order_key;
 274+ }
 275+ $session_close_link = $skin->makeKnownLinkObj( $this->getTitle(), '&#8251;', $action, '', '', 'title="Close this session."' );
 276+ return wfMsg( 'ec_list_order_' . $this->order_key, $title_link, $user_page_link, htmlspecialchars( $user_weight ), htmlspecialchars( $editing_time ), $session_close_link );
 277+ }
 278+
 279+ function getPageHeader() {
 280+ list( $link1, $link2, $link3 ) = array_values( $this->order_queries );
 281+ return '<div style="font-size:9pt;font-style:italic;">' . wfMsg( 'ec_header_warning' ) . '</div><div style="margin-top:5px;font-size:12pt;font-weight:bold;">' . wfMsg( 'ec_header_order', $link1, $link2, $link3 ) .'</div>';
 282+ }
 283+
 284+ function linkParameters() {
 285+ $params = Array();
 286+ if ( $this->order_key != EC_DEFAULT_ORDER_KEY ) {
 287+ $params[ "order" ] = $this->order_key;
 288+ }
 289+ return $params;
 290+ }
 291+
 292+}
\ No newline at end of file
Property changes on: trunk/extensions/EditConflict/CurrentEdits.php
___________________________________________________________________
Name: svn:eol-style
1293 + native
Index: trunk/extensions/EditConflict/notify.js
@@ -0,0 +1,263 @@
 2+/*
 3+ * Group-level based edit page access for MediaWiki. Monitors current edit sessions.
 4+ * Version 0.4.2
 5+ *
 6+ */
 7+
 8+var EditConflict = {
 9+
 10+ // time of sleep before looped AJAX call during the editpage (in seconds)
 11+ // !! should be at least twice shorter than 'EC_AJAX_EXPIRE_TIME' defined in PHP !!
 12+ editSleep: 30,
 13+ // non-existent editId, which means that the editing session was finished
 14+ nobodyEditing: -1,
 15+
 16+ // called from body.onload to display copied pages notification list via AJAX (if there's any)
 17+ // @param: args[0] - article id
 18+ // @param: args[1..n] - list of revid's created by current user which were copied due to edit conflict
 19+ getNotifyText : function ( args ) {
 20+ // PHP will build copied pages notification list
 21+ // ajax callback will display them (if available)
 22+ sajax_do_call( "EditConflict::getNotifyText", args , EditConflict.displayNotification );
 23+ },
 24+
 25+ // called from copied pages notification list link
 26+ clearRevId : function ( revId ) {
 27+ // PHP will remove the selected revid from the edit conflicts table
 28+ // ajax callback will remove the corresponding DOM entry
 29+ sajax_do_call( "EditConflict::clearRevId", [ revId ], EditConflict.hideCheckout );
 30+ },
 31+
 32+ // loop for watching the editing page
 33+ // can be called in two ways:
 34+ // initlal call is performed from body.onload
 35+ // @param request - numeric editId
 36+ // further calls are performed via AJAX every editSleep seconds
 37+ // @param request - AJAX request object, containing the value of editId
 38+ watchEdit : function ( request ) {
 39+ // non-existent editId indicates an error
 40+ // OR the session is over (editid entry in DB was deleted)
 41+ var editId = EditConflict.nobodyEditing;
 42+ if ( request.responseText ) {
 43+ if ( request.status == 200) {
 44+ // get editId from AJAX callback
 45+ editId = request.responseText;
 46+ }
 47+ } else {
 48+ // get editId from the header script call parameter
 49+ editId = request;
 50+ }
 51+ if ( editId != EditConflict.nobodyEditing ) {
 52+ // php will update edit_time in the current edits table so the ongoing editing session won't expire
 53+ // ajax callback will loop until the user will leave an editpage
 54+ // setTimeout() is used to reduce server load
 55+ window.setTimeout( function () { sajax_do_call( "EditConflict::markEditing", [ editId ], EditConflict.watchEdit ); },
 56+ EditConflict.editSleep * 1000 );
 57+ }
 58+ },
 59+
 60+ // AJAX callback for displaying generated copied revisions notify list before the Skin 'siteSub' element
 61+ // ( list of the copied conflicting edits )
 62+ // @param request.responseText - html text of "copied resivions notify list" that will be placed before the 'siteSub'
 63+ displayNotification : function ( request ) {
 64+ var taglist = request.responseText;
 65+ if (request.status != 200) {
 66+ taglist = "<div class='error'> " + request.status + " " + request.statusText + ": " + taglist + "</div>";
 67+ }
 68+ if ( taglist != '' ) {
 69+ try {
 70+ var div = document.createElement( 'DIV' );
 71+ div.setAttribute( 'id', 'editconflict_notify' );
 72+ div.innerHTML = taglist;
 73+ var siteSub = document.getElementById( 'siteSub' );
 74+ siteSub.parentNode.insertBefore( div, siteSub );
 75+ } catch ( e ) {
 76+ alert( 'Error: current Skin doesn\'t have \'siteSub\' element' );
 77+ }
 78+ }
 79+ },
 80+
 81+ // @return boolean: whether the element has next 'SPAN' sibling or not
 82+ nextSpanSibling : function ( element ) {
 83+ var next = element;
 84+ do {
 85+ next = next.nextSibling;
 86+ if ( next && next.nodeType == 1 && next.tagName == 'SPAN' ) {
 87+ return true;
 88+ }
 89+ } while ( next );
 90+ return false;
 91+ },
 92+
 93+ // @return boolean: whether the element has previous 'SPAN' sibling or not
 94+ prevSpanSibling : function ( element ) {
 95+ var prev = element;
 96+ do {
 97+ prev = prev.previousSibling;
 98+ if ( prev && prev.nodeType == 1 && prev.tagName == 'SPAN' ) {
 99+ return true;
 100+ }
 101+ } while ( prev );
 102+ return false;
 103+ },
 104+
 105+ // called via ajax callback
 106+ // removes a DOM node which contains revid notification entry
 107+ // from the displayed "copied revision notify list"
 108+ // @param request.responseText - single revid value
 109+ hideCheckout : function ( request ) {
 110+ var revid = request.responseText;
 111+ var parent_li, hasSpanSiblings;
 112+ // !warning! the following code is depending on the structure of generated 'DIV' id='editconflict_notify' element!
 113+ if (request.status == 200 && revid != '') {
 114+ try {
 115+ // get span with selected revid link
 116+ var span = document.getElementById( 'EditConflict_' + revid );
 117+ // check whether selected span has prev or next span siblings
 118+ hasSpanSiblings = EditConflict.nextSpanSibling( span ) || EditConflict.prevSpanSibling( span );
 119+ // get parent 'LI' of the current 'SPAN' list
 120+ parent_li = span.parentNode;
 121+ if ( hasSpanSiblings ) {
 122+ // remove only current element
 123+ parent_li.removeChild( span );
 124+ } else {
 125+ // no siblings - remove the parent (the whole 'LI')
 126+ parent_li.parentNode.removeChild( parent_li );
 127+ }
 128+ } catch ( e ) {
 129+ alert( 'Error: non-existent revid was given' );
 130+ }
 131+ }
 132+ },
 133+
 134+ // browser-independent addevent function
 135+ addEvent : function ( obj, type, fn ) {
 136+ if ( document.getElementById && document.createTextNode ) {
 137+ if (obj.addEventListener) {
 138+ obj.addEventListener( type, fn, false );
 139+ }
 140+ else if (obj.attachEvent) {
 141+ obj["e"+type+fn] = fn;
 142+ obj[type+fn] = function() { obj["e"+type+fn]( window.event ); }
 143+ obj.attachEvent( "on"+type, obj[type+fn] );
 144+ }
 145+ else {
 146+ obj["on"+type] = obj["e"+type+fn];
 147+ }
 148+ }
 149+ },
 150+
 151+ // get absolute on-page coordinates of the selected DOM obj
 152+ // @param obj - DOM obj
 153+ // @return object { left:x,top:y } - the values of coordinates
 154+ GetElementPosition : function ( obj ) {
 155+ var coords = { left: 0, top: 0 };
 156+ while ( obj ) {
 157+ coords.left += obj.offsetLeft;
 158+ coords.top += obj.offsetTop;
 159+ obj = obj.offsetParent;
 160+ }
 161+ return coords;
 162+ },
 163+
 164+ // edit action anchor
 165+ editAnchor : null,
 166+ // original (saved) value of edit action anchor href
 167+ editHref : null,
 168+ // popup div element
 169+ popup : null,
 170+
 171+ // attach ajax php call to anchor onclick event
 172+ // @param articleId - article_id of the current title
 173+ // @param a - anchor
 174+ // @param disallowEdit - whether the user of higher weight already edits the article
 175+ attachToEditAnchor : function ( articleId, a, disallowEdit ) {
 176+ EditConflict.editAnchor = a;
 177+ // save original anchor href
 178+ EditConflict.editHref = a.getAttribute( 'href' );
 179+ EditConflict.editTooltip = a.getAttribute( 'title' );
 180+ // disable anchor href because event "return false" may fail sometimes
 181+ // ( we will use window.location.href instead )
 182+ a.href = '#';
 183+ if ( disallowEdit ) {
 184+ // clear invitation tooltip
 185+ a.setAttribute( 'title', '' );
 186+ }
 187+ EditConflict.addEvent( a, "click",
 188+ function () {
 189+ // php will check, whether another user with higher edit weight alreay edits the page
 190+ // ajax callback will go to edit page or will display a warning that editing is not allowed,
 191+ // ( based on the result from php call )
 192+ sajax_do_call( "EditConflict::checkEditButton", [ articleId ] , EditConflict.clickEventResult );
 193+ return true;
 194+ }
 195+ );
 196+ },
 197+
 198+ // create popup div message
 199+ // this.popup property will store div popup object
 200+ createPopup : function ( text ) {
 201+ var div = document.createElement( 'DIV' );
 202+ div.setAttribute( 'id', 'editconflict_popup' );
 203+ div.innerHTML = text;
 204+ document.body.appendChild( div );
 205+ EditConflict.popup = div;
 206+ },
 207+
 208+ // display the popup div message just below the obj in the selected number of seconds
 209+ displayPopup : function ( obj, seconds ) {
 210+ var obj_coords = EditConflict.GetElementPosition( obj );
 211+ var displayShift = { x: 20, y: obj.offsetHeight };
 212+ var ec_size = { w: EditConflict.popup.offsetWidth, h: EditConflict.popup.offsetHeight };
 213+ delta = document.body.scrollWidth - obj_coords.left - displayShift.x - ec_size.w;
 214+ if ( delta < 0 ) {
 215+ displayShift.x -= delta;
 216+ }
 217+ delta = document.body.scrollHeight - obj_coords.top - displayShift.y - ec_size.h;
 218+ if ( delta < 0 ) {
 219+ displayShift.y -= delta;
 220+ }
 221+ EditConflict.popup.style.left = (obj_coords.left + displayShift.x) + "px";
 222+ EditConflict.popup.style.top = (obj_coords.top + displayShift.y) + "px";
 223+ EditConflict.popup.style.visibility = 'visible';
 224+ window.setTimeout(
 225+ function () {
 226+ EditConflict.popup.style.visibility = 'hidden';
 227+ },
 228+ seconds * 1000
 229+ );
 230+ },
 231+
 232+ // called from body.onload to find edit anchor and to attach an event to it
 233+ // @param articleId - article_id of current title
 234+ // @param disallowEdit - whether the user of higher weight already edits the article
 235+ findEditKey : function ( articleId, disallowEdit ) {
 236+ var as = document.body.getElementsByTagName('a');
 237+ for ( var i=0; i<as.length; i++ ) {
 238+ // find an edit access key
 239+ if ( as[i].getAttribute( 'accesskey' ) == 'e' ) {
 240+ EditConflict.createPopup( ec_already_editing );
 241+ EditConflict.attachToEditAnchor( articleId, as[i], disallowEdit );
 242+ return;
 243+ }
 244+ }
 245+ },
 246+
 247+ // ajax callback
 248+ // @param request.responseText - 'y' allows to go to edit page; 'n' - blocks edit link and outputs a popup warning
 249+ clickEventResult : function ( request ) {
 250+ var result = request.responseText;
 251+ var clickEventResult = true;
 252+ if (request.status == 200 ) {
 253+ if (request.responseText == 'y') {
 254+ // editing is allowed, going to edit location
 255+ window.location.href = EditConflict.editHref;
 256+ } else {
 257+ // editing is disabled - show a popup warning for 5 seconds
 258+ EditConflict.editAnchor.blur();
 259+ EditConflict.editAnchor.setAttribute('title','');
 260+ EditConflict.displayPopup( EditConflict.editAnchor, 5 );
 261+ }
 262+ }
 263+ }
 264+}
Property changes on: trunk/extensions/EditConflict/notify.js
___________________________________________________________________
Name: svn:eol-style
1265 + native
Index: trunk/extensions/EditConflict/INSTALL
@@ -0,0 +1,42 @@
 2+MediaWiki extension EditConflict, version 0.4.2
 3+
 4+Extract an archive with extension to your wiki extensions directory.
 5+
 6+Place the following lines to the "extensions section" of LocalSettings.php:
 7+
 8+require_once( "$IP/extensions/EditConflict/EditConflict.php" );
 9+EditConflict::$groupWeights[<usergroupname>] = <weight_of_group>;
 10+
 11+Login as wiki sysop and open Special:Currentedits page to initialize
 12+extension's database tables.
 13+
 14+Default value of EditConflict::$groupWeights =
 15+Array( '*' => 1, 'user' => 2, 'bureaucrat' => 3, 'sysop' => 4 );
 16+
 17+where the key <usergroupname> of array is name of group, value is weight of the user group.
 18+Members of lower weight groups will be denied to edit the page in case another user
 19+already edits it.
 20+
 21+Place the following line to LocalSettings.php:
 22+
 23+EditConflict::$groupWeights['*'] = 0;
 24+
 25+to set anonymous group weight = 0.
 26+
 27+Zero is special lowest possible value of weight that disables AJAX watching loop
 28+for editing sessions, which makes such edits to be invisible in Special:CurrentEdits page.
 29+However, such edits are not blocking another edits of the same page, and may reduce
 30+the server load (no AJAX / DB calls).
 31+
 32+Use the following setting:
 33+EditConflict::$useEditPageMergeChangesHook = true;
 34+
 35+and patch 'includes/EditPage.php', by replacing the following text:
 36+
 37+if ( $this->mergeChangesInto( $text ) ) {
 38+to:
 39+if ( $this->mergeChangesInto( $text ) || wfRunHooks( 'EditPageMergeChanges', array( $this, $text ) ) ) {
 40+
 41+to enable extra functionality.
 42+
 43+See http://mediawiki.org/wiki/Extension:EditConflict for further details.
\ No newline at end of file
Index: trunk/extensions/EditConflict/COPYING
@@ -0,0 +1,309 @@
 2+The EditConflict extension may be copied and redistributed under either the
 3+DWTFYWWI license or the GNU General Public License, at the option of the
 4+licensee. The text of both licenses is given below.
 5+
 6+The majority of this extension is written by (and copyright) Tim Starling. Minor
 7+modifications have been made by various members of the MediaWiki development
 8+team.
 9+
 10+-------------------------------------------------------------------------------
 11+
 12+ DWTFYWWI LICENSE
 13+ Version 1, January 2006
 14+
 15+ Copyright (C) 2010 Dmitriy Sintsov
 16+
 17+ Preamble
 18+
 19+ The licenses for most software are designed to take away your
 20+freedom to share and change it. By contrast, the DWTFYWWI or Do
 21+Whatever The Fuck You Want With It license is intended to guarantee
 22+your freedom to share and change the software--to make sure the
 23+software is free for all its users.
 24+
 25+ DWTFYWWI LICENSE
 26+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
 27+0. The author grants everyone permission to do whatever the fuck they
 28+want with the software, whatever the fuck that may be.
 29+
 30+-------------------------------------------------------------------------------
 31+
 32+ GNU GENERAL PUBLIC LICENSE
 33+ Version 2, June 1991
 34+
 35+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
 36+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 37+ Everyone is permitted to copy and distribute verbatim copies
 38+ of this license document, but changing it is not allowed.
 39+
 40+ Preamble
 41+
 42+ The licenses for most software are designed to take away your
 43+freedom to share and change it. By contrast, the GNU General Public
 44+License is intended to guarantee your freedom to share and change free
 45+software--to make sure the software is free for all its users. This
 46+General Public License applies to most of the Free Software
 47+Foundation's software and to any other program whose authors commit to
 48+using it. (Some other Free Software Foundation software is covered by
 49+the GNU Lesser General Public License instead.) You can apply it to
 50+your programs, too.
 51+
 52+ When we speak of free software, we are referring to freedom, not
 53+price. Our General Public Licenses are designed to make sure that you
 54+have the freedom to distribute copies of free software (and charge for
 55+this service if you wish), that you receive source code or can get it
 56+if you want it, that you can change the software or use pieces of it
 57+in new free programs; and that you know you can do these things.
 58+
 59+ To protect your rights, we need to make restrictions that forbid
 60+anyone to deny you these rights or to ask you to surrender the rights.
 61+These restrictions translate to certain responsibilities for you if you
 62+distribute copies of the software, or if you modify it.
 63+
 64+ For example, if you distribute copies of such a program, whether
 65+gratis or for a fee, you must give the recipients all the rights that
 66+you have. You must make sure that they, too, receive or can get the
 67+source code. And you must show them these terms so they know their
 68+rights.
 69+
 70+ We protect your rights with two steps: (1) copyright the software, and
 71+(2) offer you this license which gives you legal permission to copy,
 72+distribute and/or modify the software.
 73+
 74+ Also, for each author's protection and ours, we want to make certain
 75+that everyone understands that there is no warranty for this free
 76+software. If the software is modified by someone else and passed on, we
 77+want its recipients to know that what they have is not the original, so
 78+that any problems introduced by others will not reflect on the original
 79+authors' reputations.
 80+
 81+ Finally, any free program is threatened constantly by software
 82+patents. We wish to avoid the danger that redistributors of a free
 83+program will individually obtain patent licenses, in effect making the
 84+program proprietary. To prevent this, we have made it clear that any
 85+patent must be licensed for everyone's free use or not licensed at all.
 86+
 87+ The precise terms and conditions for copying, distribution and
 88+modification follow.
 89+
 90+ GNU GENERAL PUBLIC LICENSE
 91+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
 92+
 93+ 0. This License applies to any program or other work which contains
 94+a notice placed by the copyright holder saying it may be distributed
 95+under the terms of this General Public License. The "Program", below,
 96+refers to any such program or work, and a "work based on the Program"
 97+means either the Program or any derivative work under copyright law:
 98+that is to say, a work containing the Program or a portion of it,
 99+either verbatim or with modifications and/or translated into another
 100+language. (Hereinafter, translation is included without limitation in
 101+the term "modification".) Each licensee is addressed as "you".
 102+
 103+Activities other than copying, distribution and modification are not
 104+covered by this License; they are outside its scope. The act of
 105+running the Program is not restricted, and the output from the Program
 106+is covered only if its contents constitute a work based on the
 107+Program (independent of having been made by running the Program).
 108+Whether that is true depends on what the Program does.
 109+
 110+ 1. You may copy and distribute verbatim copies of the Program's
 111+source code as you receive it, in any medium, provided that you
 112+conspicuously and appropriately publish on each copy an appropriate
 113+copyright notice and disclaimer of warranty; keep intact all the
 114+notices that refer to this License and to the absence of any warranty;
 115+and give any other recipients of the Program a copy of this License
 116+along with the Program.
 117+
 118+You may charge a fee for the physical act of transferring a copy, and
 119+you may at your option offer warranty protection in exchange for a fee.
 120+
 121+ 2. You may modify your copy or copies of the Program or any portion
 122+of it, thus forming a work based on the Program, and copy and
 123+distribute such modifications or work under the terms of Section 1
 124+above, provided that you also meet all of these conditions:
 125+
 126+ a) You must cause the modified files to carry prominent notices
 127+ stating that you changed the files and the date of any change.
 128+
 129+ b) You must cause any work that you distribute or publish, that in
 130+ whole or in part contains or is derived from the Program or any
 131+ part thereof, to be licensed as a whole at no charge to all third
 132+ parties under the terms of this License.
 133+
 134+ c) If the modified program normally reads commands interactively
 135+ when run, you must cause it, when started running for such
 136+ interactive use in the most ordinary way, to print or display an
 137+ announcement including an appropriate copyright notice and a
 138+ notice that there is no warranty (or else, saying that you provide
 139+ a warranty) and that users may redistribute the program under
 140+ these conditions, and telling the user how to view a copy of this
 141+ License. (Exception: if the Program itself is interactive but
 142+ does not normally print such an announcement, your work based on
 143+ the Program is not required to print an announcement.)
 144+
 145+These requirements apply to the modified work as a whole. If
 146+identifiable sections of that work are not derived from the Program,
 147+and can be reasonably considered independent and separate works in
 148+themselves, then this License, and its terms, do not apply to those
 149+sections when you distribute them as separate works. But when you
 150+distribute the same sections as part of a whole which is a work based
 151+on the Program, the distribution of the whole must be on the terms of
 152+this License, whose permissions for other licensees extend to the
 153+entire whole, and thus to each and every part regardless of who wrote it.
 154+
 155+Thus, it is not the intent of this section to claim rights or contest
 156+your rights to work written entirely by you; rather, the intent is to
 157+exercise the right to control the distribution of derivative or
 158+collective works based on the Program.
 159+
 160+In addition, mere aggregation of another work not based on the Program
 161+with the Program (or with a work based on the Program) on a volume of
 162+a storage or distribution medium does not bring the other work under
 163+the scope of this License.
 164+
 165+ 3. You may copy and distribute the Program (or a work based on it,
 166+under Section 2) in object code or executable form under the terms of
 167+Sections 1 and 2 above provided that you also do one of the following:
 168+
 169+ a) Accompany it with the complete corresponding machine-readable
 170+ source code, which must be distributed under the terms of Sections
 171+ 1 and 2 above on a medium customarily used for software interchange; or,
 172+
 173+ b) Accompany it with a written offer, valid for at least three
 174+ years, to give any third party, for a charge no more than your
 175+ cost of physically performing source distribution, a complete
 176+ machine-readable copy of the corresponding source code, to be
 177+ distributed under the terms of Sections 1 and 2 above on a medium
 178+ customarily used for software interchange; or,
 179+
 180+ c) Accompany it with the information you received as to the offer
 181+ to distribute corresponding source code. (This alternative is
 182+ allowed only for noncommercial distribution and only if you
 183+ received the program in object code or executable form with such
 184+ an offer, in accord with Subsection b above.)
 185+
 186+The source code for a work means the preferred form of the work for
 187+making modifications to it. For an executable work, complete source
 188+code means all the source code for all modules it contains, plus any
 189+associated interface definition files, plus the scripts used to
 190+control compilation and installation of the executable. However, as a
 191+special exception, the source code distributed need not include
 192+anything that is normally distributed (in either source or binary
 193+form) with the major components (compiler, kernel, and so on) of the
 194+operating system on which the executable runs, unless that component
 195+itself accompanies the executable.
 196+
 197+If distribution of executable or object code is made by offering
 198+access to copy from a designated place, then offering equivalent
 199+access to copy the source code from the same place counts as
 200+distribution of the source code, even though third parties are not
 201+compelled to copy the source along with the object code.
 202+
 203+ 4. You may not copy, modify, sublicense, or distribute the Program
 204+except as expressly provided under this License. Any attempt
 205+otherwise to copy, modify, sublicense or distribute the Program is
 206+void, and will automatically terminate your rights under this License.
 207+However, parties who have received copies, or rights, from you under
 208+this License will not have their licenses terminated so long as such
 209+parties remain in full compliance.
 210+
 211+ 5. You are not required to accept this License, since you have not
 212+signed it. However, nothing else grants you permission to modify or
 213+distribute the Program or its derivative works. These actions are
 214+prohibited by law if you do not accept this License. Therefore, by
 215+modifying or distributing the Program (or any work based on the
 216+Program), you indicate your acceptance of this License to do so, and
 217+all its terms and conditions for copying, distributing or modifying
 218+the Program or works based on it.
 219+
 220+ 6. Each time you redistribute the Program (or any work based on the
 221+Program), the recipient automatically receives a license from the
 222+original licensor to copy, distribute or modify the Program subject to
 223+these terms and conditions. You may not impose any further
 224+restrictions on the recipients' exercise of the rights granted herein.
 225+You are not responsible for enforcing compliance by third parties to
 226+this License.
 227+
 228+ 7. If, as a consequence of a court judgment or allegation of patent
 229+infringement or for any other reason (not limited to patent issues),
 230+conditions are imposed on you (whether by court order, agreement or
 231+otherwise) that contradict the conditions of this License, they do not
 232+excuse you from the conditions of this License. If you cannot
 233+distribute so as to satisfy simultaneously your obligations under this
 234+License and any other pertinent obligations, then as a consequence you
 235+may not distribute the Program at all. For example, if a patent
 236+license would not permit royalty-free redistribution of the Program by
 237+all those who receive copies directly or indirectly through you, then
 238+the only way you could satisfy both it and this License would be to
 239+refrain entirely from distribution of the Program.
 240+
 241+If any portion of this section is held invalid or unenforceable under
 242+any particular circumstance, the balance of the section is intended to
 243+apply and the section as a whole is intended to apply in other
 244+circumstances.
 245+
 246+It is not the purpose of this section to induce you to infringe any
 247+patents or other property right claims or to contest validity of any
 248+such claims; this section has the sole purpose of protecting the
 249+integrity of the free software distribution system, which is
 250+implemented by public license practices. Many people have made
 251+generous contributions to the wide range of software distributed
 252+through that system in reliance on consistent application of that
 253+system; it is up to the author/donor to decide if he or she is willing
 254+to distribute software through any other system and a licensee cannot
 255+impose that choice.
 256+
 257+This section is intended to make thoroughly clear what is believed to
 258+be a consequence of the rest of this License.
 259+
 260+ 8. If the distribution and/or use of the Program is restricted in
 261+certain countries either by patents or by copyrighted interfaces, the
 262+original copyright holder who places the Program under this License
 263+may add an explicit geographical distribution limitation excluding
 264+those countries, so that distribution is permitted only in or among
 265+countries not thus excluded. In such case, this License incorporates
 266+the limitation as if written in the body of this License.
 267+
 268+ 9. The Free Software Foundation may publish revised and/or new versions
 269+of the General Public License from time to time. Such new versions will
 270+be similar in spirit to the present version, but may differ in detail to
 271+address new problems or concerns.
 272+
 273+Each version is given a distinguishing version number. If the Program
 274+specifies a version number of this License which applies to it and "any
 275+later version", you have the option of following the terms and conditions
 276+either of that version or of any later version published by the Free
 277+Software Foundation. If the Program does not specify a version number of
 278+this License, you may choose any version ever published by the Free Software
 279+Foundation.
 280+
 281+ 10. If you wish to incorporate parts of the Program into other free
 282+programs whose distribution conditions are different, write to the author
 283+to ask for permission. For software which is copyrighted by the Free
 284+Software Foundation, write to the Free Software Foundation; we sometimes
 285+make exceptions for this. Our decision will be guided by the two goals
 286+of preserving the free status of all derivatives of our free software and
 287+of promoting the sharing and reuse of software generally.
 288+
 289+ NO WARRANTY
 290+
 291+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
 292+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
 293+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
 294+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
 295+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 296+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
 297+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
 298+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
 299+REPAIR OR CORRECTION.
 300+
 301+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
 302+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
 303+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
 304+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
 305+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
 306+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
 307+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
 308+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
 309+POSSIBILITY OF SUCH DAMAGES.
 310+
Index: trunk/extensions/EditConflict/EditConflict_i18n.php
@@ -0,0 +1,49 @@
 2+<?php
 3+/*
 4+ * Group-level based edit page access for MediaWiki. Monitors current edit sessions.
 5+ * Version 0.4.2
 6+ *
 7+ */
 8+
 9+/**
 10+ * Messages list.
 11+ */
 12+
 13+$messages = array();
 14+
 15+/** English (English)
 16+ * @author QuestPC
 17+ */
 18+$messages['en'] = array(
 19+ 'currentedits' => 'Monitors current edit sessions',
 20+ 'editconflict_desc' => 'Group-level based edit page access. Monitors current edit sessions.',
 21+ 'ec_already_editing'=>'The user which has a higher group weight<br />already edits this page.<br/>Please wait until he/she ends the editing session.',
 22+ 'ec_copied_revisions'=>'Following your edits were copied into the subpage of your userpage due to edit conflict with another user who belongs to usergroup of higher weight than yours:',
 23+ 'ec_header_warning'=>'Warning: user edits which has (group weight = 0) are not included to reduce server load',
 24+ 'ec_order_page'=>'page title',
 25+ 'ec_order_user'=>'user name',
 26+ 'ec_order_time'=>'editing time',
 27+ 'ec_header_order'=>'Order by: $1, $2, $3.',
 28+ 'ec_list_order_page'=>'$1, $2 (weight=$3), editing time $4. Click to close: $5',
 29+ 'ec_list_order_user'=>'$2 (weight=$3), $1, editing time $4. Click to close: $5',
 30+ 'ec_list_order_time'=>'Editing time $4, $1, $2 (weight=$3). Click to close: $5',
 31+ 'ec_time_sprintf'=>'%02d:%02d:%02d'
 32+);
 33+
 34+/** Russian (Русский)
 35+ * @author QuestPC
 36+ */
 37+$messages['ru'] = array(
 38+ 'currentedits' => 'Просмотр текущих сессий редактирования',
 39+ 'editconflict_desc' => 'Доступ к странице правки в соответствии с правами группы. Просмотр текущих сессий редактирования.',
 40+ 'ec_already_editing'=>'Пользователь с более высоким статусом<br />уже редактирует данную страницу.<br/>Пожалуйста подождите окончания редактирования.',
 41+ 'ec_copied_revisions'=>'Следующие Ваши правки были перенесены в подстраницу Вашей пользовательской страницы из-за конфликта правок с пользователем, имеющим более высокий статус чем Ваш:',
 42+ 'ec_header_warning'=>'Предупреждение: сессии правок пользователей, входящих в группы с минимальным приоритетом (вес = 0) не включаются в список для уменьшения нагрузки на сервер',
 43+ 'ec_order_page'=>'названию страницы',
 44+ 'ec_order_user'=>'имени пользователя',
 45+ 'ec_order_time'=>'времени с начала редактирования',
 46+ 'ec_header_order'=>'Сортировать по: $1, $2, $3.',
 47+ 'ec_list_order_page'=>'$1, $2 (вес=$3), время редактирования $4. Закрыть: $5',
 48+ 'ec_list_order_user'=>'$2 (вес=$3), $1, время редактирования $4. Закрыть: $5',
 49+ 'ec_list_order_time'=>'Время редактирования $4, $1, $2 (вес=$3). Закрыть: $5'
 50+);
Property changes on: trunk/extensions/EditConflict/EditConflict_i18n.php
___________________________________________________________________
Name: svn:eol-style
151 + native
Index: trunk/extensions/EditConflict/EditConflict.php
@@ -0,0 +1,559 @@
 2+<?php
 3+/**
 4+ * ***** BEGIN LICENSE BLOCK *****
 5+ * This file is part of EditConflict.
 6+ *
 7+ * EditConflict is free software; you can redistribute it and/or modify
 8+ * it under the terms of the GNU General Public License as published by
 9+ * the Free Software Foundation; either version 2 of the License, or
 10+ * (at your option) any later version.
 11+ *
 12+ * EditConflict is distributed in the hope that it will be useful,
 13+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 14+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 15+ * GNU General Public License for more details.
 16+ *
 17+ * You should have received a copy of the GNU General Public License
 18+ * along with EditConflict; if not, write to the Free Software
 19+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 20+ *
 21+ * ***** END LICENSE BLOCK *****
 22+ *
 23+ * Group-level based edit page access for MediaWiki. Monitors current edit sessions.
 24+ * Version 0.4.2
 25+ *
 26+ */
 27+
 28+if ( !defined( 'MEDIAWIKI' ) ) {
 29+ die( "This file is part of the EditConflict extension. It is not a valid entry point.\n" );
 30+}
 31+
 32+# a "magic" edit_id value which will indicate that no such edit_id is in the DB edits table
 33+# ( valid edit_id value in DB cannot be negative number )
 34+# usually indicates that the user's edit session was finished
 35+define( 'EC_NO_EDITING', -1 );
 36+
 37+# time of edit "session" expiration (in seconds)
 38+# !! it should be at least twice longer than EditConflict.editSleep value defined in js !!
 39+define( 'EC_AJAX_EXPIRE_TIME', 1*60 );
 40+
 41+# total time of edit until the record will be deleted (three hours)
 42+# (counts when client has no javascript)
 43+# !! decrease to 3*60 for the debugging purposes !!
 44+define( 'EC_EDIT_EXPIRE_TIME', 3*60*60 );
 45+
 46+# conflict notifications will expire in 30 days (in seconds)
 47+define( 'EC_NOTIFICATION_EXPIRE_TIME', 30*24*60*60 );
 48+
 49+$wgExtensionCredits['specialpage'][] = array(
 50+ 'name' => 'EditConflict',
 51+ 'version' => '0.4.2',
 52+ 'author' => 'QuestPC',
 53+ 'url' => 'http://www.mediawiki.org/wiki/Extension:EditConflict',
 54+ 'description' => '[[Special:CurrentEdits]] page where current edits can be monitored',
 55+ 'descriptionmsg' => 'editconflict_desc'
 56+);
 57+
 58+EditConflict::startup();
 59+
 60+$wgExtensionMessagesFiles['EditConflict'] = EditConflict::$ExtDir . '/EditConflict_i18n.php';
 61+$wgAutoloadClasses['ec_CurrentEdits'] = EditConflict::$ExtDir . '/CurrentEdits.php';
 62+$wgSpecialPages['CurrentEdits'] = array( 'ec_CurrentEdits' );
 63+
 64+$wgHooks['EditPageMergeChanges'][] = 'EditConflict::doEditConflict';
 65+$wgHooks['MakeGlobalVariablesScript'][] = 'EditConflict::jsWikiMessages';
 66+$wgHooks['BeforePageDisplay'][] = 'EditConflict::checkNotification';
 67+$wgHooks['EditPage::showEditForm:fields'][] = 'EditConflict::initEditing';
 68+$wgHooks['userCan'][] = 'EditConflict::checkEditConflict';
 69+$wgHooks['UserGetRights'][] = 'EditConflict::checkUserPageDelete';
 70+$wgHooks['ArticleDeleteComplete'][] = 'EditConflict::checkArticleDelete';
 71+
 72+$wgAjaxExportList[] = 'EditConflict::getNotifyText';
 73+$wgAjaxExportList[] = 'EditConflict::clearRevId';
 74+$wgAjaxExportList[] = 'EditConflict::markEditing';
 75+$wgAjaxExportList[] = 'EditConflict::checkEditButton';
 76+
 77+class EditConflict {
 78+
 79+ # setup variable, define EditConflict::$alwaysEditClickEvent = true; in LocalSettings.php AFTER the inclusion of extension
 80+ # to always insert an edit click ajax check, whether the user is allowed to edit the page
 81+ # ( slower, will almost always capture the edit click )
 82+ # otherwise, the edit click ajax check will be performed only when there already was ongoing edit before the page was loaded
 83+ # ( faster, by default, click may sometimes display edit window with "access denied" message )
 84+ static $alwaysEditClickEvent;
 85+
 86+ static $ExtDir; // extension directory
 87+ static $ScriptPath; // extension apache web path
 88+ static $onLoadScript; // body.onload script
 89+ static $userCanEditCached = null; // previous result of userCan hook to reduce numbers of DB calls
 90+ static $groupWeights = Array();
 91+ static $useEditPageMergeChangesHook = false; // non-patched core by default
 92+
 93+ var $mTitle;
 94+ var $mArticle;
 95+ var $prev_userid;
 96+ var $prev_user;
 97+ var $prev_userpage;
 98+
 99+ # constructor, used only when hook 'EditPageMergeChanges' is available in includes/EditPage.php
 100+ # @param $title, $article - current version (before the conflict) of the existing (conflicting) page
 101+ function __construct( $title, $article ) {
 102+ $this->mTitle = $title;
 103+ $this->mArticle = $article;
 104+ # user id who saved the existing article revision
 105+ $this->prev_userid = $this->mArticle->getUser();
 106+ # his user object
 107+ $this->prev_user = User::newFromId( $this->prev_userid );
 108+ # page of user who created existing revision
 109+ $this->prev_userpage = $this->prev_user->getUserPage();
 110+ }
 111+
 112+ static function getGroupWeight( $user ) {
 113+ // check, whether we have an standard or a patched version of mediawiki
 114+ if ( method_exists( 'User', 'getGroupParameters' ) ) {
 115+ return $user->getGroupParameters()->weight;
 116+ } else {
 117+ if ( !isset( self::$groupWeights['*'] ) ) {
 118+ throw new MWException( __METHOD__ . ' EditConflict::$groupWeights[\'*\'] for anoymous user group is not defined in LocalSettings.php' );
 119+ }
 120+ // set minimal possible result (anonymous)
 121+ $result = self::$groupWeights['*'];
 122+ $usergroups = $user->getEffectiveGroups();
 123+ // now find the group of highest weight to which $user belongs
 124+ foreach( self::$groupWeights as $groupname => $groupWeight ) {
 125+ if ( in_array( $groupname, $usergroups ) &&
 126+ $groupWeight > $result ) {
 127+ $result = $groupWeight;
 128+ }
 129+ }
 130+ return $result;
 131+ }
 132+ }
 133+
 134+ # copy the conflicting article revision to subpage in user namespace
 135+ # used in includes/EditPage.php
 136+ # uses the data obtained in constructor
 137+ function copy() {
 138+ # title to which the existing revision will be moved (in NS_USER)
 139+ $moved_title_str = $this->prev_userpage->getPrefixedDBkey() . '/' . $this->mTitle->getDBkey();
 140+ $moved_title = Title::newFromText( $moved_title_str );
 141+ # corresponding article
 142+ $moved_article = new Article( $moved_title );
 143+ # get latest revision of existing (saved in progress) article
 144+ $prev_revision = Revision::newFromId( $this->mArticle->getLatest() );
 145+ # get the text of the existing revision
 146+ $prev_text = $prev_revision->userCan( Revision::DELETED_TEXT ) ? $prev_revision->getRawText() : "";
 147+ # save the existing revision into the subpage in userpage
 148+ $moved_article->doEdit( $prev_text, 'Copied from [[' . $this->mTitle->getPrefixedDBkey() . ']] due to edit conflict' );
 149+ # fill out DB-specific fields
 150+ $db = wfGetDB( DB_MASTER );
 151+ $row['page_namespace'] = $this->mTitle->getNamespace();
 152+ $row['page_title'] = $this->mTitle->getDBkey();
 153+ $row['page_touched'] = $db->timestamp( $prev_revision->getTimestamp() );
 154+ $row['user_name'] = $this->prev_user->getName();
 155+# $row['ns_user_rev_id'] = $moved_article->getRevIdFetched();
 156+# $row['ns_user_rev_id'] = $moved_title->getPreviousRevisionID( $moved_article->getLatest() );
 157+# $row['ns_user_rev_id'] = $moved_article->getOldID();
 158+ $row['ns_user_rev_id'] = $moved_title->getLatestRevID();
 159+ $db->replace( 'ec_edit_conflict', array('ns_user_rev_id'), $row, __METHOD__ );
 160+ }
 161+
 162+ # called as the 'EditPageMergeChanges' hook to handle the conflict between the users of different weights
 163+ # @return true when merge was successful, false - merge failed
 164+ static function doEditConflict( &$editpage, $text ) {
 165+ global $wgUser;
 166+ if ( self::$useEditPageMergeChangesHook ) {
 167+ # the user who made previous edit, to whom we have an conflict
 168+ $prev_user = User::newFromId( $editpage->mArticle->getUser() );
 169+ # in case of edit conflict, user who belongs to group with higher weight wins ("successful merge")
 170+ if ( self::getGroupWeight( $wgUser ) > ($prev_ug_weight = self::getGroupWeight( $prev_user ) ) ) {
 171+ wfDebug( __METHOD__ . " suppressing edit conflict, current user has higher groupweight than previous user.\n" );
 172+ if ( $prev_ug_weight > 0 && $editpage->mTitle->getNamespace() == NS_MAIN ) {
 173+ # copy the conflicting revision only when in main namespace and
 174+ # previous user groupweight is higher than zero
 175+ $ec = new EditConflict( $editpage->mTitle, $editpage->mArticle );
 176+ $ec->copy();
 177+ }
 178+ return true;
 179+ }
 180+ }
 181+ return false;
 182+ }
 183+
 184+ static function addOnLoadScript( $sourcetext ) {
 185+ self::$onLoadScript .= $sourcetext;
 186+ }
 187+
 188+ # generates OutputPage header and onload scripts, if necessary
 189+ static function generateHeader( $output ) {
 190+ global $wgJsMimeType;
 191+ if ( self::$onLoadScript !== '' ) {
 192+ $head = '<link rel="stylesheet" href="' . self::$ScriptPath . '/notify.css" />' . "\n";
 193+ $head .= '<script type="' . $wgJsMimeType . '" src="' . self::$ScriptPath . '/notify.js"></script>' . "\n";
 194+ $head .= '<script type="' . $wgJsMimeType . '">EditConflict.addEvent(window,"load",function () {' . self::$onLoadScript . '});</script>' . "\n";
 195+ $output->addScript( $head );
 196+ }
 197+ }
 198+
 199+ # delete expired edits and conflict notifications
 200+ static function deleteExpiredData( $db ) {
 201+ # delete expired edits
 202+ $query = 'DELETE FROM ' . $db->tableName( 'ec_current_edits' ) . ' WHERE edit_time < ' . $db->addQuotes( wfTimestamp( TS_MW, time() - EC_AJAX_EXPIRE_TIME ) ) . ' OR start_time < ' . $db->addQuotes( wfTimestamp( TS_MW, time() - EC_EDIT_EXPIRE_TIME ) );
 203+ $db->query( $query , __METHOD__ );
 204+ # delete expired conflict notifications
 205+ $query = 'DELETE FROM ' . $db->tableName( 'ec_edit_conflict' ) . ' WHERE page_touched < ' . $db->addQuotes( wfTimestamp( TS_MW, time() - EC_NOTIFICATION_EXPIRE_TIME ) );
 206+ $db->query( $query , __METHOD__ );
 207+ }
 208+
 209+ # places JS call which will display copied revisions notifications, if any
 210+ # @param $user - current user
 211+ # @param $title - current title
 212+ static function processConflictNotifications( $db, $user, $title ) {
 213+ $user_name = $user->getName();
 214+ $res = $db->select( 'ec_edit_conflict', 'ns_user_rev_id', 'user_name=' . $db->addQuotes( $user_name ), __METHOD__, array( 'LIMIT'=>10, 'ORDER_BY'=>'page_touched DESC' ) );
 215+ if ( $db->numRows( $res ) > 0 ) {
 216+ # ajax runs in minimal (commandline-like) environment, so $wgTitle during ajax php calls is a mainpage stub
 217+ # thus, we are passing real article id of the current title to ajax js (then to ajax php)
 218+ $notify_list = $title->getArticleId();
 219+ while ( $row = $db->fetchObject( $res ) ) {
 220+ $notify_list .= ',' . strval( intval( $row->ns_user_rev_id ) );
 221+ }
 222+ # will call JS getNotifyText() with the array of pages revid's for user notification
 223+ self::addOnLoadScript( 'EditConflict.getNotifyText([' . $notify_list . ']);' );
 224+ }
 225+ }
 226+
 227+ # checks if the user is allowed to edit the title
 228+ # (whether there are other user with higher weight editing the title already)
 229+ # @param $db - database object
 230+ # @param $user - user object
 231+ # @param $title - title object
 232+ # @return EC_NO_EDITING : when user is allowed to edit OR
 233+ # edit_id : editing 'session' number (key from 'ec_current_edits') otherwise
 234+ static function canUserEdit( $db, $user, $title ) {
 235+ $edit_id = EC_NO_EDITING; // non_existent (user can edit)
 236+ if ( $title instanceof Title ) {
 237+ # current page NS & dbkey
 238+ $where['page_namespace'] = $title->getNamespace();
 239+ $where['page_title'] = $title->getDBkey();
 240+ # select current editings of the current title, if available
 241+ $res = $db->select( 'ec_current_edits', array( 'edit_id', 'user_name' ), $where, __METHOD__, array( 'ORDER BY'=>'start_time DESC' ) );
 242+ if ( $db->numRows( $res ) > 0 ) {
 243+ $max_user_weight = -1; // below the minimal
 244+ $current_user_weight = self::getGroupWeight( $user );
 245+ # get an editId of user who has the maximal weight
 246+ # (should be checked in PHP, not DB - because weights are defined in PHP)
 247+ while ( $row = $db->fetchObject( $res ) ) {
 248+ $editing_user = User::newFromName( $row->user_name );
 249+ $editing_user_weight = -1; // below the minimal
 250+ if ( $editing_user instanceof User ) {
 251+ $editing_user_weight = self::getGroupWeight( $editing_user );
 252+ }
 253+ if ( $editing_user_weight > $current_user_weight &&
 254+ $max_user_weight < $editing_user_weight ) {
 255+ $edit_id = $row->edit_id;
 256+ $max_user_weight = $editing_user_weight;
 257+ }
 258+ }
 259+ }
 260+ }
 261+ # allow to edit the title
 262+ return $edit_id;
 263+ }
 264+
 265+ # places JS call which will attach onclick event to the edit link,
 266+ # if there's another user of higher weight already editing the title
 267+ # @param $user - current user
 268+ # @param $title - current title
 269+ static function processEditButton( $db, $user, $title ) {
 270+ # check, whether the page is being already edited ('true') or not ('false')
 271+ $disallow_edit = ( ( $edit_id = self::canUserEdit( $db, $user, $title ) ) != EC_NO_EDITING ) ? 'true' : 'false';
 272+ # ajax runs in minimal (commandline-like) environment, so $wgTitle during ajax php calls is a mainpage stub
 273+ # thus, we are passing real article id of the current title to ajax js (then to ajax php)
 274+ $article_id = $title->getArticleId();
 275+ if ( self::$alwaysEditClickEvent || $disallow_edit ) {
 276+ # insert ajax check into the edit button anchor, second parameter indicates whether the article is already edited by privileged user
 277+ self::addOnLoadScript( 'EditConflict.findEditKey(' . $article_id . ',' . $disallow_edit . ');' );
 278+ }
 279+ }
 280+
 281+ # called as the 'MakeGlobalVariablesScript' hook to make required mediawiki variables be available in JS code
 282+ static function jsWikiMessages( &$vars ) {
 283+ wfLoadExtensionMessages( 'EditConflict' );
 284+ $vars['ec_already_editing'] = wfMsg( 'ec_already_editing' );
 285+ return true;
 286+ }
 287+
 288+ # called as the 'BeforePageDisplay' hook to show "copied due to conflict" notification messages to the current user
 289+ # (if there are any such messages)
 290+ static function checkNotification( &$out, &$sk = null ) {
 291+ global $wgUser;
 292+ global $wgTitle;
 293+ # show the notifications only in main namespace and only for action=view
 294+ # (to be less annoying and to don't conflict with special pages)
 295+ if ( $wgTitle->getNamespace() == NS_MAIN && self::isViewAction() ) {
 296+ $db = wfGetDB( DB_MASTER );
 297+ self::deleteExpiredData( $db );
 298+ # set current user's conflict notifications (if any)
 299+ self::processConflictNotifications( $db, $wgUser, $wgTitle );
 300+ # set edit link button handler, in case user of higher weight already edits this page
 301+ self::processEditButton( $db, $wgUser, $wgTitle );
 302+ }
 303+ self::generateHeader( $out );
 304+ return true;
 305+ }
 306+
 307+ // called via AJAX from notify.js to get current list of copied conflicted revisions
 308+ // @param args[0] - article_id of current title
 309+ // @param args[1..n] - list of revid that were copied to the subpage in the user's page
 310+ static function getNotifyText() {
 311+ global $wgUser;
 312+ $args = func_get_args();
 313+ if ( count( $args ) < 2 ) {
 314+ return '';
 315+ }
 316+ # get current title
 317+ $current_article_id = intval( array_shift( $args ) );
 318+ $current_title = Title::newFromID( $current_article_id );
 319+ $current_title_str = $current_title->getPrefixedDBkey();
 320+
 321+ $skin = $wgUser->getSkin();
 322+ $user_name = $wgUser->getName();
 323+ $user_title = $wgUser->getUserPage();
 324+ $user_title_dbkey = $user_title->getPrefixedDBkey();
 325+
 326+ $result = '';
 327+ $entries_set = '';
 328+ $db = wfGetDB( DB_MASTER );
 329+ $first_elem = true;
 330+ foreach( $args as $ns_user_rev_id ) {
 331+ if ( $first_elem ) {
 332+ $first_elem = false;
 333+ } else {
 334+ $entries_set .= ',';
 335+ }
 336+ $entries_set .= $db->addQuotes( $ns_user_rev_id );
 337+ }
 338+ $res = $db->select( 'ec_edit_conflict', array( 'ns_user_rev_id', 'page_namespace', 'page_title', 'page_touched' ), 'ns_user_rev_id IN (' . $entries_set . ') AND user_name=' . $db->addQuotes( $user_name ), __METHOD__, array( 'ORDER'=>'page,page_touched' ) );
 339+ if ( $db->numRows( $res ) > 0 ) {
 340+ wfLoadExtensionMessages( 'EditConflict' );
 341+ $result .= '<span style="color:red;">' . wfMsg( 'ec_copied_revisions' ) . '</span> ';
 342+ $prev_title_str = '';
 343+ $first_elem = true;
 344+ while ( $row = $db->fetchObject( $res ) ) {
 345+ $source_title = Title::newFromText( $row->page_title, intval( $row->page_namespace ) );
 346+ $source_title_str = $source_title->getPrefixedDBkey();
 347+ if ( $prev_title_str != $source_title_str ) {
 348+ if ( $first_elem ) {
 349+ $first_elem = false;
 350+ } else {
 351+ $result .= '</li>';
 352+ }
 353+ $result .= '<li>';
 354+ # display
 355+ $result .= ( $source_title_str != $current_title_str ) ? $skin->makeKnownLinkObj( $source_title ) : htmlspecialchars( $source_title );
 356+ $result .= ': ';
 357+ $prev_title_str = $source_title_str;
 358+ }
 359+ $dest_title_str = $user_title_dbkey . '/' . $source_title_str;
 360+ $dest_title = Title::newFromDBkey( $dest_title_str );
 361+ $result .= '<span id="EditConflict_' . $row->ns_user_rev_id . '">(' . $skin->makeKnownLinkObj( $dest_title, 'rev. ' . $row->ns_user_rev_id . '', 'oldid=' . $row->ns_user_rev_id ) . '&nbsp;<span class="closelink" title="Закрыть предупреждение" onclick="EditConflict.clearRevId(' . $row->ns_user_rev_id . ');">&#8251;</span>' . ')</span> ';
 362+ }
 363+ $result .= '</li>';
 364+ }
 365+ return $result;
 366+ }
 367+
 368+ // called via AJAX from notify.js to remove an single entry from the list of copied conflicted revisions
 369+ // @param args[0] = revid;
 370+ static function clearRevId() {
 371+ $args = func_get_args();
 372+ if ( count( $args ) != 1 ) {
 373+ return '';
 374+ }
 375+ $ns_user_rev_id = intval( $args[0] );
 376+ $db = wfGetDB( DB_MASTER );
 377+ # delete the revision checked out by the user
 378+ $db->delete( 'ec_edit_conflict', array( 'ns_user_rev_id'=>$ns_user_rev_id ), __METHOD__ );
 379+ return strval( $ns_user_rev_id );
 380+ }
 381+
 382+ # called as 'EditPage::showEditForm:fields' hook to mark the page currently being edited in the database
 383+ # this hooks is not being called when the user has no rights to edit the specific page
 384+ # so the additional checks for such case are not required
 385+ static function initEditing( &$editpage, &$output ) {
 386+ global $wgUser;
 387+ # will add watchEdit AJAX loop (which indicates ongoing editing) only for users who has (userweight > 0)
 388+ # this should reduce server load (and, lowest status user editing can always be "overtaken" anyway)
 389+ if ( self::getGroupWeight( $wgUser ) > 0 ) {
 390+ $db = wfGetDB( DB_MASTER );
 391+ $row['page_namespace'] = $editpage->mTitle->getNamespace();
 392+ $row['page_title'] = $editpage->mTitle->getDBkey();
 393+ $row['start_time'] = $row['edit_time'] = wfTimestampNow();
 394+ $row['user_name'] = $wgUser->getName();
 395+ $db->replace( 'ec_current_edits', array( 'edit_id', 'user_page' ), $row, __METHOD__ );
 396+ # select an edit_id because replace does not support Database::insertId()
 397+ $res = $db->select( 'ec_current_edits', array( 'edit_id' ), array( 'user_name'=>$row['user_name'], 'page_namespace'=>$row['page_namespace'], 'page_title'=>$row['page_title'] ), __METHOD__ );
 398+ if ( $row = $db->fetchObject( $res ) ) {
 399+ # will call JS watchEdit() with the array of pages revid's for user notification
 400+ # cannot user self::generateHeader here because MW ajax script is included AFTER this hook
 401+ # thus, at this stage OutputPage doesn't have ajax functions defined :-/
 402+ self::addOnLoadScript('EditConflict.watchEdit([' . $row->edit_id . ']);' );
 403+ }
 404+ }
 405+ return true;
 406+ }
 407+
 408+ # called via AJAX from notify.js in a loop to mark the page in DB as "being edited"
 409+ // @param args[0] - editId of the current editing
 410+ static function markEditing() {
 411+ $args = func_get_args();
 412+ # non-existent editId indicates an error
 413+ $edit_id = EC_NO_EDITING; // non-existent (user can edit)
 414+ $db = wfGetDB( DB_MASTER );
 415+ if ( isset( $args[0] ) ) {
 416+ $edit_id = intval( $args[0] );
 417+ # the editing in progress
 418+ # update the timestamp of the editing so it won't expire on page load
 419+ $res = $db->update( 'ec_current_edits', array( 'edit_time'=>wfTimestampNow() ), array( 'edit_id'=>$edit_id ), __METHOD__ );
 420+ }
 421+ # pass editId to the AJAX callback
 422+ return strval( $edit_id );
 423+ }
 424+
 425+ # called via AJAX from notify.js to check, whether the user is allowed to click edit button
 426+ # @param args[0] - an article_id of the title to check
 427+ # @return string "y"/"n" - will be used in AJAX callback setClickEventResult() to enable / disable going to edit location
 428+ static function checkEditButton() {
 429+ global $wgUser;
 430+ $args = func_get_args();
 431+ $result = 'y';
 432+ if ( count( $args ) > 0 ) {
 433+ $current_article_id = intval( $args[0] );
 434+ $current_title = Title::newFromID( $current_article_id );
 435+ $db = wfGetDB( DB_MASTER );
 436+ self::deleteExpiredData( $db );
 437+ $result = ( self::canUserEdit( $db, $wgUser, $current_title ) == EC_NO_EDITING ) ? 'y' : 'n';
 438+ # pass the future event result to the AJAX callback
 439+ }
 440+ return $result;
 441+ }
 442+
 443+ # @param $title - Title object
 444+ # @param $ns - optional numerical code of namespace
 445+ # @return boolean true, when the title is valid local title (optionally in the selected namespace)
 446+ static function validLocalTitle( $title, $ns = null ) {
 447+ $result = $title instanceOf Title && $title->isLocal() && $title->getText() != '-';
 448+ if ( $ns !== null ) {
 449+ $result = $result && $title->getNamespace() == $ns;
 450+ }
 451+ return $result;
 452+ }
 453+
 454+ static function isViewAction() {
 455+ global $wgRequest;
 456+ $action = $wgRequest->getVal( 'action' );
 457+ return !$wgRequest->wasPosted() && ( $action === null || $action == '' || $action == 'view' );
 458+ }
 459+
 460+ static function isPageSubmit() {
 461+ global $wgRequest;
 462+ $action = $wgRequest->getVal( 'action' );
 463+ return $wgRequest->wasPosted() && $action == 'submit';
 464+ }
 465+
 466+ # called as the 'userCan' hook to disable action=edit
 467+ # in case the user with higher weight is already editing the title
 468+ static function checkEditConflict( &$title, &$user, $action, &$result ) {
 469+ # restrict rights only if proposed action is 'edit' AND title is valid local AND
 470+ # active action is NOT 'view' (otherwise edit link will be disabled in SkinTemplate.php !)
 471+ # (we will need that link for JS part of AJAX)
 472+ # AND we aren't submitting article (otherwise posted article will be lost)
 473+ if ( $action == 'edit' && self::validLocalTitle( $title, NS_MAIN ) &&
 474+ !self::isViewAction() && !self::isPageSubmit() ) {
 475+ if ( self::$userCanEditCached === null ) {
 476+ $res = self::canUserEdit( wfGetDB( DB_MASTER ), $user, $title );
 477+ if ( self::canUserEdit( wfGetDB( DB_MASTER ), $user, $title ) != EC_NO_EDITING ) {
 478+ $result = self::$userCanEditCached = false;
 479+ return false;
 480+ }
 481+ self::$userCanEditCached = true;
 482+ } else {
 483+ if ( !self::$userCanEditCached ) {
 484+ $result = false;
 485+ return false;
 486+ }
 487+ }
 488+ }
 489+ return true;
 490+ }
 491+
 492+ static function addRight( $right, &$aRights ) {
 493+ # union of rights
 494+ $aRights = array_unique( array_merge( $aRights, array( $right ) ) );
 495+ }
 496+
 497+ static function removeRight( $right, &$aRights ) {
 498+ if ( in_array( $right, $aRights ) ) {
 499+ $key = array_search( $right, $aRights );
 500+ unset( $aRights[ $key ] );
 501+ }
 502+ }
 503+
 504+ # called as the 'UserGetRights' hook to allow user to delete his own page and the subpages of that page
 505+ static function checkUserPageDelete( $user, &$aRights ) {
 506+ global $wgTitle;
 507+ # in case the copying of conflicting revisions to User's page/subpage is enabled
 508+ if ( self::$useEditPageMergeChangesHook ) {
 509+ # enable the deletion of user's page and his subpages
 510+ if ( self::validLocalTitle( $wgTitle, NS_USER ) ) {
 511+ $user_name_pq = preg_quote( $user->getName(), '`' );
 512+ $title_str = $wgTitle->getText();
 513+ if ( preg_match( '`^(' . $user_name_pq . '|' . $user_name_pq . '/.*)$`u', $title_str ) ) {
 514+ self::addRight( 'delete', $aRights );
 515+ }
 516+ }
 517+ }
 518+ return true;
 519+ }
 520+
 521+ # called as the 'ArticleDeleteComplete' hook to remove user notifications belonging to deleted article
 522+ static function checkArticleDelete( &$article, &$user, $reason, $id ) {
 523+ $title = $article->getTitle();
 524+ if ( $title->getNamespace() == NS_USER ) {
 525+ $db = wfGetDB( DB_MASTER );
 526+ $res = $db->select( 'archive', 'ar_title', array( 'ar_page_id'=>$id ), __METHOD__, array( 'LIMIT'=>1, 'ORDER BY'=>'ar_rev_id DESC' ) );
 527+ if ( $row = $db->fetchObject( $res ) ) {
 528+ $src = explode( '/', $row->ar_title );
 529+ if ( count( $src ) == 2 ) {
 530+ $where['page_namespace'] = NS_MAIN;
 531+ $where['page_title'] = $src[1];
 532+ $src_user = User::newFromName( $src[0] );
 533+ $where['user_name'] = $src_user == null ? $src[0] : $src_user->getName();
 534+ $db->delete( 'ec_edit_conflict', $where, __METHOD__ );
 535+ }
 536+ }
 537+ }
 538+ return true;
 539+ }
 540+
 541+ # extension startup
 542+ static function startup() {
 543+ global $wgScriptPath;
 544+ foreach( array( 'wgUseAjax' ) as $globalVar ) {
 545+ global $$globalVar;
 546+ if ( !$$globalVar ) {
 547+ die( "This extension requires \$$globalVar = true; in LocalSettings.php. Either disable the extension or change LocalSettings.php, accordingly.\n" );
 548+ }
 549+ }
 550+ // static properties initialization (various paths)
 551+ self::$ExtDir = str_replace( "\\", "/", dirname(__FILE__) ); // filesys path with windows path fix
 552+ $top_dir = array_pop( explode( '/', self::$ExtDir ) );
 553+ # currently two separate editings of the same page by the same user are considered a single edit
 554+ self::$ScriptPath = $wgScriptPath . '/extensions' . ( ( $top_dir == 'extensions' ) ? '' : '/' . $top_dir ); // apache virtual path
 555+ self::$alwaysEditClickEvent = false;
 556+ self::$onLoadScript = '';
 557+ self::$groupWeights = Array( '*' => 1, 'user' => 2, 'bureaucrat' => 3, 'sysop' => 4 );
 558+ }
 559+
 560+}
Property changes on: trunk/extensions/EditConflict/EditConflict.php
___________________________________________________________________
Name: svn:eol-style
1561 + native
Index: trunk/extensions/EditConflict/README
@@ -0,0 +1,11 @@
 2+MediaWiki extension EditConflict, version 0.4.2
 3+
 4+Some of not so large wikis have much less "number of pages to number of active
 5+users ratio" comparing to large wikis. Thus, edit conflicts occur more often
 6+at such wikis. Moreover, such wikis sometimes have highly experienced / professional
 7+users whose edits are much more valuable than other's edits (especially the
 8+anonymous ones). One may create (or use an existing) user group for such experienced
 9+users, then install this extension to make edits of professional users to have
 10+less probability of conflicting to other's edits.
 11+
 12+See http://mediawiki.org/wiki/Extension:EditConflict for further details.
\ No newline at end of file

Status & tagging log