1<?php 2/** 3 * Conflict Merger Plugin for Dokuwiki 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Daniel Calviño Sánchez <danxuliu@gmail.com> 7 */ 8 9// must be run within Dokuwiki 10if (!defined('DOKU_INC')) die(); 11 12if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 13require_once(DOKU_PLUGIN.'action.php'); 14 15require_once(DOKU_INC.'inc/html.php'); 16require_once(DOKU_INC.'inc/infoutils.php'); 17require_once(DOKU_INC.'inc/pageutils.php'); 18 19if (!defined('NL')) define('NL',"\n"); 20 21/** 22 * Action plugin for Dokuwiki to automatically merge changes with latest 23 * revision when saving a document. 24 * 25 * This plugin intercepts 'edit' and 'save' actions and redirects them to a 26 * custom action, 'conflictSolving', when needed. That happens when some changes 27 * were made to the document while it was being edited (being it a pure edition, 28 * or a conflict solving page). 29 * 30 * When a 'conflictSolving' action is received, a conflict solving page is 31 * shown. If the conflict could be solved, the text being edited and the latest 32 * version of the document are merged. Otherwise, no merging happens (when the 33 * changes conflict, or when the solving failed). The page also shows the 34 * differences between versions, and buttons to save the text, make further 35 * editions or cancel the edition. 36 * 37 * The edit form is bypassed to use the text already being worked on (for 38 * example, editting a conflict not mergeable), instead of the latest version of 39 * the page. 40 * 41 * It uses diff3 command. The path to it can be configured through the 42 * configuration plugin. 43 */ 44class action_plugin_conflictmerger extends DokuWiki_Action_Plugin { 45 46 /** 47 * Returns the information about conflictmerger plugin. 48 * 49 * @return The information about conflictmerger plugin. 50 */ 51 function getInfo() { 52 return array( 53 'author' => 'Daniel Calviño Sánchez', 54 'email' => 'danxuliu@gmail.com', 55 'date' => @file_get_contents(DOKU_PLUGIN.'conflictmerger/VERSION'), 56 'name' => 'Conflict Merger Plugin (action component)', 57 'desc' => 'Solves, when possible, edition conflicts automatically merging the changes using diff3', 58 'url' => 'http://wiki.splitbrain.org/plugin:conflictmerger', 59 ); 60 } 61 62 /** 63 * Registers handlers for several events. 64 */ 65 function register(&$contr) { 66 $contr->register_hook( 67 'ACTION_ACT_PREPROCESS', 68 'BEFORE', 69 $this, 70 'handle_action_act_preprocess', 71 array() 72 ); 73 $contr->register_hook( 74 'TPL_ACT_UNKNOWN', 75 'BEFORE', 76 $this, 77 'handle_tpl_act_unknown', 78 array() 79 ); 80 $contr->register_hook( 81 'HTML_EDITFORM_OUTPUT', 82 'BEFORE', 83 $this, 84 'handle_html_editform_output', 85 array() 86 ); 87 } 88 89 /** 90 * Fires "conflictSolving" events when needed. 91 * A conflict appears in edit and save actions when the page was modified 92 * after the edit or conflict forms were shown. That is, even if the page is 93 * modified again while the user is looking to a conflict form, a new 94 * conflict form will appear informing the user about it. 95 * 96 * Also, note the following scenario: user A starts editing a page, user B 97 * edits and saves the page, and user A saves the page. A conflict form will 98 * be shown to user A, as the page was changed while it was being edited. 99 * Now, the user A decides to further edit the page. 100 * 101 * If the merge was successfull, when the user hit save button in the edit 102 * form, the text will be directly saved (provided no other user changes the 103 * page in the meantime). That is, now the revision to check conflicts 104 * against is the revision the text was merged with, instead of the revision 105 * of the page when the fresh edition was started. 106 * 107 * However, it is different in the case of an unsuccessful merge. In that 108 * case, as the contents weren't merged, the revision to check conflicts 109 * against is the revision of the page when the fresh edition was started. 110 * So even if the text is edited to avoid conflicts, a conflict form will 111 * appear, although a successful one. Once a successful merge form appears, 112 * the revision to check conflicts against is updated to the latest revision 113 * of the page. 114 * 115 * @param event The TPL_ACT_UNKNOWN event. 116 * @param param The parameters of the event. 117 */ 118 function handle_action_act_preprocess(&$event, $param) { 119 //$event->data action may come as an array, so it must be cleaned 120 $action = $this->cleanAction($event->data); 121 122 if ($action != 'edit' && $action != 'save') { 123 return; 124 } 125 126 global $DATE; 127 global $INFO; 128 129 if ($DATE == 0 || $INFO['lastmod'] <= $DATE) { 130 return; 131 } 132 133 if ($action == 'edit' && $_REQUEST['conflictDate'] == $INFO['lastmod']) { 134 return; 135 } 136 137 if ($action == 'save' && $_REQUEST['conflictDate'] == $INFO['lastmod']) { 138 $DATE = $_REQUEST['conflictDate']; 139 return; 140 } 141 142 $event->data = 'conflictSolving'; 143 $event->preventDefault(); 144 } 145 146 /** 147 * Handles "conflictSolving" actions. 148 * Shows the conflict solving area and prevents the default handling of the 149 * event. 150 * 151 * @param event The TPL_ACT_UNKNOWN event. 152 * @param param The parameters of the event. 153 */ 154 function handle_tpl_act_unknown(&$event, $param) { 155 if ($event->data != 'conflictSolving') { 156 return; 157 } 158 159 global $PRE; 160 global $SUF; 161 global $TEXT; 162 163 $this->html_conflict_solving(con($PRE,$TEXT,$SUF)); 164 $event->preventDefault(); 165 } 166 167 /** 168 * Bypasses the text to be edited set by Dokuwiki when needed. 169 * When edit form is created, the text is set to the content of the latest 170 * revision of the page. However, in this plugin the edit page can also be 171 * shown from the conflict solving page, to further edit the text after a 172 * conflict was detected. 173 * 174 * In those cases, that is, when wikitext parameter is already set, the text 175 * in the edit form is changed to the text the user is working on instead of 176 * the contents of the latest revision of the page. 177 * 178 * @param event The HTML_EDITFORM_OUTPUT event. 179 * @param param The parameters of the event. 180 */ 181 function handle_html_editform_output(&$event, $param) { 182 global $_POST; 183 global $INFO; 184 185 if (isset($_POST['wikitext'])) { 186 $attr = array('tabindex'=>'1'); 187 $wr = $INFO['writable'] && !$INFO['locked']; 188 if (!$wr) $attr['readonly'] = 'readonly'; 189 190 $position = $event->data->findElementById("wiki__text"); 191 $wikitext = form_makeWikiText(cleanText($_POST['wikitext']), $attr); 192 $event->data->replaceElement($position, $wikitext); 193 } 194 } 195 196 /** 197 * Got from Dokuwiki 2008-05-05 (inc/actions.php, function act_clean) 198 * 199 * Cleans an action. 200 * Some actions may come as an array due to being created like "do[action]". 201 * It returns the cleaned action as a single string with just the action. 202 * 203 * @param action The action to clean. 204 * @return The cleaned action. 205 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 206 * @author Andreas Gohr <andi@splitbrain.org> 207 */ 208 function cleanAction($action) { 209 if (is_array($action)) { 210 list($action) = array_keys($action); 211 } 212 213 $action = strtolower($action); 214 $action = preg_replace('/[^1-9a-z_]+/', '', $action); 215 216 return $action; 217 } 218 219 /** 220 * Shows a conflict solving page. 221 * The page consists of an informative text explaining whether the text 222 * could be merged or not, the differences between the latest revision of 223 * the page and the text being edited, and a group of buttons to save the 224 * changes, further edit the text or cancel all the changes. 225 * 226 * If the execution of diff3 fails, it reverts to standard behaviour and the 227 * page shown is the default Dokuwiki conflict page. 228 * 229 * @param text The current text. 230 */ 231 function html_conflict_solving($text) { 232 global $_REQUEST; 233 global $DATE; 234 global $ID; 235 global $INFO; 236 global $lang; 237 global $SUM; 238 239 if ($this->merge($ID, rawWiki($ID, $DATE), $text, rawWiki($ID, ''), $result)) { 240 print $this->locale_xhtml('conflict-solved'); 241 242 $text = $result; 243 $DATE = $INFO['lastmod']; 244 $conflictDate = 0; 245 } else if ($result != '') { 246 print $this->locale_xhtml('conflict-unsolved'); 247 248 $conflictDate = $INFO['lastmod']; 249 } else { 250 html_conflict($text, $SUM); 251 html_diff($text, false); 252 return; 253 } 254 255 html_diff($text, false); 256 257 print '<div class="centeralign">'.NL; 258 $form = new Doku_Form('dw__editform'); 259 $form->addHidden('id', $ID); 260 $form->addHidden('date', $DATE); 261 $form->addHidden('conflictDate', $conflictDate); 262 $form->addHidden('wikitext', $text); 263 $form->addHidden('summary', $SUM); 264 $form->addElement(form_makeButton('submit', 'save', $lang['btn_save'], array('accesskey'=>'s'))); 265 $form->addElement(form_makeButton('submit', 'edit', $lang['btn_edit'], array('accesskey'=>'e'))); 266 $form->addElement(form_makeButton('submit', 'cancel', $lang['btn_cancel'])); 267 html_form('conflict', $form); 268 print '</div>'.NL; 269 } 270 271 /** 272 * Adapted from MediaWiki 1.13.2. (includes/GlobalFunctions.php) 273 * 274 * @license You may copy this code freely under the conditions of the GPL. 275 * 276 * Attempts to merge differences between three texts. 277 * 278 * It merges the changes happened from "old" to "yours" with "mine", and stores 279 * the merged text in "result". If the changes happened from "old" to "yours" 280 * overlap with those happened from "old" to "mine" there is a conflict, and the 281 * changes can't be automatically merged. That is, it can only merge changes 282 * happened in separated areas of the texts ("old" is a previous version of both 283 * "mine" and "yours"). 284 * 285 * Differences are computed in a line by line basis. That is, changes in the 286 * same line, even if they don't overlap, are seen as a conflict. 287 * 288 * Merging is done using diff3 executable. It can be configured through 'diff3' 289 * plugin configuration key. Temporary files containing old, mine and yours 290 * texts are created to be used with diff3. 291 * 292 * Further information about how merging is done can be got in diff3 info page. 293 * 294 * Even when a conflict is detected, result contains some merging attempt. Only 295 * when a failure happened (for example, if diff3 wasn't found), result is 296 * empty. 297 * 298 * @param id The id of the text to merge. 299 * @param old The base text. 300 * @param mine The first modified text. 301 * @param yours The second modified text. 302 * @param result The merged text, if any. 303 * @return True for a clean merge and false for failure or conflict. 304 */ 305 function merge( $id, $old, $mine, $yours, &$result ) { 306 $diff3 = $this->getConf('diff3'); 307 308 $result = ''; 309 310 # This check may also protect against code injection in 311 # case of broken installations. 312 if ( !isset($diff3) || !file_exists( $diff3 ) ) { 313 msg( "diff3 not found\n", -1 ); 314 return false; 315 } 316 317 # Make temporary files 318 //TODO Is there any function to create temporary files? Modify unit tests as necessary 319 $baseName = wikiFN( $id ); 320 $myTextFile = fopen( $myTextName = ($baseName . '-merge-mine'), 'w' ); 321 $oldTextFile = fopen( $oldTextName = ($baseName . '-merge-old'), 'w' ); 322 $yourTextFile = fopen( $yourTextName = ($baseName . '-merge-your'), 'w' ); 323 324 fwrite( $myTextFile, $mine ); fclose( $myTextFile ); 325 fwrite( $oldTextFile, $old ); fclose( $oldTextFile ); 326 fwrite( $yourTextFile, $yours ); fclose( $yourTextFile ); 327 328 # Check for a conflict 329 $cmd = $diff3 . ' -a --overlap-only ' . 330 escapeshellarg( $myTextName ) . ' ' . 331 escapeshellarg( $oldTextName ) . ' ' . 332 escapeshellarg( $yourTextName ); 333// FIXME: In MediaWiki code it uses wfEscapeShellArg, which includes 334// some special code for Windows 335// wfEscapeShellArg( $myTextName ) . ' ' . 336// wfEscapeShellArg( $oldTextName ) . ' ' . 337// wfEscapeShellArg( $yourTextName ); 338 $handle = popen( $cmd, 'r' ); 339 340 if ( fgets( $handle, 1024 ) ) { 341 $conflict = true; 342 } else { 343 $conflict = false; 344 } 345 pclose( $handle ); 346 347 # Merge differences 348 $cmd = $diff3 . ' -a -e --merge ' . 349 escapeshellarg( $myTextName ) . ' ' . 350 escapeshellarg( $oldTextName ) . ' ' . 351 escapeshellarg( $yourTextName ); 352// FIXME: In MediaWiki code it uses wfEscapeShellArg, which includes 353// some special code for Windows 354// wfEscapeShellArg( $myTextName, $oldTextName, $yourTextName ); 355 $handle = popen( $cmd, 'r' ); 356 357 do { 358 $data = fread( $handle, 8192 ); 359 if ( strlen( $data ) == 0 ) { 360 break; 361 } 362 $result .= $data; 363 } while ( true ); 364 pclose( $handle ); 365 unlink( $myTextName ); unlink( $oldTextName ); unlink( $yourTextName ); 366 367 if ( $result === '' && $old !== '' && $conflict == false ) { 368 msg( "Unexpected null result from diff3. Command: $cmd\n", -1 ); 369 $conflict = true; 370 } 371 return !$conflict; 372 } 373} 374 375//Setup VIM: ex: et ts=4 enc=utf-8 : 376