* * This program is free software; 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. */ /** * \file class/actions_filearchiv.class.php * \brief Hook actions for FileArchiv module - enhanced file display with gallery view */ require_once DOL_DOCUMENT_ROOT.'/core/class/commonhookactions.class.php'; class ActionsFilearchiv extends CommonHookActions { /** * @var DoliDB Database handler */ public $db; /** * @var string Error message */ public $error = ''; /** * @var array Errors */ public $errors = array(); /** * @var array Hook results */ public $results = array(); /** * @var string Returned HTML content (legacy) */ public $resPrint = ''; /** * @var string Returned HTML content (used by HookManager for addreplace hooks) */ public $resprints = ''; /** * Constructor * * @param DoliDB $db Database handler */ public function __construct($db) { $this->db = $db; // Debug: Write to file when class is instantiated - use module dir for debugging $debugFile = dirname(__FILE__) . '/../debug.log'; @file_put_contents($debugFile, date('Y-m-d H:i:s') . " === ActionsFilearchiv CONSTRUCTOR === PHP_SELF=" . $_SERVER['PHP_SELF'] . "\n", FILE_APPEND); } /** * Hook called on document pages (formObjectOptions) * Injects the gallery/tile view assets since resPrint is actually output here * * @param array $parameters Hook parameters * @param CommonObject $object Object * @param string $action Current action * @param HookManager $hookmanager Hook manager * @return int 0 = keep standard behavior */ public function formObjectOptions($parameters, &$object, &$action, $hookmanager) { global $conf, $langs; // Only run on document pages $context = $hookmanager->contextarray; $isDocumentPage = false; foreach ($context as $ctx) { if (strpos($ctx, 'documents') !== false) { $isDocumentPage = true; break; } } if (!$isDocumentPage || !isModEnabled('filearchiv')) { return 0; } // Debug logging $debugFile = dirname(__FILE__) . '/../debug.log'; @file_put_contents($debugFile, date('Y-m-d H:i:s') . " === formObjectOptions CALLED ===\n", FILE_APPEND); @file_put_contents($debugFile, " context: " . implode(', ', $context) . "\n", FILE_APPEND); // Inject the gallery assets - they will wait for DOMContentLoaded $this->resprints = $this->getGalleryAssetsDeferred(); return 0; } /** * Hook to add custom options/buttons to the file list * Called in FormFile::list_of_documents after file list rendering * * @param array $parameters Hook parameters * @param FormFile $object FormFile object * @param string $action Current action * @param HookManager $hookmanager Hook manager * @return int 0 = keep standard behavior, >0 = replace standard behavior */ public function formBuilddocLineOptions($parameters, &$object, &$action, $hookmanager) { global $conf, $langs; if (!isModEnabled('filearchiv')) { return 0; } $filearray = isset($parameters['filearray']) ? $parameters['filearray'] : array(); $file = isset($parameters['file']) ? $parameters['file'] : array(); if (empty($file) || empty($file['name'])) { return 0; } // Check if this is an image or PDF $isImage = $this->isImageFile($file['name']); $isPdf = $this->isPdfFile($file['name']); if (!$isImage && !$isPdf) { return 0; } // Get pinned status $isPinned = $this->isFilePinned($file); // Add pin button $modulepart = isset($parameters['modulepart']) ? $parameters['modulepart'] : ''; $relativepath = isset($file['relativename']) ? $file['relativename'] : $file['name']; $pinUrl = $_SERVER['PHP_SELF'] . '?' . $_SERVER['QUERY_STRING']; $pinUrl .= '&action=filearchiv_togglepin'; $pinUrl .= '&filepath=' . urlencode($relativepath); $pinUrl .= '&modulepart=' . urlencode($modulepart); $pinUrl .= '&token=' . newToken(); $pinIcon = $isPinned ? 'fas fa-thumbtack' : 'far fa-thumbtack'; $pinTitle = $isPinned ? $langs->trans('Unpin') : $langs->trans('Pin'); $pinClass = $isPinned ? 'filearchiv-pinned' : 'filearchiv-unpinned'; $this->resprints = ''; $this->resprints .= ''; $this->resprints .= ' '; return 0; } /** * Hook called before showing the file list * Used to inject CSS/JS only when needed and handle view mode switching * * @param array $parameters Hook parameters * @param FormFile $object FormFile object * @param string $action Current action * @param HookManager $hookmanager Hook manager * @return int 0 = keep standard behavior, >0 = replace standard behavior */ public function showFilesList($parameters, &$object, &$action, $hookmanager) { global $conf, $langs, $user; // Debug logging $debugFile = dirname(__FILE__) . '/../debug.log'; @file_put_contents($debugFile, date('Y-m-d H:i:s') . " === showFilesList CALLED ===\n", FILE_APPEND); @file_put_contents($debugFile, " modulepart: " . (isset($parameters['modulepart']) ? $parameters['modulepart'] : 'none') . "\n", FILE_APPEND); @file_put_contents($debugFile, " filearray count: " . (isset($parameters['filearray']) ? count($parameters['filearray']) : 0) . "\n", FILE_APPEND); if (!isModEnabled('filearchiv')) { @file_put_contents($debugFile, " -> module not enabled\n", FILE_APPEND); return 0; } $filearray = isset($parameters['filearray']) ? $parameters['filearray'] : array(); // Check if there are any images or PDFs $hasMediaFiles = false; foreach ($filearray as $file) { if ($this->isImageFile($file['name']) || $this->isPdfFile($file['name'])) { $hasMediaFiles = true; break; } } if (!$hasMediaFiles) { @file_put_contents($debugFile, " -> no media files found\n", FILE_APPEND); return 0; } @file_put_contents($debugFile, " -> injecting gallery assets\n", FILE_APPEND); // Inject CSS and JS only when needed $assets = $this->getInlineAssets($filearray, $parameters); @file_put_contents($debugFile, " -> assets length: " . strlen($assets) . "\n", FILE_APPEND); @file_put_contents($debugFile, " -> first 200 chars: " . substr($assets, 0, 200) . "\n", FILE_APPEND); $this->resprints = $assets; return 0; } /** * Generate deferred gallery assets that wait for DOM * Called from formObjectOptions which runs BEFORE the file table exists * * @return string HTML with CSS and deferred JS */ private function getGalleryAssetsDeferred() { $out = ''; // CSS - can be injected immediately $out .= ''; // JavaScript - must wait for DOM to find the table $out .= ''; return $out; } /** * Generate inline CSS and JS assets with toggle for tile view * * @param array $filearray Array of files * @param array $parameters Hook parameters * @return string HTML with inline CSS and JS */ private function getInlineAssets($filearray, $parameters) { global $conf, $langs; $langs->load('filearchiv@filearchiv'); $modulepart = isset($parameters['modulepart']) ? $parameters['modulepart'] : ''; // Build file data for all files $filesData = array(); $galleryData = array(); $imageIndex = 0; foreach ($filearray as $file) { $isImage = $this->isImageFile($file['name']); $isPdf = $this->isPdfFile($file['name']); $fileData = array( 'name' => $file['name'], 'size' => isset($file['size']) ? dol_print_size($file['size']) : '', 'date' => isset($file['date']) ? dol_print_date($file['date'], 'day') : '', 'url' => $this->getFileUrl($file, $modulepart), 'isImage' => $isImage, 'isPdf' => $isPdf, 'icon' => $this->getFileIcon($file['name']), ); // Add thumbnail for images if ($isImage) { $fileData['thumb'] = $this->getThumbUrl($file, $modulepart); $fileData['galleryIndex'] = $imageIndex; $galleryData[] = array( 'index' => $imageIndex, 'name' => $file['name'], 'path' => $this->getFileUrl($file, $modulepart), ); $imageIndex++; } $filesData[] = $fileData; } $out = ''; // CSS $out .= ''; // JavaScript $filesJson = json_encode($filesData); $galleryJson = json_encode($galleryData); $out .= ''; return $out; } /** * Get Font Awesome icon class for file type * * @param string $filename Filename * @return string Icon class */ private function getFileIcon($filename) { $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); $icons = array( 'pdf' => 'fa-file-pdf', 'doc' => 'fa-file-word', 'docx' => 'fa-file-word', 'xls' => 'fa-file-excel', 'xlsx' => 'fa-file-excel', 'ppt' => 'fa-file-powerpoint', 'pptx' => 'fa-file-powerpoint', 'jpg' => 'fa-file-image', 'jpeg' => 'fa-file-image', 'png' => 'fa-file-image', 'gif' => 'fa-file-image', 'webp' => 'fa-file-image', 'bmp' => 'fa-file-image', 'svg' => 'fa-file-image', 'zip' => 'fa-file-archive', 'rar' => 'fa-file-archive', '7z' => 'fa-file-archive', 'tar' => 'fa-file-archive', 'gz' => 'fa-file-archive', 'txt' => 'fa-file-alt', 'csv' => 'fa-file-csv', 'xml' => 'fa-file-code', 'json' => 'fa-file-code', 'html' => 'fa-file-code', 'htm' => 'fa-file-code', 'mp3' => 'fa-file-audio', 'wav' => 'fa-file-audio', 'mp4' => 'fa-file-video', 'avi' => 'fa-file-video', 'mov' => 'fa-file-video', ); return isset($icons[$ext]) ? $icons[$ext] : 'fa-file'; } /** * Get full URL for a file * * @param array $file File array * @param string $modulepart Module part * @return string URL */ private function getFileUrl($file, $modulepart) { global $conf; $relativepath = isset($file['relativename']) ? $file['relativename'] : $file['name']; return DOL_URL_ROOT . '/document.php?modulepart=' . urlencode($modulepart) . '&file=' . urlencode($relativepath); } /** * Get thumbnail URL for a file * * @param array $file File array * @param string $modulepart Module part * @return string Thumbnail URL */ private function getThumbUrl($file, $modulepart) { // For now, use the same as file URL // Could be enhanced to use actual thumbnails return $this->getFileUrl($file, $modulepart); } /** * Check if file is an image * * @param string $filename Filename * @return bool */ private function isImageFile($filename) { $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); return in_array($ext, array('jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg')); } /** * Check if file is a PDF * * @param string $filename Filename * @return bool */ private function isPdfFile($filename) { $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); return $ext === 'pdf'; } /** * Check if file is pinned * * @param array $file File array * @return bool */ private function isFilePinned($file) { // Will be implemented with database table // For now return false $filepath = isset($file['fullname']) ? $file['fullname'] : ''; if (empty($filepath)) { return false; } $sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "filearchiv_pinned"; $sql .= " WHERE filepath = '" . $this->db->escape($filepath) . "'"; $resql = $this->db->query($sql); if ($resql && $this->db->num_rows($resql) > 0) { return true; } return false; } /** * Toggle pin status for a file * * @param string $filepath Full file path * @param string $modulepart Module part * @return bool Success */ public function togglePin($filepath, $modulepart) { global $user; if ($this->isFilePinnedByPath($filepath)) { // Unpin $sql = "DELETE FROM " . MAIN_DB_PREFIX . "filearchiv_pinned"; $sql .= " WHERE filepath = '" . $this->db->escape($filepath) . "'"; } else { // Pin $sql = "INSERT INTO " . MAIN_DB_PREFIX . "filearchiv_pinned"; $sql .= " (filepath, modulepart, fk_user, datec)"; $sql .= " VALUES ('" . $this->db->escape($filepath) . "',"; $sql .= " '" . $this->db->escape($modulepart) . "',"; $sql .= " " . ((int) $user->id) . ","; $sql .= " '" . $this->db->idate(dol_now()) . "')"; } $resql = $this->db->query($sql); return $resql ? true : false; } /** * Check if file is pinned by path * * @param string $filepath File path * @return bool */ private function isFilePinnedByPath($filepath) { $sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "filearchiv_pinned"; $sql .= " WHERE filepath = '" . $this->db->escape($filepath) . "'"; $resql = $this->db->query($sql); if ($resql && $this->db->num_rows($resql) > 0) { return true; } return false; } /** * Hook called when FormMail generates the email form * This hook is called with initHooks(['formmail']) context * * @param array $parameters Hook parameters (addfileaction, removefileaction, trackid) * @param FormMail $object FormMail object * @param string $action Current action * @param HookManager $hookmanager Hook manager * @return int 0 = keep standard behavior (add resPrint to output), >0 = replace entire form */ public function getFormMail($parameters, &$object, &$action, $hookmanager) { global $conf, $langs; // DEBUG - ALWAYS write first @file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " === getFormMail ENTRY ===\n", FILE_APPEND); // Get action from URL $currentAction = GETPOST('action', 'aZ09'); $trackid = isset($parameters['trackid']) ? $parameters['trackid'] : ''; $id = GETPOST('id', 'int'); // DEBUG - detailed info @file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " getFormMail called, action=$currentAction, trackid=$trackid, id=$id, PHP_SELF=" . $_SERVER['PHP_SELF'] . "\n", FILE_APPEND); if (!isModEnabled('filearchiv')) { file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " -> module not enabled\n", FILE_APPEND); return 0; } // Only show on presend action if ($currentAction != 'presend') { file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " -> action is not presend, skipping\n", FILE_APPEND); return 0; } // Get element and ID from URL/POST $element = GETPOST('element', 'aZ09'); $id = GETPOST('id', 'int'); // Try to determine element from trackid pattern $trackid = isset($parameters['trackid']) ? $parameters['trackid'] : ''; if (empty($element) && !empty($trackid)) { // Trackid format: pro123 (propal), ord123 (order), inv123 (invoice), shi123 (shipping) $patterns = array( 'pro' => 'propal', 'ord' => 'commande', 'inv' => 'facture', 'shi' => 'expedition', 'sor' => 'order_supplier', 'sin' => 'invoice_supplier', ); foreach ($patterns as $prefix => $elemType) { if (strpos($trackid, $prefix) === 0) { $element = $elemType; $id = (int) substr($trackid, strlen($prefix)); break; } } } // Try to get from the current page URL if (empty($element)) { $script = basename($_SERVER['PHP_SELF']); $pageMap = array( 'card.php' => array( '/comm/propal/' => 'propal', '/commande/' => 'commande', '/compta/facture/' => 'facture', '/expedition/' => 'expedition', ), ); if (isset($pageMap[$script])) { foreach ($pageMap[$script] as $path => $elemType) { if (strpos($_SERVER['PHP_SELF'], $path) !== false) { $element = $elemType; break; } } } } if (empty($element) || empty($id)) { return 0; } // Load translations $langs->load('filearchiv@filearchiv'); // Build the document browser button and modal HTML $out = $this->getDocumentBrowserHTML($element, $id, $trackid); $this->resprints = $out; return 0; // Return 0 to add our HTML without replacing the form } /** * Hook called on formConfirm - adds document browser for presend action * This hook is called early on the page and allows adding HTML that will be output * * @param array $parameters Hook parameters * @param CommonObject $object Current object * @param string $action Current action * @param HookManager $hookmanager Hook manager * @return int 0 = keep standard behavior (add resPrint), >0 = replace */ /** * Hook called at the beginning of each page (doActions) * Used to inject document browser HTML on presend pages * * @param array $parameters Hook parameters * @param CommonObject $object Current object * @param string $action Current action * @param HookManager $hookmanager Hook manager * @return int 0 = keep standard behavior */ public function doActions($parameters, &$object, &$action, $hookmanager) { global $conf, $langs; // Debug log @file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " doActions called, action=" . GETPOST('action', 'aZ09') . "\n", FILE_APPEND); return 0; } public function formConfirm($parameters, &$object, &$action, $hookmanager) { global $conf, $langs; // Always write to debug log to confirm hook is being called @file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " formConfirm called, action=" . GETPOST('action', 'aZ09') . "\n", FILE_APPEND); if (!isModEnabled('filearchiv')) { return 0; } // Only show on presend action $currentAction = GETPOST('action', 'aZ09'); if ($currentAction != 'presend') { return 0; } // Get current object info $element = ''; $id = 0; if (is_object($object) && !empty($object->element)) { $element = $object->element; $id = $object->id; } @file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " formConfirm presend: element=$element, id=$id\n", FILE_APPEND); if (empty($element) || empty($id)) { return 0; } // Check if this element type is supported $supportedElements = array('propal', 'commande', 'facture', 'expedition', 'order_supplier', 'invoice_supplier'); if (!in_array($element, $supportedElements)) { return 0; } // Load translations $langs->load('filearchiv@filearchiv'); // Build the document browser button and modal HTML $out = $this->getDocumentBrowserHTML($element, $id, ''); @file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " formConfirm injecting HTML, length=" . strlen($out) . "\n", FILE_APPEND); $this->resprints = $out; return 0; } /** * Hook called after action buttons are displayed * This hook is reliably called on all card pages * * @param array $parameters Hook parameters * @param CommonObject $object Current object * @param string $action Current action * @param HookManager $hookmanager Hook manager * @return int 0 = keep standard behavior */ public function addMoreActionsButtons($parameters, &$object, &$action, $hookmanager) { global $conf, $langs; if (!isModEnabled('filearchiv')) { return 0; } // Only show on presend action $currentAction = GETPOST('action', 'aZ09'); if ($currentAction != 'presend') { return 0; } // Get current object info $element = ''; $id = 0; if (is_object($object) && !empty($object->element)) { $element = $object->element; $id = $object->id; } if (empty($element) || empty($id)) { return 0; } // Check if this element type is supported $supportedElements = array('propal', 'commande', 'facture', 'expedition', 'order_supplier', 'invoice_supplier'); if (!in_array($element, $supportedElements)) { return 0; } // Load translations $langs->load('filearchiv@filearchiv'); // Build the document browser button and modal HTML $this->resprints = $this->getDocumentBrowserHTML($element, $id, ''); return 0; } /** * Generate HTML for document browser button and modal * * @param string $element Element type (facture, propal, commande, etc.) * @param int $id Element ID * @param string $trackid Email tracking ID * @return string HTML output */ private function getDocumentBrowserHTML($element, $id, $trackid) { global $conf, $langs; // Use custom module path for AJAX URLs $ajaxUrl = dol_buildpath('/custom/filearchiv/ajax/getdocuments.php', 1); $addFileUrl = dol_buildpath('/custom/filearchiv/ajax/addfile.php', 1); // Debug: Log the URLs @file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " ajaxUrl=$ajaxUrl, addFileUrl=$addFileUrl\n", FILE_APPEND); $out = ''; // CSS for document browser - using Dolibarr native styles where possible $out .= ' '; // Button to open document browser (hidden initially, will be moved by JS) $out .= '
'; // Modal HTML $out .= ' '; // JavaScript $out .= ' '; return $out; } }