1<?php
2
3class CSSRuleset {
4  var $rules;
5  var $tag_filtered;
6  var $_lastId;
7
8  function CSSRuleset() {
9    $this->rules        = array();
10    $this->tag_filtered = array();
11    $this->_lastId      = 0;
12  }
13
14  function parse_style_node($root, &$pipeline) {
15    // Check if this style node have 'media' attribute
16    // and if we're using this media;
17    //
18    // Note that, according to the HTML 4.01 p.14.2.3
19    // This attribute specifies the intended destination medium for style information.
20    // It may be a single media descriptor or a comma-separated list.
21    // The default value for this attribute is "screen".
22    //
23    $media_list = array("screen");
24    if ($root->has_attribute("media")) {
25      // Note that there may be whitespace symbols around commas, so we should not just use 'explode' function
26      $media_list = preg_split("/\s*,\s*/",trim($root->get_attribute("media")));
27    };
28
29    if (!is_allowed_media($media_list)) {
30      if (defined('DEBUG_MODE')) {
31        error_log(sprintf('No allowed (%s) media types found in CSS stylesheet media types (%s). Stylesheet ignored.',
32                          join(',', config_get_allowed_media()),
33                          join(',', $media_list)));
34      };
35      return;
36    };
37
38    if (!isset($GLOBALS['g_stylesheet_title']) ||
39        $GLOBALS['g_stylesheet_title'] === "") {
40      $GLOBALS['g_stylesheet_title'] = $root->get_attribute("title");
41    };
42
43    if (!$root->has_attribute("title") || $root->get_attribute("title") === $GLOBALS['g_stylesheet_title']) {
44      /**
45       * Check if current node is empty (then, we don't need to parse its contents)
46       */
47      $content = trim($root->get_content());
48      if ($content != "") {
49        $this->parse_css($content, $pipeline);
50      };
51    };
52  }
53
54  function scan_styles($root, &$pipeline) {
55    switch ($root->node_type()) {
56    case XML_ELEMENT_NODE:
57      $tagname = strtolower($root->tagname());
58
59      if ($tagname === 'style') {
60        // Parse <style ...> ... </style> nodes
61        //
62        $this->parse_style_node($root, $pipeline);
63
64      } elseif ($tagname === 'link') {
65        // Parse <link rel="stylesheet" ...> nodes
66        //
67        $rel   = strtolower($root->get_attribute("rel"));
68
69        $type  = strtolower($root->get_attribute("type"));
70        if ($root->has_attribute("media")) {
71          $media = explode(",",$root->get_attribute("media"));
72        } else {
73          $media = array();
74        };
75
76        if ($rel == "stylesheet" &&
77            ($type == "text/css" || $type == "") &&
78            (count($media) == 0 || is_allowed_media($media)))  {
79          // Attempt to escape URL automaticaly
80          $url_autofix = new AutofixUrl();
81          $src = $url_autofix->apply(trim($root->get_attribute('href')));
82
83          if ($src) {
84            $this->css_import($src, $pipeline);
85          };
86        };
87      };
88
89      // Note that we continue processing here!
90    case XML_DOCUMENT_NODE:
91
92      // Scan all child nodes
93      $child = $root->first_child();
94      while ($child) {
95        $this->scan_styles($child, $pipeline);
96        $child = $child->next_sibling();
97      };
98      break;
99    };
100  }
101
102  function parse_css($css, &$pipeline, $baseindex = 0) {
103    $allowed_media = implode("|",config_get_allowed_media());
104
105    // remove the UTF8 byte-order mark from the beginning of the file (several high-order symbols at the beginning)
106    $pos = 0;
107    $len = strlen($css);
108    while (ord($css{$pos}) > 127 && $pos < $len) { $pos ++; };
109    $css = substr($css, $pos);
110
111    // Process @media rules;
112    // basic syntax is:
113    // @media <media>(,<media>)* { <rules> }
114    //
115
116    while (preg_match("/^(.*?)@media([^{]+){(.*)$/s",$css,$matches)) {
117      $head  = $matches[1];
118      $media = $matches[2];
119      $rest  = $matches[3];
120
121      // Process CSS rules placed before the first @media declaration - they should be applied to
122      // all media types
123      //
124      $this->parse_css_media($head, $pipeline, $baseindex);
125
126      // Extract the media content
127      if (!preg_match("/^((?:[^{}]*{[^{}]*})*)[^{}]*\s*}(.*)$/s", $rest, $matches)) {
128        die("CSS media syntax error\n");
129      } else {
130        $content = $matches[1];
131        $tail    = $matches[2];
132      };
133
134      // Check if this media is to be processed
135      if (preg_match("/".$allowed_media."/i", $media)) {
136        $this->parse_css_media($content, $pipeline, $baseindex);
137      };
138
139      // Process the rest of CSS file
140      $css = $tail;
141    };
142
143    // The rest of CSS file belogs to common media, process it too
144    $this->parse_css_media($css, $pipeline, $baseindex);
145  }
146
147  function css_import($src, &$pipeline) {
148    // Update the base url;
149    // all urls will be resolved relatively to the current stylesheet url
150    $url = $pipeline->guess_url($src);
151    $data = $pipeline->fetch($url);
152
153    /**
154     * If referred file could not be fetched return immediately
155     */
156    if (is_null($data)) { return; };
157
158    $css = $data->get_content();
159    if (!empty($css)) {
160      /**
161       * Sometimes, external stylesheets contain <!-- and --> at the beginning and
162       * at the end; we should remove these characters, as they may break parsing of
163       * first and last rules
164       */
165      $css = preg_replace('/^\s*<!--/', '', $css);
166      $css = preg_replace('/-->\s*$/', '', $css);
167
168      $this->parse_css($css, $pipeline);
169    };
170
171    $pipeline->pop_base_url();
172  }
173
174  function parse_css_import($import, &$pipeline) {
175    if (preg_match("/@import\s+[\"'](.*)[\"'];/",$import, $matches)) {
176      // @import "<url>"
177      $this->css_import(trim($matches[1]), $pipeline);
178    } elseif (preg_match("/@import\s+url\((.*)\);/",$import, $matches)) {
179      // @import url()
180      $this->css_import(trim(css_remove_value_quotes($matches[1])), $pipeline);
181    } elseif (preg_match("/@import\s+(.*);/",$import, $matches)) {
182      // @import <url>
183      $this->css_import(trim(css_remove_value_quotes($matches[1])), $pipeline);
184    };
185  }
186
187  function parse_css_media($css, &$pipeline, $baseindex = 0) {
188    // Remove comments
189    $css = preg_replace("#/\*.*?\*/#is","",$css);
190
191    // Extract @page rules
192    $css = parse_css_atpage_rules($css, $pipeline);
193
194    // Extract @import rules
195    if ($num = preg_match_all("/@import[^;]+;/",$css, $matches, PREG_PATTERN_ORDER)) {
196      for ($i=0; $i<$num; $i++) {
197        $this->parse_css_import($matches[0][$i], $pipeline);
198      }
199    };
200
201    // Remove @import rules so they will not break further processing
202    $css = preg_replace("/@import[^;]+;/","", $css);
203
204    while (preg_match("/([^{}]*){(.*?)}(.*)/is", $css, $matches)) {
205      // Drop extracted part
206      $css = $matches[3];
207
208      // Save extracted part
209      $raw_selectors  = $matches[1];
210      $raw_properties = $matches[2];
211
212      $selectors  = parse_css_selectors($raw_selectors);
213
214      $properties = parse_css_properties($raw_properties, $pipeline);
215
216      foreach ($selectors as $selector) {
217        $this->_lastId ++;
218        $rule = array($selector,
219                      $properties,
220                      $pipeline->get_base_url(),
221                      $this->_lastId + $baseindex);
222        $this->add_rule($rule,
223                        $pipeline);
224      };
225    };
226  }
227
228  function add_rule(&$rule, &$pipeline) {
229    $rule_obj      = new CSSRule($rule, $pipeline);
230    $this->rules[] = $rule_obj;
231
232    $tag = $this->detect_applicable_tag($rule_obj->get_selector());
233    if (is_null($tag)) {
234      $tag = "*";
235    }
236    $this->tag_filtered[$tag][] = $rule_obj;
237  }
238
239  function apply(&$root, &$state, &$pipeline) {
240    $local_css = array();
241
242    if (isset($this->tag_filtered[strtolower($root->tagname())])) {
243      $local_css = $this->tag_filtered[strtolower($root->tagname())];
244    };
245
246    if (isset($this->tag_filtered["*"])) {
247      $local_css = array_merge($local_css, $this->tag_filtered["*"]);
248    };
249
250    $applicable = array();
251
252    foreach ($local_css as $rule) {
253      if ($rule->match($root)) {
254        $applicable[] = $rule;
255      };
256    };
257
258    usort($applicable, "cmp_rule_objs");
259
260    foreach ($applicable as $rule) {
261      switch ($rule->get_pseudoelement()) {
262      case SELECTOR_PSEUDOELEMENT_BEFORE:
263        $handler =& CSS::get_handler(CSS_HTML2PS_PSEUDOELEMENTS);
264        $handler->replace($handler->get($state->getState()) | CSS_HTML2PS_PSEUDOELEMENTS_BEFORE, $state);
265        break;
266      case SELECTOR_PSEUDOELEMENT_AFTER:
267        $handler =& CSS::get_handler(CSS_HTML2PS_PSEUDOELEMENTS);
268        $handler->replace($handler->get($state->getState()) | CSS_HTML2PS_PSEUDOELEMENTS_AFTER, $state);
269        break;
270      default:
271        $rule->apply($root, $state, $pipeline);
272        break;
273      };
274    };
275  }
276
277  function apply_pseudoelement($element_type, &$root, &$state, &$pipeline) {
278    $local_css = array();
279
280    if (isset($this->tag_filtered[strtolower($root->tagname())])) {
281      $local_css = $this->tag_filtered[strtolower($root->tagname())];
282    };
283
284    if (isset($this->tag_filtered["*"])) {
285      $local_css = array_merge($local_css, $this->tag_filtered["*"]);
286    };
287
288    $applicable = array();
289
290    for ($i=0; $i<count($local_css); $i++) {
291      $rule =& $local_css[$i];
292      if ($rule->get_pseudoelement() == $element_type) {
293        if ($rule->match($root)) {
294          $applicable[] =& $rule;
295        };
296      };
297    };
298
299    usort($applicable, "cmp_rule_objs");
300
301    // Note that filtered rules already have pseudoelement mathing (see condition above)
302
303    foreach ($applicable as $rule) {
304      $rule->apply($root, $state, $pipeline);
305    };
306  }
307
308  // Check if only tag with a specific name can match this selector
309  //
310  function detect_applicable_tag($selector) {
311    switch (selector_get_type($selector)) {
312    case SELECTOR_TAG:
313      return $selector[1];
314    case SELECTOR_TAG_CLASS:
315      return $selector[1];
316    case SELECTOR_SEQUENCE:
317      foreach ($selector[1] as $subselector) {
318        $tag = $this->detect_applicable_tag($subselector);
319        if ($tag) { return $tag; };
320      };
321      return null;
322    default:
323      return null;
324    }
325  }
326}
327
328?>