/** * Checks the selector value. * * @param $value * @return bool */ public function checkValue(&$value) { if (is_string($value)) { $value = Placeholder::replaceStringsAndComments($value); $value = Placeholder::removeCommentPlaceholders($value, true); $value = preg_replace('/[ ]+/', ' ', $value); $value = Placeholder::replaceStringPlaceholders($value, true); return true; } else { throw new \InvalidArgumentException("Invalid type '" . gettype($value) . "' for argument 'value' given."); } }
/** * @param string|null $ruleString * @param StyleSheet|null $styleSheet */ public function __construct($ruleString = null, StyleSheet $styleSheet = null) { if ($styleSheet !== null) { $this->setStyleSheet($styleSheet); } if ($ruleString !== null) { $ruleString = Placeholder::replaceStringsAndComments($ruleString); $ruleString = Placeholder::removeCommentPlaceholders($ruleString, true); $ruleString = Placeholder::replaceStringPlaceholders($ruleString); $this->parseRuleString($ruleString); } }
/** * Parses the charset rule. * * @param string $ruleString */ protected function parseRuleString($ruleString) { if (is_string($ruleString)) { // Check for valid rule format // (with vendor prefix check to match e.g. "@-moz-document") if (preg_match('/^[ \\r\\n\\t\\f]*@(' . self::getVendorPrefixRegExp("/") . ')?document[ \\r\\n\\t\\f]+(.*)$/i', $ruleString, $matches)) { $vendorPrefix = $matches[1]; $ruleString = trim($matches[2], " \r\n\t\f"); $charset = $this->getCharset(); $inFunction = false; $isEscaped = false; $conditions = []; $currentCondition = ""; $currentValue = ""; for ($i = 0, $j = mb_strlen($ruleString, $charset); $i < $j; $i++) { $char = mb_substr($ruleString, $i, 1, $charset); if ($char === "\\") { if ($isEscaped === false) { $isEscaped = true; } else { $isEscaped = false; } } else { if ($char === "(") { if ($isEscaped === false) { $inFunction = true; continue; } else { $currentValue .= $char; } } else { if ($char === ")") { if ($isEscaped === false) { $conditions[$currentCondition] = trim($currentValue, " \r\n\t\f"); $currentCondition = ""; $currentValue = ""; $inFunction = false; continue; } else { $currentValue .= $char; } } else { if ($char === "," || $char === " ") { if ($currentCondition === "" && $currentValue === "") { continue; } elseif ($currentValue !== "") { $currentValue .= $char; } else { // something wrong here... } } else { if ($inFunction === false) { $currentCondition .= $char; } else { $currentValue .= $char; } } } } } // Reset escaped flag if ($isEscaped === true && $char !== "\\") { $isEscaped = false; } } foreach ($conditions as $key => $value) { $conditions[$key] = Placeholder::replaceStringPlaceholders($value, true); } if (isset($conditions["url"])) { $this->setUrl($conditions["url"]); } if (isset($conditions["url-prefix"])) { $this->setUrlPrefix($conditions["url-prefix"]); } if (isset($conditions["domain"])) { $this->setDomain($conditions["domain"]); } if (isset($conditions["regexp"])) { $this->setRegexp($conditions["regexp"]); } if ($vendorPrefix !== "") { $this->setVendorPrefix($vendorPrefix); } } else { throw new \InvalidArgumentException("Invalid format for @document rule."); } } else { throw new \InvalidArgumentException("Invalid type '" . gettype($ruleString) . "' for argument 'ruleString' given. String expected."); } }
/** * Checks the declaration value. * * @param string $value * @return bool */ public function checkValue(&$value) { if (is_string($value)) { $value = Placeholder::replaceStringsAndComments($value); $value = Placeholder::removeCommentPlaceholders($value, true); $value = Placeholder::replaceStringPlaceholders($value, true); $value = trim($value); if ($value !== '') { return true; } else { $this->setIsValid(false); } } else { throw new \InvalidArgumentException("Invalid type '" . gettype($value) . "' for argument 'value' given."); } }
/** * Parses the CSS source content. */ protected function parseCss() { // Init variables $this->styleSheet = new StyleSheet(); $cssContent = ""; // Prepare CSS content to allow easy parsing; // temporarily replace all strings. $blockCount = 0; $ruleCount = 0; $ruleBlock = 0; $inBrackets = false; $charsetIgnored = true; $charsetReplaced = false; if (($handle = $this->getCssResource()) !== false) { // Determine charset in the correct order, defined in // http://www.w3.org/TR/css-syntax-3/#input-byte-stream $charset = null; $fileContainsBom = false; if (($firstLine = fgets($handle)) === false) { $firstLine = ""; } // Check for a BOM and use it, if it exists. "The decode algorithm gives precedence to a byte order mark // (BOM), and only uses the fallback when none is found." $bom = pack("CCC", 0xef, 0xbb, 0xbf); if (strlen($firstLine) >= 3 && strncmp($firstLine, $bom, 3) === 0) { $charset = "UTF-8"; $fileContainsBom = true; } else { // Fallback 1: The encoding defined in HTTP or equivalent protocol $charset = $this->getProtocolEncoding(); // Fallback 2: The charset as defined in the CSS file if ($charset === null) { if (preg_match('/^@charset\\s+(["\'])([-a-zA-Z0-9_]+)\\g{1}/i', $firstLine, $matches)) { $charset = $matches[2]; $charsetIgnored = false; // Auto-correction of the defined charset. //"If the return value was utf-16be or utf-16le, use utf-8 as the fallback encoding". if (in_array(strtoupper($charset), ["UTF-16BE", "UTF-16LE"])) { $charset = "UTF-8"; $charsetReplaced = true; } } } // Fallback 3: The environment encoding of the referencing document if ($charset === null) { $charset = $this->getEnvironmentEncoding(); } // Fallback 4: Default to UTF-8 if ($charset === null) { $charset = "UTF-8"; } } $this->setCharset($charset); // Set position back to the beginning (but skip BOMs) fseek($handle, $fileContainsBom ? 3 : 0); while (($css = fgets($handle)) !== false) { // Required check to avoid errors when the encoding of the // file doesn't match the set/detected charset. if (mb_check_encoding($css, $charset) === false) { throw new \RuntimeException("Invalid '{$charset}' encoding in CSS file."); } if (preg_match('/[^\\x00-\\x7f]/', $css)) { $isAscii = false; $strLen = mb_strlen($css, $charset); } else { $isAscii = true; $strLen = strlen($css); } for ($i = 0, $j = $strLen; $i < $j; $i++) { if ($isAscii === true) { $char = $css[$i]; } else { $char = mb_substr($css, $i, 1, $charset); } if ($char === "{") { $blockCount++; $cssContent .= "\n_BLOCKSTART_" . $blockCount . "_\n"; if ($ruleCount > $ruleBlock) { $ruleBlock++; } } else { if ($char === "}") { $cssContent .= "\n_BLOCKEND_" . $blockCount . "_\n"; $blockCount--; if ($blockCount < $ruleCount) { if ($ruleCount > 0) { $cssContent .= "\n_RULEEND_" . $ruleCount . "_\n"; $ruleCount--; } } if ($ruleCount > 0) { $ruleBlock--; } } elseif ($char === ";") { $cssContent .= $char; if ($ruleCount > 0 && $ruleBlock === 0) { $cssContent .= "\n_RULEEND_1_\n"; $ruleCount--; } } else { // Start new at-rule, but only if we are not in brackets, which still can occur, although we // replaced all strings, e.g. in this case: "background: url(/images/myimage-@1x.png)". if ($char === "@" && $inBrackets === false) { if ($ruleCount > 0 && $blockCount === 0) { $errorCss = Placeholder::replaceCommentPlaceholders(Placeholder::replaceStringPlaceholders($css)); throw new \RuntimeException("Parse error near '{$errorCss}'."); } $ruleCount++; $cssContent .= "\n_RULESTART_" . $ruleCount . "_\n"; // Replace all white-space characters within rule definitions by normal space to get // one line only } elseif ($ruleCount >= $blockCount && in_array($char, ["\r", "\n", "\t", "\f"])) { $char = " "; } elseif ($char === "(") { $inBrackets = true; } elseif ($char === ")") { $inBrackets = false; } $cssContent .= $char; } } } } // Auto-correction as required by CSS specs while ($blockCount > 0) { $cssContent .= "\n_BLOCKEND_" . $blockCount . "_\n"; $blockCount--; } while ($ruleCount > 0) { $cssContent .= "\n_RULEEND_" . $ruleCount . "_\n"; $ruleCount--; } } // Prettify... $cssContent = preg_replace('/;/', ";\n", $cssContent); $cssContent = preg_replace('/[\\t\\f]+/', "", $cssContent); $cssContent = preg_replace('/[ ]+/', " ", $cssContent); $cssContent = preg_replace('/(\\n)[ ]|[ ](\\n)/', "\\1\\2", $cssContent); $cssContent = preg_replace('/(?<!_)[ \\t\\n\\r\\f]*(:)[ \\t\\n\\r\\f]*/', "\\1", $cssContent); $cssContent = preg_replace('/([\\r\\n])+/', "\\1", $cssContent); $cssContent = preg_replace('/^\\n|\\n$/', "", $cssContent); $cssContent = preg_replace('/^(_COMMENT_[a-f0-9]{32}_)([^\\r\\n]+)/m', "\\1\n\\2", $cssContent); // Parse $lines = explode("\n", $cssContent); $ruleCount = 0; $blockCount = 0; $lastRuleContainers = [$this->styleSheet]; $lastRuleSet = null; // Prepare vendor prefix regular expression $vendorPrefixRegExp = RuleAbstract::getVendorPrefixRegExp("/"); $comment = null; $atRuleCharsetAllowed = true; $atRuleImportAllowed = true; $atRuleNamespaceAllowed = true; foreach ($lines as $line) { if (preg_match('/^(?J)(?:_(?P<type>RULESTART|RULEEND|BLOCKSTART|BLOCKEND)_\\d+_|_(?P<type>COMMENT)_[a-f0-9]{32}_)/', $line, $matches)) { if ($matches['type'] === 'RULESTART') { $ruleCount++; } elseif ($matches['type'] === 'RULEEND') { $ruleCount--; if ($ruleCount === $blockCount) { // Current rule finished } } elseif ($matches['type'] === 'BLOCKSTART') { $blockCount++; } elseif ($matches['type'] === 'BLOCKEND') { $blockCount--; if ($blockCount === $ruleCount) { if ($comment !== null) { /** @var AtRuleAbstract $lastRuleSet */ $lastRuleSet->addComment($comment); $comment = null; } // Current rule set finished $lastRuleSet = null; } else { if ($comment !== null) { /** @var AtRuleAbstract[] $lastRuleContainers */ $lastRuleContainers[$ruleCount]->addComment($comment); $comment = null; } } } elseif ($matches['type'] === 'COMMENT') { $comment = rtrim($line); } } else { if ($blockCount < $ruleCount) { // New rule opened if (preg_match('/^@(' . $vendorPrefixRegExp . ')?([a-zA-Z_]{1}(?:[-a-zA-Z0-9_]*|[^[:ascii:]*]))/i', trim($line), $matches)) { $identifier = mb_strtolower($matches[2], $this->getCharset()); switch ($identifier) { case "charset": $atRule = new CharsetRule($line, $this->styleSheet); break; case "import": $atRule = new ImportRule($line, $this->styleSheet); break; case "namespace": $atRule = new NamespaceRule($line, $this->styleSheet); break; case "media": $atRule = new MediaRule($line, $this->styleSheet); break; case "supports": $atRule = new SupportsRule($line, $this->styleSheet); break; case "document": $atRule = new DocumentRule($line, $this->styleSheet); break; case "font-face": $atRule = new FontFaceRule($line, $this->styleSheet); break; case "page": $atRule = new PageRule($line, $this->styleSheet); break; case "keyframes": $atRule = new KeyframesRule($line, $this->styleSheet); break; default: throw new \InvalidArgumentException("Unknown at rule identifier '{$identifier}'."); } // Add vendor prefix if ($matches[1] !== "") { $vendorPrefix = mb_strtolower($matches[1], $this->getCharset()); $atRule->setVendorPrefix($vendorPrefix); } } else { throw new \InvalidArgumentException("Invalid rule format in '{$line}'."); } // IMPORTANT: // - The @charset rule must be the first element in the style sheet and not be preceded by any // character. // - Any @import rules must precede all other types of rules, except @charset rules (and other // @import rules). // - Any @namespace rules must follow all @charset and @import rules (and other @namespace rules) // and precede all other non-ignored at-rules and style rules in a style-sheet. if ($atRule instanceof CharsetRule) { if ($atRuleCharsetAllowed === false) { // As defined by CSS specs, the rule has been ignored, du to an invalid position in the // style sheet. E.g. @charset must be the first content of the file, @import must be first // or follow @charset or @import, and @namespace can only follow to @charset, @import or // @namespace. $atRule->setIsValid(false); $atRule->addValidationError("Ignored @charset rule, because at wrong position in style sheet."); } elseif ($charsetIgnored === true) { // As defined by CSS specs, the charset rule has been ignored, due to charset information // from other sources (e.g. BOMs in the file or defined protocol encoding). $atRule->setIsValid(false); $atRule->addValidationError("Ignored @charset rule, because charset got from other source with higher priority."); $atRuleCharsetAllowed = false; } elseif ($charsetReplaced === true) { // As defined by CSS specs, the charset defined by the charset rule has been replaced with // "UTF-8", because an UTF-16* charset has been used. $atRule->setIsValid(false); $atRule->addValidationError("Replaced charset in @charset rule with 'UTF-8', because defined charset is invalid."); $atRuleCharsetAllowed = false; } else { $atRuleCharsetAllowed = false; } } elseif ($atRule instanceof ImportRule) { // As defined by CSS specs, the rule has been ignored, du to an invalid position in the style // sheet. E.g. @charset must be the first content of the file, @import must be first or follow // @charset or @import, and @namespace can only follow to @charset, @import or @namespace. if ($atRuleImportAllowed === false) { $atRule->setIsValid(false); $atRule->addValidationError("Ignored @import rule, because at wrong position in style sheet."); } $atRuleCharsetAllowed = false; } elseif ($atRule instanceof NamespaceRule) { // As defined by CSS specs, the rule has been ignored, du to an invalid position in the style // sheet. E.g. @charset must be the first content of the file, @import must be first or follow // @charset or @import, and @namespace can only follow to @charset, @import or @namespace. if ($atRuleNamespaceAllowed === false) { $atRule->setIsValid(false); $atRule->addValidationError("Ignored @namespace rule, because at wrong position in style sheet."); } $atRuleCharsetAllowed = false; $atRuleImportAllowed = false; } else { $atRuleCharsetAllowed = false; $atRuleImportAllowed = false; $atRuleNamespaceAllowed = false; } $lastRuleContainers[$ruleCount - 1]->addRule($atRule); if ($atRule instanceof AtRuleConditionalAbstract) { $lastRuleContainers[$ruleCount] = $atRule; } elseif ($atRule instanceof KeyframesRule) { $lastRuleContainers[$ruleCount] = $atRule; } elseif ($atRule instanceof FontFaceRule) { $lastRuleContainers[$ruleCount] = $atRule; $lastRuleSet = $atRule; } elseif ($atRule instanceof PageRule) { $lastRuleContainers[$ruleCount] = $atRule; $lastRuleSet = $atRule; } // Not all at-rules contain other rule, e.g. in @page rules the rules are mixed with the // at-rule, so they directly contain declarations - this is filtered by checking for the // HasRulesInterface here. } elseif ($blockCount === $ruleCount && $lastRuleContainers[$ruleCount] instanceof HasRulesInterface) { // New rule set opened if ($lastRuleContainers[$ruleCount] instanceof KeyframesRule) { $ruleSet = new KeyframesRuleSet($line, $this->styleSheet); } else { $ruleSet = new StyleRuleSet($line, $this->styleSheet); } if ($comment !== null) { $ruleSet->addComment($comment); $comment = null; } $lastRuleContainers[$ruleCount]->addRule($ruleSet); $lastRuleSet = $ruleSet; $atRuleCharsetAllowed = false; } elseif ($blockCount >= $ruleCount) { // New declaration if ($lastRuleSet !== null) { $line = preg_replace('/[\\s;]+$/', '', $line); $invalidDeclaration = false; if (strpos($line, ":") === false) { $property = $line; $value = ""; $invalidDeclaration = true; } else { list($property, $value) = explode(":", $line, 2); } $declaration = null; if ($lastRuleContainers[$ruleCount] instanceof StyleSheet) { $declaration = new StyleDeclaration($property, $value, $this->styleSheet); } elseif ($lastRuleContainers[$ruleCount] instanceof AtRuleConditionalAbstract) { $declaration = new StyleDeclaration($property, $value, $this->styleSheet); } elseif ($lastRuleContainers[$ruleCount] instanceof KeyframesRule) { $declaration = new KeyframesDeclaration($property, $value, $this->styleSheet); } elseif ($lastRuleContainers[$ruleCount] instanceof FontFaceRule) { $declaration = new FontFaceDeclaration($property, $value, $this->styleSheet); } elseif ($lastRuleContainers[$ruleCount] instanceof PageRule) { $declaration = new PageDeclaration($property, $value, $this->styleSheet); } if ($declaration !== null) { if ($comment !== null) { $declaration->addComment($comment); $comment = null; } if ($invalidDeclaration === true) { $declaration->setIsValid(false); $declaration->addValidationError("Parse error. Invalid declaration at '{$line}'."); } $lastRuleSet->addDeclaration($declaration); } } $atRuleCharsetAllowed = false; } } } }
/** * Parses the namespace rule. * * @param string $ruleString */ protected function parseRuleString($ruleString) { if (is_string($ruleString)) { $charset = $this->getCharset(); // Remove at-rule and unnecessary white-spaces $ruleString = preg_replace('/^[ \\r\\n\\t\\f]*@namespace[ \\r\\n\\t\\f]+/i', '', $ruleString); $ruleString = trim($ruleString, " \r\n\t\f"); // Remove trailing semicolon $ruleString = rtrim($ruleString, ";"); $isEscaped = false; $inFunction = false; $parts = []; $currentPart = ""; for ($i = 0, $j = mb_strlen($ruleString, $charset); $i < $j; $i++) { $char = mb_substr($ruleString, $i, 1, $charset); if ($char === "\\") { if ($isEscaped === false) { $isEscaped = true; } else { $isEscaped = false; } } else { if ($char === " ") { if ($isEscaped === false) { if ($inFunction == false) { $currentPart = trim($currentPart, " \r\n\t\f"); if ($currentPart !== "") { $parts[] = trim($currentPart, " \r\n\t\f"); $currentPart = ""; } } } else { $currentPart .= $char; } } elseif ($isEscaped === false && $char === "(") { $inFunction = true; $currentPart .= $char; } elseif ($isEscaped === false && $char === ")") { $inFunction = false; $currentPart .= $char; } else { $currentPart .= $char; } } // Reset escaped flag if ($isEscaped === true && $char !== "\\") { $isEscaped = false; } } if ($currentPart !== "") { $currentPart = trim($currentPart, " \r\n\t\f"); if ($currentPart !== "") { $parts[] = trim($currentPart, " \r\n\t\f"); } } foreach ($parts as $key => $value) { $parts[$key] = Placeholder::replaceStringPlaceholders($value); } $countParts = count($parts); if ($countParts === 2) { $this->setPrefix($parts[0]); // Get URL value $name = Url::extractUrl($parts[1]); $this->setName($name); } elseif ($countParts === 1) { // Get URL value $name = Url::extractUrl($parts[0]); $this->setName($name); } else { // ERROR } } else { throw new \InvalidArgumentException("Invalid type '" . gettype($ruleString) . "' for argument 'ruleString' given."); } }