<?php if (!defined('PmWiki')) exit(); /* phAttachman.php: shows attaches as a rich sortable table, can work via AJAX Written by (c) Philip Kazakov aka Finar (www.ph-ph.ru) This text is written for PmWiki; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. ToDo: * make correct multilingual support * add support for audio and video files markup, using 'ImgsExention' as example. * make delete operation logging (in AllRecentChanges for example) */ $RecipeInfo['Attachman']['Version'] = '20200522'; $RecipeInfo['Attachman']['Developer'] = 'PhPh'; SDV($HandleActions['phAttachman'], 'HandlePhAttachman'); SDV($HandleAuth['phAttachman'], 'edit'); // this would be put in '$auth' later; Markup('attachman', '<block', '/\\(:attachman\\s*(.*?):\\)/i', "HandlePhAttachman"); // CONFIG: if needed, this should be redefined in config.php SDVA($phAttachman, array( 'EnableDelete' => false, 'TableClass' => "sortable simpletable", // default table class 'ImgsExentions' => "gif,jpg,jpeg,png,svg,svgz", 'MaxImgsCountAutoPreview' => 10, // how much images will be previewed automatically 'MaxImgSizeAutoPreview' => 1024, // max image size (KB) for be previewed automatically 'IgnoredFilenamesPatterns' => array( // filenames to be ignored (regex patterns) "th[0-9]{2}---", // this is for Mini (https://www.pmwiki.org/wiki/Cookbook/Mini) ), 'TextareaInsertCommands' => array ( '*' => 'Attach:%filename%', // makes no effect, just for logic clearing // 'gif' => 'Mini:%filename%'; // example ), 'TextareaInsertTemplate' => "<pre>%TextareaInsertCommand%</pre>", // if redefined, keep %TextareaInsertCommand% intact )); function HandlePhAttachman($HandleActions_pagename, $auth = 0){ // TRANSLATIONS (todo: completely remake using core principles) $msg_noPreview = "Too big file, or too much images attached: click to load preview."; $msg_noUploads = "There are no uploads yet."; $msgErr_UploadsDisabled = "AttachManager Error: uploads are not enabled in config.php."; $msgErr_noPermission = "AttachManager Error: current user has no permisson to handle 'Attachman' cook."; $msgErr_noPermissionDelete = "AttachManager Error: current user has no 'upload' permisson, so 'delete' is also unaviable."; $msgErr_noFileToDelete = "AttachManager Delete Error: file was not found."; $msgErr_DeleteFailure = "AttachManager Delete Error: could't delete file, check filesystem permissions."; $TH_File = "File"; $TH_Markup = "Markup"; $TH_Size = "Size (KB)"; $TH_Modification = "Modified"; $TH_Action = "Action"; // SOME DIRTY CODE // We need to use all this throught ?action=phAttachman (ajax) AND throught (:attachman:) page command in the same time using one codebase. // I don't know how to do it correct. First $HandleActions was done, serving ?action=phAttachman. // After this (:attachman:) command was binded to the same function 'HandlePhAttachman'. // That's why in the next condition some variables are redefined manually for second case only. if (is_array ($HandleActions_pagename)){ // it looks like we are working with (:attachman:) syntax $markupCall = true; global $pagename; $HandleActions_pagename = $pagename; global $HandleAuth; $auth = $HandleAuth['phAttachman']; } // CHECKS: // check if uploads are enabled global $EnableUpload; if ( !IsEnabled($EnableUpload,0)) $stop = "$msgErr_UploadsDisabled"; // check if current user can edit pages $page = RetrieveAuthPage($HandleActions_pagename, $auth, 0, READPAGE_CURRENT); if (!$page) $stop = "$msgErr_noPermission"; // exit() for ajax, return for (:attachman:) if ($stop) {if ($markupCall) return ("$stop"); else exit("$stop"); } // PREPARE global $phAttachman; $EnableDelete = $phAttachman['EnableDelete']; $TableClass = $phAttachman['TableClass']; $ImgsExentions = $phAttachman['ImgsExentions']; $MaxImgsCountAutoPreview = $phAttachman['MaxImgsCountAutoPreview']; $MaxImgSizeAutoPreview = $phAttachman['MaxImgSizeAutoPreview']; $IgnoredFilenamesPatterns = $phAttachman['IgnoredFilenamesPatterns']; $TextareaInsertCommands = $phAttachman['TextareaInsertCommands']; $TextareaInsertTemplate = $phAttachman['TextareaInsertTemplate']; // PROCESSING: scan and generate the table, delete files global $UploadDir, $UploadPrefixFmt, $UploadUrlFmt, $EnableDirectDownload; $uploadsFolder = FmtPageName("$UploadDir$UploadPrefixFmt", $HandleActions_pagename); $uploadsUrl = FmtPageName(IsEnabled($EnableDirectDownload, 1) // that's for PmiWiki inside subfolder, with EnableDirectDownload = 0 ? "$UploadUrlFmt$UploadPrefixFmt/" : "\$PageUrl?action=download&upname=", $HandleActions_pagename); $filelist = @array_diff(scandir($uploadsFolder), array('.', '..')); if (!$filelist) {if ($markupCall) return ("$msg_noUploads"); else exit("$msg_noUploads"); } // serve 'delete' action. Works only if called throuhgt ?action=phAttachman AND user has 'upload' permissions if (!$markupCall && $_REQUEST["deletefile"]){ $deletefile = "$uploadsFolder/".$_REQUEST["deletefile"]; $page = RetrieveAuthPage($HandleActions_pagename, "upload", 0, READPAGE_CURRENT); if (!$page) exit ("$msgErr_noPermissionDelete"); if (!is_file($deletefile)) exit($msgErr_noFileToDelete); if (!unlink($deletefile)) exit($msgErr_DeleteFailure); exit("1"); } // CSS-styles and JS-scripts global $HTMLFooterFmt; // must use Footer, as Header is already generated at that point $HTMLFooterFmt['styles']['phAttachman'] = " <!-- Attachman: styles --> <style> #attachman {} #attachman a.phAttachman_imgLoad { background-color: #DDD; display: block; padding: 30px 0; text-align: center; text-decoration: none;} #attachman a.phAttachman_imgLoad:hover {opacity: 0.7;} #attachman a.phAttachman_imgLoad span {border-bottom: 1px dotted blue;} #attachman td.phAttachman_size {text-align: center;} #attachman td.phAttachman_date {text-align: center;} #attachman td.phAttachman_action a {padding: 0 5px;} #attachman td.phAttachman_action a:hover {opacity: 0.7;} #attachman td.phAttachman_action span { display: none; position: fixed; left:0; top: 0px; background-color: red; color: white; text-align: center; width: 100%; } #attachman td.phAttachman_action:hover span {display: block;} </style> "; $HTMLFooterFmt['scripts']['phAttachman'] = " <!-- Attachman: scripts --> <script> function imageClick(link){ var href = link.getAttribute('href'); var linkText = link.innerHTML; var image = document.createElement('img'); image.src = href; image.setAttribute('width', '150'); var linebreak = document.createElement('br'); var text = document.createElement('small'); text.innerHTML = linkText; link.parentNode.insertBefore(text, link.nextSibling); link.parentNode.insertBefore(linebreak, link.nextSibling); link.parentNode.insertBefore(image, link.nextSibling); link.parentNode.removeChild(link); return false; } function deleteRow(el){ const request = new XMLHttpRequest(); const url = el.getAttribute('href'); request.open('GET', url); request.setRequestHeader('Content-Type', 'application/x-www-form-url'); request.addEventListener('readystatechange', () => { if (request.readyState === 4 && request.status === 200) { if(request.responseText=='1'){ el.parentNode.parentNode.parentNode.removeChild(el.parentNode.parentNode); } else { var text = document.createElement('span'); text.innerHTML = request.responseText; el.parentNode.insertBefore(text, el.nextSibling);; } } }); request.send(); } </script> <!-- //Attachman --> "; // create table foreach ($filelist as $key => $value) { // IgnoredFilenamesPatterns check foreach ($IgnoredFilenamesPatterns as $k => $v) { if (preg_match("/$v/",$value)) unset($filelist["$key"]); } } $result = "<table class='$TableClass' id='attachman'> <tr> <th>$TH_File</th> <th>$TH_Markup</th> <th>$TH_Size</th> <th>$TH_Modification</th> ".($EnableDelete ? "<th>$TH_Action</th>" : NULL)." </tr>"; foreach ($filelist as $key => $value) { // main table generation $FileExt = strtolower(pathinfo($value, PATHINFO_EXTENSION)); // get extension if (strpos($ImgsExentions, $FileExt) !== false) $FileType = 'image'; // here support for audio and video files could be added the same way if ($FileType == 'image') { if (round(filesize("$uploadsFolder/$value")/1024) < $MaxImgSizeAutoPreview && $MaxImgsCountAutoPreview){ $File = "<img src='$uploadsUrl$value' width=150px><br><small>$value</small>"; $MaxImgsCountAutoPreview--; } else $File = "<a class='phAttachman_imgLoad' href='$uploadsUrl$value' title='$msg_noPreview' onclick='imageClick(this); return false;'><span>$value</span></a>"; } else $File = "<a href='$uploadsUrl$value' target='_blank'>$value</a>"; $Markup = ""; if ($TextareaInsertCommands[$FileExt]) { $ActionAdd = $TextareaInsertCommands[$FileExt]; $ActionAdd = str_replace("%filename%","$value",$ActionAdd); $Markup .= str_replace("%TextareaInsertCommand%","$ActionAdd",$TextareaInsertTemplate); unset($ActionAdd); } $Markup .= str_replace("%TextareaInsertCommand%","Attach:$value",$TextareaInsertTemplate); $Size = round(filesize("$uploadsFolder/$value")/1024); $Date = date ("Y-m-d H:i:s",filemtime("$uploadsFolder/$value")); $Date = str_replace(" ", "<br>",$Date); $ActionDelete = "<a href='?action=phAttachman&deletefile=$value' onclick='deleteRow(this); return false;'>Delete</a>"; $result .= "<tr> <td class='phAttachman_file'>$File</td> <td class='phAttachman_markup'>$Markup</td> <td class='phAttachman_size'>$Size</td> <td class='phAttachman_date'><small>$Date</small></td> ".($EnableDelete ? "<td class='phAttachman_action'>$ActionDelete</td>" : "")." </tr>"; unset($FileExt,$FileType,$File,$Markup,$Size,$Date,$ActionDelete); } $result .= "</table>"; if ($markupCall) return $result; else { $result .= $HTMLFooterFmt['styles']['phAttachman']; $result .= $HTMLFooterFmt['scripts']['phAttachman']; exit($result); } }