* * Adds actions to delete & restore deleted attachments, as * well as an attachlist replacement to use those actions, * show file types and list attachment references. * * Developed and tested using the PmWiki 2.2 series. * * To install, add the following line to your configuration file : include_once("$FarmD/cookbook/attachtable.php"); * * For more information, please see the online documentation at * http://www.pmwiki.org/wiki/Cookbook/Attachtable * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, Version 2, as * published by the Free Software Foundation. * http://www.gnu.org/copyleft/gpl.html */ $RecipeInfo['Attachtable']['Version'] = '2009-04-17'; if ( !IsEnabled($EnableUpload,0) ) return; ## set a few values that we depend on in case upload.php hasn't been included yet SDVA( $HandleAuth, array( 'upload' => 'upload', 'download' => 'read') ); SDV( $HandleAuth['postupload'], $HandleAuth['upload'] ); ## set the default Attachtable authorization levels -- order is important for proper inheritance SDVA( $HandleAuth, array( 'delattach' => $HandleAuth['upload'], # deleting a file from PmWiki while keeping it on disk 'deldelattach' => $HandleAuth['attr'], # deleting a file from disk ) ); SDVA( $HandleAuth, array( 'undelattach' => $HandleAuth['delattach'], # restoring a file 'renameattach' => $HandleAuth['delattach'] # renaming a file ) ); SDVA( $HandleAuth, array( 'downloaddeleted' => $HandleAuth['undelattach'] # download a deleted file ) ); XLSDV( 'en', array( 'ULdelsuccess' => 'successfully deleted', 'ULundelsuccess' => 'successfully restored', 'ULrenamesuccess' => 'successfully renamed', 'ULisdir' => 'can\'t act on directory', 'ULdelfail' => 'file delete error', 'ULrenamefail' => 'file rename error' ) ); SDVA( $HandleActions, array( 'delattach' => 'HandleDeleteAttachment', 'renameattach' => 'HandleRenameAttachment', 'downloaddeleted' => 'HandleDownloadDeleted' ) ); SDV( $HTMLStylesFmt['attachtable'], " .attachtable td { padding-right: 0.8em; white-space: nowrap; } .attachtable .titlerow { color: #ccc; font-style: italic; font-size: 0.8em; text-align: right; } .attachtable a { text-decoration: none !important; } .attachtable a:hover { text-decoration: underline !important; } .attachtable .del, .attachtable .del a { color: #ccc; } .attachtable tr:hover { background-color: #eee; } .attachtable .titlerow:hover, .attachtable .del:hover, .attachtable .del:hover a { color: #666; } .attachtable .orphan { color: #c00; font-weight: bold; } .attachtable .refbox { display: none; position: absolute; margin: -0.75em 0 0 1.5em; padding: 0.25em; background-color: #eee; color: #000; border: 1px solid #ccc; text-align: left; } .attachtable .refbox ul, .attachtable .refbox li { padding-top: 0; padding-bottom: 0; margin-top: 0; margin-bottom: 0; } .attachtable .reftitle:hover .refbox { display: block; } " ); Markup( 'attachtable', 'directives', '/\\(:attachtable\\s*(.*?):\\)/ei', "Keep(FmtAttachtable(\$pagename,PSS('$1')))"); /* $AttachtableLogFmt = array( '$SiteGroup.AllRecentChanges' => '* [[Attach:{$Group}/$upname]] . . . $CurrentTime by $AuthorLink: $upreport', '$Group.RecentChanges' => '* [[Attach:$upname]] . . . $CurrentTime by $AuthorLink: $upreport' ); */ function AttachtableLog( $pagename, $upname, $upreport ) { global $RecentChangesFmt, $IsPagePosted, $FmtPV, $AttachtableLogFmt; $FmtPV['$upname'] = "'$upname'"; $FmtPV['$upreport'] = "'$upreport'"; $rc = $RecentChangesFmt; $RecentChangesFmt = $AttachtableLogFmt; $IsPagePosted = 1; $p = array(); PostRecentChanges($pagename, $p, $p); $IsPagePosted = 0; $RecentChangesFmt = $rc; } ## action to delete attachment ## usage: ?action=delattach&upname=filename function HandleDeleteAttachment($pagename, $auth = 'upload') { global $UploadFileFmt, $LastModFile, $EnableUploadVersions, $Now, $HandleAuth; $upname = $_REQUEST['upname']; if ($upname=='') Abort("?no attachment name"); $deleted = preg_match( '/^(.*?)(,\d+)$/', $upname, $delmatch ); $delfromdisk = $deleted || !IsEnabled($EnableUploadVersions, 0); if ( $delfromdisk ) $auth = $HandleAuth['deldelattach']; $page = RetrieveAuthPage( $pagename, $auth, true, READPAGE_CURRENT ); if (!$page) Abort("?cannot delete attachment from $pagename"); $upname = $deleted ? MakeUploadName( $pagename, $delmatch[1] ) . $delmatch[2] : MakeUploadName( $pagename, $upname ); $filepath = FmtPageName("$UploadFileFmt/$upname",$pagename); if ( file_exists($filepath) ) { if ( is_dir($filepath) ) $result = 'isdir'; else { $r = $delfromdisk ? unlink($filepath) : rename($filepath, "$filepath,$Now"); if ($r) { if ($LastModFile) { touch($LastModFile); fixperms($LastModFile); } AttachtableLog( $pagename, $upname, $delfromdisk ? 'deleted from disk' : 'deleted' ); $result = 'delsuccess'; } else $result = $delfromdisk ? 'delfail' : 'renamefail'; } } else $result = 'badname'; Redirect($pagename,"{\$PageUrl}?action=upload&uprname=$upname&upresult=$result"); } ## action to rename attachment ## usage: ?action=renameattach&upname=filename&newname=filename function HandleRenameAttachment($pagename, $auth = 'upload') { global $HandleAuth, $UploadFileFmt, $LastModFile, $TimeFmt; ## check file $upname = $_REQUEST['upname']; if ($upname=='') Abort("?no attachment name"); $deleted = preg_match( '/^(.*?),(\d+)$/', $upname, $delmatch ); if ($deleted) { $dname = MakeUploadName( $pagename, $delmatch[1] ); $upname = "$dname,{$delmatch[2]}"; } else { $upname = MakeUploadName( $pagename, $upname ); } $oldfilepath = FmtPageName( "$UploadFileFmt/$upname", $pagename ); if ( !file_exists($oldfilepath) ) Abort("?no such attachment: $upname"); if ( is_dir($oldfilepath) ) { Redirect( $pagename, "{\$PageUrl}?action=upload&uprname=$upname&upresult=isdir" ); return; } ## new name $newname = $_REQUEST['newname']; if ($newname=='') Abort("?no new attachment name"); $newname = MakeUploadName( $pagename, $newname ); ## check authorization if ($deleted) { if ( !RetrieveAuthPage( $pagename, $HandleAuth['undelattach'], TRUE, READPAGE_CURRENT ) ) Abort("?cannot restore attachment from $pagename"); if ( ( $dname != $newname ) && !RetrieveAuthPage( $pagename, $HandleAuth['renameattach'], TRUE, READPAGE_CURRENT ) ) Abort("?cannot restore attachment from $pagename with a different name"); } else { if ( !RetrieveAuthPage( $pagename, $HandleAuth['renameattach'], TRUE, READPAGE_CURRENT ) ) Abort("?cannot rename attachment from $pagename"); } ## verify & rename if ($newname=='') { $newname = $_REQUEST['newname']; $result = 'upresult=badname'; } else { $newfilepath = FmtPageName( "$UploadFileFmt/$newname", $pagename ); $result = UploadVerifyRename( $oldfilepath, $newfilepath ); if ( $result == '' ) { $r = rename( $oldfilepath, $newfilepath ); if ($r) { if ($LastModFile) { touch($LastModFile); fixperms($LastModFile); } AttachtableLog( $pagename, $newname, $deleted ? "restored (was $dname, deleted " . strftime( $TimeFmt, $delmatch[2] ) . ')' : "renamed (was $upname)" ); $result = $deleted ? 'upresult=undelsuccess' : 'upresult=renamesuccess'; } else $result = 'upresult=renamefail'; } } Redirect( $pagename, "{\$PageUrl}?action=upload&uprname=$newname&$result" ); } function UploadVerifyRename( $oldfilepath, $newfilepath ) { global $UploadExtSize; if ( file_exists( $newfilepath ) ) return 'upresult=exists'; preg_match( '/\\.([^.\\/]+)$/', $newfilepath, $match ); $ext = @$match[1]; $maxsize = $UploadExtSize[$ext]; if ( $maxsize <= 0 ) return "upresult=badtype&upext=$ext"; $size = filesize($oldfilepath); if ( $size > $maxsize ) return "upresult=toobigext&upext=$ext&upmax=$maxsize"; /* for now, can't rename across directories so no point in checking $UploadPrefixQuota or $UploadDirQuota global $UploadPrefixQuota; $filedirs = preg_replace( '#/[^/]*$#', '', array( $oldfilepath, $newfilepath ) ); if ( $UploadPrefixQuota && ( $filedirs[0] != $filedirs[1] ) && ( ( dirsize($filedirs[1]) + $size ) > $UploadPrefixQuota ) ) return 'upresult=pquota'; */ return ''; } ## action to download deleted (ie. renamed) attachment ## usage: ?action=downloaddeleted&upname=filename,timestamp function HandleDownloadDeleted( $pagename, $auth = 'upload' ) { global $UploadFileFmt, $UploadExts, $DownloadDisposition; SDV($DownloadDisposition, "inline"); $upname = $_REQUEST['upname']; if ( !preg_match( '/^(.*?)(,\d+)$/', $upname, $delmatch ) ) Redirect($pagename,"{\$PageUrl}?action=download&upname=$upname"); $page = RetrieveAuthPage( $pagename, $auth, true, READPAGE_CURRENT ); if (!$page) Abort("?cannot read $pagename"); $upname = MakeUploadName( $pagename, $delmatch[1] ) . $delmatch[2]; $filepath = FmtPageName("$UploadFileFmt/$upname", $pagename); if ( !$upname || !file_exists($filepath) ) { header("HTTP/1.0 404 Not Found"); Abort("?requested file not found"); exit(); } preg_match( '/\\.([^.]+)$/', $delmatch[1], $match ); if ( $UploadExts[@$match[1]] ) header( "Content-Type: {$UploadExts[@$match[1]]}" ); header( "Content-Length: ".filesize($filepath) ); header( "Content-disposition: $DownloadDisposition; filename={$delmatch[1]}" ); $fp = fopen( $filepath, "r" ); if ($fp) { while (!feof($fp)) echo fread($fp, 4096); fclose($fp); } exit(); } function AttachtableSortNameCmp( $a, $b ) { $ax = explode(',',$a); $bx = explode(',',$b); if ( ($ax[0]==$bx[0]) && (count($ax)>1) && (count($bx)>1) ) return strcmp($bx[1],$ax[1]); else return strnatcasecmp($a,$b); } function AttachFilesizeString( $size ) { global $AttachtablePrettyFilesize; $raw = number_format( $size ); if ( !IsEnabled( $AttachtablePrettyFilesize, 1 ) ) return $raw; $units = array('','K','M','G','T','P'); $c = 0; while ( $size >= 1000 ) { ++$c; $size /= 1024; } ## size ends up with at most 3 digits return "".number_format( $size, ( ($size<10) ? 1 : 0 ) ) . $units[$c].''; } function AttachtableCountUploadLinks( $pagename, $imap, $path, $alt='', $txt='', $fmt=NULL ) { global $UploadFileFmt, $AttachtableReferences; if (preg_match( '!^(.*)/([^/]+)$!', $path, $match )) { $pn_upload = MakePageName( $pagename, $match[1] ); $path = $match[2]; } else $pn_upload = $pagename; $upname = MakeUploadName( $pn_upload, $path ); $filepath = FmtPageName( "$UploadFileFmt/$upname", $pn_upload ); $AttachtableReferences[$filepath][] = $pagename; return ''; } ## show attachments ## based on FmtUploadList in upload.php function FmtAttachtable($pagename, $args) { global $UploadDir, $UploadPrefixFmt, $UploadUrlFmt, $TimeFmt, $HandleAuth, $EnableUploadOverwrite, $EnableUploadVersions, $EnableDirectDownload, $AttachtableDataFields, $AttachtableShowRows, $AttachtableDefaultFilter, $AttachtableReferences; SDV( $AttachtableDataFields, array( 'size', 'references', 'modtime', 'mimetype' ) ); SDV( $AttachtableShowRows, array( 'header', 'normal', 'deleted', 'linkonly', 'footer' ) ); $opt = ParseArgs($args); ## page to list attachments for if (@$opt[''][0]) $pagename = MakePageName($pagename, $opt[''][0]); ## filter to inc/exclude files based on their name $filter = empty($opt['filter']) ? @$AttachtableDefaultFilter : str_replace( '$', '(?:,\d+)?$', $opt['filter'] ); ## filter to include files based on their extension if (!empty( $opt['ext'] )) { $filter = (array)$filter; $filter[] = '/\\.(' . implode( '|', preg_split('/\\W+/', $opt['ext'], -1, PREG_SPLIT_NO_EMPTY) ) . ')(?:,[0-9]+)?$/i'; } ## data columns to show if ( array_key_exists('data',$opt) ) switch( $opt['data'] ) { case 'all': $data = array( 'size', 'references', 'modtime', 'mimetype', 'filetype' ); break; case 'none': $data = array(); break; case 'default': $data = array_unique($AttachtableDataFields); break; default: $data = array_unique( preg_split( '/\\W+/', $opt['data'], -1, PREG_SPLIT_NO_EMPTY ) ); } else $data = array_unique($AttachtableDataFields); ## data rows to show if ( array_key_exists('show',$opt) ) switch( $opt['show'] ) { case 'all': $show = array( 'header', 'normal', 'deleted', 'linkonly', 'footer' ); break; case 'default': $show = array_unique($AttachtableShowRows); break; default: $show = array_unique( preg_split( '/\\W+/', $opt['show'], -1, PREG_SPLIT_NO_EMPTY ) ); } else $show = array_unique($AttachtableShowRows); ## lookup attachment references, if necessary ## FIXME: these really ought to be cached $AttachtableReferences = array(); if ( in_array('references',$data) ) { global $LinkFunctions, $SearchPatterns, $UrlExcludeChars, $AttachtableProperReferenceLookupMaxPages, $AttachtableReferenceListPatterns; SDV( $AttachtableProperReferenceLookupMaxPages, 8 ); SDVA( $AttachtableReferenceListPatterns, array( 'limit' => '/^'.substr( $pagename, 0, strcspn($pagename,'./') ).'\./', 'recent' => $SearchPatterns['normal']['recent'] ) ); $ls = ListPages( $AttachtableReferenceListPatterns ); if ( count($ls) <= $AttachtableProperReferenceLookupMaxPages ) { $prev_LinkFunctions_Attach = $LinkFunctions['Attach:']; $LinkFunctions['Attach:'] = 'AttachtableCountUploadLinks'; foreach( $ls as $pn ) { $pg = RetrieveAuthPage( $pn, 'read', FALSE, READPAGE_CURRENT ); $html = MarkupToHTML( $pn, $pg['text'] ); } $LinkFunctions['Attach:'] = $prev_LinkFunctions_Attach; } else { foreach( $ls as $pn ) { $pg = RetrieveAuthPage( $pn, 'read', FALSE, READPAGE_CURRENT ); if ($pg) $txt = preg_replace( array( "/(\n[^\\S\n]*)?\\[([=@])(.*?)\\2\\]/s", ## preserved text '/\[\[[^\]]*?\bAttach:([^\]\|]*)/e', ## links to attachments "/\\bAttach:([^\\s$UrlExcludeChars]*[^\\s.,?!$UrlExcludeChars])/e" ), ## raw attachments array( ' ', "'[['.AttachtableCountUploadLinks(\$pn,'','$1')", "AttachtableCountUploadLinks(\$pn,'','$1')" ), htmlspecialchars( $pg['text'], ENT_NOQUOTES ) ); ## assumes $MarkupFrame[0]['escape'] == 1 } } } ## check authorization $auth = array( 'upload' => 0, 'delattach' => 0, 'undelattach' => 0, 'renameattach' => 0, 'deldelattach' => 0, 'downloaddeleted' => 0 ); switch( @$opt['actions'] ) { case 'all': foreach( $auth as $n => $a ) $auth[$n] = 1; case 'none': break; default: if ( @$opt[''][0] != '*' ) { $ac = array(); foreach( $auth as $n => $a ) { $h =& $HandleAuth[$n]; if ( !array_key_exists($h,$ac) ) $ac[$h] = CondAuth( $pagename, $h ); $auth[$n] = $a || $ac[$h]; } } } if ( !$EnableUploadOverwrite ) $auth['upload'] = 0; $confirmdel = !IsEnabled( $EnableUploadVersions, 0 ); if ( $confirmdel ) $auth['delattach'] = $auth['deldelattach']; ## locations $pageurl = FmtPageName( '$PageUrl', $pagename ); $uploaddir = FmtPageName( "$UploadDir$UploadPrefixFmt", $pagename ); $uploadurl = FmtPageName( IsEnabled($EnableDirectDownload, 1) ? "$UploadUrlFmt$UploadPrefixFmt/" : "\$PageUrl?action=download&upname=", $pagename ); ## read files $dirp = @opendir($uploaddir); if (!$dirp) return XL('(no attached files)'); $filelist = array(); while ( ( $file = readdir($dirp) ) !== FALSE ) { if ( $file[0] == '.' ) continue; $filelist[$file] = $file; } closedir($dirp); if ($filter) $filelist = MatchPageNames( $filelist, $filter ); ## output $restore = $deldel = $change = $rename = $delete = ''; $totalsize = $lastmod = $delcount = $dircount = 0; $out = array(); if ( in_array('header',$show) ) { $s = ''; foreach( $data as $d ) switch($d) { case 'size': $s .= 'size (bytes)'; break; case 'references': $s .= 'refs'; break; case 'filetype': $s .= "file type"; break; case 'mimetype': $s .= "MIME type"; break; case 'modtime': $s .= 'last modified'; break; default: $s .= ''; } $out[] = $s; } if (count( $filelist )) { usort($filelist,'AttachtableSortNameCmp'); $showfiles = in_array('normal',$show) || in_array('deleted',$show); foreach( $filelist as $file ) { $name = PUE("$uploadurl$file"); $filepath = "$uploaddir/$file"; if (!$showfiles) { $totalsize += filesize($filepath); continue; } $stat = stat($filepath); if ( is_dir($filepath) ) { ++$dircount; if ($EnableDirectDownload) { $s = "$file/"; foreach( $data as $d ) { $d_opt = $d_txt = ''; switch($d) { case 'filetype': case 'mimetype': $d_opt = " align='left'"; $d_txt = 'directory'; break; case 'modtime': $lastmod = max( $lastmod, $stat['mtime'] ); $d_txt = strftime($TimeFmt, $stat['mtime']); break; } $s .= "$d_txt"; } $out[] = $s; } continue; } $confirm = "onclick=\"return confirm('Really delete $file?')\""; $aopt = "rel='nofollow' class='createlink' href='$pageurl?upname=$file&action"; if ( preg_match( '/^(.*?),([0-9]+)$/', $file, $delmatch ) ) { ++$delcount; if ( !in_array('deleted',$show) ) continue; $dname = $delmatch[1]; $dtime = strftime( $TimeFmt, $delmatch[2] ); if ($auth['undelattach']) $restore = " R '; if ($auth['deldelattach']) $deldel = " X "; if ($auth['downloaddeleted']) $dname = "$dname"; $s = "$dname $restore $deldel"; } else { if ( !in_array('normal',$show) ) continue; if ($auth['upload']) $change = " Δ "; if ($auth['renameattach']) $rename = " R "; if ($auth['delattach']) $delete = ( $confirmdel ? " X " : " X " ); $s = "$file $change $rename $delete"; } foreach( $data as $d ) { $d_opt = $d_txt = ''; switch($d) { case 'size': $totalsize += $stat['size']; $d_txt = AttachFilesizeString($stat['size']); break; case 'references': if ($delmatch) break; $c = count( @$AttachtableReferences[$filepath] ); if ($c) { $ls = array(); foreach( array_unique($AttachtableReferences[$filepath]) as $pn ) $ls[] = Keep(MakeLink( $pagename, $pn ),'L'); $d_opt = " class='reftitle'"; $d_txt = "$c
Referring pages:
"; unset( $AttachtableReferences[$filepath] ); } else { $d_opt = " class='orphan'"; $d_txt = '0'; } break; case 'filetype': case 'mimetype': $d_opt = " align='left'"; $d_txt = AttachFiletype( $filepath, ($d=='mimetype') ); break; case 'modtime': $lastmod = max( $lastmod, $stat['mtime'] ); $d_txt = strftime( $TimeFmt, $stat['mtime'] ); break; } $s .= "$d_txt"; } $out[] = $s; } } if ( in_array('linkonly',$show) && !empty($AttachtableReferences) ) { foreach( $AttachtableReferences as $filepath => $refs ) { if ( dirname($filepath) != $uploaddir ) continue; if (file_exists( $filepath )) continue; $refcount = count( $refs ); if ( !$refcount ) continue; $file = basename($filepath); if ( $filter && !MatchPageNames( $file, $filter ) ) continue; $name = PUE("$uploadurl$file"); $aopt = "rel='nofollow' title='Upload missing file' href='$pageurl?upname=$file&action=upload'"; if ($auth['upload']) $file = "$file  Δ "; $s = "$file"; foreach( $data as $d ) switch($d) { case 'references': $ls = array(); foreach( array_unique($refs) as $pn ) $ls[] = Keep(MakeLink( $pagename, $pn ),'L'); $s .= "$refcount
Referring pages:
    \n
  • " . implode( "
  • \n
  • ", $ls ) . "
"; break; default: $s .= ''; } $out[] = $s; } } if ( in_array('footer',$show) ) { $fc = count($filelist) - $dircount; if ( !in_array('deleted',$show) ) $fc -= $delcount; if ($fc<0) $fc = 0; $s = 'total ' . ( $dircount ? "$dircount director".($dircount==1?'y':'ies').', ' : '' ) . "$fc file".($fc==1?'':'s') . ''; foreach( $data as $d ) { $d_txt = ''; switch($d) { case 'size': if ($totalsize) $d_txt = AttachFilesizeString($totalsize); break; case 'modtime': if ($lastmod) $d_txt = strftime( $TimeFmt, $lastmod ); break; } $s .= "$d_txt"; } $out[] = $s; } return '' . implode("\n",$out) . '
'; } function AttachFiletype( $filename, $mime ) { global $UploadExts; if (!$mime) return trim( exec( 'file -b ' . escapeshellarg($filename) ) ); preg_match( '/\.([^.]+)$/', $filename, $match ); $ext = @$match[1]; if ( function_exists('finfo_open') ) { $finfo = finfo_open(FILEINFO_MIME); $m = finfo_file($finfo, $filename); finfo_close($finfo); } else if ( function_exists('mime_content_type') ) { $m = mime_content_type($filename); } else $m = ''; if ($m) $m = substr( $m, 0, strcspn($m,'; ') ); switch($m) { case 'application/msword': if ( isset($UploadExts[$ext]) ) return $UploadExts[$ext]; switch($ext) { case 'ppt': return 'application/vnd.ms-powerpoint'; case 'xls': return 'application/vnd.ms-excel'; } break; case '': case 'text/plain': $m = trim( exec( 'file -bi ' . escapeshellarg($filename) ) ); if ($m) $m = substr( $m, 0, strcspn($m,'; ') ); if ( !$m && $ext && array_key_exists( $ext, $UploadExts ) ) $m = $UploadExts[$ext]; } return $m; }