Sunday, December 31, 2017

How to create PDF downloader in CakePHP3

1.  How to create uploader in CakePHP3
2.  How to create downloader in CakePHP3
3.  How to create PDF downloader in CakePHP3

At first, we need to install libreoffice for the PDF conversion and language packs if needed. (Without language packs, the PDF can not display the language properly.)
$ sudo yum install libreoffice libreoffice-langpack-fr

And edit the sudo command by "visudo" and add "apache ALL=(ALL) NOPASSWD: /usr/bin/libreoffice" in the file:
$ sudo visudo
(Add "apache ALL=(ALL) NOPASSWD: /usr/bin/libreoffice" in the file)

Restart apache:
$ sudo service httpd restart

Add the following function in index.ctp:
    function downloadPdf(){
        var files = getSelectedDocuments(true);
        files_not_excel = files.files_notexcel;
        files = files.checkeditems;
        console.log(files);
        if(files.length < 1 || files === undefined){
            alert('Please select at least one Excel document.');
        } else {
            if(files_not_excel.length > 0 && files_not_excel !== undefined){
                var r = confirm("The following files will not be converted to PDF because they are not excel files:\n"
                + files_not_excel.join(', '));
            } else {
                var r = true;
            }
            if(r === true){
                var csrfToken = $('[name=_csrfToken]').val();
                $.ajax({
                    type: "POST",
                    url: '<?= $this->Url->build(array("controller" => "Customers", "action" => "pdfDownload")); ?>',
                    data: {'files' : files},
                    beforeSend: function(xhr){
                        xhr.setRequestHeader('X-CSRF-Token', csrfToken);
                    },
                    success: function(data){
                        var url = '/cake/temp/zip/' + data;
                        console.log(url);
                        download(url);
                    }
                });
            }
        }
    }

And a button to download the PDF files:
<p><a href="#" onclick="downloadPdf()">Download As PDF</a></p>

The index.ctp as a whole:
<?php
/**
 * @var \App\View\AppView $this
 * @var \App\Model\Entity\Customer[]|\Cake\Collection\CollectionInterface $customers
 */
?>
<nav class="large-3 medium-4 columns" id="actions-sidebar">
    <ul class="side-nav">
        <li class="heading"><?= __('Actions') ?></li>
        <li><?= $this->Html->link(__('New Customer'), ['action' => 'add']) ?></li>
    </ul>
</nav>
<div class="customers index large-9 medium-8 columns content">
    <h3><?= __('Customers') ?></h3>
    <p><a href="#" onclick="downloadDocuments()">Download As Zip</a></p>
    <p><a href="#" onclick="downloadPdf()">Download As PDF</a></p>
    <table cellpadding="0" cellspacing="0">
        <thead>
            <tr>
                <th scope="col" class="text-center" style="vertical-align: middle;"><?= $this->Form->checkbox('check_all', ['id'=>'check_all']);?></th>
                <th scope="col"><?= $this->Paginator->sort('id') ?></th>
                <th scope="col"><?= $this->Paginator->sort('name') ?></th>
                <th scope="col"><?= $this->Paginator->sort('file_location', 'File') ?></th>
                <th scope="col"><?= $this->Paginator->sort('created') ?></th>
                <th scope="col"><?= $this->Paginator->sort('modified') ?></th>
                <th scope="col" class="actions"><?= __('Actions') ?></th>
            </tr>
        </thead>
        <tbody>
            <?php foreach ($customers as $customer): ?>
            <tr>
                <?php $temp = explode('.', $customer->file_location); $extension = strtolower(end($temp)); ?>
                <?php $file = ['file_location' => $customer->file_location, 'file_name'=>$customer->name, 'extension'=>$extension] ?>
                <td class="text-center"><?= $this->Form->checkbox('checkbox',['name'=>'check_box', 'value'=>h(json_encode($file))]) ?></td>
                <td><?= $this->Number->format($customer->id) ?></td>
                <td><?= h($customer->name) ?></td>
                <td><img src="/cake/uploads/customers/<?= $customer->file_location ?>" width="100" /></td>
                <td><?= h($customer->created) ?></td>
                <td><?= h($customer->modified) ?></td>
                <td class="actions">
                    <?= $this->Html->link(__('View'), ['action' => 'view', $customer->id]) ?>
                    <?= $this->Html->link(__('Edit'), ['action' => 'edit', $customer->id]) ?>
                    <?= $this->Form->postLink(__('Delete'), ['action' => 'delete', $customer->id], ['confirm' => __('Are you sure you want to delete # {0}?', $customer->id)]) ?>
                </td>
            </tr>
            <?php endforeach; ?>
        </tbody>
    </table>
    <div class="paginator">
        <ul class="pagination">
            <?= $this->Paginator->first('<< ' . __('first')) ?>
            <?= $this->Paginator->prev('< ' . __('previous')) ?>
            <?= $this->Paginator->numbers() ?>
            <?= $this->Paginator->next(__('next') . ' >') ?>
            <?= $this->Paginator->last(__('last') . ' >>') ?>
        </ul>
        <p><?= $this->Paginator->counter(['format' => __('Page {{page}} of {{pages}}, showing {{current}} record(s) out of {{count}} total')]) ?></p>
    </div>
</div>
<script
  src="https://code.jquery.com/jquery-3.2.1.min.js"
  integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
  crossorigin="anonymous"></script>
<script>
    $(document).ready(function(){
        $('#check_all').on('click', function() {
            $('input:checkbox[name=check_box]').prop('checked', this.checked);
        });
    });
    function getSelectedDocuments(pdfonly){
        if (pdfonly === undefined) {
            pdfonly = false;
        }
        var checkeditems = [];
        var files_notexcel = [];
        $("input[type=checkbox][name=check_box]").each(function(){
            if (this.checked) {
                var value = JSON.parse($(this).val());
                if(pdfonly === true){
                    var extension = value.extension.toLowerCase();
                    if($.inArray(extension, ['xlsx','xlsm','xls']) !== -1){
                        checkeditems.push(value.file_location);
                    } else {
                        var name = value.file_location.split("/");
                        files_notexcel.push(name[name.length - 1]);
                    }
                } else {
                    checkeditems.push(value.file_location);
                }
            }
        });
        var files = {"checkeditems" : checkeditems, "files_notexcel" : files_notexcel};
        return files;
    }
    function download(url){
        var link=document.createElement('a');
        document.body.appendChild(link);
        link.href=url;
        link.download = 'download';
        link.click();
        link.parentNode.removeChild(link);
    }
    function downloadDocuments(){
        var checkeditems = getSelectedDocuments();
        checkeditems = checkeditems.checkeditems;
        if(checkeditems.length < 1 || checkeditems === undefined){
            alert('Please select at least one document.');
        } else {
            var csrfToken = $('[name=_csrfToken]').val();
            $.ajax({
                type: "POST",
                url: '<?= $this->Url->build(array("controller" => "Customers", "action" => "zipDownload")); ?>',
                data: {'files' : checkeditems},
                beforeSend: function(xhr){
                    xhr.setRequestHeader('X-CSRF-Token', csrfToken);
                },
                success: function(data){
                    var url = '/cake/temp/zip/'+data;
                    console.log(url);
                    download(url);
                }
            });
        }
    }
    function downloadPdf(){
        var files = getSelectedDocuments(true);
        files_not_excel = files.files_notexcel;
        files = files.checkeditems;
        console.log(files);
        if(files.length < 1 || files === undefined){
            alert('Please select at least one Excel document.');
        } else {
            if(files_not_excel.length > 0 && files_not_excel !== undefined){
                var r = confirm("The following files will not be converted to PDF because they are not excel files:\n"
                + files_not_excel.join(', '));
            } else {
                var r = true;
            }
            if(r === true){
                var csrfToken = $('[name=_csrfToken]').val();
                $.ajax({
                    type: "POST",
                    url: '<?= $this->Url->build(array("controller" => "Customers", "action" => "pdfDownload")); ?>',
                    data: {'files' : files},
                    beforeSend: function(xhr){
                        xhr.setRequestHeader('X-CSRF-Token', csrfToken);
                    },
                    success: function(data){
                        var url = '/cake/temp/zip/' + data;
                        console.log(url);
                        download(url);
                    }
                });
            }
        }
    }
</script>

Change customers controller and add the following functions:
    function createZip($uploadToDir = null, $copyFromDir = null, $files = null, $containsYearmonth = false){
        if(!file_exists($uploadToDir)){
            if(!mkdir($uploadToDir, 0755, true)){
                throw new BadRequestException('Can not mkdir.');
            }
            chmod($uploadToDir, 0755);
        }
        try {
            $zip = new ZipArchive();
            $zip_file_name = sha1(time() . rand()).".zip";
            $zip_name = $uploadToDir.$zip_file_name; // Zip name
            $zip->open($zip_name,  ZipArchive::CREATE);
            foreach ($files as $file) {
                $path = $copyFromDir.$file;
                if(file_exists($path)){
                    $zip->addFromString(basename($path), file_get_contents($path));
                }
                else{
                    echo "file does not exist";
                }
            }
            $zip->close();
            return $zip_file_name;
        } catch (BadRequestException $e) {
            throw $e;
        }
    }

    public function pdfDownload(){
        if ($this->request->is('ajax')) {
            $files = $this->request->data['files'];
            $layout = 'ajax';
            $this->loadModel('Documents');
            $this->autoRender = false;
            try {
                $filesPdf = [];
                $uploadToDirPdf = WWW_ROOT.'temp/zip/';
                $copyFromDirPdf = WWW_ROOT.'uploads/customers/';
                $uploadToDirZip = WWW_ROOT.'temp/zip/';
                $copyFromDirZip = WWW_ROOT;
                foreach($files as $file){
                    $extension = $this->getExtension($file);
                    if(in_array($extension,array_map('strtolower',['xlsx','xlsm','xls']))){
                        $pathPdf = $this->excelToPdf($uploadToDirPdf, $copyFromDirPdf, $file);
                        array_push($filesPdf, $pathPdf);
                    }
                }
                $pathZip = $this->createZip($uploadToDirZip, $copyFromDirZip, $filesPdf);
            }catch(BadRequestException $b){
                //$this->Flash->error(__($b->getMessage()));
            }
            echo $pathZip;
        }
    }

    private function excelToPdf($uploadToDir = null, $copyFromDir = null, $file = null){
        $filePath = explode("/", $file);
        $today = new Date();
        $yearmonth = $today->i18nFormat('yyyyMM');
        $dir = $uploadToDir.$filePath[0];
        if(!file_exists($dir)){
            if(!mkdir($dir, 0755, true)){
                throw new BadRequestException('Couldn\'t make directory.');
            }
            chmod($dir, 0755);
        }
        $command = 'sudo /usr/bin/libreoffice --headless --invisible --norestore --nologo --nofirststartwizard --convert-to pdf --outdir "'
        .$uploadToDir.$filePath[0].'" "'
        .$copyFromDir.$file.'"';
        $result = shell_exec($command);
        if(is_null($result)){
            throw new BadRequestException('pdf couldn\'t be created.');
        }
        $filenameArray = explode(".", $file);
        $filename = "";
        for($i = 0; $i < count($filenameArray)-1; $i++){
            $filename .= $filenameArray[$i];
        }
        return 'temp/zip/'.$filename.'.pdf';
    }

Now you can download excel files as PDF from index.ctp: