classes/Model/CssFormat.php
<?php
/**
* A11yc\Model\CssFormat
*
* @package part of A11yc
* @author Jidaikobo Inc.
* @license The MIT License (MIT)
* @copyright Jidaikobo Inc.
* @link http://www.jidaikobo.com
*/
namespace A11yc\Model;
use A11yc\Model;
trait CssFormat
{
protected static $vendors = array(
'-ms-', '-moz-', '-webkit-', '-o-', '-moz-osx-'
);
protected static $css_props = array(
'color', 'opacity', 'background', 'background-attachment', 'background-clip',
'background-color', 'background-image', 'background-origin', 'background-position',
'background-repeat', 'background-size', 'border', 'border-bottom',
'border-bottom-color', 'border-bottom-left-radius', 'border-bottom-right-radius',
'border-bottom-style', 'border-bottom-width', 'border-color', 'border-image',
'border-image-outset', 'border-image-repeat', 'border-image-slice',
'border-image-source', 'border-image-width', 'border-left', 'border-left-color',
'border-left-style', 'border-left-width', 'border-radius', 'border-right',
'border-right-color', 'border-right-style', 'border-right-width', 'border-style',
'border-top', 'border-top-color', 'border-top-left-radius',
'border-top-right-radius', 'border-top-style', 'border-top-width', 'border-width',
'box-decoration-break', 'box-shadow', 'image-resolution', 'object-fit',
'object-position', 'marquee-direction', 'marquee-play-count', 'marquee-speed',
'marquee-style', 'break-after', 'break-before', 'break-inside', 'column-count',
'column-fill', 'column-gap', 'column-rule', 'column-rule-color', 'column-rule-style',
'column-rule-width', 'column-span', 'column-width', 'columns', 'cue', 'cue-after',
'cue-before', 'pause', 'pause-after', 'pause-before', 'rest', 'rest-after',
'rest-before', 'speak', 'speak-as', 'voice-balance', 'voice-duration', 'voice-family',
'voice-pitch', 'voice-range', 'voice-rate', 'voice-stress', 'voice-volume',
'backface-visibility', 'perspective', 'perspective-origin',
'transform', 'transform-origin', 'transform-style', 'transition', 'transition-delay',
'transition-duration', 'transition-property', 'transition-timing-function',
'animation', 'animation-delay', 'animation-direction', 'animation-duration',
'animation-fill-mode', 'animation-iteration-count', 'animation-name',
'animation-play-state', 'animation-timing-function',
'align-content', 'align-items', 'align-self', 'flex', 'flex-basis', 'flex-direction',
'flex-flow', 'flex-grow', 'flex-shrink', 'flex-wrap', 'justify-content', 'order',
'font', 'font-family', 'font-feature-settings', 'font-kerning',
'font-language-override', 'font-size', 'font-size-adjust', 'font-stretch',
'font-style', 'font-synthesis', 'font-variant', 'font-variant-alternates',
'font-variant-caps', 'font-variant-east-asian', 'font-variant-ligatures',
'font-variant-numeric', 'font-variant-position', 'font-weight',
'fit', 'fit-position', 'image-orientation', 'orphans', 'page', 'page-break-after',
'page-break-before', 'page-break-inside', 'size', 'widows', 'hanging-punctuation',
'hyphens', 'letter-spacing', 'line-break', 'overflow-wrap', 'tab-size',
'text-align', 'text-align-last', 'text-decoration', 'text-decoration-color',
'text-decoration-line', 'text-decoration-skip', 'text-decoration-style',
'text-emphasis', 'text-emphasis-color', 'text-emphasis-position',
'text-emphasis-style', 'text-indent', 'text-justify', 'text-shadow',
'text-transform', 'text-underline-position', 'white-space', 'word-break',
'word-spacing', 'box-sizing', 'cursor', 'icon', 'ime-mode',
'nav-down', 'nav-index', 'nav-left', 'nav-right', 'nav-up', 'outline',
'outline-color', 'outline-offset', 'outline-style', 'outline-width',
'resize', 'text-overflow', 'direction', 'text-combine-horizontal',
'text-combine-mode', 'text-orientation', 'unicode-bidi', 'writing-mode',
'marks', 'grid-cell', 'grid-column', 'grid-column-align', 'grid-column-sizing',
'grid-column-span', 'grid-columns', 'grid-flow', 'grid-row', 'grid-row-align',
'grid-row-sizing', 'grid-row-span', 'grid-rows', 'grid-template', 'list-style',
'list-style-image', 'list-style-position', 'list-style-type', 'bottom', 'clip',
'left', 'position', 'right', 'top', 'z-index', 'border-collapse', 'border-spacing',
'caption-side', 'empty-cells', 'table-layout',
'clear', 'display', 'float', 'height', 'margin', 'margin-bottom', 'margin-left',
'margin-right', 'margin-top', 'max-height', 'max-width', 'min-height', 'min-width',
'overflow', 'overflow-style', 'overflow-x', 'overflow-y', 'padding',
'padding-bottom', 'padding-left', 'padding-right', 'padding-top',
'visibility', 'width', 'content', 'counter-increment', 'counter-reset', 'crop',
'move-to', 'page-policy', 'quotes', 'alignment-adjust', 'alignment-baseline',
'baseline-shift', 'dominant-baseline', 'drop-initial-after-adjust',
'drop-initial-after-align', 'drop-initial-before-adjust',
'drop-initial-before-align', 'drop-initial-size', 'drop-initial-value',
'inline-box-align', 'line-height', 'line-stacking', 'line-stacking-ruby',
'line-stacking-shift', 'line-stacking-strategy', 'text-height', 'vertical-align',
'ruby-align', 'ruby-overhang', 'ruby-position', 'ruby-span',
'target', 'target-name', 'target-new', 'target-position',
'filter', 'text-rendering', 'font-smoothing', 'appearance'
);
/**
* makeArray
*
* @param String $css
* @return Array
*/
public static function makeArray($css)
{
// remove comments, import or so
$css = preg_replace('/\/\*.+?\*\//is', '', $css);
$css = preg_replace('/^@import.+?$/mis', '', $css);
$css = preg_replace('/^@charset.+?$/mis', '', $css);
$css = preg_replace('/^@(?:page|media)[^{]*?{[\n\s\t]*?}/mis', '', $css); // empty
// check paren num
$start = mb_substr_count($css, '{');
$end = mb_substr_count($css, '}');
Model\Css::$is_suspicious_paren_num = $start != $end;
// media query and keyframes
preg_match_all(
'/@(?:page|media|font-face|keyframes|-webkit-keyframes).+?}.*?}/is',
$css,
$ms
);
$css = str_replace($ms[0], '', $css);
// divide blocks
$csses = self::divideBlocks($ms[0], $css);
// divide selectors and properties
$rets = self::divideSelectorsAndProperties($csses);
// remove vendor prefix
foreach (Model\Css::$suspicious_props as $k => $v)
{
foreach (self::$css_props as $prop)
{
foreach (self::$vendors as $vendor)
{
if ($v == $vendor.$prop) unset(Model\Css::$suspicious_props[$k]);
}
}
}
return $rets;
}
/**
* divide blocks
*
* @param Array $arr
* @param String $css
* @return Array
*/
private static function divideBlocks($arr, $css)
{
$csses = array();
$csses['base'] = explode('}', $css);
foreach ($arr as $m)
{
$atmarks = substr($m, 0, strpos($m, '{'));
$atmarks = trim($atmarks);
$vals = substr($m, strpos($m, '{'));
$vals = trim(trim($vals), '}');
$csses[$atmarks] = explode('}', $vals);
}
return $csses;
}
/**
* divide selectors and properties
*
* @param Array $csses
* @return Array
*/
private static function divideSelectorsAndProperties($csses)
{
$rets = array();
foreach ($csses as $type => $type_css)
{
$rets[$type] = array();
foreach ($type_css as $each)
{
if (strpos($each, '{') === false) continue; // invalid
list($selectors, $properties) = explode('{', $each);
$selectors = trim($selectors);
$properties = trim($properties);
if (empty($selectors) || empty($properties)) continue;
// divide selector and properties
$each_selectors = self::divideStrs($selectors, ',');
$each_properties = self::divideStrs($properties, ';');
// divide each properties
$props = self::divideEachProperties($each_properties);
foreach ($each_selectors as $each_selector)
{
if ( ! isset($rets[$type][$each_selector])) $rets[$type][$each_selector] = array();
$tmps = array_merge($rets[$type][$each_selector], $props);
ksort($tmps);
$rets[$type][$each_selector] = $tmps;
}
}
ksort($rets[$type]);
}
return $rets;
}
/**
* divideStr
*
* @param String $strs
* @param String $delimiter
* @return Array
*/
private static function divideStrs($strs, $delimiter)
{
if (strpos($strs, $delimiter) !== false)
{
$each_strs = explode($delimiter, $strs);
$each_strs = array_map('trim', $each_strs);
}
else
{
$each_strs = array(trim($strs));
}
return $each_strs;
}
/**
* divide each properties
*
* @param Array $each_properties
* @return Array
*/
private static function divideEachProperties($each_properties)
{
$props = array();
foreach ($each_properties as $prop_and_val)
{
$prop_and_val = trim($prop_and_val);
// property does't have colon
$prop_and_vals = array();
if (strpos($prop_and_val, ':') !== false)
{
$prop_and_vals = explode(':', $prop_and_val);
$prop_and_vals = array_map('trim', $prop_and_vals);
}
else if( ! empty($prop_and_val))
{
Model\Css::$suspicious_prop_and_vals[] = $prop_and_val;
continue;
}
if (empty($prop_and_vals)) continue;
// suspicious properties
if ( ! in_array($prop_and_vals[0], self::$css_props))
{
Model\Css::$suspicious_props[] = $prop_and_vals[0];
}
if ( ! preg_match('/^[a-zA-Z0-9! \.,\(\)\/#"\'%_+\\\-]+$/', $prop_and_vals[1]))
{
Model\Css::$suspicious_val_prop[] = array($prop_and_vals[0], $prop_and_vals[1]);
}
$props[$prop_and_vals[0]] = $prop_and_vals[1];
}
return $props;
}
}