Forráskód Böngészése

implements pdf encoding and tempfile storage

Olivier Massot 3 éve
szülő
commit
caf5bc4988

+ 8 - 0
.env

@@ -20,6 +20,9 @@ APP_SECRET=6a76497c8658bb23e2236f97a2627df3
 #TRUSTED_HOSTS='^(localhost|example\.com)$'
 ###< symfony/framework-bundle ###
 
+###> files management ###
+INTERNAL_FILES_DOWNLOAD_URI=https://api.opentalent.fr/_internal/secure/files
+###< files management ###
 
 ###> doctrine/doctrine-bundle ###
 # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
@@ -51,3 +54,8 @@ DOLIBARR_API_TOKEN='Bocc4zC0J186v8J6QCqu7DnoIw4I7mCJ'
 ###> mobyt client ###
 MOBYT_API_BASE_URI='https://app.mobyt.fr/API/v1.0/REST/'
 ###< mobyt client ###
+
+###> knplabs/knp-snappy-bundle ###
+WKHTMLTOPDF_PATH=/usr/local/bin/wkhtmltopdf
+WKHTMLTOIMAGE_PATH=/usr/local/bin/wkhtmltoimage
+###< knplabs/knp-snappy-bundle ###

+ 1 - 0
composer.json

@@ -19,6 +19,7 @@
         "doctrine/orm": "^2.9",
         "egulias/email-validator": "^3.0",
         "jbouzekri/phumbor-bundle": "^2.1",
+        "knplabs/knp-snappy-bundle": "^1.9",
         "lexik/jwt-authentication-bundle": "^2.8",
         "myclabs/php-enum": "^1.7",
         "nelmio/cors-bundle": "^2.1",

+ 201 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "9378454b34ddc471984f942bfa334d77",
+    "content-hash": "4cc2f432e0c9bbe68a5bd9380f6c220d",
     "packages": [
         {
             "name": "api-platform/core",
@@ -2142,6 +2142,144 @@
             },
             "time": "2021-01-04T21:17:45+00:00"
         },
+        {
+            "name": "knplabs/knp-snappy",
+            "version": "v1.4.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/KnpLabs/snappy.git",
+                "reference": "5126fb5b335ec929a226314d40cd8dad497c3d67"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/5126fb5b335ec929a226314d40cd8dad497c3d67",
+                "reference": "5126fb5b335ec929a226314d40cd8dad497c3d67",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1",
+                "psr/log": "^1.0||^2.0||^3.0",
+                "symfony/process": "~3.4||~4.3||~5.0||~6.0"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "^2.16||^3.0",
+                "pedrotroller/php-cs-custom-fixer": "^2.19",
+                "phpstan/phpstan": "^0.12.7",
+                "phpstan/phpstan-phpunit": "^0.12.6",
+                "phpunit/phpunit": "~7.4||~8.5"
+            },
+            "suggest": {
+                "h4cc/wkhtmltoimage-amd64": "Provides wkhtmltoimage-amd64 binary for Linux-compatible machines, use version `~0.12` as dependency",
+                "h4cc/wkhtmltoimage-i386": "Provides wkhtmltoimage-i386 binary for Linux-compatible machines, use version `~0.12` as dependency",
+                "h4cc/wkhtmltopdf-amd64": "Provides wkhtmltopdf-amd64 binary for Linux-compatible machines, use version `~0.12` as dependency",
+                "h4cc/wkhtmltopdf-i386": "Provides wkhtmltopdf-i386 binary for Linux-compatible machines, use version `~0.12` as dependency",
+                "wemersonjanuario/wkhtmltopdf-windows": "Provides wkhtmltopdf executable for Windows, use version `~0.12` as dependency"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Knp\\Snappy\\": "src/Knp/Snappy"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "KNP Labs Team",
+                    "homepage": "http://knplabs.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "http://github.com/KnpLabs/snappy/contributors"
+                }
+            ],
+            "description": "PHP library allowing thumbnail, snapshot or PDF generation from a url or a html page. Wrapper for wkhtmltopdf/wkhtmltoimage.",
+            "homepage": "http://github.com/KnpLabs/snappy",
+            "keywords": [
+                "knp",
+                "knplabs",
+                "pdf",
+                "snapshot",
+                "thumbnail",
+                "wkhtmltopdf"
+            ],
+            "support": {
+                "issues": "https://github.com/KnpLabs/snappy/issues",
+                "source": "https://github.com/KnpLabs/snappy/tree/v1.4.1"
+            },
+            "time": "2022-01-07T13:03:38+00:00"
+        },
+        {
+            "name": "knplabs/knp-snappy-bundle",
+            "version": "v1.9.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/KnpLabs/KnpSnappyBundle.git",
+                "reference": "da11c2d083f5d8586c9bd59d2948ab7d39d57b34"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/KnpLabs/KnpSnappyBundle/zipball/da11c2d083f5d8586c9bd59d2948ab7d39d57b34",
+                "reference": "da11c2d083f5d8586c9bd59d2948ab7d39d57b34",
+                "shasum": ""
+            },
+            "require": {
+                "knplabs/knp-snappy": "^1.2",
+                "php": ">=7.4",
+                "symfony/framework-bundle": "^4.4|^5.1|^6.0"
+            },
+            "require-dev": {
+                "doctrine/annotations": "^1.11",
+                "symfony/asset": "^4.4|^5.1|^6.0",
+                "symfony/finder": "^4.4|^5.1|^6.0",
+                "symfony/phpunit-bridge": "^4.4|^5.1|^6.0",
+                "symfony/security-csrf": "^4.4|^5.1|^6.0",
+                "symfony/templating": "^4.4|^5.1|^6.0",
+                "symfony/validator": "^4.4|^5.1|^6.0",
+                "symfony/yaml": "^4.4|^5.1|^6.0"
+            },
+            "type": "symfony-bundle",
+            "autoload": {
+                "psr-4": {
+                    "Knp\\Bundle\\SnappyBundle\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "KnpLabs Team",
+                    "homepage": "http://knplabs.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "http://github.com/KnpLabs/KnpSnappyBundle/contributors"
+                }
+            ],
+            "description": "Easily create PDF and images in Symfony by converting Twig/HTML templates.",
+            "homepage": "http://github.com/KnpLabs/KnpSnappyBundle",
+            "keywords": [
+                "bundle",
+                "knp",
+                "knplabs",
+                "pdf",
+                "snappy"
+            ],
+            "support": {
+                "issues": "https://github.com/KnpLabs/KnpSnappyBundle/issues",
+                "source": "https://github.com/KnpLabs/KnpSnappyBundle/tree/v1.9.0"
+            },
+            "time": "2022-01-05T15:50:51+00:00"
+        },
         {
             "name": "laminas/laminas-code",
             "version": "4.4.3",
@@ -5682,6 +5820,68 @@
             ],
             "time": "2021-05-21T13:25:03+00:00"
         },
+        {
+            "name": "symfony/process",
+            "version": "v5.3.14",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/process.git",
+                "reference": "8bbae08c19308b9493ad235386144cbefec83cb0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/process/zipball/8bbae08c19308b9493ad235386144cbefec83cb0",
+                "reference": "8bbae08c19308b9493ad235386144cbefec83cb0",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2.5",
+                "symfony/polyfill-php80": "^1.16"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Process\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Executes commands in sub-processes",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/process/tree/v5.3.14"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-01-24T19:35:44+00:00"
+        },
         {
             "name": "symfony/property-access",
             "version": "v5.3.8",

+ 1 - 0
config/bundles.php

@@ -14,4 +14,5 @@ return [
     Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
     Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
     FOS\ElasticaBundle\FOSElasticaBundle::class => ['all' => true],
+    Knp\Bundle\SnappyBundle\KnpSnappyBundle::class => ['all' => true],
 ];

+ 9 - 0
config/packages/knp_snappy.yaml

@@ -0,0 +1,9 @@
+knp_snappy:
+    pdf:
+        enabled:    true
+        binary:     '%env(WKHTMLTOPDF_PATH)%'
+        options:    []
+    image:
+        enabled:    true
+        binary:     '%env(WKHTMLTOIMAGE_PATH)%'
+        options:    []

+ 23 - 0
src/ApiResources/Export/ExportRequest.php

@@ -5,6 +5,7 @@ namespace App\ApiResources\Export;
 
 use ApiPlatform\Core\Annotation\ApiProperty;
 use ApiPlatform\Core\Annotation\ApiResource;
+use App\Entity\Access\Access;
 use Symfony\Component\Validator\Constraints as Assert;
 
 /**
@@ -22,6 +23,12 @@ abstract class ExportRequest
     #[Assert\Choice(self::SUPPORTED_FORMATS)]
     protected string $format;
 
+    /**
+     * The access requesting this export
+     * @var Access
+     */
+    protected Access $requester;
+
     /**
      * @return string
      */
@@ -37,4 +44,20 @@ abstract class ExportRequest
     {
         $this->format = $format;
     }
+
+    /**
+     * @return Access
+     */
+    public function getRequester(): Access
+    {
+        return $this->requester;
+    }
+
+    /**
+     * @param Access $requester
+     */
+    public function setRequester(Access $requester): void
+    {
+        $this->requester = $requester;
+    }
 }

+ 7 - 0
src/DataPersister/Export/LicenceCmf/LicenceCmfOrganizationERDataPersister.php

@@ -4,13 +4,16 @@ namespace App\DataPersister\Export\LicenceCmf;
 
 use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
 use App\ApiResources\Export\LicenceCmf\LicenceCmfOrganizationER;
+use App\Entity\Access\Access;
 use App\Service\Export\LicenceCmfExporter;
 use HttpRequestException;
+use Symfony\Component\Security\Core\Security;
 
 class LicenceCmfOrganizationERDataPersister implements ContextAwareDataPersisterInterface
 {
     public function __construct(
         private LicenceCmfExporter $licenceCmfExporter,
+        private Security $security,
     ) {}
 
     public function supports($data, array $context = []): bool
@@ -24,6 +27,10 @@ class LicenceCmfOrganizationERDataPersister implements ContextAwareDataPersister
      */
     public function persist($licenceCmfExportRequest, array $context = [])
     {
+        /** @var Access $access */
+        $access = $this->security->getUser();
+
+        $licenceCmfExportRequest->setRequester($access);
         $this->licenceCmfExporter->export($licenceCmfExportRequest);
     }
 

+ 73 - 0
src/Service/Export/Encoder/PdfEncoder.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace App\Service\Export\Encoder;
+
+use App\Entity\Core\File;
+use \Knp\Snappy\Pdf;
+
+/**
+ * Encode HTML to PDF
+ */
+class PdfEncoder
+{
+    /**
+     * Default encoding options
+     * @see https://wkhtmltopdf.org/libwkhtmltox/
+     *
+     * @var array
+     */
+    private array $defaultOptions = [
+        'margin-top'    => 35,
+        'margin-right'  => 10,
+        'margin-bottom' => 15,
+        'margin-left'   => 15,
+        'header-spacing'   => 5,
+        'enable-local-file-access' => true
+    ];
+
+    public function __construct(
+        private Pdf $knpSnappy
+    ) {}
+
+    /**
+     * Default encoding options
+     * @return array
+     */
+    public function getDefaultOptions() {
+        return $this->defaultOptions;
+    }
+
+    /**
+     * Encode the given HTML content into PDF, and
+     * return the encoded content
+     *
+     * @param string $html
+     * @param array $options
+     * @return string
+     */
+    public function encode(string $html, array $options = []): string
+    {
+        $options = array_merge($this->defaultOptions, $options);
+
+        return $this->knpSnappy->getOutputFromHtml($html, $options);
+    }
+
+    /**
+     * Encode the given HTML content into PDF,
+     * write it into a file at the given location,
+     * and returns a File entity
+     *
+     * @param string $html
+     * @param string $path
+     * @param array $options
+     * @return string
+     */
+    public function encodeToFile(string $html, string $path, array $options = []): string
+    {
+        $options = array_merge($this->defaultOptions, $options);
+
+        $this->knpSnappy->generateFromHtml($html, $path, $options);
+
+        return $path;
+    }
+}

+ 34 - 6
src/Service/Export/LicenceCmfExporter.php

@@ -3,10 +3,14 @@
 namespace App\Service\Export;
 
 
+use App\Entity\Core\File;
 use App\Entity\Export\LicenceCmf;
 use App\Enum\Access\FunctionEnum;
 use App\Repository\Access\AccessRepository;
 use App\Repository\Organization\OrganizationRepository;
+use App\Service\Export\Encoder\PdfEncoder;
+use App\Service\Storage\TemporaryFileStorage;
+use App\Service\Utils\Path;
 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 use Twig\Environment;
 
@@ -28,6 +32,7 @@ class LicenceCmfExporter extends BaseExporter
         private AccessRepository $accessRepository,
         private Environment $twig,
         private UrlGeneratorInterface $router,
+        private PdfEncoder $pdfEncoder
     )
     {}
 
@@ -45,8 +50,8 @@ class LicenceCmfExporter extends BaseExporter
         $model->setOrganizationName($organization->getName());
         $model->setOrganizationIdentifier($organization->getIdentifier());
 
-//        $parentNetwork = $organization->getParent()
-        $model->setFederationName('');
+        $parentFederation = $organization->getNetworkOrganizations()[0]->getParent();
+        $model->setFederationName($parentFederation->getName());
 
         $model->setColor(
             $this->getLicenceColor($currentYear)
@@ -55,7 +60,8 @@ class LicenceCmfExporter extends BaseExporter
         $logoId = $organization->getLogo()?->getId();
         if ($logoId) {
             $model->setLogoUri(
-                $this->router->generate('ot_internal_secure_file_download', array('id' => $logoId))
+//                $this->router->generate('ot_legacy_file_download', array('id' => $logoId))
+                rtrim($_SERVER['INTERNAL_FILES_DOWNLOAD_URI'], '/') . '/' . $logoId
             );
         }
 
@@ -72,19 +78,41 @@ class LicenceCmfExporter extends BaseExporter
         $qrCodeId = $cmf->getParameters()?->getQrCode()?->getId();
         if ($qrCodeId) {
             $model->setQrCodeUri(
-                $this->router->generate('ot_internal_secure_file_download', array('id' => $qrCodeId))
+                rtrim($_SERVER['INTERNAL_FILES_DOWNLOAD_URI'], '/') . '/' . $qrCodeId
             );
         }
 
         $html = $this->twig->render(
-            '@templates/export/licence_cmf/licence_cmf_organization.html.twig',
+            '@templates/export/licence_cmf.html.twig',
             [
                 'models' => [$model],
                 'isOrganizationLicence' => true
             ]
         );
 
-        var_dump($html); die;
+        $filename = 'licence_cmf_' . $currentYear . '.pdf';
+        $tempDir = TemporaryFileStorage::makeStorageDirFor($organization->getId());
+        $tempPath = Path::join($tempDir, $filename);
+
+//        $file = new File();
+//        $file->setOrganization($organization);
+//        $file->setPath($location);
+//        $file->setSlug($location);
+//        $file->setName(basename($location));
+//        $file->setMimeType('application/pdf');
+//        $file->setVisibility('NOBODY');
+//        $file->setFolder('DOCUMENTS');
+//        $file->setType($model::TYPE);
+//        $file->setCreateDate(new \DateTime());
+//        $file->setCreatedBy($exportRequest->getRequester()->getId());
+
+        if (file_exists($tempPath)) {
+            unlink($tempPath);
+        }
+
+        $this->pdfEncoder->encodeToFile($html, $tempPath);
+
+        return $tempPath;
     }
 
     /**

+ 17 - 0
src/Service/Storage/FileStorage.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Service\Storage;
+
+use App\Service\Utils\Path;
+
+/**
+ * Base class for file storage
+ */
+abstract class FileStorage
+{
+    protected static function getStorageBaseDir(): string {
+        return Path::join(Path::getAppDir(), 'var', 'files');
+    }
+
+
+}

+ 52 - 0
src/Service/Storage/TemporaryFileStorage.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace App\Service\Storage;
+
+use App\Service\Utils\Path;
+
+/**
+ * Gère le stockage des fichiers temporaires, comme les documents générés par les utilisateurs
+ * comme des fichiers d'export
+ */
+class TemporaryFileStorage extends FileStorage
+{
+    protected static function getStorageBaseDir(): string {
+        return Path::join(parent::getStorageBaseDir(), 'temp');
+    }
+
+    /**
+     * Return the path of the temporary storage dir for the given organization and access
+     *
+     * @param int $organizationId
+     * @param int|null $accessId
+     * @return string
+     */
+    protected static function getStorageDirFor(int $organizationId, ?int $accessId = null): string
+    {
+        return Path::join(
+            self::getStorageBaseDir(),
+            date('ymd'),
+            (string)$organizationId,
+            (string)$accessId ?? ''
+        );
+    }
+
+    /**
+     * Recursively make the temporary storage dir for the given organization and access
+     * and return its path
+     *
+     * (If the directory already exists, simply returns its path)
+     *
+     * @param int $organizationId
+     * @param int|null $accessId
+     * @return string
+     */
+    public static function makeStorageDirFor(int $organizationId, ?int $accessId = null): string
+    {
+        $dirPath = self::getStorageDirFor($organizationId, $accessId);
+        if (!file_exists($dirPath)) {
+            mkdir($dirPath, 0775, true);
+        }
+        return $dirPath;
+    }
+}

+ 8 - 0
src/Service/Storage/UploadStorage.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace App\Service\Storage;
+
+class UploadStorage
+{
+
+}

+ 45 - 0
src/Service/Utils/Path.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace App\Service\Utils;
+
+/**
+ * Various methods to manipulate file paths
+ */
+class Path
+{
+    /**
+     * Returns the application directory
+     *
+     * @return string
+     */
+    public static function getAppDir(): string
+    {
+        return dirname(__file__, 4);
+    }
+
+    /**
+     * Properly join the $path and $tail as a new valid file's path
+     * @see https://stackoverflow.com/a/15575293/4279120
+     *
+     * Ex:
+     *   Path.join('/var/www/', '/html/') > '/var/www/html'
+     *
+     * Input                   Result
+     * ['','']              >  ''
+     * ['','/']             >  '/'
+     * ['/','a']            >  '/a'
+     * ['/','/a']           >  '/a'
+     * ['abc','def']        >  'abc/def'
+     * ['abc','/def']       >  'abc/def'
+     * ['/abc','def']       >  '/abc/def'
+     * ['','foo.jpg']       >  'foo.jpg'
+     * ['dir','0','a.jpg']  >  'dir/0/a.jpg'
+     *
+     * @return string
+     */
+    public static function join(): string
+    {
+        $paths = array_filter(func_get_args(), function ($s) { return $s !== ''; });
+        return preg_replace('#/+#','/',join('/', $paths));
+    }
+}

+ 18 - 0
symfony.lock

@@ -128,6 +128,21 @@
     "jbouzekri/phumbor-bundle": {
         "version": "2.1.0"
     },
+    "knplabs/knp-snappy": {
+        "version": "v1.4.1"
+    },
+    "knplabs/knp-snappy-bundle": {
+        "version": "1.9",
+        "recipe": {
+            "repo": "github.com/symfony/recipes-contrib",
+            "branch": "master",
+            "version": "1.5",
+            "ref": "c81bdcf4a9d4e7b1959071457f9608631865d381"
+        },
+        "files": [
+            "config/packages/knp_snappy.yaml"
+        ]
+    },
     "laminas/laminas-code": {
         "version": "4.4.2"
     },
@@ -382,6 +397,9 @@
     "symfony/polyfill-php81": {
         "version": "v1.23.0"
     },
+    "symfony/process": {
+        "version": "v5.3.14"
+    },
     "symfony/profiler-pack": {
         "version": "v1.0.5"
     },

+ 3 - 3
templates/export/licence_cmf/licence_cmf_organization.html.twig → templates/export/licence_cmf.html.twig

@@ -176,13 +176,13 @@
                             <tbody>
                             <tr>
                                 <td width="340" class="relative">
-                                    <img src="{{ asset('public/static/cmf_licence.png') }}"
+                                    <img src="{{ asset('static/cmf_licence.png') }}"
                                             width="170" height="86"/>
                                     <span id="year_head">{{ model.year }}</span>
                                 </td>
                                 <td width="340">
                                     <div align="right">
-                                        <img src="{{ asset('public/static/cmf-reseau.png') }}"
+                                        <img src="{{ asset('static/cmf-reseau.png') }}"
                                                 width="200" height="86"/>
                                     </div>
                                 </td>
@@ -243,7 +243,7 @@
                                              width="85"
                                              height="82"/>
                                     {% else %}
-                                        <img src="{{ asset(model.logoUri) }}"
+                                        <img src="{{ model.logoUri }}"
                                              width="85"
                                              height="82"/>
                                     {% endif %}