[Faites le vous même] Module pour upload des images sur le serveur

Bonjour tout le monde,
Me revoila pour partager un module que j’ai fait avec Claude.ai
J’utilise le super module DocEdit et j’avais un problème avec le liens des images à insérer dans les pdf avec l’éditeur WYSIWYG.
Les liens de type /htdoc/viewimage.php?modulepart=medias&entity=1&file=image/monimage.jpg ne fonctionnent pas car DocEdit utilise getimagesize() qui ne peut pas traiter directement les URLs et a besoin d’un chemin physique du fichier.

ATM m’a répondu que c’était pas leur problème et ce n’était clairement pas un problème de Dolibarr non plus.

Du coup j’ai cherché une solution de contournement avec un module à ma portée.
Ce module permet d’upload dans un dossier de notre choix des images dont on pourra récupérer le lien pour l’éditeur WYSIWYG
Avant ça j’uplodais avec filezilla mes images et copias collais le lien.
Mais mes collègues au bureaux galéraient avec cette manip et ça prennait un max de temps.


Structure du module :

htdocs/custom/myimages/
├── core/
│   └── modules/
│       └── modMyImages.class.php
├── langs/
│   ├── en_US/
│   │   └── myimages.lang
│   └── fr_FR/
│       └── myimages.lang
└── index.php

Le fichier descripteur modMyImages.class.php :

<?php
include_once DOL_DOCUMENT_ROOT .'/core/modules/DolibarrModules.class.php';

class modMyImages extends DolibarrModules
{
    public function __construct($db)
    {
        global $langs, $conf;
        
        $this->db = $db;
        
        // Id for module (must be unique)
        $this->numero = 500020;
        
        // Key text used to identify module (for permissions, menus, etc...)
        $this->rights_class = 'myimages';
        
        // Module label (no space allowed), used if translation string 'ModuleXXXName' not found
        $this->name = preg_replace('/^mod/i', '', get_class($this));
        
        // Module description, used if translation string 'ModuleXXXDesc' not found
        $this->description = "Gestion des images";
        $this->editor_name = 'Votre nom';
        $this->editor_url = 'http://www.example.com';
        
        // Possible values for version are: 'development', 'experimental', 'dolibarr' or version
        $this->version = '1.0';
        
        // Key used in llx_const table to save module status enabled/disabled
        $this->const_name = 'MAIN_MODULE_'.strtoupper($this->name);
        
        // Name of image file used for this module
        $this->picto = 'fa-upload';
        
        // Defined all module parts (triggers, login, substitutions, menus, css, etc...)
        $this->module_parts = array(
            'triggers' => 0,
            'login' => 0,
            'substitutions' => 0,
            'menus' => 0,
            'theme' => 0,
            'tpl' => 0,
            'barcode' => 0,
            'models' => 0,
            'css' => array(),
            'js' => array(),
            'hooks' => array(),
            'dir' => array("/myimages/temp")
        );

        // Dependencies
        $this->depends = array();
        $this->requiredby = array();
        $this->conflictwith = array();
        $this->phpmin = array(7,0);
        $this->need_dolibarr_version = array(12,0);
        $this->langfiles = array("myimages@myimages");
        
        // Config page
        $this->config_page_url = array("setup.php@myimages");
        
        // Constants
        $this->const = array();
        
        // Boxes/Widgets
        $this->boxes = array();
        
        // Permissions
        $this->rights = array();
        $r = 0;
        
        $this->rights[$r][0] = 500021;
        $this->rights[$r][1] = 'Voir les images';
        $this->rights[$r][2] = 'r';
        $this->rights[$r][3] = 0;
        $this->rights[$r][4] = 'read';
        $r++;
        
        $this->rights[$r][0] = 500022;
        $this->rights[$r][1] = 'Uploader des images';
        $this->rights[$r][2] = 'w';
        $this->rights[$r][3] = 0;
        $this->rights[$r][4] = 'write';
        $r++;
        
                // Main menu entries
                $this->menu = array();
                $r = 0;
                
                $this->menu[$r] = array(
                    'fk_menu' => '', // '' = menu racine
                    'type' => 'top',
                    'titre' => 'Mes Images',
                    'prefix' => img_picto('', $this->picto, 'class="paddingright pictofixedwidth valignmiddle"'),
                    'mainmenu' => 'myimages',
                    'leftmenu' => '',
                    'url' => '/custom/myimages/index.php',
                    'langs' => 'myimages@myimages',
                    'position' => 100,
                    'enabled' => '1',
                    'perms' => '$user->rights->myimages->read',
                    'target' => '',
                    'user' => 0
                );
    }

    /**
     * Function called when module is enabled.
     * The init function add permissions, constants, boxes, permissions and menus
     * (defined in constructor) into Dolibarr database.
     * It also creates data directories
     *
     * @param string $options Options when enabling module ('', 'noboxes')
     * @return int 1 if OK, 0 if KO
     */
    public function init($options = '')
    {
        $sql = array();
        
        $result = $this->_load_tables('/myimages/sql/');
        
        // Create extrafields during init
        //include_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
        //$extrafields = new ExtraFields($this->db);
        
        return $this->_init($sql, $options);
    }

    /**
     * Function called when module is disabled.
     * Remove from database constants, boxes and permissions from Dolibarr database.
     * Data directories are not deleted
     *
     * @param string $options Options when enabling module ('', 'noboxes')
     * @return int 1 if OK, 0 if KO
     */
    public function remove($options = '')
    {
        $sql = array();
        return $this->_remove($sql, $options);
    }
}

Le fichier index.php :

<?php
$res = 0;
if (! $res && file_exists("../main.inc.php")) 
    $res = @include "../main.inc.php";
if (! $res && file_exists("../../main.inc.php")) 
    $res = @include "../../main.inc.php";
if (! $res && file_exists("../../../main.inc.php"))
    $res = @include "../../../main.inc.php";
if (! $res) 
    die("Include of main fails");

require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';

// Contrôle des droits
if (!$user->rights->myimages->read) accessforbidden();

$action = GETPOST('action', 'alpha');
$file = GETPOST('file', 'alpha');
$sortfield = GETPOST('sortfield', 'aZ09comma');
$sortorder = GETPOST('sortorder', 'aZ09comma');
$upload_dir = '/home/moi/www/image'; //chemin absolu du dossier
$web_url = 'https://monsite.com/image'; // url du dossier
$relative_path = '/image'; //chemin relatif

// Tri par défaut
if (!$sortfield) $sortfield="date";
if (!$sortorder) $sortorder="DESC";

// Suppression d'image
if ($action == 'delete' && $file && $user->rights->myimages->write) {
    $file_path = $upload_dir.'/'.$file;
    if (file_exists($file_path) && unlink($file_path)) {
        setEventMessages($langs->trans("FileDeleted"), null);
    } else {
        setEventMessages($langs->trans("ErrorDeletingFile"), null, 'errors');
    }
}

// Traitement de l'upload
if ($action == 'upload' && $user->rights->myimages->write) {
    if (! empty($_FILES['userfile']['name'])) {
        $upload_file = $_FILES['userfile']['name'];
        
        // Vérification du type de fichier
        $mime = dol_mimetype($upload_file);
        if (! preg_match('/image/i', $mime)) {
            setEventMessages($langs->trans("OnlyImageFilesAllowed"), null, 'errors');
        } else {
            $result = move_uploaded_file(
                $_FILES['userfile']['tmp_name'],
                $upload_dir.'/'.$upload_file
            );
            
            if ($result) {
                chmod($upload_dir.'/'.$upload_file, 0644);
                setEventMessages($langs->trans("FileTransferComplete"), null);
            } else {
                setEventMessages($langs->trans("ErrorFileNotUploaded"), null, 'errors');
            }
        }
    }
}

// Load translation files required by the page
$langs->loadLangs(array("myimages@myimages"));

// CSS et JavaScript pour la modal
print '<style>
.modal {
    display: none;
    position: fixed;
    z-index: 1000;
    padding-top: 100px;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0,0,0,0.9);
}
.modal-content {
    margin: auto;
    display: block;
    max-width: 90%;
    max-height: 90%;
}
.close {
    position: absolute;
    right: 35px;
    top: 15px;
    color: #f1f1f1;
    font-size: 40px;
    font-weight: bold;
    cursor: pointer;
}
.table-container {
    width: 100%;
    margin: 0;
    padding: 0;
    overflow-x: auto;
}
.noborder.centpercent {
    width: 100% !important;
    margin: 0 !important;
    border-collapse: collapse;
}
.width100 {
    width: 100% !important;
}
</style>';

print '<script>
function copyToClipboard(text) {
    navigator.clipboard.writeText(text).then(function() {
        alert("Lien copié !");
    }, function() {
        alert("Erreur lors de la copie");
    });
}

var modal = null;
var modalImg = null;

document.addEventListener("DOMContentLoaded", function() {
    modal = document.getElementById("imageModal");
    modalImg = document.getElementById("modalImage");
    
    modal.onclick = function(e) {
        if (e.target == modal) {
            modal.style.display = "none";
        }
    }
    
    document.getElementsByClassName("close")[0].onclick = function() {
        modal.style.display = "none";
    }
});

function showImage(src) {
    modalImg.src = src;
    modal.style.display = "block";
}
</script>';

llxHeader();
// Ajout du JavaScript pour la copie dans le presse-papier
?>
<script type="text/javascript">
function copyToClipboard(text) {
    navigator.clipboard.writeText(text).then(function() {
        /* Succès */
        alert('Lien copié avec succès !');
    }, function() {
        /* Échec */
        alert('Erreur lors de la copie du lien');
    });
}
</script>
<?php

print load_fiche_titre($langs->trans('MYIMAGES_MODULE_NAME'));

// Formulaire d'upload
if ($user->rights->myimages->write) {
    print '<form name="formupload" action="'.$_SERVER["PHP_SELF"].'" method="POST" enctype="multipart/form-data">';
    print '<input type="hidden" name="token" value="'.newToken().'">';
    print '<input type="hidden" name="action" value="upload">';
    print '<input type="file" name="userfile" class="flat">';
    print '<input type="submit" class="button" value="'.$langs->trans("Upload").'">';
    print '</form><br>';
}

// Modal pour l'agrandissement des images
print '<div id="imageModal" class="modal">';
print '<span class="close">&times;</span>';
print '<img class="modal-content" id="modalImage">';
print '</div>';

// Récupération et tri des fichiers
$files = array();
if (is_dir($upload_dir)) {
    $handle = opendir($upload_dir);
    if ($handle) {
        while (($file = readdir($handle)) !== false) {
            if (is_file($upload_dir.'/'.$file) && preg_match('/\.(jpg|jpeg|png|gif)$/i', $file)) {
                $files[] = array(
                    'name' => $file,
                    'date' => filemtime($upload_dir.'/'.$file),
                    'url' => $web_url.'/'.$file,
                    'relative_url' => $relative_path.'/'.$file
                );
            }
        }
        closedir($handle);
    }
}

// Tri des fichiers
if ($sortfield == 'name') {
    usort($files, function($a, $b) use ($sortorder) {
        return $sortorder == 'DESC' ? 
            strcasecmp($b['name'], $a['name']) : 
            strcasecmp($a['name'], $b['name']);
    });
} else { // tri par date par défaut
    usort($files, function($a, $b) use ($sortorder) {
        return $sortorder == 'DESC' ? 
            $b['date'] - $a['date'] : 
            $a['date'] - $b['date'];
    });
}

// Affichage des images
print '<div class="table-container">';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print_liste_field_titre("Image", $_SERVER["PHP_SELF"], "", "", "", "", $sortfield, $sortorder);
print_liste_field_titre("Name", $_SERVER["PHP_SELF"], "name", "", "", "", $sortfield, $sortorder);
print_liste_field_titre("Date", $_SERVER["PHP_SELF"], "date", "", "", "", $sortfield, $sortorder);
print_liste_field_titre("Copier", $_SERVER["PHP_SELF"], "", "", "", "", $sortfield, $sortorder);
print_liste_field_titre("Supprimer", $_SERVER["PHP_SELF"], "", "", "", 'class="center"', $sortfield, $sortorder);
print '</tr>';

foreach ($files as $fileinfo) {
    print '<tr class="oddeven">';
    print '<td><img src="'.$fileinfo['url'].'" height="50" style="cursor:pointer" onclick="showImage(\''.$fileinfo['url'].'\')" /></td>';
    print '<td>'.$fileinfo['name'].'</td>';
    print '<td>'.dol_print_date($fileinfo['date'], 'dayhour').'</td>';
    print '<td class="center">';
    print '<a class="reposition" href="#" onclick="copyToClipboard(\''.$fileinfo['relative_url'].'\'); return false;">';
    print '<i class="fa fa-clipboard" aria-hidden="true"></i>';
    print '</a>';
    print '</td>';
    print '<td class="center">';
    if ($user->rights->myimages->write) {
        print '<a href="'.$_SERVER["PHP_SELF"].'?action=delete&token='.newToken().'&file='.urlencode($fileinfo['name']).'" onclick="return confirm(\''.$langs->trans("ConfirmDelete").'\');">';
        print img_picto('', 'delete');
        print '</a>';
    }
    print '</td>';
    print '</tr>';
}

print '</table>';
print '</div>';

llxFooter();

Le fichier en_US/myimages.lang :

// langs/en_US/myimages.lang
MYIMAGES_MODULE_NAME = Image Management
Module500020Name = Image Management
Module500020Desc = Module to upload and manage images

OnlyImageFilesAllowed = Only image files are allowed
FileTransferComplete = File transfer completed
ErrorFileNotUploaded = Error during file upload
FileDeleted = File deleted
ErrorDeletingFile = Error deleting file
ConfirmDelete = Are you sure you want to delete this image?
Upload = Upload
Image = Image
Name = Name
Size = Size
Date = Date
Link = Link
Actions = Actions

Le fichier fr_FR/myimages.lang :

// langs/fr_FR/myimages.lang
MYIMAGES_MODULE_NAME = Gestion des images
Module500020Name = Gestion des images
Module500020Desc = Module pour uploader et gérer des images

OnlyImageFilesAllowed = Seuls les fichiers image sont autorisés
FileTransferComplete = Transfert de fichier terminé
ErrorFileNotUploaded = Erreur lors du transfert du fichier
FileDeleted = Fichier supprimé
ErrorDeletingFile = Erreur lors de la suppression du fichier
ConfirmDelete = Êtes-vous sûr de vouloir supprimer cette image ?
Upload = Envoyer
Image = Image
Name = Nom
Size = Taille
Date = Date
Link = Lien
Actions = Actions

Attention à bien changer dans le code de index.php les variables :

$upload_dir = '/home/moi/www/image'; //chemin absolu du dossier
$web_url = 'https://monsite.com/image'; // url du dossier
$relative_path = '/image'; //chemin relatif

Et éventuellement (les ID doivent être uniques) dans le code de modMyImages.class.php :

// Id for module (must be unique)
$this->numero = 500020;
$this->rights[$r][0] = 500021;
$this->rights[$r][0] = 500022;

Ca m’a pris environ 3 semaines pour faire ça avec l’aide de Claude
J’ai essayé les hooks pour intercepter l’upload d’images de l’éditeur WYSIWYG mais ça marchait pas bien du coup j’ai opté pour cette solution.
Il y a surement des méthodes plus propres et plus optimisées mais c’est en dehors de mes compétences.

A moi ça m’a été utile, ça pourra peut être en aider d’autres :smile:

4 « J'aime »

Bonjour,

Je ne sais pas si cela peu servir, mais j’ai un bout de code en python qui envoie des fichiers (upload) dans la GED via l’api native.

import os
import requests
import base64
#  on récupère le token et le mot de passe du mail
apiToken = open("apitoken.txt", 'r').read()

headers = {
	'DOLAPIKEY': apiToken,
	'DOLAPIENTITY' : '2',				# l'entité de la société (ICI 2)
	'Content-Type': 'application/json', 
	'Accept': 'application/json'
}
### URL du server Dolibarr
urlBase = "http://127.0.0.1/dolibarr-20.0.0/htdocs/api/index.php/"

# on récupère le contenu  DU fichier file
filecontent = open(file, 'rb').read()
encoded_string = base64.b64encode(filecontent)
# récupération du nom du fichier en elevant le chemin
filename = os.path.basename(file)

# on transfert le fichier sur le serveur
url = urlBase + "documents/upload"
data = {
			"filename": filename, 
			"modulepart": "invoice", 
			"ref": "(PROV" + str(invoiceID) + ")",
			"subdir": "", 
			"filecontent": encoded_string,
			"fileencoding": "base64", 
			"overwriteifexists": "0" 
	}

r = requests.post(url, headers=headers, json=data)

A titre d’info j’ai aussi poussé un PR dans l’api de dolibarr pour récupérer les fichiers (download) via l’api native

3 « J'aime »

Intéressant. Pour suivre… :slight_smile: