*/ // phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols // must be run within Dokuwiki if (!defined('DOKU_INC')) die(); if (!defined('DOKU_LF')) define('DOKU_LF', "\n"); if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t"); if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); require_once __DIR__ . '/../loader.php'; use dokuwiki\Extension\ActionPlugin; use dokuwiki\Extension\EventHandler; use dokuwiki\Extension\Event; use dokuwiki\Search\Indexer; use woolfg\dokuwiki\plugin\gitbacked\Git; use woolfg\dokuwiki\plugin\gitbacked\GitRepo; use woolfg\dokuwiki\plugin\gitbacked\GitBackedUtil; // phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace // phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps class action_plugin_gitbacked_editcommit extends ActionPlugin { /** * Temporary directory for this gitbacked plugin. * * @var string */ private $temp_dir; public function __construct() { $this->temp_dir = GitBackedUtil::getTempDir(); } public function register(EventHandler $controller) { $controller->register_hook('IO_WIKIPAGE_WRITE', 'AFTER', $this, 'handleIOWikiPageWrite'); $controller->register_hook('MEDIA_UPLOAD_FINISH', 'AFTER', $this, 'handleMediaUpload'); $controller->register_hook('MEDIA_DELETE_FILE', 'AFTER', $this, 'handleMediaDeletion'); $controller->register_hook('DOKUWIKI_DONE', 'AFTER', $this, 'handlePeriodicPull'); } private function initRepo() { //get path to the repo root (by default DokuWiki's savedir) $repoPath = GitBackedUtil::getEffectivePath($this->getConf('repoPath')); $gitPath = trim($this->getConf('gitPath')); if ($gitPath !== '') { Git::setBin($gitPath); } //init the repo and create a new one if it is not present io_mkdir_p($repoPath); $repo = new GitRepo($repoPath, $this, true, true); //set git working directory (by default DokuWiki's savedir) $repoWorkDir = $this->getConf('repoWorkDir'); if (!empty($repoWorkDir)) { $repoWorkDir = GitBackedUtil::getEffectivePath($repoWorkDir); } Git::setBin(empty($repoWorkDir) ? Git::getBin() : Git::getBin() . ' --work-tree ' . escapeshellarg($repoWorkDir)); $params = str_replace( ['%mail%', '%user%'], [$this->getAuthorMail(), $this->getAuthor()], $this->getConf('addParams') ); if ($params) { Git::setBin(Git::getBin() . ' ' . $params); } return $repo; } private function isIgnored($filePath) { $ignore = false; $ignorePaths = trim($this->getConf('ignorePaths')); if ($ignorePaths !== '') { $paths = explode(',', $ignorePaths); foreach ($paths as $path) { if (strstr($filePath, $path)) { $ignore = true; } } } return $ignore; } private function commitFile($filePath, $message) { if (!$this->isIgnored($filePath)) { try { $repo = $this->initRepo(); //add the changed file and set the commit message $repo->add($filePath); $repo->commit($message); //if the push after Commit option is set we push the active branch to origin if ($this->getConf('pushAfterCommit')) { $repo->push('origin', $repo->activeBranch()); } } catch (Exception $e) { if (!$this->isNotifyByEmailOnGitCommandError()) { throw new Exception('Git committing or pushing failed: ' . $e->getMessage(), 1, $e); } return; } } } private function getUserInfo($key) { return $GLOBALS['USERINFO'][$key] ?? ''; } private function getAuthor() { return $this->getUserInfo('name'); } private function getAuthorMail() { return $this->getUserInfo('mail'); } private function computeLocalPath() { global $conf; $repoPath = str_replace('\\', '/', realpath(GitBackedUtil::getEffectivePath($this->getConf('repoPath')))); $datadir = $conf['datadir']; // already normalized if (substr($datadir, 0, strlen($repoPath)) !== $repoPath) { throw new Exception('Datadir not inside repoPath ??'); } return substr($datadir, strlen($repoPath) + 1); } private function updatePage($page) { if (is_callable(Indexer::class . '::getInstance')) { $Indexer = Indexer::getInstance(); $success = $Indexer->addPage($page, false, false); } elseif (class_exists('Doku_Indexer')) { $success = idx_addPage($page, false, false); } else { // Failed to index the page. Your DokuWiki is older than release 2011-05-25 "Rincewind" $success = false; } echo "Update $page: $success
"; } public function handlePeriodicPull(Event &$event, $param) { if ($this->getConf('periodicPull')) { $enableIndexUpdate = $this->getConf('updateIndexOnPull'); $lastPullFile = $this->temp_dir . '/lastpull.txt'; //check if the lastPullFile exists if (is_file($lastPullFile)) { $lastPull = unserialize(file_get_contents($lastPullFile)); } else { $lastPull = 0; } //calculate time between pulls in seconds $timeToWait = $this->getConf('periodicMinutes') * 60; $now = time(); //if it is time to run a pull request if ($lastPull + $timeToWait < $now) { try { $repo = $this->initRepo(); if ($enableIndexUpdate) { $localPath = $this->computeLocalPath(); // store current revision id $revBefore = $repo->run('rev-parse HEAD'); } //execute the pull request $repo->pull('origin', $repo->activeBranch()); if ($enableIndexUpdate) { // store new revision id $revAfter = $repo->run('rev-parse HEAD'); if (strcmp($revBefore, $revAfter) != 0) { // if there were some changes, get the list of all changed files $changedFilesPage = $repo->run('diff --name-only ' . $revBefore . ' ' . $revAfter); $changedFiles = preg_split("/\r\n|\n|\r/", $changedFilesPage); foreach ($changedFiles as $cf) { // check if the file is inside localPath, that is, it's a page if (substr($cf, 0, strlen($localPath)) === $localPath) { // convert from relative filename to page name // for example: local/path/dir/subdir/test.txt -> dir:subdir:test // -4 removes .txt $page = str_replace('/', ':', substr($cf, strlen($localPath) + 1, -4)); // update the page $this->updatePage($page); } else { echo "Page NOT to update: $cf
"; } } } } } catch (Exception $e) { if (!$this->isNotifyByEmailOnGitCommandError()) { throw new Exception('Git command failed to perform periodic pull: ' . $e->getMessage(), 2, $e); } return; } //save the current time to the file to track the last pull execution file_put_contents($lastPullFile, serialize(time())); } } } public function handleMediaDeletion(Event &$event, $param) { $mediaPath = $event->data['path']; $mediaName = $event->data['name']; $message = str_replace( ['%media%', '%user%'], [$mediaName, $this->getAuthor()], $this->getConf('commitMediaMsgDel') ); $this->commitFile($mediaPath, $message); } public function handleMediaUpload(Event &$event, $param) { $mediaPath = $event->data[1]; $mediaName = $event->data[2]; $message = str_replace( ['%media%', '%user%'], [$mediaName, $this->getAuthor()], $this->getConf('commitMediaMsg') ); $this->commitFile($mediaPath, $message); } public function handleIOWikiPageWrite(Event &$event, $param) { $rev = $event->data[3]; /* On update to an existing page this event is called twice, * once for the transfer of the old version to the attic (rev will have a value) * and once to write the new version of the page into the wiki (rev is false) */ if (!$rev) { $pagePath = $event->data[0][0]; $pageName = $event->data[2]; $pageContent = $event->data[0][1]; // get the summary directly from the form input // as the metadata hasn't updated yet $editSummary = $GLOBALS['INPUT']->str('summary'); // empty content indicates a page deletion if ($pageContent == '') { // get the commit text for deletions $msgTemplate = $this->getConf('commitPageMsgDel'); // bad hack as DokuWiki deletes the file after this event // thus, let's delete the file by ourselves, so git can recognize the deletion // DokuWiki uses @unlink as well, so no error should be thrown if we delete it twice @unlink($pagePath); } else { //get the commit text for edits $msgTemplate = $this->getConf('commitPageMsg'); } $message = str_replace( ['%page%', '%summary%', '%user%'], [$pageName, $editSummary, $this->getAuthor()], $msgTemplate ); $this->commitFile($pagePath, $message); } } // ====== Error notification helpers ====== /** * Notifies error on create_new * * @access public * @param string repository path * @param string reference path / remote reference * @param string error message * @return bool */ public function notifyCreateNewError($repo_path, $reference, $error_message) { $template_replacements = [ 'GIT_REPO_PATH' => $repo_path, 'GIT_REFERENCE' => (empty($reference) ? 'n/a' : $reference), 'GIT_ERROR_MESSAGE' => $error_message ]; return $this->notifyByMail('mail_create_new_error_subject', 'mail_create_new_error', $template_replacements); } /** * Notifies error on setting repo path * * @access public * @param string repository path * @param string error message * @return bool */ public function notifyRepoPathError($repo_path, $error_message) { $template_replacements = [ 'GIT_REPO_PATH' => $repo_path, 'GIT_ERROR_MESSAGE' => $error_message ]; return $this->notifyByMail('mail_repo_path_error_subject', 'mail_repo_path_error', $template_replacements); } /** * Notifies error on git command * * @access public * @param string repository path * @param string current working dir * @param string command line * @param int exit code of command (status) * @param string error message * @return bool */ public function notifyCommandError($repo_path, $cwd, $command, $status, $error_message) { $template_replacements = [ 'GIT_REPO_PATH' => $repo_path, 'GIT_CWD' => $cwd, 'GIT_COMMAND' => $command, 'GIT_COMMAND_EXITCODE' => $status, 'GIT_ERROR_MESSAGE' => $error_message ]; return $this->notifyByMail('mail_command_error_subject', 'mail_command_error', $template_replacements); } /** * Notifies success on git command * * @access public * @param string repository path * @param string current working dir * @param string command line * @return bool */ public function notifyCommandSuccess($repo_path, $cwd, $command) { if (!$this->getConf('notifyByMailOnSuccess')) { return false; } $template_replacements = [ 'GIT_REPO_PATH' => $repo_path, 'GIT_CWD' => $cwd, 'GIT_COMMAND' => $command ]; return $this->notifyByMail('mail_command_success_subject', 'mail_command_success', $template_replacements); } /** * Send an eMail, if eMail address is configured * * @access public * @param string lang id for the subject * @param string lang id for the template(.txt) * @param array array of replacements * @return bool */ public function notifyByMail($subject_id, $template_id, $template_replacements) { $ret = false; //dbglog("GitBacked - notifyByMail: [subject_id=" . $subject_id // . ", template_id=" . $template_id // . ", template_replacements=" . $template_replacements . "]"); if (!$this->isNotifyByEmailOnGitCommandError()) { return $ret; } //$template_text = rawLocale($template_id); // this works for core artifacts only - not for plugins $template_filename = $this->localFN($template_id); $template_text = file_get_contents($template_filename); $template_html = $this->render_text($template_text); $mailer = new \Mailer(); $mailer->to($this->getEmailAddressOnErrorConfigured()); //dbglog("GitBacked - lang check['".$subject_id."']: ".$this->getLang($subject_id)); //dbglog("GitBacked - template text['".$template_id."']: ".$template_text); //dbglog("GitBacked - template html['".$template_id."']: ".$template_html); $mailer->subject($this->getLang($subject_id)); $mailer->setBody($template_text, $template_replacements, null, $template_html); $ret = $mailer->send(); return $ret; } /** * Check, if eMail is to be sent on a Git command error. * * @access public * @return bool */ public function isNotifyByEmailOnGitCommandError() { $emailAddressOnError = $this->getEmailAddressOnErrorConfigured(); return !empty($emailAddressOnError); } /** * Get the eMail address configured for notifications. * * @access public * @return string */ public function getEmailAddressOnErrorConfigured() { $emailAddressOnError = trim($this->getConf('emailAddressOnError')); return $emailAddressOnError; } } // phpcs:enable Squiz.Classes.ValidClassName.NotCamelCaps // phpcs:enable PSR1.Classes.ClassDeclaration.MissingNamespace // vim:ts=4:sw=4:et: