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