<?php if (!defined('PmWiki')) exit(); /** === Attachtable === * Copyright 2007-2009 Eemeli Aro <eemeli@gmail.com> * * 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/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-29'; 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' ) ); Markup( 'attachtable', 'directives', '/\\(:attachtable\\s*(.*?):\\)/ei', "Keep(FmtAttachtable(\$pagename,PSS('$1')))"); SDV( $AttachtablePubDirUrl, "$PubDirUrl/attachtable" ); SDV( $AttachtableCSS, "$AttachtablePubDirUrl/attachtable.css" ); if (!empty( $AttachtableCSS )) $HTMLHeaderFmt['attachtable-css'] = "\n<link rel='stylesheet' type='text/css' href='$AttachtableCSS' />"; include_once('attachtable-util.php'); 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 $PubDirUrl, $UploadDir, $UploadPrefixFmt, $UploadUrlFmt, $TimeFmt, $HandleAuth, $EnableUploadOverwrite, $EnableUploadVersions, $EnableDirectDownload, $EnableAttachtableSortable, $EnableAttachtableFileInfo, $AttachtableDataFields, $AttachtableShowRows, $AttachtableDefaultFilter, $AttachtableReferences, $AttachtablePubDirUrl; SDV( $AttachtableDataFields, array( 'fileinfo', 'name', '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'; } ## (dis)allow re-sorting table $sortable = ( IsEnabled( $EnableAttachtableSortable, 0 ) && !in_array( 'sortable', (array)$opt['-'] ) ); ## data columns to show if ( array_key_exists('data',$opt) ) switch( $opt['data'] ) { case 'all': $data = array( 'fileinfo', 'name', 'size', 'references', 'modtime', 'mimetype', 'filetype' ); break; case 'none': $data = array('name'); 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); if ( !IsEnabled( $EnableAttachtableFileInfo, 0 ) && ( ( $key = array_search('fileinfo',$data) ) !== FALSE ) ) unset( $data[$key] ); if ( count($data) && !in_array('name',$data) ) { if ( ( $k=array_search('fileinfo',$data) ) !== FALSE ) array_splice( $data, $k+1, 0, 'name' ); else array_unshift( $data, 'name' ); } ## 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 $filelist = array(); if ( $dirp = @opendir($uploaddir) ) { 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 (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 = "<tr class='dir'>"; foreach( $data as $d ) { $d_opt = $d_txt = ''; switch($d) { case 'fileinfo': $d_txt = "<div class='fileinfo'></div>"; break; case 'name': $d_opt = " align='left'"; $d_txt = "<a href='$name' title='$[View directory]'>$file/</a>"; break; case 'filetype': case 'mimetype': $d_opt = " align='left'"; $d_txt = XL('directory'); break; case 'modtime': $lastmod = max( $lastmod, $stat['mtime'] ); $d_txt = strftime($TimeFmt, $stat['mtime']); break; } $s .= "<td$d_opt>$d_txt</td>"; } $out[] = $s; } continue; } if (preg_match( '/^(.*?),([0-9]+)$/', $file, $delmatch )) ++$delcount; $r_opt = $s = ''; foreach( $data as $d ) { $d_opt = $d_txt = ''; switch($d) { case 'fileinfo': $d_txt = "<a class='fileinfo' id='$pagename:" . str_replace(',',':',$file) . "' href='$pageurl?action=fileinfo&upname=$file' title='$[More information...]'>+</a>"; break; case 'name': $d_opt = " align='left'"; $confirm = "onclick=\"return confirm('$[Permanently delete] $file?')\""; $aopt = "rel='nofollow' class='createlink' href='$pageurl?upname=$file&action"; if ($delmatch) { if ( !in_array('deleted',$show) ) continue; $r_opt = " class='del' title='$[File removed] $dtime'"; $dname = $delmatch[1]; $dtime = strftime( $TimeFmt, $delmatch[2] ); if ($auth['undelattach']) $restore = "<a $aopt=renameattach&newname=$dname' title='$[Restore] ($[removed] $dtime)'" . ( $auth['renameattach'] ? " onclick=\"return atr('$file')\"" : '' ) . '> R </a>'; if ($auth['deldelattach']) $deldel = "<a $aopt=delattach' title='$[Delete from disk] ($[removed] $dtime)' $confirm> X </a>"; if ($auth['downloaddeleted']) $dname = "<a rel='nofollow' href='$pageurl?action=downloaddeleted&upname=$file' title='$[Download] ($[removed] $dtime)'>$dname</a>"; $d_txt = "$dname $restore $deldel"; } else { if ( !in_array('normal',$show) ) continue; if ($auth['upload']) $change = "<a $aopt=upload' title='$[Upload new version]'> Δ </a>"; if ($auth['renameattach']) $rename = "<a $aopt=renameattach' class='' title='$[Rename]' onclick=\"return atr('$file')\"> R </a>"; if ($auth['delattach']) $delete = ( $confirmdel ? "<a $aopt=delattach' title='$[Delete from disk]' $confirm> X </a>" : "<a $aopt=delattach' title='$[Delete]'> X </a>" ); $d_txt = "<a href='$name' title='$[View]'>$file</a> $change $rename $delete"; } break; case 'size': $totalsize += $stat['size']; if ($sortable) $d_opt = " sortval='{$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<div class='refbox'>$[Referring pages:]<ul><li>" . implode( "</li><li>", $ls ) . "</li></ul></div>"; 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'] ); if ($sortable) $d_opt = " sortval='{$stat['mtime']}'"; $d_txt = strftime( $TimeFmt, $stat['mtime'] ); break; } $s .= "<td$d_opt>$d_txt</td>"; } $out[] = "<tr$r_opt>$s"; } } else { $s = '<tr>'; foreach( $data as $d ) { if ( $d == 'name' ) $s .= "<td align='left'>($[no attached files])</td>"; else $s .= '<td></td>'; } $out[] = $s; } if ( in_array('linkonly',$show) && !empty($AttachtableReferences) ) { uksort( $AttachtableReferences, 'AttachtableSortNameCmp' ); 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 = "<a $aopt>$file</a> <a $aopt class='createlink'> U </a>"; $s = "<tr title='$[Missing file]'>"; foreach( $data as $d ) switch($d) { case 'name': $s .= "<td align='left' class='missing'>$file</td>"; break; case 'references': $ls = array(); foreach( array_unique($refs) as $pn ) $ls[] = Keep(MakeLink( $pagename, $pn ),'L'); $s .= "<td class='reftitle'><span class='orphan'>$refcount</span><div class='refbox'>$[Referring pages:]<ul><li>" . implode( "</li><li>", $ls ) . "</li></ul></div></td>"; break; default: $s .= '<td></td>'; } $out[] = $s; } } $thead = $tfoot = ''; if ( in_array('header',$show) ) { $thead = '<thead><tr>'; foreach( $data as $d ) { $d_opt = $d_txt = ''; switch($d) { case 'fileinfo': $d_opt = " class='unsortable'"; break; case 'name': $d_opt = " style='text-align:left;'"; $d_txt = XL('file name'); break; case 'size': $d_txt = XL('size (bytes)'); break; case 'references': $d_opt = " class='reftitle' title='$[references to this file, NOTE: may be incomplete]'"; $d_txt = XL('refs'); break; case 'filetype': $d_opt = " style='text-align:left;'"; $d_txt = XL('file type'); break; case 'mimetype': $d_opt = " style='text-align:left;'"; $d_txt = XL('MIME type'); break; case 'modtime': $d_txt = XL('last modified'); break; } $thead .= "<th$d_opt>$d_txt</th>"; } $thead .= "</tr></thead>\n"; } if ( in_array('footer',$show) ) { $fc = count($filelist) - $dircount; if ( !in_array('deleted',$show) ) $fc -= $delcount; if ($fc<0) $fc = 0; $tfoot = '<tfoot><tr>'; foreach( $data as $d ) { $d_txt = ''; switch($d) { case 'name': $d_txt = '$[total] ' . ( $dircount ? "$dircount $[director".($dircount==1?'y':'ies').'], ' : '' ) . "$fc $[file".($fc==1?']':'s]'); break; case 'size': if ($totalsize) $d_txt = AttachFilesizeString($totalsize); break; case 'modtime': if ($lastmod) $d_txt = strftime( $TimeFmt, $lastmod ); break; } $tfoot .= "<th>$d_txt</th>"; } $tfoot .= "</tr></tfoot>\n"; } $jshead = array(); if ( $auth['undelattach'] || $auth['renameattach'] ) $jshead[] = "<script type='text/javascript'> function atr(fn){ var ar,nn=fn,rt='$[Rename] '+fn+' $[as]:'; if(ar=fn.match(/^(.*?),([0-9]+)$/)){nn = ar[1];rt = '$[Restore] '+ar[1]+' ($[removed] '+(new Date(ar[2]*1000)).toUTCString()+') $[as]:';} if(n=prompt(rt,nn)) document.location='$pageurl?action=renameattach&upname='+fn+'&newname='+n; return false;}\n</script>"; if ( in_array('fileinfo',$data) ) { $jshead[] = "<script type='text/javascript'>var pageurl = '".PageVar($pagename,'$PageUrl')."';</script> <script type='text/javascript' src='$AttachtablePubDirUrl/util.js'></script> <script type='text/javascript' src='$AttachtablePubDirUrl/fileinfo.js'></script>"; } if ($sortable) $jshead[] = "<script type='text/javascript' src='$AttachtablePubDirUrl/sortable.js'></script>"; return preg_replace( '/\\$\\[(?>([^\\]]+))\\]/e', "XL(PSS('$1'))", implode("\n",$jshead) . "\n<table cellspacing='0' style='text-align:right;' class='attachtable sortable'>\n$thead$tfoot<tbody>\n" . implode("</tr>\n",$out) . "</tr>\n</tbody></table>" ); }