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