CakePhp 3.x, TCPDF, htmlspecialchars - php

I´ve installed the plugin "CakePDF" following the documentation: https://github.com/FriendsOfCake/CakePdf
Now I want to build the first PDF and I´ve got the following error:
This is my configuration in the bootstrap.php:
Configure::write('CakePdf', [
'engine' => 'CakePdf.Tcpdf',
'margin' => [
'bottom' => 15,
'left' => 50,
'right' => 30,
'top' => 45
],
'download' => true,
'encoding' => 'UTF-8'
]);
The only code I´ve written is the following one in the template:
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
This is the code from line 68 in functions.php:
function h($text, $double = true, $charset = null)
{
if (is_string($text)) {
//optimize for strings
} elseif (is_array($text)) {
$texts = [];
foreach ($text as $k => $t) {
$texts[$k] = h($t, $double, $charset);
}
return $texts;
} elseif (is_object($text)) {
if (method_exists($text, '__toString')) {
$text = (string)$text;
} else {
$text = '(object)' . get_class($text);
}
} elseif (is_bool($text)) {
return $text;
}
static $defaultCharset = false;
if ($defaultCharset === false) {
$defaultCharset = mb_internal_encoding();
if ($defaultCharset === null) {
$defaultCharset = 'UTF-8';
}
}
if (is_string($double)) {
$charset = $double;
}
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, ($charset) ? $charset : $defaultCharset, $double);
}
I´m absolutely confused and can´t find any solution. Has anyone an idea?

After trying to find and debug the same error for over an hour, I just reset the value to UTF-8 after the usage of TCPDF - and everything works like before:
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
///...create, save, display your pdf
// Reset the encoding forced from tcpdf
mb_internal_encoding('UTF-8');
I also tried resetting it directly after the call of new TCPDF and everything was fine, too. I don't know what could go wrong with this reset :) My PDFs still look the same after this but emails get send again.

As you've figured, the problem is/was that the TCPDF class messes with mb_internal_encoding(), which is used in CakePHPs h() function to determine the apps default encoding, in case no explicit one is passed as an argument.
I'm no TCPDF expert, haven't used it in ages, but from a quick look at the current source, I'm having a hard time to understand why it fiddles with the internal encoding at all, since the only mb_* function the library uses, is mb_convert_encoding(), where both encoding arguments are passed, so the internal encoding isn't being used at all. That being said, I'd report this as a bug/issue to the TCPDF developer(s).
Anyways, you are only experiencing this problem because you are using CakePDF the wrong way. There is no need to manually create PDF engine instances, that's what CakePDF automatically does for you, which is pretty much the whole point of the plugin, it abstracts the PDF creation so that you just need to build the proper HTML in your view templates. That way you'll also avoid the encoding problems with TCPDF that you are currently experiencing, as the view template is being rendered before the PDF engine instance is being created.
tl;dr
Long story short, build only the HTML in your view template, and if you actually need to have access to a PDF engine instance because there's something you need to do that can only be achieved that way, then CakePDF is not the plugin you are looking for.

I think, I´ve found the culprit. The constructor method of TCPDF sets the mb_internal_encoding to ASCII. (tcpdf.php line 1838)
I found a hint in the comments: Please note that this method sets the mb_internal_encoding to ASCII, so if you are using the mbstring module functions with TCPDF you need to correctly set/unset the mb_internal_encoding when needed.
But now I need an advice how to use tcpdf and mb_internal_encoding correctly (without issues with cake or tcpdf).
Sorry for asking, I´m an absolutely beginner. ;)

put back de initial encoding, by extending the TCPDF class, and use the new class:
class TCPDF_repaired extends TCPDF{
public function __construct($orientation='P', $unit='mm', $format='A4', $unicode=true, $encoding='UTF-8', $diskcache=false, $pdfa=false) {
parent::__construct($orientation, $unit, $format, $unicode, $encoding, $diskcache, $pdfa);
mb_internal_encoding($this->internal_encoding);
}
};

Related

How to ignore specific private static function with PHPUnit?

I'm new to PHPUnit and wondering is it possible to write a test for which ignore specific method.
The code is like examine whether $data is Valid or not, and if it find irregular data, send message to slack with it.
My question is, is it possible to run a test without sending alert message, like ignore sendAlert function?
If possible, I want to know how to write it, If not, I want know why and how to make this code testable.
Thanks!!
example code )
public static function isValid($data) {
// some code here
if (Valid) {
return true;
} else {
// some code here to find irregular
if (irregular) {
self::sendAlert($data);
}
return false;
}
}
private static function sendAlert($data) {
// send alert to slack
Example_Model_Slack::post($slackMsg, $channel);
}
<?
class Example_Model_Slack
{
public static function post($text, $channel = '') {
// make $params from $text and $channel
// POST
$stream = [
'http' => [
'method' => 'POST',
'protocol_version' => 1.1,
'content' => http_build_query($params),
],
];
return file_get_contents(self::POST_URL, false, stream_context_create($stream));
}
}
Edit after the question edit
If your code is in a namespace (which should be, it's good practice), it's extremely easy:
Create a new function in a separate file that is only included by your UnitTest file. This file should have the same namespace as your code. In this example, Example_Model_Slack is in the namespace Foobar\Models.
<?php
namespace Foobar\Models;
function file_get_contents(string $filename, bool $use_include_path = false, resource $context = ?)
{
return 'Whatever you want';
}
When you call a function, the code looks for it:
In the specifically used functions.
In the same namespace.
In the built-in functions.
Therefore, your code will use the built-in file_get_contents (namely \file_get_contents), but your test will use the one in the same namespace (namely \Foobar\Models\file_get_contents).
Original answer
The easiest would be to actually call sendAlert, but to mock the call to its content. As you didn't provide the code of that method, I can't be more precise, juste browse through the doc and figure it out by yourself or, alternatively, show us the code.
For a theorectical and general answer: your sendAlert method probably uses one that is provided by an external vendor, let's say \SlackApi\Slack::send($message). In that case, you could mock the provided \SlackApi\Slack class to replace the send method with one that doesn't actually send anything but still returns the expected data.

"Preview" mpdf directly to browser

I'm using mpdf and writing a bunch of HTML to the pdf object via mpdf::writeHTML(). Is there any way, rather than outputting an PDF, to simply dump it right back to the browser instead? So, rather than creating the PDF just write it out as a web page?
I want to give the user the option of a PDF or a web page, and rather than branching off for echo or writeHTML for each line, I'd like to build the document and then either output web or PDF.
EDIT TO ADD:
Something like this:
$mpdf = new mpdf();
$mpdf->writeHTML( "<p>Hello World</p>" );
$mpdf->addPage( 'L' );
$mpdf->writeHTML( "<p>Lorem ipsum egg foo yung.</p>" );
if( $_GET['format'] == 'pdf' ) {
$mpdf->output(); //spit out a PDF
} elseif ( $_GET['format'] == 'web' ) {
echo $mpdf->contents_as_html(); // write a web page
}
I'm currently writing each line to a giant string, and then either passing the string to mpdf::writeHTML() or echo; but this doesn't allow me to use various mpdf functions such as addPage(), bookmark(), and so forth.
This is what I did, per a suggestion by #CBroe. When writeHTML() is called, it writes to an internal variable $this->strHTML, and then does its normal process. If the object is cast to string, it returns $this->strHTML
class myPDF extends Mpdf {
private $strHtml = '';
public function writeHTML( $html, $mode = 0, $init = true, $close = true ) {
$this->strHtml .= $html . "\n";
return parent::writeHTML( $html, $mode, $init, $close );
}
public function __toString() {
return $this->strHtml;
}
}
You can choose the output with mPDF by changing the second parameter.
I = send the file inline to the browser.
F enter code here= save to a local file with the name given by $filename.
S = return the document as a string. $filename is ignored.
D = send to the browser and force a file download with the name given by $filename.
Output mPDF:
$mpdf->Output($filename, "I"); // Change "I" to your preferred output
If you choose output the file in your browser, just make sure that you target the output on an empty page. Otherwise it's possible that the header and footer will interfere.
Artikel mPDF output:
https://mpdf.github.io/reference/mpdf-functions/output.html

PHPUnit test a function that write in file and check the content into created file

I am contributing to sonata/exporter, a library used for export data in many formats (CSV, JSON, XML, XLS, ...).
I work on a Writer that converts boolean values into strings (e.g. yes/no) by encapsulating another Writer (like CsvWriter or XlsWriter).
It's my first experience with phpunit.
All unit tests made on the existing Writers use this logic :
- Create a file.
- Write data in file using the corresponding format.
- Make an assertEquals on file_get_contents(filename).
So, I've written this test :
public function setUp()
{
$this->filename = 'formatedbool.xls';
$this->sampleWriter = new XlsWriter($this->filename, false);
$this->trueLabel = 'oui';
$this->falseLabel = 'non';
if (is_file($this->filename)) {
unlink($this->filename);
}
}
public function testValidDataFormat()
{
$writer = new FormatedBoolWriter($this->sampleWriter, $this->trueLabel, $this->falseLabel);
$writer->open();
$writer->write(array('john', 'doe', false, true));
$writer->close();
$expected = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><meta name=ProgId content=Excel.Sheet><meta name=Generator content="https://github.com/sonata-project/exporter"></head><body><table><tr><td>john</td><td>doe</td><td>non</td><td>oui</td></tr></table></body></html>';
$this->assertEquals($expected, trim(file_get_contents($this->filename)));
}
When submitting my PR, the owner says me :
just use a mock with expected method call and check calling argument, this will avoid creating the file. see https://phpunit.de/manual/current/en/test-doubles.html#test-doubles.mock-objects.examples.with-consecutive.php
I have begin to rewrite tests using a Mock but I have an error on file_get_contents because file is not created.
The "write" method just write in a file and return nothing.
I think he want I test the data after converting bools, but before writing in file.
How can I check the result of the file content without really create it ?
Or simply access to my $data during the method call ?
EDIT Thanks to #Cerad, the code I've submitted :
public function testValidDataFormat()
{
$data = array('john', 'doe', false, true);
$expected = array('john', 'doe', 'no', 'yes');
$mock = $this->getMockBuilder('Exporter\Writer\XlsWriter')
->setConstructorArgs(array('formattedbool.xls', false))
->getMock();
$mock->expects($this->any())
->method('write')
->with($this->equalTo($expected));
$writer = new FormattedBoolWriter($mock, $this->trueLabel, $this->falseLabel);
$writer->open();
$writer->write($data);
$writer->close();
}
I'm waiting for answer of the project owner.
EDIT PR merged at https://github.com/sonata-project/exporter/pull/56
This question has been answered by #Cerad by commenting on the question.
The PR has been accepted and merged, see https://github.com/sonata-project/exporter/pull/56

Is there a way to hide header and footer in a pdf file created with FPDF in PHP?

I have created a pdf file with FPDF in PHP. When i insert the header and footers in it, they automatically gets displayed on all the pages of the pdf file. But i want to stop these header and footer from getting displayed on the first page and display them starting from the second page of the pdf file. I have searched the net but unable to find a solution.
In other words i want to dynamically create a cover page for the pdf report i have created with FPDF.
Can anybody give me some tips on how to perform this task of hidinh header and footer from the first page in pdf file!
Any help will be appreciated!
That's an easy task. Try the following:
class PDF extends FPDF {
...
function Header() {
if ( $this->PageNo() !== 1 ) {
// Add your stuff here
}
}
function Footer() {
if ( $this->PageNo() !== 1 ) {
// Add your stuff here
}
}
}
The problem is the Footers are created in Close() method at line 288 which is called from Output() at line 987 what means you're effectively turning the Footer off and then on just to display it anyways. What I would do if I needed the flexibility is something like:
class PDF extends FPDF {
function Header() {
if (!isset($this->header[$this->page]) || !$this->header[$this->page]) {
// ...
}
}
function Footer() {
if (!isset($this->footer[$this->page]) || !$this->footer[$this->page]) {
// ...
}
}
}
and then use it like:
$pdf->header[1] = false;
$pdf->footer[1] = false;
$pdf->AddPage();
$pdf->header[2] = true;
$pdf->footer[2] = true;
$pdf->AddPage();
It might be not the most elegant solution, but it works and it effectively allows you to change the visibility of the footers dynamically (p.s.: not specifying the state would also leave you with headers on effectively reducing the amount of code you need)
I'd like to add an answer for people coming here that don't want to skip the first, but the last (or any) page. Especially handy if you have dynamically changing text and cant foresee page numbers.
This can be done by setting a boolean while adding the page to the PDF.
Define your Header / Footer as
class PDF extends FPDF {
function Header() {
if (!$this->skipHeader) {
// ...
}
}
function Footer() {
if (!$this->skipFooter) {
// ...
}
}
}
Then, when initializing the pdf make sure to set these bools to false, so you will get headers/footers in general.
$pdf = new PDF();
$pdf->skipHeader = false;
$pdf->skipFooter = false;
Once you actually want to skip a Header or Footer, set the respective bool to true
$pdf->AddPage();
$pdf->skipHeader = true;
$pdf->AddPageContents();
Remember to set them back to false if you want headers/footers on the next page!
As an extension of what Paul's said, the footer is rendered after any content, so set skipFooter to true after rendering content.
$pdf->AddPage();
$pdf->skipHeader = true;
$pdf->AddPageContents();
$pdf->skipFooter = true;

Request for IRC URI Scheme for HTML Purifier 4.2.0

Can someone help me to establish using IRC URI Scheme for HTML Purifier 4.2.0? I can't seem to figure out how to configure or which files to modify so that purified html allows for irc:// links.
Is it possible I can simply modify configuration within the following code block?
require_once "htmlpurifier-4.2.0/library/HTMLPurifier.standalone.php";
$purifier_config = HTMLPurifier_Config::createDefault();
$purifier_config->set("HTML.Doctype", "XHTML 1.0 Strict");
$purifier = new HTMLPurifier($purifier_config);
Update:
I edited library/standalone/HTMLPurifier/ConfigSchema/schema.ser changing both instances of "4:nntp" to "3:irc" and found error:
Warning: Directory htmlpurifier-4.2.0/library/standalone/HTMLPurifier/DefinitionCache/Serializer/URI not writable, please chmod to 777
I believe this will help to establish support for IRC URI Scheme after making this change. I'll report back in a bit.
Hmm, after making it writable, no error appeared, but no results =\
HTML Purifier doesn't seem to have a native support for the IRC scheme. But: Have you tried something like this? Put this in /library/HTMLPurifier/URIScheme, or otherwise make sure that autoloading finds it:
class HTMLPurifier_URIScheme_irc extends HTMLPurifier_URIScheme {
public $default_port = 6667;
public $browsable = false;
public function validate(&$uri, $config, $context) {
if (parent::validate($uri, $config, $context) === false) {
return false;
}
if (!is_null($uri->path)) {
// get channel name
$uri->path = array_shift(explode('/', $uri->path));
}
$uri->userinfo = null;
$uri->query = null;
$uri->fragment = null;
return true;
}
}
...and change your configuration with...
$purifier->config->set(
'URI.AllowedSchemes',
array('irc' => true, /* ... other schemes here ... */)
);
That may not work out of the box, but I'm thinking that should be the right direction...

Categories