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 |
1 | 11 | + 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(), '※', $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 |
1 | 293 | + 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 |
1 | 265 | + 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 |
1 | 51 | + 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 ) . ' <span class="closelink" title="Закрыть предупреждение" onclick="EditConflict.clearRevId(' . $row->ns_user_rev_id . ');">※</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 |
1 | 561 | + 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 |