1<?php
2/**
3 * @package dompdf
4 * @link    http://dompdf.github.com/
5 * @author  Benj Carson <benjcarson@digitaljunkies.ca>
6 * @author  Helmut Tischer <htischer@weihenstephan.org>
7 * @author  Fabien Ménager <fabien.menager@gmail.com>
8 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
9 */
10namespace Dompdf\Css;
11
12use DOMElement;
13use DOMXPath;
14use Dompdf\Dompdf;
15use Dompdf\Helpers;
16use Dompdf\Exception;
17use Dompdf\FontMetrics;
18use Dompdf\Frame\FrameTree;
19
20/**
21 * The master stylesheet class
22 *
23 * The Stylesheet class is responsible for parsing stylesheets and style
24 * tags/attributes.  It also acts as a registry of the individual Style
25 * objects generated by the current set of loaded CSS files and style
26 * elements.
27 *
28 * @see Style
29 * @package dompdf
30 */
31class Stylesheet
32{
33    /**
34     * The location of the default built-in CSS file.
35     */
36    const DEFAULT_STYLESHEET = "/lib/res/html.css";
37
38    /**
39     * User agent stylesheet origin
40     *
41     * @var int
42     */
43    const ORIG_UA = 1;
44
45    /**
46     * User normal stylesheet origin
47     *
48     * @var int
49     */
50    const ORIG_USER = 2;
51
52    /**
53     * Author normal stylesheet origin
54     *
55     * @var int
56     */
57    const ORIG_AUTHOR = 3;
58
59    /*
60     * The highest possible specificity is 0x01000000 (and that is only for author
61     * stylesheets, as it is for inline styles). Origin precedence can be achieved by
62     * adding multiples of 0x10000000 to the actual specificity. Important
63     * declarations are handled in Style; though technically they should be handled
64     * here so that user important declarations can be made to take precedence over
65     * user important declarations, this doesn't matter in practice as Dompdf does
66     * not support user stylesheets, and user agent stylesheets can not include
67     * important declarations.
68     */
69    private static $_stylesheet_origins = array(
70        self::ORIG_UA => 0x00000000, // user agent declarations
71        self::ORIG_USER => 0x10000000, // user normal declarations
72        self::ORIG_AUTHOR => 0x30000000, // author normal declarations
73    );
74
75    /*
76     * Non-CSS presentational hints (i.e. HTML 4 attributes) are handled as if added
77     * to the beginning of an author stylesheet, i.e. anything in author stylesheets
78     * should override them.
79     */
80    const SPEC_NON_CSS = 0x20000000;
81
82    /**
83     * Current dompdf instance
84     *
85     * @var Dompdf
86     */
87    private $_dompdf;
88
89    /**
90     * Array of currently defined styles
91     *
92     * @var Style[]
93     */
94    private $_styles;
95
96    /**
97     * Base protocol of the document being parsed
98     * Used to handle relative urls.
99     *
100     * @var string
101     */
102    private $_protocol;
103
104    /**
105     * Base hostname of the document being parsed
106     * Used to handle relative urls.
107     *
108     * @var string
109     */
110    private $_base_host;
111
112    /**
113     * Base path of the document being parsed
114     * Used to handle relative urls.
115     *
116     * @var string
117     */
118    private $_base_path;
119
120    /**
121     * The styles defined by @page rules
122     *
123     * @var array<Style>
124     */
125    private $_page_styles;
126
127    /**
128     * List of loaded files, used to prevent recursion
129     *
130     * @var array
131     */
132    private $_loaded_files;
133
134    /**
135     * Current stylesheet origin
136     *
137     * @var int
138     */
139    private $_current_origin = self::ORIG_UA;
140
141    /**
142     * Accepted CSS media types
143     * List of types and parsing rules for future extensions:
144     * http://www.w3.org/TR/REC-html40/types.html
145     *   screen, tty, tv, projection, handheld, print, braille, aural, all
146     * The following are non standard extensions for undocumented specific environments.
147     *   static, visual, bitmap, paged, dompdf
148     * Note, even though the generated pdf file is intended for print output,
149     * the desired content might be different (e.g. screen or projection view of html file).
150     * Therefore allow specification of content by dompdf setting Options::defaultMediaType.
151     * If given, replace media "print" by Options::defaultMediaType.
152     * (Previous version $ACCEPTED_MEDIA_TYPES = $ACCEPTED_GENERIC_MEDIA_TYPES + $ACCEPTED_DEFAULT_MEDIA_TYPE)
153     */
154    static $ACCEPTED_DEFAULT_MEDIA_TYPE = "print";
155    static $ACCEPTED_GENERIC_MEDIA_TYPES = array("all", "static", "visual", "bitmap", "paged", "dompdf");
156    static $VALID_MEDIA_TYPES = array("all", "aural", "bitmap", "braille", "dompdf", "embossed", "handheld", "paged", "print", "projection", "screen", "speech", "static", "tty", "tv", "visual");
157
158    /**
159     * @var FontMetrics
160     */
161    private $fontMetrics;
162
163    /**
164     * The class constructor.
165     *
166     * The base protocol, host & path are initialized to those of
167     * the current script.
168     */
169    function __construct(Dompdf $dompdf)
170    {
171        $this->_dompdf = $dompdf;
172        $this->setFontMetrics($dompdf->getFontMetrics());
173        $this->_styles = array();
174        $this->_loaded_files = array();
175        $script = __FILE__;
176        if(isset($_SERVER["SCRIPT_FILENAME"])){
177            $script = $_SERVER["SCRIPT_FILENAME"];
178        }
179        list($this->_protocol, $this->_base_host, $this->_base_path) = Helpers::explode_url($script);
180        $this->_page_styles = array("base" => new Style($this));
181    }
182
183    /**
184     * Set the base protocol
185     *
186     * @param string $protocol
187     */
188    function set_protocol($protocol)
189    {
190        $this->_protocol = $protocol;
191    }
192
193    /**
194     * Set the base host
195     *
196     * @param string $host
197     */
198    function set_host($host)
199    {
200        $this->_base_host = $host;
201    }
202
203    /**
204     * Set the base path
205     *
206     * @param string $path
207     */
208    function set_base_path($path)
209    {
210        $this->_base_path = $path;
211    }
212
213    /**
214     * Return the Dompdf object
215     *
216     * @return Dompdf
217     */
218    function get_dompdf()
219    {
220        return $this->_dompdf;
221    }
222
223    /**
224     * Return the base protocol for this stylesheet
225     *
226     * @return string
227     */
228    function get_protocol()
229    {
230        return $this->_protocol;
231    }
232
233    /**
234     * Return the base host for this stylesheet
235     *
236     * @return string
237     */
238    function get_host()
239    {
240        return $this->_base_host;
241    }
242
243    /**
244     * Return the base path for this stylesheet
245     *
246     * @return string
247     */
248    function get_base_path()
249    {
250        return $this->_base_path;
251    }
252
253    /**
254     * Return the array of page styles
255     *
256     * @return Style[]
257     */
258    function get_page_styles()
259    {
260        return $this->_page_styles;
261    }
262
263    /**
264     * Add a new Style object to the stylesheet
265     * add_style() adds a new Style object to the current stylesheet, or
266     * merges a new Style with an existing one.
267     *
268     * @param string $key the Style's selector
269     * @param Style $style the Style to be added
270     *
271     * @throws \Dompdf\Exception
272     */
273    function add_style($key, Style $style)
274    {
275        if (!is_string($key)) {
276            throw new Exception("CSS rule must be keyed by a string.");
277        }
278
279        if (!isset($this->_styles[$key])) {
280            $this->_styles[$key] = array();
281        }
282        $new_style = clone $style;
283        $new_style->set_origin($this->_current_origin);
284        $this->_styles[$key][] = $new_style;
285    }
286
287    /**
288     * lookup a specific Style collection
289     *
290     * lookup() returns the Style collection specified by $key, or null if the Style is
291     * not found.
292     *
293     * @param string $key the selector of the requested Style
294     * @return Style
295     *
296     * @Fixme _styles is a two dimensional array. It should produce wrong results
297     */
298    function lookup($key)
299    {
300        if (!isset($this->_styles[$key])) {
301            return null;
302        }
303
304        return $this->_styles[$key];
305    }
306
307    /**
308     * create a new Style object associated with this stylesheet
309     *
310     * @param Style $parent The style of this style's parent in the DOM tree
311     * @return Style
312     */
313    function create_style(Style $parent = null)
314    {
315        return new Style($this, $this->_current_origin);
316    }
317
318    /**
319     * load and parse a CSS string
320     *
321     * @param string $css
322     * @param int $origin
323     */
324    function load_css(&$css, $origin = self::ORIG_AUTHOR)
325    {
326        if ($origin) {
327            $this->_current_origin = $origin;
328        }
329        $this->_parse_css($css);
330    }
331
332
333    /**
334     * load and parse a CSS file
335     *
336     * @param string $file
337     * @param int $origin
338     */
339    function load_css_file($file, $origin = self::ORIG_AUTHOR)
340    {
341        if ($origin) {
342            $this->_current_origin = $origin;
343        }
344
345        // Prevent circular references
346        if (isset($this->_loaded_files[$file])) {
347            return;
348        }
349
350        $this->_loaded_files[$file] = true;
351
352        if (strpos($file, "data:") === 0) {
353            $parsed = Helpers::parse_data_uri($file);
354            $css = $parsed["data"];
355        } else {
356            $parsed_url = Helpers::explode_url($file);
357
358            list($this->_protocol, $this->_base_host, $this->_base_path, $filename) = $parsed_url;
359
360            // Fix submitted by Nick Oostveen for aliased directory support:
361            if ($this->_protocol == "") {
362                $file = $this->_base_path . $filename;
363            } else {
364                $file = Helpers::build_url($this->_protocol, $this->_base_host, $this->_base_path, $filename);
365            }
366
367            list($css, $http_response_header) = Helpers::getFileContent($file, $this->_dompdf->getHttpContext());
368
369            $good_mime_type = true;
370
371            // See http://the-stickman.com/web-development/php/getting-http-response-headers-when-using-file_get_contents/
372            if (isset($http_response_header) && !$this->_dompdf->getQuirksmode()) {
373                foreach ($http_response_header as $_header) {
374                    if (preg_match("@Content-Type:\s*([\w/]+)@i", $_header, $matches) &&
375                        ($matches[1] !== "text/css")
376                    ) {
377                        $good_mime_type = false;
378                    }
379                }
380            }
381
382            if (!$good_mime_type || $css == "") {
383                Helpers::record_warnings(E_USER_WARNING, "Unable to load css file $file", __FILE__, __LINE__);
384                return;
385            }
386        }
387
388        $this->_parse_css($css);
389    }
390
391    /**
392     * @link http://www.w3.org/TR/CSS21/cascade.html#specificity
393     *
394     * @param string $selector
395     * @param int $origin :
396     *    - Stylesheet::ORIG_UA: user agent style sheet
397     *    - Stylesheet::ORIG_USER: user style sheet
398     *    - Stylesheet::ORIG_AUTHOR: author style sheet
399     *
400     * @return int
401     */
402    private function _specificity($selector, $origin = self::ORIG_AUTHOR)
403    {
404        // http://www.w3.org/TR/CSS21/cascade.html#specificity
405        // ignoring the ":" pseudoclass modifiers
406        // also ignored in _css_selector_to_xpath
407
408        $a = ($selector === "!attr") ? 1 : 0;
409
410        $b = min(mb_substr_count($selector, "#"), 255);
411
412        $c = min(mb_substr_count($selector, ".") +
413            mb_substr_count($selector, "["), 255);
414
415        $d = min(mb_substr_count($selector, " ") +
416            mb_substr_count($selector, ">") +
417            mb_substr_count($selector, "+"), 255);
418
419        //If a normal element name is at the beginning of the string,
420        //a leading whitespace might have been removed on whitespace collapsing and removal
421        //therefore there might be one whitespace less as selected element names
422        //this can lead to a too small specificity
423        //see _css_selector_to_xpath
424
425        if (!in_array($selector[0], array(" ", ">", ".", "#", "+", ":", "[")) && $selector !== "*") {
426            $d++;
427        }
428
429        if ($this->_dompdf->getOptions()->getDebugCss()) {
430            /*DEBUGCSS*/
431            print "<pre>\n";
432            /*DEBUGCSS*/
433            printf("_specificity(): 0x%08x \"%s\"\n", self::$_stylesheet_origins[$origin] + (($a << 24) | ($b << 16) | ($c << 8) | ($d)), $selector);
434            /*DEBUGCSS*/
435            print "</pre>";
436        }
437
438        return self::$_stylesheet_origins[$origin] + (($a << 24) | ($b << 16) | ($c << 8) | ($d));
439    }
440
441    /**
442     * Converts a CSS selector to an XPath query.
443     *
444     * @param string $selector
445     * @param bool $first_pass
446     *
447     * @throws Exception
448     * @return array
449     */
450    private function _css_selector_to_xpath($selector, $first_pass = false)
451    {
452
453        // Collapse white space and strip whitespace around delimiters
454        //$search = array("/\\s+/", "/\\s+([.>#+:])\\s+/");
455        //$replace = array(" ", "\\1");
456        //$selector = preg_replace($search, $replace, trim($selector));
457
458        // Initial query (non-absolute)
459        $query = "//";
460
461        // Will contain :before and :after
462        $pseudo_elements = array();
463
464        // Will contain :link, etc
465        $pseudo_classes = array();
466
467        // Parse the selector
468        //$s = preg_split("/([ :>.#+])/", $selector, -1, PREG_SPLIT_DELIM_CAPTURE);
469
470        $delimiters = array(" ", ">", ".", "#", "+", ":", "[", "(");
471
472        // Add an implicit * at the beginning of the selector
473        // if it begins with an attribute selector
474        if ($selector[0] === "[") {
475            $selector = "*$selector";
476        }
477
478        // Add an implicit space at the beginning of the selector if there is no
479        // delimiter there already.
480        if (!in_array($selector[0], $delimiters)) {
481            $selector = " $selector";
482        }
483
484        $tok = "";
485        $len = mb_strlen($selector);
486        $i = 0;
487
488        while ($i < $len) {
489
490            $s = $selector[$i];
491            $i++;
492
493            // Eat characters up to the next delimiter
494            $tok = "";
495            $in_attr = false;
496            $in_func = false;
497
498            while ($i < $len) {
499                $c = $selector[$i];
500                $c_prev = $selector[$i - 1];
501
502                if (!$in_func && !$in_attr && in_array($c, $delimiters) && !(($c == $c_prev) == ":")) {
503                    break;
504                }
505
506                if ($c_prev === "[") {
507                    $in_attr = true;
508                }
509                if ($c_prev === "(") {
510                    $in_func = true;
511                }
512
513                $tok .= $selector[$i++];
514
515                if ($in_attr && $c === "]") {
516                    $in_attr = false;
517                    break;
518                }
519                if ($in_func && $c === ")") {
520                    $in_func = false;
521                    break;
522                }
523            }
524
525            switch ($s) {
526
527                case " ":
528                case ">":
529                    // All elements matching the next token that are direct children of
530                    // the current token
531                    $expr = $s === " " ? "descendant" : "child";
532
533                    if (mb_substr($query, -1, 1) !== "/") {
534                        $query .= "/";
535                    }
536
537                    // Tag names are case-insensitive
538                    $tok = strtolower($tok);
539
540                    if (!$tok) {
541                        $tok = "*";
542                    }
543
544                    $query .= "$expr::$tok";
545                    $tok = "";
546                    break;
547
548                case ".":
549                case "#":
550                    // All elements matching the current token with a class/id equal to
551                    // the _next_ token.
552
553                    $attr = $s === "." ? "class" : "id";
554
555                    // empty class/id == *
556                    if (mb_substr($query, -1, 1) === "/") {
557                        $query .= "*";
558                    }
559
560                    // Match multiple classes: $tok contains the current selected
561                    // class.  Search for class attributes with class="$tok",
562                    // class=".* $tok .*" and class=".* $tok"
563
564                    // This doesn't work because libxml only supports XPath 1.0...
565                    //$query .= "[matches(@$attr,\"^${tok}\$|^${tok}[ ]+|[ ]+${tok}\$|[ ]+${tok}[ ]+\")]";
566
567                    // Query improvement by Michael Sheakoski <michael@mjsdigital.com>:
568                    $query .= "[contains(concat(' ', @$attr, ' '), concat(' ', '$tok', ' '))]";
569                    $tok = "";
570                    break;
571
572                case "+":
573                    // All sibling elements that follow the current token
574                    if (mb_substr($query, -1, 1) !== "/") {
575                        $query .= "/";
576                    }
577
578                    $query .= "following-sibling::$tok";
579                    $tok = "";
580                    break;
581
582                case ":":
583                    $i2 = $i - strlen($tok) - 2; // the char before ":"
584                    if (($i2 < 0 || !isset($selector[$i2]) || (in_array($selector[$i2], $delimiters) && $selector[$i2] != ":")) && substr($query, -1) != "*") {
585                        $query .= "*";
586                    }
587
588                    $last = false;
589
590                    // Pseudo-classes
591                    switch ($tok) {
592
593                        case "first-child":
594                            $query .= "[1]";
595                            $tok = "";
596                            break;
597
598                        case "last-child":
599                            $query .= "[not(following-sibling::*)]";
600                            $tok = "";
601                            break;
602
603                        case "first-of-type":
604                            $query .= "[position() = 1]";
605                            $tok = "";
606                            break;
607
608                        case "last-of-type":
609                            $query .= "[position() = last()]";
610                            $tok = "";
611                            break;
612
613                        // an+b, n, odd, and even
614                        /** @noinspection PhpMissingBreakStatementInspection */
615                        case "nth-last-of-type":
616                            $last = true;
617                        case "nth-of-type":
618                            //FIXME: this fix-up is pretty ugly, would parsing the selector in reverse work better generally?
619                            $descendant_delimeter = strrpos($query, "::");
620                            $isChild = substr($query, $descendant_delimeter-5, 5) == "child";
621                            $el = substr($query, $descendant_delimeter+2);
622                            $query = substr($query, 0, strrpos($query, "/")) . ($isChild ? "/" : "//") . $el;
623
624                            $pseudo_classes[$tok] = true;
625                            $p = $i + 1;
626                            $nth = trim(mb_substr($selector, $p, strpos($selector, ")", $i) - $p));
627
628                            // 1
629                            if (preg_match("/^\d+$/", $nth)) {
630                                $condition = "position() = $nth";
631                            } // odd
632                            elseif ($nth === "odd") {
633                                $condition = "(position() mod 2) = 1";
634                            } // even
635                            elseif ($nth === "even") {
636                                $condition = "(position() mod 2) = 0";
637                            } // an+b
638                            else {
639                                $condition = $this->_selector_an_plus_b($nth, $last);
640                            }
641
642                            $query .= "[$condition]";
643                            $tok = "";
644                            break;
645                        /** @noinspection PhpMissingBreakStatementInspection */
646                        case "nth-last-child":
647                            $last = true;
648                        case "nth-child":
649                            //FIXME: this fix-up is pretty ugly, would parsing the selector in reverse work better generally?
650                            $descendant_delimeter = strrpos($query, "::");
651                            $isChild = substr($query, $descendant_delimeter-5, 5) == "child";
652                            $el = substr($query, $descendant_delimeter+2);
653                            $query = substr($query, 0, strrpos($query, "/")) . ($isChild ? "/" : "//") . "*";
654
655                            $pseudo_classes[$tok] = true;
656                            $p = $i + 1;
657                            $nth = trim(mb_substr($selector, $p, strpos($selector, ")", $i) - $p));
658
659                            // 1
660                            if (preg_match("/^\d+$/", $nth)) {
661                                $condition = "position() = $nth";
662                            } // odd
663                            elseif ($nth === "odd") {
664                                $condition = "(position() mod 2) = 1";
665                            } // even
666                            elseif ($nth === "even") {
667                                $condition = "(position() mod 2) = 0";
668                            } // an+b
669                            else {
670                                $condition = $this->_selector_an_plus_b($nth, $last);
671                            }
672
673                            $query .= "[$condition]";
674                            if ($el != "*") {
675                                $query .= "[name() = '$el']";
676                            }
677                            $tok = "";
678                            break;
679
680                        //TODO: bit of a hack attempt at matches support, currently only matches against elements
681                        case "matches":
682                            $pseudo_classes[$tok] = true;
683                            $p = $i + 1;
684                            $matchList = trim(mb_substr($selector, $p, strpos($selector, ")", $i) - $p));
685
686                            // Tag names are case-insensitive
687                            $elements = array_map("trim", explode(",", strtolower($matchList)));
688                            foreach ($elements as &$element) {
689                                $element = "name() = '$element'";
690                            }
691
692                            $query .= "[" . implode(" or ", $elements) . "]";
693                            $tok = "";
694                            break;
695
696                        case "link":
697                            $query .= "[@href]";
698                            $tok = "";
699                            break;
700
701                        case "first-line":
702                        case ":first-line":
703                        case "first-letter":
704                        case ":first-letter":
705                            // TODO
706                            $el = trim($tok, ":");
707                            $pseudo_elements[$el] = true;
708                            break;
709
710                            // N/A
711                        case "focus":
712                        case "active":
713                        case "hover":
714                        case "visited":
715                            $query .= "[false()]";
716                            $tok = "";
717                            break;
718
719                        /* Pseudo-elements */
720                        case "before":
721                        case ":before":
722                        case "after":
723                        case ":after":
724                            $pos = trim($tok, ":");
725                            $pseudo_elements[$pos] = true;
726                            if (!$first_pass) {
727                                $query .= "/*[@$pos]";
728                            }
729
730                            $tok = "";
731                            break;
732
733                        case "empty":
734                            $query .= "[not(*) and not(normalize-space())]";
735                            $tok = "";
736                            break;
737
738                        case "disabled":
739                        case "checked":
740                            $query .= "[@$tok]";
741                            $tok = "";
742                            break;
743
744                        case "enabled":
745                            $query .= "[not(@disabled)]";
746                            $tok = "";
747                            break;
748
749                        // the selector is not handled, until we support all possible selectors force an empty set (silent failure)
750                        default:
751                            $query = "/../.."; // go up two levels because generated content starts on the body element
752                            $tok = "";
753                            break;
754                    }
755
756                    break;
757
758                case "[":
759                    // Attribute selectors.  All with an attribute matching the following token(s)
760                    $attr_delimiters = array("=", "]", "~", "|", "$", "^", "*");
761                    $tok_len = mb_strlen($tok);
762                    $j = 0;
763
764                    $attr = "";
765                    $op = "";
766                    $value = "";
767
768                    while ($j < $tok_len) {
769                        if (in_array($tok[$j], $attr_delimiters)) {
770                            break;
771                        }
772                        $attr .= $tok[$j++];
773                    }
774
775                    switch ($tok[$j]) {
776
777                        case "~":
778                        case "|":
779                        case "$":
780                        case "^":
781                        case "*":
782                            $op .= $tok[$j++];
783
784                            if ($tok[$j] !== "=") {
785                                throw new Exception("Invalid CSS selector syntax: invalid attribute selector: $selector");
786                            }
787
788                            $op .= $tok[$j];
789                            break;
790
791                        case "=":
792                            $op = "=";
793                            break;
794
795                    }
796
797                    // Read the attribute value, if required
798                    if ($op != "") {
799                        $j++;
800                        while ($j < $tok_len) {
801                            if ($tok[$j] === "]") {
802                                break;
803                            }
804                            $value .= $tok[$j++];
805                        }
806                    }
807
808                    if ($attr == "") {
809                        throw new Exception("Invalid CSS selector syntax: missing attribute name");
810                    }
811
812                    $value = trim($value, "\"'");
813
814                    switch ($op) {
815
816                        case "":
817                            $query .= "[@$attr]";
818                            break;
819
820                        case "=":
821                            $query .= "[@$attr=\"$value\"]";
822                            break;
823
824                        case "~=":
825                            // FIXME: this will break if $value contains quoted strings
826                            // (e.g. [type~="a b c" "d e f"])
827                            $values = explode(" ", $value);
828                            $query .= "[";
829
830                            foreach ($values as $val) {
831                                $query .= "@$attr=\"$val\" or ";
832                            }
833
834                            $query = rtrim($query, " or ") . "]";
835                            break;
836
837                        case "|=":
838                            $values = explode("-", $value);
839                            $query .= "[";
840
841                            foreach ($values as $val) {
842                                $query .= "starts-with(@$attr, \"$val\") or ";
843                            }
844
845                            $query = rtrim($query, " or ") . "]";
846                            break;
847
848                        case "$=":
849                            $query .= "[substring(@$attr, string-length(@$attr)-" . (strlen($value) - 1) . ")=\"$value\"]";
850                            break;
851
852                        case "^=":
853                            $query .= "[starts-with(@$attr,\"$value\")]";
854                            break;
855
856                        case "*=":
857                            $query .= "[contains(@$attr,\"$value\")]";
858                            break;
859                    }
860
861                    break;
862            }
863        }
864        $i++;
865
866//       case ":":
867//         // Pseudo selectors: ignore for now.  Partially handled directly
868//         // below.
869
870//         // Skip until the next special character, leaving the token as-is
871//         while ( $i < $len ) {
872//           if ( in_array($selector[$i], $delimiters) )
873//             break;
874//           $i++;
875//         }
876//         break;
877
878//       default:
879//         // Add the character to the token
880//         $tok .= $selector[$i++];
881//         break;
882//       }
883
884//    }
885
886
887        // Trim the trailing '/' from the query
888        if (mb_strlen($query) > 2) {
889            $query = rtrim($query, "/");
890        }
891
892        return array("query" => $query, "pseudo_elements" => $pseudo_elements);
893    }
894
895    /**
896     * https://github.com/tenderlove/nokogiri/blob/master/lib/nokogiri/css/xpath_visitor.rb
897     *
898     * @param $expr
899     * @param bool $last
900     * @return string
901     */
902    protected function _selector_an_plus_b($expr, $last = false)
903    {
904        $expr = preg_replace("/\s/", "", $expr);
905        if (!preg_match("/^(?P<a>-?[0-9]*)?n(?P<b>[-+]?[0-9]+)?$/", $expr, $matches)) {
906            return "false()";
907        }
908
909        $a = ((isset($matches["a"]) && $matches["a"] !== "") ? intval($matches["a"]) : 1);
910        $b = ((isset($matches["b"]) && $matches["b"] !== "") ? intval($matches["b"]) : 0);
911
912        $position = ($last ? "(last()-position()+1)" : "position()");
913
914        if ($b == 0) {
915            return "($position mod $a) = 0";
916        } else {
917            $compare = (($a < 0) ? "<=" : ">=");
918            $b2 = -$b;
919            if ($b2 >= 0) {
920                $b2 = "+$b2";
921            }
922            return "($position $compare $b) and ((($position $b2) mod " . abs($a) . ") = 0)";
923        }
924    }
925
926    /**
927     * applies all current styles to a particular document tree
928     *
929     * apply_styles() applies all currently loaded styles to the provided
930     * {@link FrameTree}.  Aside from parsing CSS, this is the main purpose
931     * of this class.
932     *
933     * @param \Dompdf\Frame\FrameTree $tree
934     */
935    function apply_styles(FrameTree $tree)
936    {
937        // Use XPath to select nodes.  This would be easier if we could attach
938        // Frame objects directly to DOMNodes using the setUserData() method, but
939        // we can't do that just yet.  Instead, we set a _node attribute_ in
940        // Frame->set_id() and use that as a handle on the Frame object via
941        // FrameTree::$_registry.
942
943        // We create a scratch array of styles indexed by frame id.  Once all
944        // styles have been assigned, we order the cached styles by specificity
945        // and create a final style object to assign to the frame.
946
947        // FIXME: this is not particularly robust...
948
949        $styles = array();
950        $xp = new DOMXPath($tree->get_dom());
951        $DEBUGCSS = $this->_dompdf->getOptions()->getDebugCss();
952
953        // Add generated content
954        foreach ($this->_styles as $selector => $selector_styles) {
955            /** @var Style $style */
956            foreach ($selector_styles as $style) {
957                if (strpos($selector, ":before") === false && strpos($selector, ":after") === false) {
958                    continue;
959                }
960
961                $query = $this->_css_selector_to_xpath($selector, true);
962
963                // Retrieve the nodes, limit to body for generated content
964                //TODO: If we use a context node can we remove the leading dot?
965                $nodes = @$xp->query('.' . $query["query"]);
966                if ($nodes == null) {
967                    Helpers::record_warnings(E_USER_WARNING, "The CSS selector '$selector' is not valid", __FILE__, __LINE__);
968                    continue;
969                }
970
971                /** @var \DOMElement $node */
972                foreach ($nodes as $node) {
973                    // Only DOMElements get styles
974                    if ($node->nodeType != XML_ELEMENT_NODE) {
975                        continue;
976                    }
977
978                    foreach (array_keys($query["pseudo_elements"], true, true) as $pos) {
979                        // Do not add a new pseudo element if another one already matched
980                        if ($node->hasAttribute("dompdf_{$pos}_frame_id")) {
981                            continue;
982                        }
983
984                        if (($src = $this->_image($style->get_prop('content'))) !== "none") {
985                            $new_node = $node->ownerDocument->createElement("img_generated");
986                            $new_node->setAttribute("src", $src);
987                        } else {
988                            $new_node = $node->ownerDocument->createElement("dompdf_generated");
989                        }
990
991                        $new_node->setAttribute($pos, $pos);
992                        $new_frame_id = $tree->insert_node($node, $new_node, $pos);
993                        $node->setAttribute("dompdf_{$pos}_frame_id", $new_frame_id);
994                    }
995                }
996            }
997        }
998
999        // Apply all styles in stylesheet
1000        foreach ($this->_styles as $selector => $selector_styles) {
1001            /** @var Style $style */
1002            foreach ($selector_styles as $style) {
1003                $query = $this->_css_selector_to_xpath($selector);
1004
1005                // Retrieve the nodes
1006                $nodes = @$xp->query($query["query"]);
1007                if ($nodes == null) {
1008                    Helpers::record_warnings(E_USER_WARNING, "The CSS selector '$selector' is not valid", __FILE__, __LINE__);
1009                    continue;
1010                }
1011
1012                $spec = $this->_specificity($selector, $style->get_origin());
1013
1014                foreach ($nodes as $node) {
1015                    // Retrieve the node id
1016                    // Only DOMElements get styles
1017                    if ($node->nodeType != XML_ELEMENT_NODE) {
1018                        continue;
1019                    }
1020
1021                    $id = $node->getAttribute("frame_id");
1022
1023                    // Assign the current style to the scratch array
1024                    $styles[$id][$spec][] = $style;
1025                }
1026            }
1027        }
1028
1029        // Set the page width, height, and orientation based on the canvas paper size
1030        $canvas = $this->_dompdf->getCanvas();
1031        $paper_width = $canvas->get_width();
1032        $paper_height = $canvas->get_height();
1033        $paper_orientation = ($paper_width > $paper_height ? "landscape" : "portrait");
1034
1035        if ($this->_page_styles["base"] && is_array($this->_page_styles["base"]->size)) {
1036            $paper_width = $this->_page_styles['base']->size[0];
1037            $paper_height = $this->_page_styles['base']->size[1];
1038            $paper_orientation = ($paper_width > $paper_height ? "landscape" : "portrait");
1039        }
1040
1041        // Now create the styles and assign them to the appropriate frames. (We
1042        // iterate over the tree using an implicit FrameTree iterator.)
1043        $root_flg = false;
1044        foreach ($tree->get_frames() as $frame) {
1045            // Helpers::pre_r($frame->get_node()->nodeName . ":");
1046            if (!$root_flg && $this->_page_styles["base"]) {
1047                $style = $this->_page_styles["base"];
1048            } else {
1049                $style = $this->create_style();
1050            }
1051
1052            // Find nearest DOMElement parent
1053            $p = $frame;
1054            while ($p = $p->get_parent()) {
1055                if ($p->get_node()->nodeType == XML_ELEMENT_NODE) {
1056                    break;
1057                }
1058            }
1059
1060            // Styles can only be applied directly to DOMElements; anonymous
1061            // frames inherit from their parent
1062            if ($frame->get_node()->nodeType != XML_ELEMENT_NODE) {
1063                if ($p) {
1064                    $style->inherit($p->get_style());
1065                }
1066
1067                $frame->set_style($style);
1068                continue;
1069            }
1070
1071            $id = $frame->get_id();
1072
1073            // Handle HTML 4.0 attributes
1074            AttributeTranslator::translate_attributes($frame);
1075            if (($str = $frame->get_node()->getAttribute(AttributeTranslator::$_style_attr)) !== "") {
1076                $styles[$id][self::SPEC_NON_CSS][] = $this->_parse_properties($str);
1077            }
1078
1079            // Locate any additional style attributes
1080            if (($str = $frame->get_node()->getAttribute("style")) !== "") {
1081                // Destroy CSS comments
1082                $str = preg_replace("'/\*.*?\*/'si", "", $str);
1083
1084                $spec = $this->_specificity("!attr", self::ORIG_AUTHOR);
1085                $styles[$id][$spec][] = $this->_parse_properties($str);
1086            }
1087
1088            // Grab the applicable styles
1089            if (isset($styles[$id])) {
1090
1091                /** @var array[][] $applied_styles */
1092                $applied_styles = $styles[$frame->get_id()];
1093
1094                // Sort by specificity
1095                ksort($applied_styles);
1096
1097                if ($DEBUGCSS) {
1098                    $debug_nodename = $frame->get_node()->nodeName;
1099                    print "<pre>\n[$debug_nodename\n";
1100                    foreach ($applied_styles as $spec => $arr) {
1101                        printf("specificity: 0x%08x\n", $spec);
1102                        /** @var Style $s */
1103                        foreach ($arr as $s) {
1104                            print "[\n";
1105                            $s->debug_print();
1106                            print "]\n";
1107                        }
1108                    }
1109                }
1110
1111                // Merge the new styles with the inherited styles
1112                $acceptedmedia = self::$ACCEPTED_GENERIC_MEDIA_TYPES;
1113                $acceptedmedia[] = $this->_dompdf->getOptions()->getDefaultMediaType();
1114                foreach ($applied_styles as $arr) {
1115                    /** @var Style $s */
1116                    foreach ($arr as $s) {
1117                        $media_queries = $s->get_media_queries();
1118                        foreach ($media_queries as $media_query) {
1119                            list($media_query_feature, $media_query_value) = $media_query;
1120                            // if any of the Style's media queries fail then do not apply the style
1121                            //TODO: When the media query logic is fully developed we should not apply the Style when any of the media queries fail or are bad, per https://www.w3.org/TR/css3-mediaqueries/#error-handling
1122                            if (in_array($media_query_feature, self::$VALID_MEDIA_TYPES)) {
1123                                if ((strlen($media_query_feature) === 0 && !in_array($media_query, $acceptedmedia)) || (in_array($media_query, $acceptedmedia) && $media_query_value == "not")) {
1124                                    continue (3);
1125                                }
1126                            } else {
1127                                switch ($media_query_feature) {
1128                                    case "height":
1129                                        if ($paper_height !== (float)$style->length_in_pt($media_query_value)) {
1130                                            continue (3);
1131                                        }
1132                                        break;
1133                                    case "min-height":
1134                                        if ($paper_height < (float)$style->length_in_pt($media_query_value)) {
1135                                            continue (3);
1136                                        }
1137                                        break;
1138                                    case "max-height":
1139                                        if ($paper_height > (float)$style->length_in_pt($media_query_value)) {
1140                                            continue (3);
1141                                        }
1142                                        break;
1143                                    case "width":
1144                                        if ($paper_width !== (float)$style->length_in_pt($media_query_value)) {
1145                                            continue (3);
1146                                        }
1147                                        break;
1148                                    case "min-width":
1149                                        //if (min($paper_width, $media_query_width) === $paper_width) {
1150                                        if ($paper_width < (float)$style->length_in_pt($media_query_value)) {
1151                                            continue (3);
1152                                        }
1153                                        break;
1154                                    case "max-width":
1155                                        //if (max($paper_width, $media_query_width) === $paper_width) {
1156                                        if ($paper_width > (float)$style->length_in_pt($media_query_value)) {
1157                                            continue (3);
1158                                        }
1159                                        break;
1160                                    case "orientation":
1161                                        if ($paper_orientation !== $media_query_value) {
1162                                            continue (3);
1163                                        }
1164                                        break;
1165                                    default:
1166                                        Helpers::record_warnings(E_USER_WARNING, "Unknown media query: $media_query_feature", __FILE__, __LINE__);
1167                                        break;
1168                                }
1169                            }
1170                        }
1171
1172                        $style->merge($s);
1173                    }
1174                }
1175            }
1176
1177            // Inherit parent's styles if required
1178            if ($p) {
1179
1180                if ($DEBUGCSS) {
1181                    print "inherit:\n";
1182                    print "[\n";
1183                    $p->get_style()->debug_print();
1184                    print "]\n";
1185                }
1186
1187                $style->inherit($p->get_style());
1188            }
1189
1190            if ($DEBUGCSS) {
1191                print "DomElementStyle:\n";
1192                print "[\n";
1193                $style->debug_print();
1194                print "]\n";
1195                print "/$debug_nodename]\n</pre>";
1196            }
1197
1198            /*DEBUGCSS print: see below different print debugging method
1199            Helpers::pre_r($frame->get_node()->nodeName . ":");
1200            echo "<pre>";
1201            echo $style;
1202            echo "</pre>";*/
1203            $frame->set_style($style);
1204
1205            if (!$root_flg && $this->_page_styles["base"]) {
1206                $root_flg = true;
1207
1208                // set the page width, height, and orientation based on the parsed page style
1209                if ($style->size !== "auto") {
1210                    list($paper_width, $paper_height) = $style->size;
1211                }
1212                $paper_width = $paper_width - (float)$style->length_in_pt($style->margin_left) - (float)$style->length_in_pt($style->margin_right);
1213                $paper_height = $paper_height - (float)$style->length_in_pt($style->margin_top) - (float)$style->length_in_pt($style->margin_bottom);
1214                $paper_orientation = ($paper_width > $paper_height ? "landscape" : "portrait");
1215            }
1216        }
1217
1218        // We're done!  Clean out the registry of all styles since we
1219        // won't be needing this later.
1220        foreach (array_keys($this->_styles) as $key) {
1221            $this->_styles[$key] = null;
1222            unset($this->_styles[$key]);
1223        }
1224
1225    }
1226
1227    /**
1228     * parse a CSS string using a regex parser
1229     * Called by {@link Stylesheet::parse_css()}
1230     *
1231     * @param string $str
1232     *
1233     * @throws Exception
1234     */
1235    private function _parse_css($str)
1236    {
1237
1238        $str = trim($str);
1239
1240        // Destroy comments and remove HTML comments
1241        $css = preg_replace(array(
1242            "'/\*.*?\*/'si",
1243            "/^<!--/",
1244            "/-->$/"
1245        ), "", $str);
1246
1247        // FIXME: handle '{' within strings, e.g. [attr="string {}"]
1248
1249        // Something more legible:
1250        $re =
1251            "/\s*                                   # Skip leading whitespace                             \n" .
1252            "( @([^\s{]+)\s*([^{;]*) (?:;|({)) )?   # Match @rules followed by ';' or '{'                 \n" .
1253            "(?(1)                                  # Only parse sub-sections if we're in an @rule...     \n" .
1254            "  (?(4)                                # ...and if there was a leading '{'                   \n" .
1255            "    \s*( (?:(?>[^{}]+) ({)?            # Parse rulesets and individual @page rules           \n" .
1256            "            (?(6) (?>[^}]*) }) \s*)+?                                                        \n" .
1257            "       )                                                                                     \n" .
1258            "   })                                  # Balancing '}'                                       \n" .
1259            "|                                      # Branch to match regular rules (not preceded by '@') \n" .
1260            "([^{]*{[^}]*}))                        # Parse normal rulesets                               \n" .
1261            "/xs";
1262
1263        if (preg_match_all($re, $css, $matches, PREG_SET_ORDER) === false) {
1264            // An error occurred
1265            throw new Exception("Error parsing css file: preg_match_all() failed.");
1266        }
1267
1268        // After matching, the array indices are set as follows:
1269        //
1270        // [0] => complete text of match
1271        // [1] => contains '@import ...;' or '@media {' if applicable
1272        // [2] => text following @ for cases where [1] is set
1273        // [3] => media types or full text following '@import ...;'
1274        // [4] => '{', if present
1275        // [5] => rulesets within media rules
1276        // [6] => '{', within media rules
1277        // [7] => individual rules, outside of media rules
1278        //
1279
1280        $media_query_regex = "/(?:((only|not)?\s*(" . implode("|", self::$VALID_MEDIA_TYPES) . "))|(\s*\(\s*((?:(min|max)-)?([\w\-]+))\s*(?:\:\s*(.*?)\s*)?\)))/isx";
1281
1282        //Helpers::pre_r($matches);
1283        foreach ($matches as $match) {
1284            $match[2] = trim($match[2]);
1285
1286            if ($match[2] !== "") {
1287                // Handle @rules
1288                switch ($match[2]) {
1289
1290                    case "import":
1291                        $this->_parse_import($match[3]);
1292                        break;
1293
1294                    case "media":
1295                        $acceptedmedia = self::$ACCEPTED_GENERIC_MEDIA_TYPES;
1296                        $acceptedmedia[] = $this->_dompdf->getOptions()->getDefaultMediaType();
1297
1298                        $media_queries = preg_split("/\s*,\s*/", mb_strtolower(trim($match[3])));
1299                        foreach ($media_queries as $media_query) {
1300                            if (in_array($media_query, $acceptedmedia)) {
1301                                //if we have a media type match go ahead and parse the stylesheet
1302                                $this->_parse_sections($match[5]);
1303                                break;
1304                            } elseif (!in_array($media_query, self::$VALID_MEDIA_TYPES)) {
1305                                // otherwise conditionally parse the stylesheet assuming there are parseable media queries
1306                                if (preg_match_all($media_query_regex, $media_query, $media_query_matches, PREG_SET_ORDER) !== false) {
1307                                    $mq = array();
1308                                    foreach ($media_query_matches as $media_query_match) {
1309                                        if (empty($media_query_match[1]) === false) {
1310                                            $media_query_feature = strtolower($media_query_match[3]);
1311                                            $media_query_value = strtolower($media_query_match[2]);
1312                                            $mq[] = array($media_query_feature, $media_query_value);
1313                                        } else if (empty($media_query_match[4]) === false) {
1314                                            $media_query_feature = strtolower($media_query_match[5]);
1315                                            $media_query_value = (array_key_exists(8, $media_query_match) ? strtolower($media_query_match[8]) : null);
1316                                            $mq[] = array($media_query_feature, $media_query_value);
1317                                        }
1318                                    }
1319                                    $this->_parse_sections($match[5], $mq);
1320                                    break;
1321                                }
1322                            }
1323                        }
1324                        break;
1325
1326                    case "page":
1327                        //This handles @page to be applied to page oriented media
1328                        //Note: This has a reduced syntax:
1329                        //@page { margin:1cm; color:blue; }
1330                        //Not a sequence of styles like a full.css, but only the properties
1331                        //of a single style, which is applied to the very first "root" frame before
1332                        //processing other styles of the frame.
1333                        //Working properties:
1334                        // margin (for margin around edge of paper)
1335                        // font-family (default font of pages)
1336                        // color (default text color of pages)
1337                        //Non working properties:
1338                        // border
1339                        // padding
1340                        // background-color
1341                        //Todo:Reason is unknown
1342                        //Other properties (like further font or border attributes) not tested.
1343                        //If a border or background color around each paper sheet is desired,
1344                        //assign it to the <body> tag, possibly only for the css of the correct media type.
1345
1346                        // If the page has a name, skip the style.
1347                        $page_selector = trim($match[3]);
1348
1349                        $key = null;
1350                        switch ($page_selector) {
1351                            case "":
1352                                $key = "base";
1353                                break;
1354
1355                            case ":left":
1356                            case ":right":
1357                            case ":odd":
1358                            case ":even":
1359                            /** @noinspection PhpMissingBreakStatementInspection */
1360                            case ":first":
1361                                $key = $page_selector;
1362
1363                            default:
1364                                break 2;
1365                        }
1366
1367                        // Store the style for later...
1368                        if (empty($this->_page_styles[$key])) {
1369                            $this->_page_styles[$key] = $this->_parse_properties($match[5]);
1370                        } else {
1371                            $this->_page_styles[$key]->merge($this->_parse_properties($match[5]));
1372                        }
1373                        break;
1374
1375                    case "font-face":
1376                        $this->_parse_font_face($match[5]);
1377                        break;
1378
1379                    default:
1380                        // ignore everything else
1381                        break;
1382                }
1383
1384                continue;
1385            }
1386
1387            if ($match[7] !== "") {
1388                $this->_parse_sections($match[7]);
1389            }
1390
1391        }
1392    }
1393
1394    /**
1395     * See also style.cls Style::_image(), refactoring?, works also for imported css files
1396     *
1397     * @param $val
1398     * @return string
1399     */
1400    protected function _image($val)
1401    {
1402        $DEBUGCSS = $this->_dompdf->getOptions()->getDebugCss();
1403        $parsed_url = "none";
1404
1405        if (mb_strpos($val, "url") === false) {
1406            $path = "none"; //Don't resolve no image -> otherwise would prefix path and no longer recognize as none
1407        } else {
1408            $val = preg_replace("/url\(\s*['\"]?([^'\")]+)['\"]?\s*\)/", "\\1", trim($val));
1409
1410            // Resolve the url now in the context of the current stylesheet
1411            $parsed_url = Helpers::explode_url($val);
1412            if ($parsed_url["protocol"] == "" && $this->get_protocol() == "") {
1413                if ($parsed_url["path"][0] === '/' || $parsed_url["path"][0] === '\\') {
1414                    $path = $_SERVER["DOCUMENT_ROOT"] . '/';
1415                } else {
1416                    $path = $this->get_base_path();
1417                }
1418
1419                $path .= $parsed_url["path"] . $parsed_url["file"];
1420                $path = realpath($path);
1421                // If realpath returns FALSE then specifically state that there is no background image
1422                // FIXME: Is this causing problems for imported CSS files? There are some './none' references when running the test cases.
1423                if (!$path) {
1424                    $path = 'none';
1425                }
1426            } else {
1427                $path = Helpers::build_url($this->get_protocol(),
1428                    $this->get_host(),
1429                    $this->get_base_path(),
1430                    $val);
1431            }
1432        }
1433
1434        if ($DEBUGCSS) {
1435            print "<pre>[_image\n";
1436            print_r($parsed_url);
1437            print $this->get_protocol() . "\n" . $this->get_base_path() . "\n" . $path . "\n";
1438            print "_image]</pre>";;
1439        }
1440
1441        return $path;
1442    }
1443
1444    /**
1445     * parse @import{} sections
1446     *
1447     * @param string $url the url of the imported CSS file
1448     */
1449    private function _parse_import($url)
1450    {
1451        $arr = preg_split("/[\s\n,]/", $url, -1, PREG_SPLIT_NO_EMPTY);
1452        $url = array_shift($arr);
1453        $accept = false;
1454
1455        if (count($arr) > 0) {
1456            $acceptedmedia = self::$ACCEPTED_GENERIC_MEDIA_TYPES;
1457            $acceptedmedia[] = $this->_dompdf->getOptions()->getDefaultMediaType();
1458
1459            // @import url media_type [media_type...]
1460            foreach ($arr as $type) {
1461                if (in_array(mb_strtolower(trim($type)), $acceptedmedia)) {
1462                    $accept = true;
1463                    break;
1464                }
1465            }
1466
1467        } else {
1468            // unconditional import
1469            $accept = true;
1470        }
1471
1472        if ($accept) {
1473            // Store our current base url properties in case the new url is elsewhere
1474            $protocol = $this->_protocol;
1475            $host = $this->_base_host;
1476            $path = $this->_base_path;
1477
1478            // $url = str_replace(array('"',"url", "(", ")"), "", $url);
1479            // If the protocol is php, assume that we will import using file://
1480            // $url = Helpers::build_url($protocol == "php://" ? "file://" : $protocol, $host, $path, $url);
1481            // Above does not work for subfolders and absolute urls.
1482            // Todo: As above, do we need to replace php or file to an empty protocol for local files?
1483
1484            $url = $this->_image($url);
1485
1486            $this->load_css_file($url);
1487
1488            // Restore the current base url
1489            $this->_protocol = $protocol;
1490            $this->_base_host = $host;
1491            $this->_base_path = $path;
1492        }
1493
1494    }
1495
1496    /**
1497     * parse @font-face{} sections
1498     * http://www.w3.org/TR/css3-fonts/#the-font-face-rule
1499     *
1500     * @param string $str CSS @font-face rules
1501     */
1502    private function _parse_font_face($str)
1503    {
1504        $descriptors = $this->_parse_properties($str);
1505
1506        preg_match_all("/(url|local)\s*\([\"\']?([^\"\'\)]+)[\"\']?\)\s*(format\s*\([\"\']?([^\"\'\)]+)[\"\']?\))?/i", $descriptors->src, $src);
1507
1508        $sources = array();
1509        $valid_sources = array();
1510
1511        foreach ($src[0] as $i => $value) {
1512            $source = array(
1513                "local" => strtolower($src[1][$i]) === "local",
1514                "uri" => $src[2][$i],
1515                "format" => strtolower($src[4][$i]),
1516                "path" => Helpers::build_url($this->_protocol, $this->_base_host, $this->_base_path, $src[2][$i]),
1517            );
1518
1519            if (!$source["local"] && in_array($source["format"], array("", "truetype"))) {
1520                $valid_sources[] = $source;
1521            }
1522
1523            $sources[] = $source;
1524        }
1525
1526        // No valid sources
1527        if (empty($valid_sources)) {
1528            return;
1529        }
1530
1531        $style = array(
1532            "family" => $descriptors->get_font_family_raw(),
1533            "weight" => $descriptors->font_weight,
1534            "style" => $descriptors->font_style,
1535        );
1536
1537        $this->getFontMetrics()->registerFont($style, $valid_sources[0]["path"], $this->_dompdf->getHttpContext());
1538    }
1539
1540    /**
1541     * parse regular CSS blocks
1542     *
1543     * _parse_properties() creates a new Style object based on the provided
1544     * CSS rules.
1545     *
1546     * @param string $str CSS rules
1547     * @return Style
1548     */
1549    private function _parse_properties($str)
1550    {
1551        $properties = preg_split("/;(?=(?:[^\(]*\([^\)]*\))*(?![^\)]*\)))/", $str);
1552        $DEBUGCSS = $this->_dompdf->getOptions()->getDebugCss();
1553
1554        if ($DEBUGCSS) {
1555            print '[_parse_properties';
1556        }
1557
1558        // Create the style
1559        $style = new Style($this, Stylesheet::ORIG_AUTHOR);
1560
1561        foreach ($properties as $prop) {
1562            // If the $prop contains an url, the regex may be wrong
1563            // @todo: fix the regex so that it works every time
1564            /*if (strpos($prop, "url(") === false) {
1565              if (preg_match("/([a-z-]+)\s*:\s*[^:]+$/i", $prop, $m))
1566                $prop = $m[0];
1567            }*/
1568            //A css property can have " ! important" appended (whitespace optional)
1569            //strip this off to decode core of the property correctly.
1570            //Pass on in the style to allow proper handling:
1571            //!important properties can only be overridden by other !important ones.
1572            //$style->$prop_name = is a shortcut of $style->__set($prop_name,$value);.
1573            //If no specific set function available, set _props["prop_name"]
1574            //style is always copied completely, or $_props handled separately
1575            //Therefore set a _important_props["prop_name"]=true to indicate the modifier
1576
1577            /* Instead of short code, prefer the typical case with fast code
1578          $important = preg_match("/(.*?)!\s*important/",$prop,$match);
1579            if ( $important ) {
1580              $prop = $match[1];
1581            }
1582            $prop = trim($prop);
1583            */
1584            if ($DEBUGCSS) print '(';
1585
1586            $important = false;
1587            $prop = trim($prop);
1588
1589            if (substr($prop, -9) === 'important') {
1590                $prop_tmp = rtrim(substr($prop, 0, -9));
1591
1592                if (substr($prop_tmp, -1) === '!') {
1593                    $prop = rtrim(substr($prop_tmp, 0, -1));
1594                    $important = true;
1595                }
1596            }
1597
1598            if ($prop === "") {
1599                if ($DEBUGCSS) print 'empty)';
1600                continue;
1601            }
1602
1603            $i = mb_strpos($prop, ":");
1604            if ($i === false) {
1605                if ($DEBUGCSS) print 'novalue' . $prop . ')';
1606                continue;
1607            }
1608
1609            $prop_name = rtrim(mb_strtolower(mb_substr($prop, 0, $i)));
1610            $value = ltrim(mb_substr($prop, $i + 1));
1611            if ($DEBUGCSS) print $prop_name . ':=' . $value . ($important ? '!IMPORTANT' : '') . ')';
1612            //New style, anyway empty
1613            //if ($important || !$style->important_get($prop_name) ) {
1614            //$style->$prop_name = array($value,$important);
1615            //assignment might be replaced by overloading through __set,
1616            //and overloaded functions might check _important_props,
1617            //therefore set _important_props first.
1618            if ($important) {
1619                $style->important_set($prop_name);
1620            }
1621            //For easier debugging, don't use overloading of assignments with __set
1622            $style->$prop_name = $value;
1623            //$style->props_set($prop_name, $value);
1624        }
1625        if ($DEBUGCSS) print '_parse_properties]';
1626
1627        return $style;
1628    }
1629
1630    /**
1631     * parse selector + rulesets
1632     *
1633     * @param string $str CSS selectors and rulesets
1634     * @param array $media_queries
1635     */
1636    private function _parse_sections($str, $media_queries = array())
1637    {
1638        // Pre-process: collapse all whitespace and strip whitespace around '>',
1639        // '.', ':', '+', '#'
1640
1641        $patterns = array("/[\\s\n]+/", "/\\s+([>.:+#])\\s+/");
1642        $replacements = array(" ", "\\1");
1643        $str = preg_replace($patterns, $replacements, $str);
1644        $DEBUGCSS = $this->_dompdf->getOptions()->getDebugCss();
1645
1646        $sections = explode("}", $str);
1647        if ($DEBUGCSS) print '[_parse_sections';
1648        foreach ($sections as $sect) {
1649            $i = mb_strpos($sect, "{");
1650            if ($i === false) { continue; }
1651
1652            //$selectors = explode(",", mb_substr($sect, 0, $i));
1653            $selectors = preg_split("/,(?![^\(]*\))/", mb_substr($sect, 0, $i),0, PREG_SPLIT_NO_EMPTY);
1654            if ($DEBUGCSS) print '[section';
1655
1656            $style = $this->_parse_properties(trim(mb_substr($sect, $i + 1)));
1657
1658            // Assign it to the selected elements
1659            foreach ($selectors as $selector) {
1660                $selector = trim($selector);
1661
1662                if ($selector == "") {
1663                    if ($DEBUGCSS) print '#empty#';
1664                    continue;
1665                }
1666                if ($DEBUGCSS) print '#' . $selector . '#';
1667                //if ($DEBUGCSS) { if (strpos($selector,'p') !== false) print '!!!p!!!#'; }
1668
1669                //FIXME: tag the selector with a hash of the media query to separate it from non-conditional styles (?), xpath comments are probably not what we want to do here
1670                if (count($media_queries) > 0) {
1671                    $style->set_media_queries($media_queries);
1672                }
1673                $this->add_style($selector, $style);
1674            }
1675
1676            if ($DEBUGCSS) {
1677                print 'section]';
1678            }
1679        }
1680
1681        if ($DEBUGCSS) {
1682            print '_parse_sections]';
1683        }
1684    }
1685
1686    /**
1687     * @return string
1688     */
1689    public static function getDefaultStylesheet()
1690    {
1691        $dir = realpath(__DIR__ . "/../..");
1692        return $dir . self::DEFAULT_STYLESHEET;
1693    }
1694
1695    /**
1696     * @param FontMetrics $fontMetrics
1697     * @return $this
1698     */
1699    public function setFontMetrics(FontMetrics $fontMetrics)
1700    {
1701        $this->fontMetrics = $fontMetrics;
1702        return $this;
1703    }
1704
1705    /**
1706     * @return FontMetrics
1707     */
1708    public function getFontMetrics()
1709    {
1710        return $this->fontMetrics;
1711    }
1712
1713    /**
1714     * dumps the entire stylesheet as a string
1715     *
1716     * Generates a string of each selector and associated style in the
1717     * Stylesheet.  Useful for debugging.
1718     *
1719     * @return string
1720     */
1721    function __toString()
1722    {
1723        $str = "";
1724        foreach ($this->_styles as $selector => $selector_styles) {
1725            /** @var Style $style */
1726            foreach ($selector_styles as $style) {
1727                $str .= "$selector => " . $style->__toString() . "\n";
1728            }
1729        }
1730
1731        return $str;
1732    }
1733}
1734