1<?php 2/** 3 * DokuWiki Plugin gitbacked (Action Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Wolfgang Gassler <wolfgang@gassler.org> 7 */ 8 9// must be run within Dokuwiki 10if (!defined('DOKU_INC')) die(); 11 12if (!defined('DOKU_LF')) define('DOKU_LF', "\n"); 13if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t"); 14if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 15 16require_once dirname(__FILE__).'/../lib/Git.php'; 17require_once dirname(__FILE__).'/../lib/GitBackedUtil.php'; 18 19class action_plugin_gitbacked_editcommit extends DokuWiki_Action_Plugin { 20 21 function __construct() { 22 $this->temp_dir = GitBackedUtil::getTempDir(); 23 } 24 25 public function register(Doku_Event_Handler $controller) { 26 27 $controller->register_hook('IO_WIKIPAGE_WRITE', 'AFTER', $this, 'handle_io_wikipage_write'); 28 $controller->register_hook('MEDIA_UPLOAD_FINISH', 'AFTER', $this, 'handle_media_upload'); 29 $controller->register_hook('MEDIA_DELETE_FILE', 'AFTER', $this, 'handle_media_deletion'); 30 $controller->register_hook('DOKUWIKI_DONE', 'AFTER', $this, 'handle_periodic_pull'); 31 } 32 33 private function initRepo() { 34 //get path to the repo root (by default DokuWiki's savedir) 35 $repoPath = GitBackedUtil::getEffectivePath($this->getConf('repoPath')); 36 $gitPath = trim($this->getConf('gitPath')); 37 if ($gitPath !== '') { 38 Git::set_bin($gitPath); 39 } 40 //init the repo and create a new one if it is not present 41 io_mkdir_p($repoPath); 42 $repo = new GitRepo($repoPath, $this, true, true); 43 //set git working directory (by default DokuWiki's savedir) 44 $repoWorkDir = $this->getConf('repoWorkDir'); 45 if (!empty($repoWorkDir)) { 46 $repoWorkDir = GitBackedUtil::getEffectivePath($repoWorkDir); 47 } 48 Git::set_bin(empty($repoWorkDir) ? Git::get_bin() : Git::get_bin().' --work-tree '.escapeshellarg($repoWorkDir)); 49 $params = str_replace( 50 array('%mail%','%user%'), 51 array($this->getAuthorMail(),$this->getAuthor()), 52 $this->getConf('addParams')); 53 if ($params) { 54 Git::set_bin(Git::get_bin().' '.$params); 55 } 56 return $repo; 57 } 58 59 private function isIgnored($filePath) { 60 $ignore = false; 61 $ignorePaths = trim($this->getConf('ignorePaths')); 62 if ($ignorePaths !== '') { 63 $paths = explode(',',$ignorePaths); 64 foreach($paths as $path) { 65 if (strstr($filePath,$path)) { 66 $ignore = true; 67 } 68 } 69 } 70 return $ignore; 71 } 72 73 private function commitFile($filePath,$message) { 74 if (!$this->isIgnored($filePath)) { 75 try { 76 $repo = $this->initRepo(); 77 78 //add the changed file and set the commit message 79 $repo->add($filePath); 80 $repo->commit($message); 81 82 //if the push after Commit option is set we push the active branch to origin 83 if ($this->getConf('pushAfterCommit')) { 84 $repo->push('origin',$repo->active_branch()); 85 } 86 } catch (Exception $e) { 87 if (!$this->isNotifyByEmailOnGitCommandError()) { 88 throw new Exception('Git committing or pushing failed: '.$e->getMessage(), 1, $e); 89 } 90 return; 91 } 92 } 93 } 94 95 private function getAuthor() { 96 return $GLOBALS['USERINFO']['name']; 97 } 98 99 private function getAuthorMail() { 100 return $GLOBALS['USERINFO']['mail']; 101 } 102 103 private function computeLocalPath() { 104 global $conf; 105 $repoPath = str_replace('\\', '/', realpath(GitBackedUtil::getEffectivePath($this->getConf('repoPath')))); 106 $datadir = $conf['datadir']; // already normalized 107 if(!(substr($datadir, 0, strlen($repoPath)) === $repoPath)) 108 { 109 throw new Exception('Datadir not inside repoPath ??'); 110 } 111 return substr($datadir, strlen($repoPath)+1); 112 } 113 114 private function updatePage($page){ 115 116 if (is_callable('dokuwiki\Search\Indexer::getInstance')) { 117 $Indexer = Indexer::getInstance(); 118 $success = $Indexer->addPage($page, false, false); 119 } elseif (class_exists('Doku_Indexer')) { 120 $success = idx_addPage($page, false, false); 121 } else { 122 // Failed to index the page. Your DokuWiki is older than release 2011-05-25 "Rincewind" 123 $success = false; 124 } 125 126 echo "Update $page: $success <br/>"; 127 128 } 129 130 public function handle_periodic_pull(Doku_Event &$event, $param) { 131 if ($this->getConf('periodicPull')) { 132 $enableIndexUpdate = $this->getConf('updateIndexOnPull'); 133 $lastPullFile = $this->temp_dir.'/lastpull.txt'; 134 //check if the lastPullFile exists 135 if (is_file($lastPullFile)) { 136 $lastPull = unserialize(file_get_contents($lastPullFile)); 137 } else { 138 $lastPull = 0; 139 } 140 //calculate time between pulls in seconds 141 $timeToWait = $this->getConf('periodicMinutes')*60; 142 $now = time(); 143 144 145 //if it is time to run a pull request 146 if ($lastPull+$timeToWait < $now) { 147 try { 148 149 $repo = $this->initRepo(); 150 if($enableIndexUpdate) 151 { 152 $localPath = $this -> computeLocalPath(); 153 154 // store current revision id 155 $revBefore = $repo->run('rev-parse HEAD'); 156 } 157 158 //execute the pull request 159 $repo->pull('origin',$repo->active_branch()); 160 161 if($enableIndexUpdate) 162 { 163 // store new revision id 164 $revAfter = $repo->run('rev-parse HEAD'); 165 166 if(strcmp($revBefore, $revAfter) != 0) 167 { 168 // if there were some changes, get the list of all changed files 169 $changedFilesPage = $repo->run('diff --name-only '.$revBefore.' '.$revAfter); 170 $changedFiles = preg_split("/\r\n|\n|\r/", $changedFilesPage); 171 172 foreach ($changedFiles as $cf) 173 { 174 // check if the file is inside localPath, that is, it's a page 175 if(substr($cf, 0, strlen($localPath)) === $localPath) 176 { 177 // convert from relative filename to page name 178 // for example: local/path/dir/subdir/test.txt -> dir:subdir:test 179 $page = str_replace('/', ':',substr($cf, strlen($localPath)+1, -4)); // -4 removes .txt 180 181 // update the page 182 $this -> updatePage($page); 183 } 184 else 185 { 186 echo "Page NOT to update: $cf <br/>"; 187 } 188 } 189 190 } 191 } 192 193 } catch (Exception $e) { 194 if (!$this->isNotifyByEmailOnGitCommandError()) { 195 throw new Exception('Git command failed to perform periodic pull: '.$e->getMessage(), 2, $e); 196 } 197 return; 198 } 199 200 //save the current time to the file to track the last pull execution 201 file_put_contents($lastPullFile,serialize(time())); 202 } 203 } 204 } 205 206 public function handle_media_deletion(Doku_Event &$event, $param) { 207 $mediaPath = $event->data['path']; 208 $mediaName = $event->data['name']; 209 210 $message = str_replace( 211 array('%media%','%user%'), 212 array($mediaName,$this->getAuthor()), 213 $this->getConf('commitMediaMsgDel') 214 ); 215 216 $this->commitFile($mediaPath,$message); 217 218 } 219 220 public function handle_media_upload(Doku_Event &$event, $param) { 221 222 $mediaPath = $event->data[1]; 223 $mediaName = $event->data[2]; 224 225 $message = str_replace( 226 array('%media%','%user%'), 227 array($mediaName,$this->getAuthor()), 228 $this->getConf('commitMediaMsg') 229 ); 230 231 $this->commitFile($mediaPath,$message); 232 233 } 234 235 public function handle_io_wikipage_write(Doku_Event &$event, $param) { 236 237 $rev = $event->data[3]; 238 239 /* On update to an existing page this event is called twice, 240 * once for the transfer of the old version to the attic (rev will have a value) 241 * and once to write the new version of the page into the wiki (rev is false) 242 */ 243 if (!$rev) { 244 245 $pagePath = $event->data[0][0]; 246 $pageName = $event->data[2]; 247 $pageContent = $event->data[0][1]; 248 249 // get the summary directly from the form input 250 // as the metadata hasn't updated yet 251 $editSummary = $GLOBALS['INPUT']->str('summary'); 252 253 // empty content indicates a page deletion 254 if ($pageContent == '') { 255 // get the commit text for deletions 256 $msgTemplate = $this->getConf('commitPageMsgDel'); 257 258 // bad hack as DokuWiki deletes the file after this event 259 // thus, let's delete the file by ourselves, so git can recognize the deletion 260 // DokuWiki uses @unlink as well, so no error should be thrown if we delete it twice 261 @unlink($pagePath); 262 263 } else { 264 //get the commit text for edits 265 $msgTemplate = $this->getConf('commitPageMsg'); 266 } 267 268 $message = str_replace( 269 array('%page%','%summary%','%user%'), 270 array($pageName,$editSummary,$this->getAuthor()), 271 $msgTemplate 272 ); 273 274 $this->commitFile($pagePath,$message); 275 276 } 277 } 278 279 // ====== Error notification helpers ====== 280 /** 281 * Notifies error on create_new 282 * 283 * @access public 284 * @param string repository path 285 * @param string reference path / remote reference 286 * @param string error message 287 * @return bool 288 */ 289 public function notify_create_new_error($repo_path, $reference, $error_message) { 290 $template_replacements = array( 291 'GIT_REPO_PATH' => $repo_path, 292 'GIT_REFERENCE' => (empty($reference) ? 'n/a' : $reference), 293 'GIT_ERROR_MESSAGE' => $error_message 294 ); 295 return $this->notifyByMail('mail_create_new_error_subject', 'mail_create_new_error', $template_replacements); 296 } 297 298 /** 299 * Notifies error on setting repo path 300 * 301 * @access public 302 * @param string repository path 303 * @param string error message 304 * @return bool 305 */ 306 public function notify_repo_path_error($repo_path, $error_message) { 307 $template_replacements = array( 308 'GIT_REPO_PATH' => $repo_path, 309 'GIT_ERROR_MESSAGE' => $error_message 310 ); 311 return $this->notifyByMail('mail_repo_path_error_subject', 'mail_repo_path_error', $template_replacements); 312 } 313 314 /** 315 * Notifies error on git command 316 * 317 * @access public 318 * @param string repository path 319 * @param string current working dir 320 * @param string command line 321 * @param int exit code of command (status) 322 * @param string error message 323 * @return bool 324 */ 325 public function notify_command_error($repo_path, $cwd, $command, $status, $error_message) { 326 $template_replacements = array( 327 'GIT_REPO_PATH' => $repo_path, 328 'GIT_CWD' => $cwd, 329 'GIT_COMMAND' => $command, 330 'GIT_COMMAND_EXITCODE' => $status, 331 'GIT_ERROR_MESSAGE' => $error_message 332 ); 333 return $this->notifyByMail('mail_command_error_subject', 'mail_command_error', $template_replacements); 334 } 335 336 /** 337 * Notifies success on git command 338 * 339 * @access public 340 * @param string repository path 341 * @param string current working dir 342 * @param string command line 343 * @return bool 344 */ 345 public function notify_command_success($repo_path, $cwd, $command) { 346 if (!$this->getConf('notifyByMailOnSuccess')) { 347 return false; 348 } 349 $template_replacements = array( 350 'GIT_REPO_PATH' => $repo_path, 351 'GIT_CWD' => $cwd, 352 'GIT_COMMAND' => $command 353 ); 354 return $this->notifyByMail('mail_command_success_subject', 'mail_command_success', $template_replacements); 355 } 356 357 /** 358 * Send an eMail, if eMail address is configured 359 * 360 * @access public 361 * @param string lang id for the subject 362 * @param string lang id for the template(.txt) 363 * @param array array of replacements 364 * @return bool 365 */ 366 public function notifyByMail($subject_id, $template_id, $template_replacements) { 367 $ret = false; 368 //dbglog("GitBacked - notifyByMail: [subject_id=".$subject_id.", template_id=".$template_id.", template_replacements=".$template_replacements."]"); 369 if (!$this->isNotifyByEmailOnGitCommandError()) { 370 return $ret; 371 } 372 //$template_text = rawLocale($template_id); // this works for core artifacts only - not for plugins 373 $template_filename = $this->localFN($template_id); 374 $template_text = file_get_contents($template_filename); 375 $template_html = $this->render_text($template_text); 376 377 $mailer = new \Mailer(); 378 $mailer->to($this->getEmailAddressOnErrorConfigured()); 379 //dbglog("GitBacked - lang check['".$subject_id."']: ".$this->getLang($subject_id)); 380 //dbglog("GitBacked - template text['".$template_id."']: ".$template_text); 381 //dbglog("GitBacked - template html['".$template_id."']: ".$template_html); 382 $mailer->subject($this->getLang($subject_id)); 383 $mailer->setBody($template_text, $template_replacements, null, $template_html); 384 $ret = $mailer->send(); 385 386 return $ret; 387 } 388 389 /** 390 * Check, if eMail is to be sent on a Git command error. 391 * 392 * @access public 393 * @return bool 394 */ 395 public function isNotifyByEmailOnGitCommandError() { 396 $emailAddressOnError = $this->getEmailAddressOnErrorConfigured(); 397 return !empty($emailAddressOnError); 398 } 399 400 /** 401 * Get the eMail address configured for notifications. 402 * 403 * @access public 404 * @return string 405 */ 406 public function getEmailAddressOnErrorConfigured() { 407 $emailAddressOnError = trim($this->getConf('emailAddressOnError')); 408 return $emailAddressOnError; 409 } 410 411} 412 413// vim:ts=4:sw=4:et: 414