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