added small interface
[GITManagedWebpage.git] / GITManagedWebpage.class.php
1 <?php
2 /*************************** GITManagedWebpage **************************
3  * Copyright (C) 2013       Philipp Kreil (pk910)                       *
4  *                                                                      *
5  * This program is free software: you can redistribute it and/or modify *
6  * it under the terms of the GNU General Public License as published by *
7  * the Free Software Foundation, either version 3 of the License, or    *
8  * (at your option) any later version.                                  *
9  *                                                                      *
10  * This program is distributed in the hope that it will be useful,      *
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of       *
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the        *
13  * GNU General Public License for more details.                         *
14  *                                                                      *
15  * You should have received a copy of the GNU General Public License    *
16  * along with this program. If not, see <http://www.gnu.org/licenses/>. *
17  *                                                                      *
18  ************************************************************************
19  *
20  *  GITManagedWebpage.class.php
21  *
22  * Functions to manage a Website using GIT Version Control
23  *
24  */
25
26 class GITManagedWebpage {
27     const ERROR_CRITICAL = 1;
28     const SESSION_PREFIX = "GITManagedWebpage_";
29
30     private $giturl;
31     private $workdir, $localdir;
32     private $ready = false;
33     private $loopedcall = false;
34     private $config = null;
35     private $config_changed = true;
36     private $activeSession = null;
37     
38     public function __construct($giturl, $workdir = null, $localdir = null) {
39         if(session_status() != PHP_SESSION_ACTIVE) {
40             session_start();
41         }
42         
43         if(defined("GITMANAGED_EXECUTED")) {
44             $this->loopedcall = true;
45             return;
46         }
47         
48         if($workdir === null) {
49             $workdir = dirname(__FILE__);
50             if(substr($workdir, -1) != "/")
51                 $workdir .= "/";
52             $workdir .= ".gitmanaged/";
53         }
54         else if(substr($workdir, -1) != "/")
55             $workdir .= "/";
56         
57         if($localdir === null) {
58             $localdir = dirname(__FILE__);
59             if(substr($localdir, -1) != "/")
60                 $localdir .= "/";
61         }
62         else if(substr($localdir, -1) != "/")
63             $localdir .= "/";
64         
65         $this->giturl = $giturl;
66         $this->workdir = $workdir;
67         $this->localdir = $localdir;
68         
69         if(!file_exists($this->workdir) || !is_dir($this->workdir)) {
70             if(file_exists($this->workdir) && !is_dir($this->workdir)) {
71                 $this->error(self::ERROR_CRITICAL, "local workdir (".htmlspecialchars($this->workdir).") is not a directory.");
72                 return;
73             }
74             $this->setupWorkdir();
75         }
76         $this->ready = true;
77     }
78     
79     /* private function gitcmd(...)
80     * Execute a git command and return output
81     */
82     private function gitcmd() {
83         $args = func_get_args();
84         $argstr = "";
85         foreach($args as $arg) {
86             if(is_array($arg)) {
87                 foreach($arg as $subarg) {
88                     $argstr .= " ".escapeshellarg($subarg);
89                 }
90             } else
91                 $argstr .= " ".escapeshellarg($arg);
92         }
93         $gitcmd = 'git '.escapeshellarg('--git-dir='.$this->workdir.'repository/.git').' '.escapeshellarg('--work-tree='.$this->workdir.'repository').$argstr;
94         $output = shell_exec($gitcmd);
95         return $output;
96     }
97     
98     /* private function setConfig($name, $value)
99     * store a option in the configuration
100     */
101     private function setConfig($name, $value) {
102         $this->config[strtolower($name)] = $value;
103         $this->config_changed = true;
104     }
105     
106     private function checkConfigIntegrity() {
107         foreach($this->config as $key => $value) {
108             if(substr($key, 0, strlen('branch_')) == 'branch_') {
109                 if(!file_exists($this->workdir.$key)) {
110                     unset($this->config['key']);
111                 }
112             }
113         }
114     }
115     
116     /* private function getConfig($name)
117     * get an option from the configuration
118     */
119     private function getConfig($name) {
120         if($this->config == null) {
121             if(!$this->ready)
122                 return null;
123             if(file_exists($this->workdir."config.txt")) {
124                 $config_txt = @file_get_contents($this->workdir."config.txt");
125                 $this->config = unserialize($config_txt);
126                 $this->checkConfigIntegrity();
127             } else {
128                 $this->config = array();
129                 return null;
130             }
131         }
132         if(array_key_exists(strtolower($name), $this->config))
133             return $this->config[strtolower($name)];
134         else
135             return null;
136     }
137     
138     private function saveConfig() {
139         if($this->config_changed && $this->ready) {
140             $fp = fopen($this->workdir."config.txt", "w");
141             fwrite($fp, serialize($this->config));
142             fclose($fp);
143         }
144     }
145     
146     /* private function setupWorkdir()
147     * Setup local GITManagedWebpage Work directory with git repository
148     */
149     private function setupWorkdir() {
150         // check requirements
151         $git_exec = shell_exec('which git');
152         if(!preg_match('#git#', $git_exec)) {
153             $this->error(self::ERROR_CRITICAL, "git not installed locally.");
154             return;
155         }
156         
157         mkdir($this->workdir);
158         mkdir($this->workdir.'repository');
159         shell_exec('git clone '.escapeshellarg($this->giturl).' '.escapeshellarg($this->workdir.'repository'));
160         $gitok = $this->gitcmd("status");
161         if(preg_match("#Not a git repository#", $gitok)) {
162             rmdir($this->workdir.'repository');
163             rmdir($this->workdir);
164             $this->error(self::ERROR_CRITICAL, "error cloning git repository.");
165             return;
166         }
167         
168         $fp = fopen($this->workdir.'.htaccess');
169         fwrite($fp, '
170 Order deny,allow
171 Deny from all
172 ');
173         fclose($fp);
174         
175         $this->ready = true;
176         
177         $default_branch = str_replace(array("\r", "\n"), array("", ""), $this->gitcmd("rev-parse", "--abbrev-ref", "HEAD"));
178         $this->setConfig("defaultbranch", $default_branch);
179         $this->saveConfig();
180     }
181     
182     private function getActiveBranch() {
183         if($this->activeSession)
184             return $this->activeSession;
185         else if(isset($_SESSION[self::SESSION_PREFIX.'branch']))
186             return $_SESSION[self::SESSION_PREFIX.'branch'];
187         else
188             return $this->getConfig("defaultbranch");
189     }
190     
191     private function setActiveBranch($branch, $remember) {
192         $this->activeSession = $branch;
193         if($remember)
194             $_SESSION[self::SESSION_PREFIX.'branch'] = $branch;
195     }
196     
197     private function getLocalUntrackedFiles() {
198         $default_branch = $this->getConfig("defaultbranch");
199         $tracked_files = $this->gitcmd("ls-tree", $default_branch, "--full-name", "--name-only");
200         $local_files = shell_exec("find ".escapeshellarg($this->localdir));
201         $untracked_files = array();
202         
203         $tracked_files = explode("\n", str_replace(array("\r"), array(""), $tracked_files));
204         $local_files = explode("\n", str_replace(array("\r"), array(""), $local_files));
205         
206         foreach($local_files as $local_file) {
207             if(!$local_file)
208                 continue;
209             if($strip_local || (($strip_local = strlen($this->localdir)) && substr($local_file, 0, $strip_local) == $this->localdir)) {
210                 $local_file = substr($local_file, $strip_local);
211             }
212             $tracked = false;
213             foreach($tracked_files as $tracked_file) {
214                 if($tracked_file == $local_file) {
215                     $tracked = true;
216                     break;
217                 }
218             }
219             if(!$tracked) {
220                 $untracked_files[] = $local_file;
221             }
222         }
223         return $untracked_files;
224     }
225     
226     private function branchExists($branch) {
227         //check if branch exists
228         $gitret = $this->gitcmd("rev-list", "--max-count=1", $branch);
229         if(!preg_match("#([a-z0-9]{40})#", $gitret, $match))
230             return false;
231         else
232             return $match[1];
233     }
234     
235     private function localBranchPath($branch, $create = false) {
236         $default_branch = $this->getConfig("defaultbranch");
237         if($branch == $default_branch)
238             $dir = $this->localdir;
239         else
240             $dir = $this->workdir.'branch_'.str_replace(array('/'), array('_'), $branch).'/';
241         if(file_exists($dir))
242             return $dir;
243         else if($create) {
244             mkdir($dir);
245             return $dir;
246         } else 
247             return false;
248     }
249     
250     private function updateBranch($branch, $path, $force = false) {
251         if(substr($path, -1) != '/')
252             $path .= '/';
253         $current_branch = str_replace(array("\r", "\n"), array("", ""), $this->gitcmd("rev-parse", "--abbrev-ref", "HEAD"));
254         $this->gitcmd("fetch");
255         $this->gitcmd("reset", "--hard", "origin/".$branch);
256         $gitret = $this->gitcmd("rev-list", "--max-count=1", $branch);
257         preg_match("#([a-z0-9]{40})#", $gitret, $match);
258         $newest_version = $match[1];
259         
260         $deleted_files = array();
261         if(($current_version = $this->getConfig('version_'.$branch))) {
262             if($current_version == $newest_version && !$force)
263                 return;
264             else {
265                 $override_all = true;
266                 $delfiles = $this->gitcmd("diff", "--diff-filter=D", "--name-only", $current_version, $newest_version);
267                 $delfiles = explode("\n", str_replace(array("\r"), array(""), $delfiles));
268                 foreach($delfiles as $file) {
269                     if(!$file)
270                         continue;
271                     $deleted_files[] = $file;
272                 }
273             }
274         } else
275             $override_all = true;
276         if($override_all) {
277             $rsync_present = preg_match("#rsync#", `which rsync`);
278             if($rsync_present)
279                 shell_exec('rsync -avz --exclude ".git" '.escapeshellarg($this->workdir."repository/").' '.escapeshellarg($path));
280             else
281                 shell_exec('tar -c --exclude ".git" -C '.escapeshellarg($this->workdir."repository").' . | tar -x -C '.escapeshellarg($path));
282             
283             // remove deleted files
284             foreach($deleted_files as $file) {
285                 unlink($path.$file);
286             }
287         }
288         $this->setConfig('version_'.$branch, $newest_version);
289         $this->saveConfig();
290     }
291     
292     /* public function update()
293     * Pulls latest commit of active branch and overwrites files in branch folder
294     */
295     public function update() {
296         if($this->loopedcall)
297             return;
298         
299         $active_branch = $this->getActiveBranch();
300         
301         if(!$this->branchExists($active_branch))
302             return false;
303         $dir = $this->localBranchPath($active_branch, true);
304         $this->updateBranch($active_branch, $dir);
305     }
306     
307     public function setBranch($branch, $remember = false) {
308         if($this->loopedcall)
309             return;
310         
311         if(!$this->branchExists($branch)) {
312             $this->gitcmd("fetch");
313             if(!$this->branchExists('origin/'.$branch))
314                 return false;
315         }
316         $this->setActiveBranch($branch, $remember);
317         
318         if(!$this->localBranchPath($branch)) {
319             $dir = $this->localBranchPath($branch, true);
320             $this->updateBranch($branch, $dir, true);
321         }
322     }
323     
324     public function getExecFile($file = null) {
325         if($this->loopedcall)
326             return;
327         define("GITMANAGED_EXECUTED", true);
328         
329         if(!$file)
330             $file = $_SERVER['PHP_SELF'];
331         if($file[0] == '/')
332             $file = substr($file, 1);
333         
334         $default_branch = $this->getConfig("defaultbranch");
335         $active_branch = $this->getActiveBranch();
336         if($active_branch != $default_branch) {
337             if(!($dir = $this->localBranchPath($active_branch))) {
338                 $dir = $this->localBranchPath($active_branch, true);
339                 $this->updateBranch($active_branch, $dir, true);
340             }
341             chdir($dir);
342             return $dir.$file;
343         } else {
344             return $file;
345         }
346     }
347     
348     public function showMngrWindow() {
349         $html = '<script type="text/javascript" src="http://lib.pk910.de/jquery.min.js"></script>';
350         $html .= '<script type="text/javascript" src="http://lib.pk910.de/jquery-ui/jquery-ui.min.js"></script>';
351         $html .= '<script type="text/javascript" src="http://lib.pk910.de/jquery-migrate.min.js"></script>';
352         $html .= '<script type="text/javascript" src="http://lib.pk910.de/jquery.window/jquery.window.js"></script>';
353         $html .= '<script type="text/javascript" src="http://lib.pk910.de/jquery.cookie/jquery.cookie.js"></script>';
354         $html .= '<link type="text/css" href="http://lib.pk910.de/jquery-ui/jquery-ui.min.css" rel="stylesheet" />';
355         $html .= '<link type="text/css" href="http://lib.pk910.de/jquery.window/css/jquery.window.css" rel="stylesheet" />';
356         
357         $current_branch = $this->getActiveBranch();
358         $current_version = substr($this->getConfig('version_'.$current_branch), 0, 7);
359         $html .= <<<HTML_END
360 <div id="gitmanagedwindow" style="display:none;">
361     <table width="100%" class="gitmanaged">
362         <tr>
363             <td colspan="2" align="center">
364                 <h2>GITManagedWebpage</h2>
365             </td>
366         </tr>
367         <tr>
368             <td width="150px">current branch:</td>
369             <td>{$current_branch}</td>
370         </tr>
371         <tr>
372             <td>current version:</td>
373             <td>{$current_version}</td>
374         </tr>
375         <tr>
376             <td colspan="2" align="center"><input type="button" value="update branch" class="update"></td>
377         </tr>
378     </table>
379 </div>
380
381 <script type="text/javascript">
382 $.window.prepare({
383    dock: 'right',
384    animationSpeed: 200,
385    minWinLong: 65
386 });
387
388 var cookie_prefix = 'gitmanaged_';
389 var win = $.window({
390     icon: 'http://lib.pk910.de/_ico/git.ico',
391     title: "GIT Managed Webpage",
392     content: $('#gitmanagedwindow').html(),
393     checkBoundary: true,
394     withinBrowserWindow: true,
395     
396     maximizable: false,
397     closable: false,
398     
399     width: 200,
400     height: 150,
401     
402     onMinimize: function(wnd) {
403         $.cookie(cookie_prefix+'minimized', '1');
404     },
405     onCascade: function(wnd) {
406         $.removeCookie(cookie_prefix+'minimized');
407     },
408     afterDrag: function(wnd) {
409         var pos = wnd.getContainer().position();
410         $.cookie(cookie_prefix+'pos', pos.left + ';' + pos.top)
411     },
412 });
413
414 function updateURLParameter(url, param, paramVal) {
415     var TheAnchor = null;
416     var newAdditionalURL = "";
417     var tempArray = url.split("?");
418     var baseURL = tempArray[0];
419     var additionalURL = tempArray[1];
420     var temp = "";
421     if (additionalURL) {
422         var tmpAnchor = additionalURL.split("#");
423         var TheParams = tmpAnchor[0];
424             TheAnchor = tmpAnchor[1];
425         if(TheAnchor)
426             additionalURL = TheParams;
427         tempArray = additionalURL.split("&");
428         for (i=0; i<tempArray.length; i++) {
429             if(tempArray[i].split('=')[0] != param) {
430                 newAdditionalURL += temp + tempArray[i];
431                 temp = "&";
432             }
433         }        
434     } else {
435         var tmpAnchor = baseURL.split("#");
436         var TheParams = tmpAnchor[0];
437             TheAnchor  = tmpAnchor[1];
438         if(TheParams)
439             baseURL = TheParams;
440     }
441     if(TheAnchor)
442         paramVal += "#" + TheAnchor;
443     var rows_txt = temp + "" + param + "=" + paramVal;
444     return baseURL + "?" + newAdditionalURL + rows_txt;
445 }
446
447 var container = win.getContainer();
448 container.find('.update').on('click', function() {
449     var url = updateURLParameter(window.location.href, "update", "1");
450     window.location.href = url;
451 });
452
453 if($.cookie(cookie_prefix+'pos')) {
454     var pos = $.cookie(cookie_prefix+'pos').split(';');
455     container.css("left", pos[0]);
456     container.css("top", pos[1]);
457 }
458 if($.cookie(cookie_prefix+'minimized'))
459     win.minimize();
460
461 </script>
462 HTML_END;
463         return $html;
464     }
465     
466 }
467
468 ?>