diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php index 461e6766bbb1..d17287d7e93a 100644 --- a/app/Services/Template/TemplateService.php +++ b/app/Services/Template/TemplateService.php @@ -11,31 +11,32 @@ namespace App\Services\Template; +use App\Models\Quote; +use App\Utils\Number; use App\Models\Client; -use App\Models\Company; use App\Models\Credit; use App\Models\Design; +use App\Models\Vendor; +use App\Models\Company; use App\Models\Invoice; use App\Models\Payment; use App\Models\Project; -use App\Models\PurchaseOrder; -use App\Models\Quote; -use App\Models\RecurringInvoice; -use App\Models\Vendor; -use App\Transformers\ProjectTransformer; -use App\Transformers\PurchaseOrderTransformer; -use App\Transformers\QuoteTransformer; -use App\Transformers\TaskTransformer; -use App\Utils\HostedPDF\NinjaPdf; use App\Utils\HtmlEngine; -use App\Utils\Number; +use League\Fractal\Manager; +use App\Models\PurchaseOrder; +use App\Utils\VendorHtmlEngine; +use App\Models\RecurringInvoice; use App\Utils\PaymentHtmlEngine; use App\Utils\Traits\MakesDates; +use App\Utils\HostedPDF\NinjaPdf; use App\Utils\Traits\Pdf\PdfMaker; -use App\Utils\VendorHtmlEngine; -use League\Fractal\Manager; -use League\Fractal\Serializer\ArraySerializer; use Twig\Extra\Intl\IntlExtension; +use App\Transformers\TaskTransformer; +use App\Transformers\QuoteTransformer; +use App\Transformers\ProjectTransformer; +use League\CommonMark\CommonMarkConverter; +use App\Transformers\PurchaseOrderTransformer; +use League\Fractal\Serializer\ArraySerializer; class TemplateService { @@ -61,6 +62,8 @@ class TemplateService private Payment $payment; + private CommonMarkConverter $commonmark; + public function __construct(public ?Design $template = null) { $this->template = $template; @@ -74,6 +77,12 @@ class TemplateService */ private function init(): self { + + + $this->commonmark = new CommonMarkConverter([ + 'allow_unsafe_links' => false, + ]); + $this->document = new \DOMDocument(); $this->document->validateOnParse = true; @@ -111,6 +120,7 @@ class TemplateService { $this->compose() ->processData($data) + ->parseGlobalStacks() ->parseNinjaBlocks() ->processVariables($data) ->parseVariables(); @@ -135,6 +145,7 @@ class TemplateService $this->parseNinjaBlocks() + ->parseGlobalStacks() ->parseVariables(); return $this; @@ -563,8 +574,6 @@ class TemplateService 'refund_activity' => $this->getPaymentRefundActivity($payment), ]; - nlog($data); - return $data; } @@ -806,7 +815,7 @@ class TemplateService * * @return self */ - private function parseGlobalStacks(): self + public function parseGlobalStacks(): self { $stacks = [ 'entity-details', @@ -857,7 +866,7 @@ class TemplateService }) ->map(function ($variable) { return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'company_details-' . substr($variable, 1)]]; - }); + })->toArray(); return $this; @@ -901,24 +910,130 @@ class TemplateService { - $elements = []; + // $elements = []; - if (!$this->vendor) { - return $elements; + // if (!$this->vendor) { + // return $elements; + // } + + // $variables = $this->context['pdf_variables']['vendor_details']; + + // foreach ($variables as $variable) { + // $elements[] = ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'vendor_details-' . substr($variable, 1)]]; + // } + + // return $elements; + + + return $this; + } + + //////////////////////////////////////// + // Dom Traversal + /////////////////////////////////////// + + public function updateElementProperties(string $element_id, array $elements): self + { + $node = $this->document->getElementById($element_id); + + $this->createElementContent($node, $elements); + + return $this; + } + + public function updateElementProperty($element, string $attribute, ?string $value) + { + + if ($attribute == 'hidden' && ($value == false || $value == 'false')) { + return $element; } - $variables = $this->context['pdf_variables']['vendor_details']; + $element->setAttribute($attribute, $value); - foreach ($variables as $variable) { - $elements[] = ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'vendor_details-' . substr($variable, 1)]]; + if ($element->getAttribute($attribute) === $value) { + return $element; } - return $elements; + return $element; + } + + public function createElementContent($element, $children) :self + { + foreach ($children as $child) { + $contains_html = false; + + if ($child['element'] !== 'script') { + if ($this->company->markdown_enabled && array_key_exists('content', $child)) { + $child['content'] = str_replace('
', "\r", $child['content']); + $child['content'] = $this->commonmark->convert($child['content'] ?? ''); + } + } + + if (isset($child['content'])) { + if (isset($child['is_empty']) && $child['is_empty'] === true) { + continue; + } + + $contains_html = preg_match('#(?<=<)\w+(?=[^<]*?>)#', $child['content'], $m) != 0; + } + + if ($contains_html) { + // If the element contains the HTML, we gonna display it as is. Backend is going to + // encode it for us, preventing any errors on the processing stage. + // Later, we decode this using Javascript so it looks like it's normal HTML being injected. + // To get all elements that need frontend decoding, we use 'data-state' property. + + $_child = $this->document->createElement($child['element'], ''); + $_child->setAttribute('data-state', 'encoded-html'); + $_child->nodeValue = htmlspecialchars($child['content']); + } else { + // .. in case string doesn't contain any HTML, we'll just return + // raw $content. + + $_child = $this->document->createElement($child['element'], isset($child['content']) ? htmlspecialchars($child['content']) : ''); + } + + $element->appendChild($_child); + + if (isset($child['properties'])) { + foreach ($child['properties'] as $property => $value) { + $this->updateElementProperty($_child, $property, $value); + } + } + + if (isset($child['elements'])) { + $this->createElementContent($_child, $child['elements']); + } + } return $this; } + + + + + + + + + + + + + + + + + + + + + + + + } diff --git a/tests/Feature/Template/TemplateTest.php b/tests/Feature/Template/TemplateTest.php index 39109ce8855b..2268891fadb9 100644 --- a/tests/Feature/Template/TemplateTest.php +++ b/tests/Feature/Template/TemplateTest.php @@ -165,6 +165,8 @@ class TemplateTest extends TestCase '; + private string $stack = '
'; + protected function setUp() :void { parent::setUp(); @@ -177,6 +179,23 @@ class TemplateTest extends TestCase } + public function testStackResolution() + { + + $partials['design']['includes'] = ''; + $partials['design']['header'] = ''; + $partials['design']['body'] = $this->stack; + $partials['design']['footer'] = ''; + + $ts = new TemplateService(); + $x = $ts->setTemplate($partials) + ->setCompany($this->company) + ->parseGlobalStacks() + ->getHtml(); + + nlog($x); + } + public function testDataMaps() { $start = microtime(true); @@ -319,19 +338,8 @@ class TemplateTest extends TestCase ]; }); - $queries = \DB::getQueryLog(); - $count = count($queries); - - nlog("query count = {$count}"); - $x = $invoices->toArray(); - // nlog(json_encode($x)); - // nlog(json_encode(htmlspecialchars(json_encode($x), ENT_QUOTES, 'UTF-8'))); - // nlog($invoices->toJson()); - $this->assertIsArray($invoices->toArray()); - nlog("end invoices = " . microtime(true) - $start); - } private function transformPayment(Payment $payment): array