Completed
Push — develop ( b5b2e8...c95aa7 )
by John
03:06
created

TCPDFFacade::serveCachedPDF()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 24
rs 8.9713
cc 1
eloc 16
nc 1
nop 1

1 Method

Rating   Name   Duplication   Size   Complexity  
A TCPDFFacade::getPDFData() 0 9 1
1
<?php
2
3
namespace Alpha\Util\Extension;
4
5
use Alpha\Util\Config\ConfigProvider;
6
use Alpha\Util\Logging\Logger;
7
use Alpha\Util\Extension\Markdown;
8
use Alpha\View\Widget\Image;
9
use Alpha\Exception\AlphaException;
10
11
/**
12
 * A facade class for the TCPDF library which is used to convert some HTML content provided by the
13
 * Markdown library to a PDF file using FPDF.
14
 *
15
 * @since 1.0
16
 *
17
 * @author John Collins <[email protected]>
18
 * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
19
 * @copyright Copyright (c) 2017, John Collins (founder of Alpha Framework).
20
 * All rights reserved.
21
 *
22
 * <pre>
23
 * Redistribution and use in source and binary forms, with or
24
 * without modification, are permitted provided that the
25
 * following conditions are met:
26
 *
27
 * * Redistributions of source code must retain the above
28
 *   copyright notice, this list of conditions and the
29
 *   following disclaimer.
30
 * * Redistributions in binary form must reproduce the above
31
 *   copyright notice, this list of conditions and the
32
 *   following disclaimer in the documentation and/or other
33
 *   materials provided with the distribution.
34
 * * Neither the name of the Alpha Framework nor the names
35
 *   of its contributors may be used to endorse or promote
36
 *   products derived from this software without specific
37
 *   prior written permission.
38
 *
39
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
40
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
41
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
42
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
43
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
44
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
45
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
46
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
47
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
48
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
49
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
50
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
51
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
52
 * </pre>
53
 */
54
class TCPDFFacade
55
{
56
    /**
57
     * The HTML-format content that we will render as a PDF.
58
     *
59
     * @var string
60
     *
61
     * @since 1.0
62
     */
63
    private $content;
64
65
    /**
66
     * The PDF object that will be generated from the Markdown HTML content.
67
     *
68
     * @var \Alpha\Util\Extension\TCPDF
69
     *
70
     * @since 1.0
71
     */
72
    private $pdf;
73
74
    /**
75
     * The business object that stores the content will be rendered to Markdown.
76
     *
77
     * @var \Alpha\Model\Article
78
     *
79
     * @since 1.0
80
     */
81
    private $article = null;
82
83
    /**
84
     * The auto-generated name of the PDF cache file for the article.
85
     *
86
     * @var string
87
     *
88
     * @since 1.0
89
     */
90
    private $PDFFilename;
91
92
    /**
93
     * The auto-generated name of the HTML cache file for the article generated by Markdown.
94
     *
95
     * @var string
96
     *
97
     * @since 1.0
98
     */
99
    private $HTMLFilename;
100
101
    /**
102
     * Trace logger.
103
     *
104
     * @var \Alpha\Util\Logging\Logger
105
     *
106
     * @since 1.0
107
     */
108
    private static $logger = null;
109
110
    /**
111
     * The constructor.
112
     *
113
     * @param \Alpha\Model\ActiveRecord $article the business object that stores the content will be rendered to Markdown
114
     *
115
     * @since 1.0
116
     */
117
    public function __construct($article)
118
    {
119
        self::$logger = new Logger('TCPDFFacade');
120
        self::$logger->debug('>>__construct()');
121
122
        $config = ConfigProvider::getInstance();
123
124
        $this->article = $article;
0 ignored issues
show
Documentation Bug introduced by
$article is of type object<Alpha\Model\ActiveRecord>, but the property $article was declared to be of type object<Alpha\Model\Article>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
125
126
        $reflect = new \ReflectionClass($this->article);
127
        $classname = $reflect->getShortName();
128
129
        $this->PDFFilename = $config->get('app.file.store.dir').'cache/pdf/'.$classname.'_'.$this->article->getID().'_'.$this->article->getVersion().'.pdf';
130
        $this->HTMLFilename = $config->get('app.file.store.dir').'cache/html/'.$classname.'_'.$this->article->getID().'_'.$this->article->getVersion().'.html';
131
132
        // first check the PDF cache
133
        if ($this->checkPDFCache()) {
134
            return;
135
        }
136
137
        if (method_exists($this->article, 'getAttachmentsURL')) {
138
            $attachURL = $this->article->getAttachmentsURL();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Alpha\Model\ActiveRecord as the method getAttachmentsURL() does only exist in the following sub-classes of Alpha\Model\ActiveRecord: Alpha\Model\Article. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
Unused Code introduced by
$attachURL is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
139
        } else {
140
            $attachURL = '';
0 ignored issues
show
Unused Code introduced by
$attachURL is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
141
        }
142
143
        if ($this->checkHTMLCache()) {
144
            $this->loadHTMLCache();
145
        } else {
146
            $this->content = $this->markdown($this->article->get('content', true));
147
            $this->HTMLCache();
148
        }
149
150
        // Replace all instances of $attachURL in link tags to links to the ViewAttachment controller
151
        $attachments = array();
152
        preg_match_all('/href\=\"\$attachURL\/.*\"/', $this->content, $attachments);
153
154
        foreach ($attachments[0] as $attachmentURL) {
155
            $start = mb_strpos($attachmentURL, '/');
156
            $end = mb_strrpos($attachmentURL, '"');
157
            $fileName = mb_substr($attachmentURL, $start + 1, $end - ($start + 1));
158
159
            if (method_exists($this->article, 'getAttachmentSecureURL')) {
160
                $this->content = str_replace($attachmentURL, 'href='.$this->article->getAttachmentSecureURL($fileName), $this->content);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Alpha\Model\ActiveRecord as the method getAttachmentSecureURL() does only exist in the following sub-classes of Alpha\Model\ActiveRecord: Alpha\Model\Article. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
161
            }
162
        }
163
164
        // Handle image attachments
165
        $attachments = array();
166
        preg_match_all('/\<img\ src\=\"\$attachURL\/.*\".*\>/', $this->content, $attachments);
167
168
        foreach ($attachments[0] as $attachmentURL) {
169
            $start = mb_strpos($attachmentURL, '/');
170
            $end = mb_strrpos($attachmentURL, '" alt');
171
            $fileName = mb_substr($attachmentURL, $start + 1, $end - ($start + 1));
172
173
            if ($config->get('cms.images.widget')) {
174
                // get the details of the source image
175
                $path = $this->article->getAttachmentsLocation().'/'.$fileName;
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Alpha\Model\ActiveRecord as the method getAttachmentsLocation() does only exist in the following sub-classes of Alpha\Model\ActiveRecord: Alpha\Model\Article. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
176
                $image_details = getimagesize($path);
177
                $imgType = $image_details[2];
178
                if ($imgType == 1) {
179
                    $type = 'gif';
180
                } elseif ($imgType == 2) {
181
                    $type = 'jpg';
182
                } else {
183
                    $type = 'png';
184
                }
185
186
                $img = new Image($path, $image_details[0], $image_details[1], $type, 0.95, false, (boolean) $config->get('cms.images.widget.secure'));
187
                $this->content = str_replace($attachmentURL, $img->renderHTMLLink(), $this->content);
188
            } else {
189
                // render a normal image link to the ViewAttachment controller
190
                if (method_exists($this->article, 'getAttachmentSecureURL')) {
191
                    $this->content = str_replace($attachmentURL, '<img src="'.$this->article->getAttachmentSecureURL($fileName).'">', $this->content);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Alpha\Model\ActiveRecord as the method getAttachmentSecureURL() does only exist in the following sub-classes of Alpha\Model\ActiveRecord: Alpha\Model\Article. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
192
                }
193
            }
194
        }
195
196
        $this->pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
197
        $this->pdf->SetCreator(PDF_CREATOR);
198
        $this->pdf->SetAuthor($this->article->get('author'));
199
        $this->pdf->SetTitle($this->article->get('title'));
200
        $this->pdf->SetSubject($this->article->get('description'));
201
202
        //set margins
203
        $this->pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT);
204
        $this->pdf->SetHeaderMargin(PDF_MARGIN_HEADER);
205
        $this->pdf->SetFooterMargin(PDF_MARGIN_FOOTER);
206
207
        //set auto page breaks
208
        $this->pdf->SetAutoPageBreak(true, PDF_MARGIN_BOTTOM);
209
210
        //set image scale factor
211
        $this->pdf->setImageScale(2.5);
212
213
        // add a page
214
        $this->pdf->AddPage();
215
216
        // add the title
217
        $title = '<h1>'.$this->article->get('title').'</h1>';
218
        // add some custom footer info about the article
219
        $footer = '<br><p>Article URL: <a href="'.$this->article->get('URL').'">'.$this->article->get('URL').'</a><br>Title: '.$this->article->get('title').'<br>Author: '.$this->article->get('author').'</p>';
220
221
        // write the title
222
        self::$logger->debug('Writing the title ['.$title.'] to the PDF');
223
        $this->pdf->writeHTML(utf8_encode($title), true, false, true, false, '');
224
        // output the HTML content
225
        self::$logger->debug('Writing the content ['.$this->content.'] to the PDF');
226
        $this->pdf->writeHTML(utf8_encode($this->content), true, false, true, false, '');
227
        // write the article footer
228
        $this->pdf->writeHTML(utf8_encode($footer), true, false, true, false, '');
229
        self::$logger->debug('Writing the footer ['.$footer.'] to the PDF');
230
231
        // save this PDF to the cache
232
        $this->pdf->Output($this->PDFFilename, 'F');
233
234
        self::$logger->debug('<<__construct()');
235
    }
236
237
     /**
238
      * Facade method which will invoke our custom markdown class rather than the standard one.
239
      *
240
      * @param $text The markdown content to parse
241
      *
242
      * @since 1.0
243
      */
244
     private function markdown($text)
245
     {
246
         $config = ConfigProvider::getInstance();
247
248
        /*
249
         * Initialize the parser and return the result of its transform method.
250
         *
251
         */
252
        static $parser;
253
254
         if (!isset($parser)) {
255
             $parser = new Markdown();
256
         }
257
258
        /*
259
         * Replace all instances of $sysURL in the text with the app.url setting from config
260
         *
261
         */
262
        $text = str_replace('$sysURL', $config->get('app.url'), $text);
263
264
        // transform text using parser.
265
        return $parser->transform($text);
266
     }
267
268
    /**
269
     * Fetter for the content.
270
     *
271
     * @return string HTML rendered the content
272
     *
273
     * @since 1.0
274
     */
275
    public function getContent()
276
    {
277
        return $this->content;
278
    }
279
280
    /**
281
     * Saves the HTML generated by Markdown to the cache directory.
282
     *
283
     * @throws \Alpha\Exception\AlphaException
284
     *
285
     * @since 1.0
286
     */
287
    private function HTMLCache()
288
    {
289
        // check to ensure that the article is not transient before caching it
290
        if ($this->article->getID() != '00000000000') {
291
            $fp = fopen($this->HTMLFilename, 'w');
292
            if (!$fp) {
293
                throw new AlphaException('Failed to open the cache file for writing, directory permissions my not be set correctly!');
294
            } else {
295
                flock($fp, 2); // locks the file for writting
296
                fwrite($fp, $this->content);
297
                flock($fp, 3); // unlocks the file
298
                fclose($fp); //closes the file
299
            }
300
        }
301
    }
302
303
    /**
304
     * Used to check the HTML cache for the article cache file.
305
     *
306
     * @return bool true if the file exists, false otherwise
307
     *
308
     * @since 1.0
309
     */
310
    private function checkHTMLCache()
311
    {
312
        return file_exists($this->HTMLFilename);
313
    }
314
315
    /**
316
     * Method to load the content of the cache file to the $content attribute of this object.
317
     *
318
     * @throws \Alpha\Exception\AlphaException
319
     *
320
     * @since 1.0
321
     */
322
    private function loadHTMLCache()
323
    {
324
        $fp = fopen($this->HTMLFilename, 'r');
325
326
        if (!$fp) {
327
            throw new AlphaException('Failed to open the cache file for reading, directory permissions my not be set correctly!', 'loadHTMLCache()');
328
        } else {
329
            $this->content = fread($fp, filesize($this->HTMLFilename));
330
            fclose($fp); //closes the file
331
        }
332
    }
333
334
    /**
335
     * Used to check the PDF cache for the article cache file.
336
     *
337
     * @return bool true if the file exists, false otherwise
338
     *
339
     * @since 1.0
340
     */
341
    private function checkPDFCache()
342
    {
343
        return file_exists($this->PDFFilename);
344
    }
345
346
    /**
347
     * Returns the raw PDF data stream.
348
     *
349
     * @return string
350
     *
351
     * @since 2.0
352
     */
353
    public function getPDFData()
354
    {
355
        // first load the file
356
        $handle = fopen($this->PDFFilename, 'r');
357
        $data = fread($handle, filesize($this->PDFFilename));
358
        fclose($handle);
359
360
        return $data;
361
    }
362
}
363