MTrackWeb/Wiki.php
[web.mtrack] / MTrackWeb / Wiki.php
1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
3
4 /**
5  *  This is alot simpler now...
6  *
7  *  Mostly just dumps out raw text from files..
8  *
9  *
10  */
11
12
13 require_once 'MTrack/Attachment.php';
14 require_once 'MTrackWeb.php';
15
16 class MTrackWeb_Wiki extends MTrackWeb
17 {
18     
19     var $conflicted = 0;
20     var $message = false;
21     var $hasHistory = false;
22     
23     function getAuth()
24     {
25         return parent::getAuth();
26     }
27     
28     function get($pi)
29     {
30         $this->pi = empty($pi) ? 'WikiStart' : ($pi . $this->bootLoader->ext);
31  
32         if (!isset($_REQUEST['ajax_body'])) {
33             $this->title = "Browse: " . $this->pi;
34             return;
35         }
36         $this->masterTemplate = 'wiki.html';
37         
38         
39         $this->edit = isset($_REQUEST['edit']) ? (int)$_REQUEST['edit'] : 0;
40         
41  
42         $this->hasHistory = 0;
43         
44         
45         $this->doc = new MTrack_Wiki_Item($this->pi);
46          $this->canEdit = $this->hasPerm('MTrack.Wiki','E');
47         
48          // we might add more perms based on project later..
49         
50         if (!$this->canEdit && $this->edit) {
51             return HTML_FlexyFramework::run('Noperm');
52         }
53         
54         $this->hasHistory = $this->doc->file ? 1: 0;
55         
56         // blank doc.. on edit..
57         if ($this->doc->file === null && $this->edit) {
58             $this->doc = new MTrack_Wiki_Item($this->pi);
59             $this->doc->content = " = {$this->pi} =\n";
60         }
61          
62
63         /* now just render */
64
65         $this->title = $this->pi;
66         if ($this->edit) {
67           $this->title .= " (edit)";
68         }
69         
70         $this->canEdit = $this->edit ? false : $this->canEdit ; // if they are editing remove that permission..
71        
72  
73         if ($this->doc->file) {
74             $this->evt = $this->doc->file->getChangeEvent();
75             if (!strlen($this->evt->changelog)) {
76                 $this->evt->changelog = 'Changed';
77             }
78         }
79         
80         if (!$this->edit  && !$this->hasHistory) {
81             if ($this->canEdit) {
82                 $this->canCreate = 1;
83             } else {
84                 $this->notExist = 1;
85             }
86         }
87         if ($this->edit) {
88             if (isset($_POST['preview'])) {
89                 $this->showPreview = 1;
90                 $this->preview =  MTrackWiki::format_to_html($content);
91             }
92             $this->doc->contentb64= base64_encode($this->doc->content);
93             $this->doc->content = isset($_POST['content']) ? $_POST['content'] : $this->doc->content;
94             $this->comment = isset($_POST['comment']) ? $_POST['comment'] : '';
95
96         }
97    
98         $action = isset($_GET['action']) ? $_GET['action'] : 'view';
99
100         switch ($action) {
101             case 'view':
102                 $this->actionView = 1;
103                 break;
104
105             case 'list':
106                 $this->actionList = 1;
107                 
108                 $htree = array();
109                 $this->build_help_tree($htree, dirname(__FILE__) . '/../defaults/help');
110                 $this->helptree = $htree;
111                 
112   
113                 /* get the page names into a tree format */
114                 $tree = array();
115                 $repo  = false; // 
116                 $root = MTrack_Wiki_Item::getRepoAndRoot($repo);
117                 $suf = MTrackConfig::get('core', 'wikifilenamesuffix'); // fixme!!!!
118                 build_tree($tree, $repo, $root, $suf);
119                 $this->tree = $tree;
120              
121                 break;
122
123             case 'recent':
124                 $this->actionRecent = 1;
125                 $root = MTrack_Wiki_Item::getRepoAndRoot($repo);
126                 $this->recent = $repo->history(null, 100) ;
127                 foreach ($this->recent as $e) {
128                     list($e->page) = $e->files;
129                     if (strlen($root)) {
130                         $e->page = substr($e->page, strlen($root)+1);
131                     }
132                 }
133                  
134                 break;
135
136         }
137         //echo '<PRE>';print_r($this);echo '</PRE>';
138     }
139
140     function is_content_conflicted($content)
141     {
142         if (preg_match("/^(<+)\s+(mine|theirs|original)\s*$/m", $content)) {
143             return true;
144         }
145         return false;
146     }
147     function normalize_text($text) {
148         return rtrim($text) . "\n";
149     }
150
151     function post()
152     {
153         $this->get();
154         if (isset($_POST['cancel'])) {
155             header("Location: {$this->baseURL}/Wiki/{$this->pi}");
156             exit;
157         }
158         
159         
160         if (!MTrackCaptcha::check('wiki')) {
161             $this->message = 'CAPTCHA failed, please try again';
162             return;
163         }
164         
165           /* to avoid annoying "you lose because someone else edited" errors,
166            * we compute the diff from the original content we had, and apply
167            * that to the current content of the object */
168
169           $saved = false;
170
171           $orig = base64_decode($_POST['orig']);
172           $content = $_POST['content'];
173
174           /* for consistency, we always want a newline at the end, otherwise
175            * we can end up with some weird output from diff3 */
176           $orig = normalize_text($orig);
177           $content = normalize_text($content);
178           $this->doc->content = normalize_text($this->doc->content);
179           $this->conflicted = is_content_conflicted($content);
180           $tempdir = sys_get_temp_dir();
181
182           if (!$this->conflicted) {
183             $ofile = tempnam($tempdir, "mtrack");
184             $nfile = tempnam($tempdir, "mtrack");
185             $tfile = tempnam($tempdir, "mtrack");
186             $pfile = tempnam($tempdir, "mtrack");
187             
188             require_once 'System.php';
189             $diff3 = System::which('diff3');
190             if (empty($diff3)) {
191               $diff3 = 'diff3';
192             }
193
194             file_put_contents($ofile, $orig);
195             file_put_contents($nfile, $content);
196             file_put_contents($tfile, $this->doc->content);
197
198             if (PHP_OS == 'SunOS') { // seriously does anyone use SunOS anymore???
199                 exec("($diff3 -X $nfile $ofile $tfile ; echo '1,\$p') | ed - $nfile > $pfile",
200                     $output = array(), $retval = 0);
201             } else {
202                 exec("$diff3 -mX --label mine $nfile --label original $ofile --label theirs $tfile > $pfile",
203                     $output = array(), $retval = 0);
204             }
205
206             if ($retval == 0) {
207               /* see if there were merge conflicts */
208               $content = '';
209               $mine = preg_quote($nfile, '/');
210               $theirs = preg_quote($tfile, '/');
211               $orig = preg_quote($ofile, '/');
212               $content = file_get_contents($pfile);
213
214               if (PHP_OS == 'SunOS') {
215                 $content = str_replace($nfile, 'mine', $content);
216                 $content = str_replace($ofile, 'original', $content);
217                 $content = str_replace($tfile, 'theirs', $content);
218               }
219             }
220             unlink($ofile);
221             unlink($nfile);
222             unlink($tfile);
223             unlink($pfile);
224
225             $this->conflicted = is_content_conflicted($content);
226           }
227
228           /* keep the merged version for editing purposes */
229           $_POST['content'] = $content;
230           /* our concept of the the original content is now what
231            * is currently saved */
232           $_POST['orig'] = base64_encode($this->doc->content);
233
234           if ($this->conflicted) {
235             $this->message = "Conflicting edits were detected; please correct them before saving";
236           } else {
237             $this->doc->content = $content;
238             try {
239               $cs = MTrackChangeset::begin("wiki:{$this->pi}", $_POST['comment']);
240               $this->doc->save($cs);
241               if (is_array($_FILES['attachments'])) {
242                 foreach ($_FILES['attachments']['name'] as $fileid => $name) {
243                   $do_attach = false;
244                   switch ($_FILES['attachments']['error'][$fileid]) {
245                     case UPLOAD_ERR_OK:
246                       $do_attach = true;
247                       break;
248                     case UPLOAD_ERR_NO_FILE:
249                       break;
250                     case UPLOAD_ERR_INI_SIZE:
251                     case UPLOAD_ERR_FORM_SIZE:
252                       $this->message = "Attachment(s) exceed the upload file size limit";
253                       break;
254                     case UPLOAD_ERR_PARTIAL:
255                     case UPLOAD_ERR_CANT_WRITE:
256                       $this->message = "Attachment file upload failed";
257                       break;
258                     case UPLOAD_ERR_NO_TMP_DIR:
259                       $this->message = "Server configuration prevents upload due to missing temporary dir";
260                       break;
261                     case UPLOAD_ERR_EXTENSION:
262                       $this->message = "An extension prevented an upload from running";
263                   }
264                   if ($this->message !== null) {
265                     throw new Exception($this->message);
266                   }
267                   if ($do_attach) {
268                     MTrackAttachment::add("wiki:{$this->pi}",
269                       $_FILES['attachments']['tmp_name'][$fileid],
270                       $_FILES['attachments']['name'][$fileid],
271                       $cs);
272                   }
273                 }
274               }
275               MTrackAttachment::process_delete("wiki:{$this->pi}", $cs);
276               $cs->commit();
277               MTrack_Wiki_Item::commitNow();
278               $saved = true;
279             } catch (Exception $e) {
280               $this->message = $e->getMessage();
281             }
282           }
283
284           if ($saved) {
285                 /* we're good; go back to view mode */
286                 header("Location: {$this->baseURL}/Wiki/{$this->pi}");
287                 exit;
288           }
289         }
290         
291         function build_help_tree(&$tree, $dir) {
292             foreach (scandir($dir) as $ent) {
293               if ($ent[0] == '.') {
294                 continue;
295               }
296               $full = $dir . DIRECTORY_SEPARATOR . $ent;
297               if (is_dir($full)) {
298                 $kid = array();
299                 $this->build_help_tree($kid, $full);
300                 $tree[$ent] = $kid;
301               } else {
302                 $tree[$ent] = array();
303               }
304             }
305         }
306         function build_tree(&$tree, $repo, $dir, $suf)
307         {
308             $items = $repo->readdir($dir);
309             foreach ($items as $file) {
310               $label = basename($file->name);
311               if ($file->is_dir) {
312                 $kid = array();
313                 $this->build_tree($kid, $repo, $file->name, $suf);
314                 $tree[$label] = $kid;
315               } else {
316                 if ($suf && substr($label, -strlen($suf)) == $suf) {
317                   $label = substr($label, 0, strlen($label) - strlen($suf));
318                 }
319                 $tree[$label] = array();
320               }
321             }
322         }   
323         function emit_tree($root,  $phppage,$parent='')
324         {
325  
326             if (strlen($parent)) {
327               echo "<ul>\n";
328             } else {
329               echo "<ul class='wikitree'>\n";
330             }
331             $knames = array_keys($root);
332             usort($knames, 'strnatcasecmp');
333             foreach ($knames as $key) {
334               $kids = $root[$key];
335               $n = htmlentities($key, ENT_QUOTES, 'utf-8');
336               echo "<li>";
337               if (count($kids)) {
338                 echo $n;
339                 emit_tree($kids,  $phppage,"$parent$key/");
340               } else {
341                 echo "<a href=\"{$this->baseURL}/$phppage/$parent$n\">$n</a>";
342               }
343               echo "</li>\n";
344             }
345             echo "</ul>\n";
346         }
347
348         
349         function renderDeleteList() {
350             //return MTrackAttachment::renderDeleteList("wiki:{$this->pi}");
351         }
352         function renderList() {
353             //return MTrackAttachment::renderList("wiki:{$this->pi}");
354         }
355               
356
357         function captcha(){
358             return MTrackCaptcha::emit('wiki');
359         }
360     }