*/ if(!defined('DOKU_INC')) die(); if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); require_once(DOKU_PLUGIN.'git/lib/Git.php'); require_once(DOKU_INC.'inc/search.php'); require_once(DOKU_INC.'/inc/DifferenceEngine.php'); function git_callback_search_wanted(&$data,$base,$file,$type,$lvl,$opts) { global $conf; if($type == 'd'){ return true; // recurse all directories, but we don't store namespaces } if(!preg_match("/.*\.txt$/", $file)) { // Ignore everything but TXT return true; } // get id of this file $id = pathID($file); $item = &$data["$id"]; if(! isset($item)) { $data["$id"]= array('id' => $id, 'file' => $file); } } class helper_plugin_git extends DokuWiki_Plugin { var $dt = null; var $sqlite = null; function getMethods(){ $result = array(); $result[] = array( 'name' => 'cloneRepo', 'desc' => 'Creates a new clone of a repository', 'params' => array( 'destination' => 'string'), 'return' => array('result' => 'array'), ); // and more supported methods... return $result; } function rebuild_data_plugin_data() { // Load the data plugin only if we need to if(!$this->dt) { $this->dt =& plugin_load('syntax', 'data_entry'); if(!$this->dt) { msg('Error loading the data table class from GIT Helper. Make sure the data plugin is installed.',-1); return; } } global $conf; $result = ''; $data = array(); search($data,$conf['datadir'],'git_callback_search_wanted',array('ns' => $ns)); $output = array(); foreach($data as $entry) { // Get the content of the file $filename = $conf['datadir'].$entry['file']; if (strpos($filename, 'syntax') > 0) continue; // Skip instructional pages $body = @file_get_contents($filename); // Run the regular expression to get the dataentry section $pattern = '/----.*dataentry.*\R----/s'; if (preg_match($pattern, $body, $matches) === false) { continue; } foreach ($matches as $match) { // Re-use the handle method to get the formatted data $cleanedMatch = htmlspecialchars($match); $dummy = ""; $formatted = $this->dt->handle($cleanedMatch, null, null, $dummy); $output['id'.count($output)] = $formatted; // Re-use the save_data method to .... (drum roll) save the data. // Ignore the returned html, just move on to the next file $html = $this->dt->_saveData($formatted, $entry['id'], 'Title'.count($output)); } } msg('Data entry plugin found and refreshed all '.count($output).' entries.'); } /** * Resets a GIT cache by setting the timestamp to ZERO (1st of jan 1970) * * @param string repository name. Either: 'Local' or 'upstream' */ function resetGitStatusCache($repo) { $res = $this->loadSqlite(); if (!$res) { msg('Error loading sqlite'); return; } // Set the time to zero, so the first alert msg will set the correct status $sql = "INSERT OR REPLACE INTO git (repo, timestamp, status ) VALUES ('".$repo."', 0, 'clean');"; $this->sqlite->query($sql); } function haveChangesBeenSubmitted() { $changesAwaiting = true; $res = $this->loadSqlite(); if (!$res) return; $res = $this->sqlite->query("SELECT status FROM git WHERE repo = 'local'"); $rows = $this->sqlite->res2arr($res); $status = $rows[0]['status']; if ($status !== 'submitted' ) $changesAwaiting = false; return $changesAwaiting; } function submittChangesForApproval() { $res = $this->loadSqlite(); if (!$res) return; // Set the time to zero, so the first alert msg will set the correct status $hundred_years_into_future = time() + (60 * 60 * 24 * 365 * 100); $sql = "INSERT OR REPLACE INTO git (repo, timestamp, status ) VALUES ('local', ".$hundred_years_into_future.", 'submitted');"; $this->sqlite->query($sql); $this->changeReadOnly(true); $this->sendNotificationEMail(); } function sendNotificationEMail() { global $conf; $this->getConf(''); $notify = $conf['plugin']['git']['commit_notifcations']; $local_status_page = wl($conf['plugin']['git']['local_status_page'],'',true); $mail = new Mailer(); $mail->to($notify); $mail->subject('An improvement has been submitted for approval!'); $mail->setBody('Please review the proposed changes before the next meeting: '.$local_status_page); return $mail->send(); } function cloneRepo($origin, $destination) { global $conf; $this->getConf(''); $git_exe_path = $conf['plugin']['git']['git_exe_path']; try { $repo = new GitRepo($destination, true, false); $repo->git_path = $git_exe_path; $repo->clone_from($origin); } catch (Exception $e) { msg($e->getMessage()); } } function changeReadOnly($readonly = true) { global $config_cascade; $AUTH_ACL = file($config_cascade['acl']['default']); $lines = array(); foreach($AUTH_ACL as $line){ if(strpos(strtolower($line), strtolower('@USER')) === FALSE) { $lines[] = $line; continue; } if ($readonly) { $lines[] = '* @user '.AUTH_READ; } else { $lines[] = '* @user '.AUTH_DELETE; } $lines[] = $replaced; } // save it io_saveFile($config_cascade['acl']['default'], join('',$lines)); } function render_commit_selector($renderer, $commits) { // When viewing file content differences, the hash gets set in the html request, so we can select the correct option $selected_hash = trim($_REQUEST['hash']); if ($selected_hash === '') $selected_hash = 'all'; // By default select "all". $renderer->doc .= "'; } function render_changed_files_table($renderer, $commits, $repo) { $selected_hash = trim($_REQUEST['hash']); if ($selected_hash === '') $selected_hash = 'all'; // By default select "all". foreach($commits as $commit) { $hash = $commit['hash']; if($hash === $selected_hash || $hash === 'new') $divVisibility = ""; // Show the selected else $divVisibility = " display:none;"; // Hide the rest $renderer->doc .= "
"; // Commits selected to show changes for if ($hash === 'new') { $files = explode("\n", $repo->get_status()); } else if($hash === 'all') { $files = explode("\n", $repo->get_files_by_commit('origin/master..HEAD')); } else { $files = explode("\n", $repo->get_files_by_commit($hash)); } // No files if ($files === null || count($files) === 1) { $renderer->doc .= "


No files have changed for the selected item. If a merge is selected, then no conflicts were detected.

"; } else { $renderer->doc .= '

The content of the selected commit:

'; $renderer->doc .= ""; foreach ($files as $file) { if ($file === "") continue; $renderer->doc .= ""; $renderer->doc .= ""; } $renderer->doc .= "
Change typePageChanges
"; $change = substr($file, 0, 2); if (strpos($change, '?') !== false) $renderer->doc .= "Added:"; else if (strpos($change, 'M') !== false) $renderer->doc .= "Modified:"; else if (strpos($change, 'A') !== false) $renderer->doc .= "Added:"; else if (strpos($change, 'D') !== false) $renderer->doc .= "Removed:"; else if (strpos($change, 'R') !== false) $renderer->doc .= "Removed:"; else if (strpos($change, 'r') !== false) $renderer->doc .= "Removed:"; $renderer->doc .= ""; $file = trim(substr($file, 2)); $page = $this->getPageFromFile($file); $renderer->doc .= ''.$page.''; $renderer->doc .= ""; $renderer->doc .= '
'; $renderer->doc .= ' '; $renderer->doc .= ' '; $renderer->doc .= ' '; $renderer->doc .= '
'; $renderer->doc .= "
"; } $renderer->doc .= "
\n"; // Initially, hide second and further tables $divVisibility = " display:none;"; } } function getPageFromFile($file) { // If it's not a wiki page, just return the normal filename if (strpos($file, 'pages/') === false) return $file; // Replace all sorts of stuff so it makes sense to non-technical users. $page = str_replace('pages/', '', $file); $page = str_replace('.txt', '', $page); $page = str_replace('/', ':', $page); $page = trim($page); return $page; } function renderChangesMade(&$renderer, &$repo, $mode) { global $conf; $this->getConf(''); $fileForDiff = trim($_REQUEST['filename']); $page = $this->getPageFromFile($fileForDiff); $hash = trim($_REQUEST['hash']); if ($fileForDiff !== '') { $renderer->doc .= '
'; //Write header $renderer->doc .= '

Changes to: '.$page.'

'; if ($mode == 'Approve Local') { if ($hash === 'all') $renderer->doc .= '

Left = The current page in Live
'; else $renderer->doc .= '

Left = The page before the selected commited retrieved from GIT
'; $renderer->doc .= 'Right = The page after the selected commit

'; // LEFT: Find the file before for the selected commit if ($hash === 'all') $l_text = $repo->getFile($fileForDiff, 'origin/master'); else $l_text = $repo->getFile($fileForDiff, $hash."~1"); // RIGHT: Find the file for the selected commit if ($hash === 'all') $r_text = $repo->getFile($fileForDiff, 'HEAD'); else $r_text = $repo->getFile($fileForDiff, $hash); } else if ($mode == 'Commit local') { $renderer->doc .= '

Left = The last page commited to GIT
'; $renderer->doc .= 'Right = Current wiki content

'; // LEFT: Latest in GIT $l_text = $repo->getFile($fileForDiff, 'HEAD'); // RIGHT: Current $current_filename = $conf['savedir'].'/'.$fileForDiff; $current_filename = str_replace("/", "\\", $current_filename); $r_text = $this->getFileContents($current_filename); } else if ($mode == 'Merge upstream') { $renderer->doc .= '

Left = Current wiki content
'; $renderer->doc .= 'Right = Upstream changes to be merged

'; // LEFT: Current $current_filename = $conf['savedir'].'/'.$fileForDiff; $current_filename = str_replace("/", "\\", $current_filename); $l_text = $this->getFileContents($current_filename); // RIGHT: Latest in GIT to be merged $r_text = $repo->getFile($fileForDiff, $hash); } // Show diff $df = new Diff(explode("\n",htmlspecialchars($l_text)), explode("\n",htmlspecialchars($r_text))); $tdf = new TableDiffFormatter(); $renderer->doc .= ''; $renderer->doc .= $tdf->format($df); $renderer->doc .= '
'; $renderer->doc .= '
'; } } function renderAdminApproval(&$renderer) { $isAdmin = $this->isCurrentUserAnAdmin(); if ($isAdmin) { $renderer->doc .= '
'; $renderer->doc .= ' '; $renderer->doc .= ' '; $renderer->doc .= '
'; } } function isCurrentUserAnAdmin() { function isCurrentUserAnAdmin() { global $INFO; return ($INFO['isadmin']); } } function getFileContents($filename) { // get contents of a file into a string $handle = fopen($filename, "r"); $contents = fread($handle, filesize($filename)); fclose($handle); return $contents; } function loadSqlite() { if ($this->sqlite) return true; $this->sqlite =& plugin_load('helper', 'sqlite'); if (is_null($this->sqlite)) { msg('The sqlite plugin could not loaded from the GIT Plugin helper', -1); return false; } if($this->sqlite->init('git',DOKU_PLUGIN.'git/db/')){ return true; }else{ msg('Submitting changes failed as the GIT cache failed to initialise.', -1); return false; } } function hasLocalCacheTimedOut() { $hasCacheTimedOut = false; $res = $this->loadSqlite(); if (!$res) return; $sql = "SELECT timestamp FROM git WHERE repo = 'local'"; $res = $this->sqlite->query($sql); $rows = $this->sqlite->res2arr($res); $timestamp = $rows[0]['timestamp']; if ($timestamp < time() - (60 * 30)) // 60 seconds x 5 minutes { $hasCacheTimedOut = true; } return $hasCacheTimedOut; } function readLocalChangesAwaitingFromCache() { $changesAwaiting = true; $res = $this->loadSqlite(); if (!$res) return; $sql = "SELECT status FROM git WHERE repo = 'local'"; $res = $this->sqlite->query($sql); $rows = $this->sqlite->res2arr($res); $status = $rows[0]['status']; if ($status !== 'submitted' ) $changesAwaiting = false; return $changesAwaiting; } function hasUpstreamCacheTimedOut() { $hasCacheTimedOut = false; $res = $this->loadSqlite(); if (!$res) return; $sql = "SELECT timestamp FROM git WHERE repo = 'upstream';"; $res = $this->sqlite->query($sql); $rows = $this->sqlite->res2arr($res); $timestamp = $rows[0]['timestamp']; if ($timestamp < time() - (60 * 60)) // 60 seconds x 60 minutes = 1 hour { $hasCacheTimedOut = true; } return $hasCacheTimedOut; } function readUpstreamStatusFromCache() { $updatesAvailable = true; $res = $this->loadSqlite(); if (!$res) return; $sql = "SELECT status FROM git WHERE repo = 'upstream'"; $res = $this->sqlite->query($sql); $rows = $this->sqlite->res2arr($res); $status = $rows[0]['status']; if ($status === 'clean') $updatesAvailable = false; return $updatesAvailable; } function CheckForUpstreamUpdates() { global $conf; $this->getConf(''); $git_exe_path = $conf['plugin']['git']['git_exe_path']; $datapath = $conf['savedir']; $res = $this->loadSqlite(); if (!$res) return; $updatesAvailable = false; if ($this->hasUpstreamCacheTimedOut()) { $repo = new GitRepo($datapath); $repo->git_path = $git_exe_path; if ($repo->test_origin() === false) { msg('Repository seems to have an invalid remote (origin)'); return $updatesAvailable; } $repo->fetch(); $log = $repo->get_log(); if ($log !== "") { $updatesAvailable = true; $sql = "INSERT OR REPLACE INTO git (repo, timestamp, status ) VALUES ('upstream', ".time().", 'alert');"; $this->sqlite->query($sql); } else { $sql = "INSERT OR REPLACE INTO git (repo, timestamp, status ) VALUES ('upstream', ".time().", 'clean');"; $this->sqlite->query($sql); } } else { $updatesAvailable = $this->readUpstreamStatusFromCache(); } return $updatesAvailable; } }