1<?php if (!defined('PmWiki')) exit();
2/*  Copyright 2004-2021 Patrick R. Michaud (pmichaud@pobox.com)
3    This file is part of PmWiki; you can redistribute it and/or modify
4    it under the terms of the GNU General Public License as published
5    by the Free Software Foundation; either version 2 of the License, or
6    (at your option) any later version.  See pmwiki.php for full details.
7
8    This script adds upload capabilities to PmWiki.  Uploads can be
9    enabled by setting
10        $EnableUpload = 1;
11    in config.php.  In addition, an upload password must be set, as
12    the default is to lock uploads.  In some configurations it may also
13    be necessary to set values for $UploadDir and $UploadUrlFmt,
14    especially if any form of URL rewriting is being performed.
15    See the PmWiki.UploadsAdmin page for more information.
16
17    Script maintained by Petko YOTOV www.pmwiki.org/petko
18*/
19
20## $EnableUploadOverwrite determines if we allow previously uploaded
21## files to be overwritten.
22SDV($EnableUploadOverwrite,1);
23
24## $UploadExts contains the list of file extensions we're willing to
25## accept, along with the Content-Type: value appropriate for each.
26SDVA($UploadExts,array(
27  'gif' => 'image/gif', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg',
28  'png' => 'image/png', 'apng' => 'image/apng', 'bmp' => 'image/bmp', 'ico' => 'image/x-icon',
29  'wbmp'=> 'image/vnd.wap.wbmp', 'xcf' => 'image/x-xcf', 'webp' => 'image/webp',
30  'svg' => 'image/svg+xml', 'svgz' => 'image/svg+xml',
31  'mp3' => 'audio/mpeg', 'au' => 'audio/basic', 'wav' => 'audio/x-wav',
32  'ogg' => 'audio/ogg', 'flac' => 'audio/x-flac', 'opus' => 'audio/opus',
33  'ogv' => 'video/ogg', 'mp4' => 'video/mp4', 'webm' => 'video/webm',
34  'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg', 'mkv' => 'video/x-matroska',
35  'm4v' => 'video/x-m4v', '3gp' => 'video/3gpp',
36  'mov' => 'video/quicktime', 'qt' => 'video/quicktime',
37  'wmf' => 'text/plain', 'avi' => 'video/x-msvideo',
38  'zip' => 'application/zip', '7z' => 'application/x-7z-compressed',
39  'gz'  => 'application/x-gzip', 'tgz' => 'application/x-gzip',
40  'rpm' => 'application/x-rpm',
41  'hqx' => 'application/mac-binhex40', 'sit' => 'application/x-stuffit',
42  'doc' => 'application/msword', 'ppt' => 'application/vnd.ms-powerpoint',
43  'xls' => 'application/vnd.ms-excel', 'mdb' => 'text/plain',
44  'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
45  'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
46  'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
47  'exe' => 'application/octet-stream',
48  'pdf' => 'application/pdf', 'psd' => 'text/plain',
49  'ps'  => 'application/postscript', 'ai' => 'application/postscript',
50  'eps' => 'application/postscript',
51  'htm' => 'text/html', 'html' => 'text/html', 'css' => 'text/css',
52  'fla' => 'application/x-shockwave-flash',
53  'swf' => 'application/x-shockwave-flash',
54  'txt' => 'text/plain', 'rtf' => 'application/rtf',
55  'tex' => 'application/x-tex', 'dvi' => 'application/x-dvi',
56  'odt' => 'application/vnd.oasis.opendocument.text',
57  'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
58  'odp' => 'application/vnd.oasis.opendocument.presentation',
59  'odg' => 'application/vnd.oasis.opendocument.graphics',
60  'epub'=> 'application/epub+zip',
61  'kml' => 'application/vnd.google-earth.kml+xml',
62  'kmz' => 'application/vnd.google-earth.kmz',
63  'vtt' => 'text/vtt',
64  '' => 'text/plain'));
65
66# Array containing forbidden strings in a filename, array('.php', '.cgi')
67SDV($UploadBlacklist, array());
68
69SDV($UploadMaxSize,50000);
70SDV($UploadPrefixQuota,0);
71SDV($UploadDirQuota,0);
72foreach($UploadExts as $k=>$v)
73  if (!isset($UploadExtSize[$k])) $UploadExtSize[$k]=$UploadMaxSize;
74
75SDV($UploadDir,'uploads');
76SDV($UploadPermAdd,0444);
77SDV($UploadPermSet,0);
78SDV($UploadPrefixFmt,'/$Group');
79SDV($UploadFileFmt,"$UploadDir$UploadPrefixFmt");
80$v = preg_replace('#^/(.*/)#', '', $UploadDir);
81SDV($UploadUrlFmt,preg_replace('#/[^/]*$#', "/$v", $PubDirUrl, 1));
82SDV($LinkUploadCreateFmt, "<a rel='nofollow' class='createlinktext' href='\$LinkUpload'>\$LinkText</a><a rel='nofollow' class='createlink' href='\$LinkUpload'>&nbsp;&Delta;</a>");
83SDVA($ActionTitleFmt, array('upload' => '| $[Attach]'));
84
85
86if ($EnablePostAuthorRequired)
87  SDV($EnableUploadAuthorRequired, $EnablePostAuthorRequired);
88
89SDV($PageUploadFmt,array("
90  <div id='wikiupload'>
91  <h2 class='wikiaction'>$[Attachments for] {\$FullName}</h2>
92  <h3>\$UploadResult</h3>
93  <form enctype='multipart/form-data' action='{\$PageUrl}?action=postupload' method='post'>
94  <input type='hidden' name='n' value='{\$FullName}' />
95  <input type='hidden' name='action' value='postupload' />
96  <input type='hidden' name='\$TokenName' value='\$TokenValue' />
97  <table border='0'>
98    <tr><td align='right'>$[File to upload:]</td><td><input
99      name='uploadfile' type='file' required='required' /></td></tr>
100    <tr><td align='right'>$[Name attachment as:]</td>
101      <td><input type='text' name='upname' value='\$UploadName' />
102        </td></tr>
103    <tr><td align='right'>$[Uploader]:</td>
104      <td><input type='text' name='author' value='\$UploadAuthor' \$UploadAuthorRequired />
105        <input type='submit' value=' $[Upload] ' />
106        </td></tr></table></form></div>",
107  'wiki:$[{$SiteGroup}/UploadQuickReference]'));
108XLSDV('en',array(
109  'ULsuccess' => 'successfully uploaded',
110  'ULinvalidtoken' => 'Token invalid or missing.',
111  'ULauthorrequired' => 'An author name is required.',
112  'ULbadname' => 'invalid attachment name',
113  'ULbadtype' => '\'$upext\' is not an allowed file extension',
114  'ULtoobig' => 'file is larger than maximum allowed by webserver',
115  'ULtoobigext' => 'file is larger than allowed maximum of $upmax
116     bytes for \'$upext\' files',
117  'ULpartial' => 'incomplete file received',
118  'ULnofile' => 'no file uploaded',
119  'ULexists' => 'file with that name already exists',
120  'ULpquota' => 'group quota exceeded',
121  'ULtquota' => 'upload quota exceeded'));
122SDV($PageAttributes['passwdupload'],'$[Set new upload password:]');
123SDV($DefaultPasswords['upload'],'@lock');
124SDV($AuthCascade['upload'], 'read');
125SDV($FmtPV['$PasswdUpload'], 'PasswdVar($pn, "upload")');
126
127Markup('attachlist', 'directives',
128  '/\\(:attachlist\\s*(.*?):\\)/i',
129  "MarkupFmtUploadList");
130function MarkupFmtUploadList($m) {
131  extract($GLOBALS["MarkupToHTML"]); # get $pagename
132  return Keep('<ul>'.FmtUploadList($pagename,$m[1]).'</ul>');
133}
134SDV($GUIButtons['attach'], array(220, 'Attach:', '', '$[file.ext]',
135  '$GUIButtonDirUrlFmt/attach.gif"$[Attach file]"'));
136SDV($LinkFunctions['Attach:'], 'LinkUpload');
137SDV($IMap['Attach:'], '$1');
138SDVA($HandleActions, array('upload' => 'HandleUpload',
139  'postupload' => 'HandlePostUpload',
140  'download' => 'HandleDownload'));
141SDVA($HandleAuth, array('upload' => 'upload',
142  'download' => 'read'));
143SDV($HandleAuth['postupload'], $HandleAuth['upload']);
144SDV($UploadVerifyFunction, 'UploadVerifyBasic');
145
146function MakeUploadName($pagename,$x) {
147  global $UploadNameChars, $MakeUploadNamePatterns;
148  SDV($UploadNameChars, "-\\w. ");
149  SDV($MakeUploadNamePatterns, array(
150    "/[^$UploadNameChars]/" => '',
151    '/(\\.[^.]*)$/' => 'cb_tolower',
152    '/^[^[:alnum:]_]+/' => '',
153    '/[^[:alnum:]_]+$/' => ''));
154   return PPRA($MakeUploadNamePatterns, $x);
155}
156
157function LinkUpload($pagename, $imap, $path, $alt, $txt, $fmt=NULL) {
158  global $FmtV, $UploadFileFmt, $LinkUploadCreateFmt,
159    $UploadUrlFmt, $UploadPrefixFmt, $EnableDirectDownload;
160  if (preg_match('!^(.*)/([^/]+)$!', $path, $match)) {
161    $pagename = MakePageName($pagename, $match[1]);
162    $path = $match[2];
163  }
164  $upname = MakeUploadName($pagename, $path);
165  $encname = rawurlencode($upname);
166  $filepath = FmtPageName("$UploadFileFmt/$upname", $pagename);
167  $FmtV['$LinkUpload'] =
168    FmtPageName("\$PageUrl?action=upload&amp;upname=$encname", $pagename);
169  $FmtV['$LinkText'] = $txt;
170  if (!file_exists($filepath))
171    return FmtPageName($LinkUploadCreateFmt, $pagename);
172  $path = PUE(FmtPageName(IsEnabled($EnableDirectDownload, 1)
173                            ? "$UploadUrlFmt$UploadPrefixFmt/$encname"
174                            : "{\$PageUrl}?action=download&amp;upname=$encname",
175                          $pagename));
176  return LinkIMap($pagename, $imap, $path, $alt, $txt, $fmt);
177}
178
179# Authenticate group downloads with the group password
180function UploadAuth($pagename, $auth, $cache=0){
181  global $GroupAttributesFmt, $EnableUploadGroupAuth;
182  if (IsEnabled($EnableUploadGroupAuth,0)){
183    SDV($GroupAttributesFmt,'$Group/GroupAttributes');
184    $pn_upload = FmtPageName($GroupAttributesFmt, $pagename);
185  } else $pn_upload = $pagename;
186  $page = RetrieveAuthPage($pn_upload, $auth, true, READPAGE_CURRENT);
187  if (!$page) Abort("?No '$auth' permissions for $pagename");
188  if ($cache) PCache($pn_upload,$page);
189  return true;
190}
191
192function UploadSetVars($pagename) {
193  global $Author, $FmtV, $UploadExtMax, $EnableReadOnly,
194    $EnablePostAuthorRequired, $EnableUploadAuthorRequired;
195  $FmtV['$UploadName'] = MakeUploadName($pagename,@$_REQUEST['upname']);
196  $FmtV['$UploadAuthor'] = PHSC($Author,  ENT_QUOTES);
197  $upresult = PHSC(@$_REQUEST['upresult']);
198  $uprname = PHSC(@$_REQUEST['uprname']);
199  $FmtV['$upext'] = PHSC(@$_REQUEST['upext']);
200  $FmtV['$upmax'] = PHSC(@$_REQUEST['upmax']);
201  $FmtV['$TokenValue'] = pmtoken();
202  $FmtV['$UploadResult'] = ($upresult) ?
203    FmtPageName("<i>$uprname</i>: $[UL$upresult]",$pagename) :
204      (@$EnableReadOnly ? XL('Cannot modify site -- $EnableReadOnly is set'): '');
205  $FmtV['$UploadAuthorRequired'] = @$EnableUploadAuthorRequired ?
206    'required="required"' : '';
207}
208
209function HandleUpload($pagename, $auth = 'upload') {
210  global $HandleUploadFmt,$PageStartFmt,$PageEndFmt,$PageUploadFmt;
211  UploadAuth($pagename, $auth, 1);
212  UploadSetVars($pagename);
213  SDV($HandleUploadFmt,array(&$PageStartFmt,&$PageUploadFmt,&$PageEndFmt));
214  PrintFmt($pagename,$HandleUploadFmt);
215}
216
217function HandleDownload($pagename, $auth = 'read') {
218  global $UploadFileFmt, $UploadExts, $DownloadDisposition, $EnableIMSCaching;
219  SDV($DownloadDisposition, "inline");
220  UploadAuth($pagename, $auth);
221  $upname = MakeUploadName($pagename, @$_REQUEST['upname']);
222  $filepath = FmtPageName("$UploadFileFmt/$upname", $pagename);
223  if (!$upname || !file_exists($filepath)) {
224    header("HTTP/1.0 404 Not Found");
225    Abort("?requested file not found");
226    exit();
227  }
228  if (IsEnabled($EnableIMSCaching, 0)) {
229    header('Cache-Control: private');
230    header('Expires: ');
231    $filelastmod = gmdate('D, d M Y H:i:s \G\M\T', filemtime($filepath));
232    if (@$_SERVER['HTTP_IF_MODIFIED_SINCE'] == $filelastmod)
233      { header("HTTP/1.0 304 Not Modified"); exit(); }
234    header("Last-Modified: $filelastmod");
235  }
236  preg_match('/\\.([^.]+)$/',$filepath,$match);
237  if ($UploadExts[@$match[1]])
238    header("Content-Type: {$UploadExts[@$match[1]]}");
239  $fsize = $length = filesize($filepath);
240  $end = $fsize-1;
241  header("Accept-Ranges: bytes");
242  if (@$_SERVER['HTTP_RANGE']) {
243    if(! preg_match('/^\\s*bytes\\s*=\\s*(\\d*)\\s*-\\s*(\\d*)\\s*$/i', $_SERVER['HTTP_RANGE'], $r)
244      || intval($r[1])>$end
245      || intval($r[2])>$end
246      || ($r[2] && intval($r[1])>intval($r[2]))
247    ) {
248      header('HTTP/1.1 416 Requested Range Not Satisfiable');
249      header("Content-Range: bytes 0-$end/$fsize");
250      exit;
251    }
252    if ($r[2]=='') $r[2] = $end;
253    if ($r[1]=='') $r[1] = $end - $r[2];
254    $length = $r[2] - $r[1] + 1;
255    header('HTTP/1.1 206 Partial Content');
256    header("Content-Range: bytes $r[1]-$r[2]/$fsize");
257  }
258  else {
259    $r = array( null, 0, $end);
260  }
261  header("Content-Length: $length");
262  header("Content-Disposition: $DownloadDisposition; filename=\"$upname\"");
263  $fp = fopen($filepath, "rb");
264  if ($fp) {
265    $bf = 8192;
266    fseek($fp, $r[1]);
267    while (!feof($fp) && ($pos = ftell($fp)) <= $r[2]) {
268      $bf = max($bf, $r[2] - $pos + 1);
269      echo fread($fp, $bf);
270      flush();
271    }
272    fclose($fp);
273  }
274  exit();
275}
276
277function HandlePostUpload($pagename, $auth = 'upload') {
278  global $UploadVerifyFunction, $UploadFileFmt, $LastModFile,
279    $EnableUploadVersions, $Now, $RecentUploadsFmt, $FmtV,
280    $NotifyItemUploadFmt, $NotifyItemFmt, $IsUploadPosted,
281    $UploadRedirectFunction, $UploadPermAdd, $UploadPermSet,
282    $EnableReadOnly;
283
284  if (IsEnabled($EnableReadOnly, 0))
285    Abort('Cannot modify site -- $EnableReadOnly is set', 'readonly');
286
287  UploadAuth($pagename, $auth);
288  $uploadfile = $_FILES['uploadfile'];
289  $upname = @$_REQUEST['upname'];
290  if ($upname=='') $upname=$uploadfile['name'];
291  $upname = MakeUploadName($pagename,$upname);
292  if (!function_exists($UploadVerifyFunction))
293    Abort('?no UploadVerifyFunction available');
294  $filepath = FmtPageName("$UploadFileFmt/$upname",$pagename);
295  $result = $UploadVerifyFunction($pagename,$uploadfile,$filepath);
296  if ($result=='') {
297    $filedir = preg_replace('#/[^/]*$#','',$filepath);
298    mkdirp($filedir);
299    if (IsEnabled($EnableUploadVersions, 0))
300      @rename($filepath, "$filepath,$Now");
301    if (!move_uploaded_file($uploadfile['tmp_name'],$filepath))
302      { Abort("?cannot move uploaded file to $filepath"); return; }
303    fixperms($filepath, $UploadPermAdd, $UploadPermSet);
304    if ($LastModFile) { touch($LastModFile); fixperms($LastModFile); }
305    $result = "upresult=success";
306    $FmtV['$upname'] = $upname;
307    $FmtV['$upsize'] = $uploadfile['size'];
308    if (IsEnabled($RecentUploadsFmt, 0)) {
309      PostRecentChanges($pagename, '', '', $RecentUploadsFmt);
310    }
311    if (IsEnabled($NotifyItemUploadFmt, 0) && function_exists('NotifyUpdate')) {
312      $NotifyItemFmt = $NotifyItemUploadFmt;
313      $IsUploadPosted = 1;
314      register_shutdown_function('NotifyUpdate', $pagename, getcwd());
315    }
316  }
317  $FmtV['$upresult'] = $result;
318  SDV($UploadRedirectFunction, 'Redirect');
319  $UploadRedirectFunction($pagename,"{\$PageUrl}?action=upload&uprname=$upname&$result");
320}
321
322function UploadVerifyBasic($pagename,$uploadfile,$filepath) {
323  global $EnableUploadOverwrite,$UploadExtSize,$UploadPrefixQuota,
324    $UploadDirQuota,$UploadDir, $UploadBlacklist,
325    $Author, $EnablePostAuthorRequired, $EnableUploadAuthorRequired;
326
327  if(! AutoCheckToken()) {
328    return 'upresult=invalidtoken';
329  }
330
331  if (IsEnabled($EnableUploadAuthorRequired,0) && !$Author)
332    return 'upresult=authorrequired';
333
334  if (count($UploadBlacklist)) {
335    $tmp = explode("/", $filepath);
336    $upname = strtolower(end($tmp));
337    foreach($UploadBlacklist as $needle) {
338      if (strpos($upname, $needle)!==false) return 'upresult=badname';
339    }
340  }
341  if (!$EnableUploadOverwrite && file_exists($filepath))
342    return 'upresult=exists';
343  preg_match('/\\.([^.\\/]+)$/',$filepath,$match); $ext=@$match[1];
344  $maxsize = $UploadExtSize[$ext];
345  if ($maxsize<=0) return "upresult=badtype&upext=$ext";
346  if ($uploadfile['size']>$maxsize)
347    return "upresult=toobigext&upext=$ext&upmax=$maxsize";
348  switch (@$uploadfile['error']) {
349    case 1: return 'upresult=toobig';
350    case 2: return 'upresult=toobig';
351    case 3: return 'upresult=partial';
352    case 4: return 'upresult=nofile';
353  }
354  if (!is_uploaded_file($uploadfile['tmp_name'])) return 'upresult=nofile';
355  $filedir = preg_replace('#/[^/]*$#','',$filepath);
356  if ($UploadPrefixQuota &&
357      (dirsize($filedir)-@filesize($filepath)+$uploadfile['size']) >
358        $UploadPrefixQuota) return 'upresult=pquota';
359  if ($UploadDirQuota &&
360      (dirsize($UploadDir)-@filesize($filepath)+$uploadfile['size']) >
361        $UploadDirQuota) return 'upresult=tquota';
362  return '';
363}
364
365function dirsize($dir) {
366  $size = 0;
367  $dirp = @opendir($dir);
368  if (!$dirp) return 0;
369  while (($file=readdir($dirp)) !== false) {
370    if ($file[0]=='.') continue;
371    if (is_dir("$dir/$file")) $size+=dirsize("$dir/$file");
372    else $size+=filesize("$dir/$file");
373  }
374  closedir($dirp);
375  return $size;
376}
377
378function FmtUploadList($pagename, $args) {
379  global $UploadDir, $UploadPrefixFmt, $UploadUrlFmt, $EnableUploadOverwrite,
380    $TimeFmt, $EnableDirectDownload, $IMapLinkFmt, $UrlLinkFmt, $FmtV;
381
382  $opt = ParseArgs($args);
383  if (@$opt[''][0]) $pagename = MakePageName($pagename, $opt[''][0]);
384
385  $matchfnames = '';
386  if (@$opt['names'] ) $matchfnames = $opt['names'];
387  if (@$opt['ext'])
388    $matchfnames .= FixGlob($opt['ext'], '$1*.$2');
389
390  $uploaddir = FmtPageName("$UploadDir$UploadPrefixFmt", $pagename);
391  $uploadurl = FmtPageName(IsEnabled($EnableDirectDownload, 1)
392                          ? "$UploadUrlFmt$UploadPrefixFmt/"
393                          : "\$PageUrl?action=download&amp;upname=",
394                      $pagename);
395
396  $dirp = @opendir($uploaddir);
397  if (!$dirp) return '';
398  $filelist = array();
399  while (($file=readdir($dirp)) !== false) {
400    if ($file[0] == '.') continue;
401    if ($matchfnames && ! MatchNames($file, $matchfnames)) continue;
402    $filelist[$file] = rawurlencode($file);
403  }
404  closedir($dirp);
405  $out = array();
406  natcasesort($filelist);
407  $overwrite = '';
408  $fmt = IsEnabled($IMapLinkFmt['Attach:'], $UrlLinkFmt);
409  foreach($filelist as $file=>$encfile) {
410    $FmtV['$LinkUrl'] = PUE("$uploadurl$encfile");
411    $FmtV['$LinkText'] = $file;
412    $FmtV['$LinkUpload'] =
413      FmtPageName("\$PageUrl?action=upload&amp;upname=$encfile", $pagename);
414    $stat = stat("$uploaddir/$file");
415    if ($EnableUploadOverwrite)
416      $overwrite = FmtPageName("<a rel='nofollow' class='createlink'
417        href='\$LinkUpload'>&nbsp;&Delta;</a>",
418        $pagename);
419    $lnk = FmtPageName($fmt, $pagename);
420    $out[] = "<li> $lnk$overwrite ... ".
421      number_format($stat['size']) . " bytes ... " .
422      strftime($TimeFmt, $stat['mtime']) . "</li>";
423  }
424  return implode("\n",$out);
425}
426
427# this adds (:if [!]attachments filepattern pagename:) to the markup
428$Conditions['attachments'] = "AttachExist(\$pagename, \$condparm)";
429function AttachExist($pagename, $condparm='*') {
430  global $UploadFileFmt;
431  @list($fpat, $pn) = explode(' ', $condparm, 2);
432  $pn = ($pn > '') ? MakePageName($pagename, $pn) : $pagename;
433
434  $uploaddir = FmtPageName($UploadFileFmt, $pn);
435  $flist = array();
436  $dirp = @opendir($uploaddir);
437  if ($dirp) {
438    while (($file = readdir($dirp)) !== false)
439      if ($file[0] != '.') $flist[] = $file;
440    closedir($dirp);
441    $flist = MatchNames($flist, $fpat);
442  }
443  return count($flist);
444}
445