1<?php 2/** 3 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 4 * @author Stephan Dekker <Stephan@SparklingSoftware.com.au> 5 */ 6 7if(!defined('DOKU_INC')) die(); 8if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 9require_once(DOKU_PLUGIN.'git/lib/Git.php'); 10require_once(DOKU_INC.'inc/search.php'); 11require_once(DOKU_INC.'/inc/DifferenceEngine.php'); 12 13function git_callback_search_wanted(&$data,$base,$file,$type,$lvl,$opts) { 14 global $conf; 15 16 if($type == 'd'){ 17 return true; // recurse all directories, but we don't store namespaces 18 } 19 20 if(!preg_match("/.*\.txt$/", $file)) { // Ignore everything but TXT 21 return true; 22 } 23 24 // get id of this file 25 $id = pathID($file); 26 27 $item = &$data["$id"]; 28 if(! isset($item)) { 29 $data["$id"]= array('id' => $id, 30 'file' => $file); 31 } 32} 33 34 35class helper_plugin_git extends DokuWiki_Plugin { 36 37 var $dt = null; 38 var $sqlite = null; 39 40 function getMethods(){ 41 $result = array(); 42 $result[] = array( 43 'name' => 'cloneRepo', 44 'desc' => 'Creates a new clone of a repository', 45 'params' => array( 46 'destination' => 'string'), 47 'return' => array('result' => 'array'), 48 ); 49 50 // and more supported methods... 51 return $result; 52 } 53 54 function rebuild_data_plugin_data() { 55 // Load the data plugin only if we need to 56 if(!$this->dt) 57 { 58 $this->dt =& plugin_load('syntax', 'data_entry'); 59 if(!$this->dt) 60 { 61 msg('Error loading the data table class from GIT Helper. Make sure the data plugin is installed.',-1); 62 return; 63 } 64 } 65 66 global $conf; 67 $result = ''; 68 $data = array(); 69 search($data,$conf['datadir'],'git_callback_search_wanted',array('ns' => $ns)); 70 71 $output = array(); 72 foreach($data as $entry) { 73 74 // Get the content of the file 75 $filename = $conf['datadir'].$entry['file']; 76 if (strpos($filename, 'syntax') > 0) continue; // Skip instructional pages 77 $body = @file_get_contents($filename); 78 79 // Run the regular expression to get the dataentry section 80 $pattern = '/----.*dataentry.*\R----/s'; 81 if (preg_match($pattern, $body, $matches) === false) { 82 continue; 83 } 84 85 foreach ($matches as $match) { 86 87 // Re-use the handle method to get the formatted data 88 $cleanedMatch = htmlspecialchars($match); 89 $dummy = ""; 90 $formatted = $this->dt->handle($cleanedMatch, null, null, $dummy); 91 $output['id'.count($output)] = $formatted; 92 93 // Re-use the save_data method to .... (drum roll) save the data. 94 // Ignore the returned html, just move on to the next file 95 $html = $this->dt->_saveData($formatted, $entry['id'], 'Title'.count($output)); 96 } 97 } 98 99 msg('Data entry plugin found and refreshed all '.count($output).' entries.'); 100 } 101 102 /** 103 * Resets a GIT cache by setting the timestamp to ZERO (1st of jan 1970) 104 * 105 * @param string repository name. Either: 'Local' or 'upstream' 106 */ 107 function resetGitStatusCache($repo) 108 { 109 $res = $this->loadSqlite(); 110 if (!$res) 111 { 112 msg('Error loading sqlite'); 113 return; 114 } 115 116 // Set the time to zero, so the first alert msg will set the correct status 117 $sql = "INSERT OR REPLACE INTO git (repo, timestamp, status ) VALUES ('".$repo."', 0, 'clean');"; 118 $this->sqlite->query($sql); 119 } 120 121 function haveChangesBeenSubmitted() 122 { 123 $changesAwaiting = true; 124 125 $res = $this->loadSqlite(); 126 if (!$res) return; 127 128 $res = $this->sqlite->query("SELECT status FROM git WHERE repo = 'local'"); 129 $rows = $this->sqlite->res2arr($res); 130 $status = $rows[0]['status']; 131 if ($status !== 'submitted' ) $changesAwaiting = false; 132 133 return $changesAwaiting; 134 } 135 136 function submittChangesForApproval() 137 { 138 $res = $this->loadSqlite(); 139 if (!$res) return; 140 141 // Set the time to zero, so the first alert msg will set the correct status 142 $hundred_years_into_future = time() + (60 * 60 * 24 * 365 * 100); 143 $sql = "INSERT OR REPLACE INTO git (repo, timestamp, status ) VALUES ('local', ".$hundred_years_into_future.", 'submitted');"; 144 $this->sqlite->query($sql); 145 146 $this->changeReadOnly(true); 147 $this->sendNotificationEMail(); 148 } 149 150 function sendNotificationEMail() 151 { 152 global $conf; 153 $this->getConf(''); 154 155 $notify = $conf['plugin']['git']['commit_notifcations']; 156 $local_status_page = wl($conf['plugin']['git']['local_status_page'],'',true); 157 158 $mail = new Mailer(); 159 $mail->to($notify); 160 $mail->subject('An improvement has been submitted for approval!'); 161 $mail->setBody('Please review the proposed changes before the next meeting: '.$local_status_page); 162 163 return $mail->send(); 164 } 165 166 167 function cloneRepo($origin, $destination) { 168 global $conf; 169 $this->getConf(''); 170 $git_exe_path = $conf['plugin']['git']['git_exe_path']; 171 172 try 173 { 174 $repo = new GitRepo($destination, true, false); 175 $repo->git_path = $git_exe_path; 176 $repo->clone_from($origin); 177 178 179 } 180 catch (Exception $e) 181 { 182 msg($e->getMessage()); 183 } 184 } 185 186 function changeReadOnly($readonly = true) 187 { 188 global $config_cascade; 189 190 $AUTH_ACL = file($config_cascade['acl']['default']); 191 192 $lines = array(); 193 foreach($AUTH_ACL as $line){ 194 if(strpos(strtolower($line), strtolower('@USER')) === FALSE) 195 { 196 $lines[] = $line; 197 continue; 198 } 199 200 if ($readonly) 201 { 202 $lines[] = '* @user '.AUTH_READ; 203 } 204 else 205 { 206 $lines[] = '* @user '.AUTH_DELETE; 207 } 208 209 $lines[] = $replaced; 210 } 211 212 // save it 213 io_saveFile($config_cascade['acl']['default'], join('',$lines)); 214 } 215 216 function render_commit_selector($renderer, $commits) 217 { 218 // When viewing file content differences, the hash gets set in the html request, so we can select the correct option 219 $selected_hash = trim($_REQUEST['hash']); 220 if ($selected_hash === '') $selected_hash = 'all'; // By default select "all". 221 222 $renderer->doc .= "<select id='git_commit' width=\"800\" style=\"width: 800px\" onchange='ChangeGitCommit();'>"; 223 $index = 1; 224 $renderer->doc .= "<option>Select a commit</option>"; 225 foreach($commits as $commit) 226 { 227 // Replace merge commit message with a more user friendly msg, leaving the orrigional 228 $raw_message = $commit['message']; 229 $pos = strpos(strtolower($raw_message), 'merge'); 230 if ($pos !== false) $msg = 'Merge'; 231 else $msg = $raw_message; 232 233 // Create option in DDL 234 $renderer->doc .= "<option value=\"".$commit['hash']."\""; 235 // Is this option already selected before an html round-trip ?? 236 if ($commit['hash'] === $selected_hash) $renderer->doc .= "selected=\"selected\""; 237 if ($commit['hash'] === 'all') $renderer->doc .= ">".$msg."</option>"; 238 else $renderer->doc .= ">".$index." - ".$msg."</option>"; 239 $index++; 240 } 241 $renderer->doc .= '</select>'; 242 } 243 244 function render_changed_files_table($renderer, $commits, $repo) 245 { 246 $selected_hash = trim($_REQUEST['hash']); 247 if ($selected_hash === '') $selected_hash = 'all'; // By default select "all". 248 249 foreach($commits as $commit) 250 { 251 $hash = $commit['hash']; 252 253 if($hash === $selected_hash || $hash === 'new') $divVisibility = ""; // Show the selected 254 else $divVisibility = " display:none;"; // Hide the rest 255 256 $renderer->doc .= "<div class=\"commit_div\" id='".$hash."' style=\"".$divVisibility." width: 100%;\">"; 257 258 // Commits selected to show changes for 259 if ($hash === 'new') 260 { 261 $files = explode("\n", $repo->get_status()); 262 } 263 else if($hash === 'all') 264 { 265 $files = explode("\n", $repo->get_files_by_commit('origin/master..HEAD')); 266 } 267 else 268 { 269 $files = explode("\n", $repo->get_files_by_commit($hash)); 270 } 271 272 // No files 273 if ($files === null || count($files) === 1) 274 { 275 $renderer->doc .= "<p><br/>No files have changed for the selected item. If a merge is selected, then no conflicts were detected.</p>"; 276 } 277 else 278 { 279 $renderer->doc .= '<br/><h3>The content of the selected commit:</h3>'; 280 281 $renderer->doc .= "<table><tr><th>Change type</th><th>Page</th><th>Changes</th></tr>"; 282 foreach ($files as $file) 283 { 284 if ($file === "") continue; 285 286 $renderer->doc .= "<tr><td>"; 287 288 $change = substr($file, 0, 2); 289 if (strpos($change, '?') !== false) 290 $renderer->doc .= "Added:"; 291 else if (strpos($change, 'M') !== false) 292 $renderer->doc .= "Modified:"; 293 else if (strpos($change, 'A') !== false) 294 $renderer->doc .= "Added:"; 295 else if (strpos($change, 'D') !== false) 296 $renderer->doc .= "Removed:"; 297 else if (strpos($change, 'R') !== false) 298 $renderer->doc .= "Removed:"; 299 else if (strpos($change, 'r') !== false) 300 $renderer->doc .= "Removed:"; 301 302 $renderer->doc .= "</td><td>"; 303 $file = trim(substr($file, 2)); 304 $page = $this->getPageFromFile($file); 305 $renderer->doc .= '<a href="'.DOKU_URL.'doku.php?id='.$page.'">'.$page.'</a>'; 306 307 $renderer->doc .= "</td><td>"; 308 $renderer->doc .= ' <form method="post">'; 309 $renderer->doc .= ' <input type="hidden" name="filename" value="'.$file.'" />'; 310 $renderer->doc .= ' <input type="hidden" name="hash" value="'.$commit['hash'].'" />'; 311 $renderer->doc .= ' <input type="submit" value="View Changes" />'; 312 $renderer->doc .= ' </form>'; 313 $renderer->doc .= "</td>"; 314 $renderer->doc .= "</tr>"; 315 } 316 $renderer->doc .= "</table>"; 317 } 318 $renderer->doc .= "</div>\n"; 319 320 // Initially, hide second and further tables 321 $divVisibility = " display:none;"; 322 } 323 } 324 325 function getPageFromFile($file) 326 { 327 // If it's not a wiki page, just return the normal filename 328 if (strpos($file, 'pages/') === false) return $file; 329 330 // Replace all sorts of stuff so it makes sense to non-technical users. 331 $page = str_replace('pages/', '', $file); 332 $page = str_replace('.txt', '', $page); 333 $page = str_replace('/', ':', $page); 334 $page = trim($page); 335 336 return $page; 337 } 338 339 340 function renderChangesMade(&$renderer, &$repo, $mode) 341 { 342 global $conf; 343 $this->getConf(''); 344 345 $fileForDiff = trim($_REQUEST['filename']); 346 $page = $this->getPageFromFile($fileForDiff); 347 $hash = trim($_REQUEST['hash']); 348 if ($fileForDiff !== '') 349 { 350 $renderer->doc .= '<div id="diff_table" class="table">'; 351 352 //Write header 353 $renderer->doc .= '<h2>Changes to: '.$page.'</h2>'; 354 355 if ($mode == 'Approve Local') { 356 if ($hash === 'all') $renderer->doc .= '<p>Left = The current page in Live<br/>'; 357 else $renderer->doc .= '<p>Left = The page before the selected commited retrieved from GIT <br/>'; 358 $renderer->doc .= 'Right = The page after the selected commit</p>'; 359 360 // LEFT: Find the file before for the selected commit 361 if ($hash === 'all') $l_text = $repo->getFile($fileForDiff, 'origin/master'); 362 else $l_text = $repo->getFile($fileForDiff, $hash."~1"); 363 364 // RIGHT: Find the file for the selected commit 365 if ($hash === 'all') $r_text = $repo->getFile($fileForDiff, 'HEAD'); 366 else $r_text = $repo->getFile($fileForDiff, $hash); 367 } 368 else if ($mode == 'Commit local') { 369 $renderer->doc .= '<p>Left = The last page commited to GIT <br/>'; 370 $renderer->doc .= 'Right = Current wiki content</p>'; 371 372 // LEFT: Latest in GIT 373 $l_text = $repo->getFile($fileForDiff, 'HEAD'); 374 375 // RIGHT: Current 376 $current_filename = $conf['savedir'].'/'.$fileForDiff; 377 $current_filename = str_replace("/", "\\", $current_filename); 378 $r_text = $this->getFileContents($current_filename); 379 } 380 else if ($mode == 'Merge upstream') { 381 $renderer->doc .= '<p>Left = Current wiki content<br/>'; 382 $renderer->doc .= 'Right = Upstream changes to be merged</p>'; 383 384 // LEFT: Current 385 $current_filename = $conf['savedir'].'/'.$fileForDiff; 386 $current_filename = str_replace("/", "\\", $current_filename); 387 $l_text = $this->getFileContents($current_filename); 388 389 // RIGHT: Latest in GIT to be merged 390 $r_text = $repo->getFile($fileForDiff, $hash); 391 } 392 393 // Show diff 394 $df = new Diff(explode("\n",htmlspecialchars($l_text)), explode("\n",htmlspecialchars($r_text))); 395 $tdf = new TableDiffFormatter(); 396 $renderer->doc .= '<table class="diff diff_inline">'; 397 $renderer->doc .= $tdf->format($df); 398 $renderer->doc .= '</table>'; 399 $renderer->doc .= '</div>'; 400 } 401 } 402 403 function renderAdminApproval(&$renderer) 404 { 405 $isAdmin = $this->isCurrentUserAnAdmin(); 406 if ($isAdmin) 407 { 408 $renderer->doc .= '<form method="post">'; 409 $renderer->doc .= ' <input type="submit" name="cmd[revert]" value="Reject and revert Approval Submission" />'; 410 $renderer->doc .= ' <input type="submit" name="cmd[push]" value="Push to live!" />'; 411 $renderer->doc .= '</form>'; 412 } 413 } 414 415 function isCurrentUserAnAdmin() 416 { 417 function isCurrentUserAnAdmin() 418 { 419 global $INFO; 420 return ($INFO['isadmin']); 421 } 422 } 423 424 function getFileContents($filename) 425 { 426 // get contents of a file into a string 427 $handle = fopen($filename, "r"); 428 $contents = fread($handle, filesize($filename)); 429 fclose($handle); 430 431 return $contents; 432 } 433 434 function loadSqlite() 435 { 436 if ($this->sqlite) return true; 437 438 $this->sqlite =& plugin_load('helper', 'sqlite'); 439 if (is_null($this->sqlite)) { 440 msg('The sqlite plugin could not loaded from the GIT Plugin helper', -1); 441 return false; 442 } 443 if($this->sqlite->init('git',DOKU_PLUGIN.'git/db/')){ 444 return true; 445 }else{ 446 msg('Submitting changes failed as the GIT cache failed to initialise.', -1); 447 return false; 448 } 449 } 450 451 function hasLocalCacheTimedOut() 452 { 453 $hasCacheTimedOut = false; 454 455 $res = $this->loadSqlite(); 456 if (!$res) return; 457 458 $sql = "SELECT timestamp FROM git WHERE repo = 'local'"; 459 $res = $this->sqlite->query($sql); 460 $rows = $this->sqlite->res2arr($res); 461 $timestamp = $rows[0]['timestamp']; 462 if ($timestamp < time() - (60 * 30)) // 60 seconds x 5 minutes 463 { 464 $hasCacheTimedOut = true; 465 } 466 467 return $hasCacheTimedOut; 468 } 469 470 function readLocalChangesAwaitingFromCache() 471 { 472 $changesAwaiting = true; 473 474 $res = $this->loadSqlite(); 475 if (!$res) return; 476 477 $sql = "SELECT status FROM git WHERE repo = 'local'"; 478 $res = $this->sqlite->query($sql); 479 $rows = $this->sqlite->res2arr($res); 480 $status = $rows[0]['status']; 481 if ($status !== 'submitted' ) $changesAwaiting = false; 482 483 return $changesAwaiting; 484 } 485 486 function hasUpstreamCacheTimedOut() 487 { 488 $hasCacheTimedOut = false; 489 490 $res = $this->loadSqlite(); 491 if (!$res) return; 492 493 $sql = "SELECT timestamp FROM git WHERE repo = 'upstream';"; 494 $res = $this->sqlite->query($sql); 495 $rows = $this->sqlite->res2arr($res); 496 $timestamp = $rows[0]['timestamp']; 497 if ($timestamp < time() - (60 * 60)) // 60 seconds x 60 minutes = 1 hour 498 { 499 $hasCacheTimedOut = true; 500 } 501 502 return $hasCacheTimedOut; 503 } 504 505 function readUpstreamStatusFromCache() { 506 $updatesAvailable = true; 507 508 $res = $this->loadSqlite(); 509 if (!$res) return; 510 511 $sql = "SELECT status FROM git WHERE repo = 'upstream'"; 512 $res = $this->sqlite->query($sql); 513 $rows = $this->sqlite->res2arr($res); 514 $status = $rows[0]['status']; 515 if ($status === 'clean') $updatesAvailable = false; 516 517 return $updatesAvailable; 518 } 519 520 function CheckForUpstreamUpdates() { 521 global $conf; 522 $this->getConf(''); 523 524 $git_exe_path = $conf['plugin']['git']['git_exe_path']; 525 $datapath = $conf['savedir']; 526 527 $res = $this->loadSqlite(); 528 if (!$res) return; 529 530 $updatesAvailable = false; 531 if ($this->hasUpstreamCacheTimedOut()) 532 { 533 $repo = new GitRepo($datapath); 534 $repo->git_path = $git_exe_path; 535 536 if ($repo->test_origin() === false) { 537 msg('Repository seems to have an invalid remote (origin)'); 538 return $updatesAvailable; 539 } 540 541 $repo->fetch(); 542 $log = $repo->get_log(); 543 544 if ($log !== "") 545 { 546 $updatesAvailable = true; 547 $sql = "INSERT OR REPLACE INTO git (repo, timestamp, status ) VALUES ('upstream', ".time().", 'alert');"; 548 $this->sqlite->query($sql); 549 } 550 else 551 { 552 $sql = "INSERT OR REPLACE INTO git (repo, timestamp, status ) VALUES ('upstream', ".time().", 'clean');"; 553 $this->sqlite->query($sql); 554 } 555 } 556 else 557 { 558 $updatesAvailable = $this->readUpstreamStatusFromCache(); 559 } 560 return $updatesAvailable; 561 } 562} 563