00001 <?php
00024 class quailTest {
00025
00029 var $dom;
00030
00034 var $css;
00035
00039 var $path;
00040
00045 var $cms = true;
00046
00050 var $base_path;
00051
00055 var $report;
00056
00060 var $default_severity = QUAIL_TEST_SUGGESTION;
00061
00065 var $image_extensions = array('gif', 'jpg', 'png', 'jpeg', 'tiff', 'svn');
00066
00070 var $lang = 'en';
00071
00076 var $services = array();
00077
00081 var $strings = array('en' => '');
00082
00091 function __construct(&$dom, &$css, &$path, $language_domain = 'en', $options = null) {
00092 $this->dom = $dom;
00093 $this->css = $css;
00094 $this->path = $path;
00095 $this->lang = $language_domain;
00096 $this->options = $options;
00097 $this->report = array();
00098 $this->loadServices();
00099 $this->check();
00100 }
00101
00102
00103
00109 function loadServices() {
00110 foreach($this->services as $service => $file_name) {
00111 if(!class_exists($service .'Service')) {
00112 require_once('services/'. $file_name .'.php');
00113 }
00114 $service_name = $service .'Service';
00115 $this->services[$service] = new $service_name();
00116 }
00117 }
00118
00124 function getReport() {
00125 $this->report['severity'] = $this->default_severity;
00126 return $this->report;
00127 }
00128
00133 function getSeverity() {
00134 return $this->default_severity;
00135 }
00144 function addReport($element = null, $message = null, $pass = null) {
00145 $report = new quailReportItem();
00146 $report->element = $element;
00147 $report->message = $message;
00148 $report->pass = $pass;
00149 $this->report[] = $report;
00150 }
00151
00157 function getPath($file) {
00158 if(substr($file, 0, 7) == 'http://' || substr($file, 0, 8) == 'https://')
00159 return $file;
00160 $file = explode('/', $file);
00161 if(count($file) == 1)
00162 return implode('/', $this->path) .'/'. $file[0];
00163
00164 $path = $this->path;
00165 foreach($file as $directory) {
00166 if($directory == '..')
00167 array_pop($path);
00168 else
00169 $file_path[] = $directory;
00170 }
00171
00172 return implode('/', $path) .'/'. implode('/', $file_path);
00173
00174 }
00175
00181 function translation() {
00182 if(isset($this->strings[$this->lang])) {
00183 return $this->strings[$this->lang];
00184 }
00185 if(isset($this->strings['en'])) {
00186 return $this->strings['en'];
00187 }
00188 return false;
00189 }
00190
00200 function getAllElements($tags = null, $options = false, $value = true) {
00201 if(!is_array($tags))
00202 $tags = array($tags);
00203 if($options !== false)
00204 $tags = htmlElements::getElementsByOption($options, $value);
00205 $result = array();
00206
00207 if(!is_array($tags))
00208 return array();
00209 foreach($tags as $tag) {
00210 $elements = $this->dom->getElementsByTagName($tag);
00211 if($elements) {
00212 foreach($elements as $element) {
00213 $result[] = $element;
00214 }
00215 }
00216 }
00217 if(count($result) == 0)
00218 return array();
00219 return $result;
00220 }
00221
00229 function elementHasChild($element, $child_tag) {
00230 foreach($element->childNodes as $child) {
00231 if(property_exists($child, 'tagName') && $child->tagName == $child_tag)
00232 return true;
00233 }
00234 return false;
00235 }
00236
00244 function getElementAncestor($element, $ancestor_tag, $limit_tag = 'body') {
00245 while(property_exists($element, 'parentNode')) {
00246 if($element->parentNode->tagName == $ancestor_tag) {
00247 return $element->parentNode;
00248 }
00249 if($element->parentNode->tagName == $limit_tag) {
00250 return false;
00251 }
00252 $element = $element->parentNode;
00253 }
00254 return false;
00255 }
00256
00267 function getElementsByAttribute($tag, $attribute, $unique = false) {
00268 $results = array();
00269 foreach($this->getAllElements($tag) as $element) {
00270 if($element->hasAttribute($attribute)) {
00271 if($unique)
00272 $results[$element->getAttribute($attribute)] = $element;
00273 else
00274 $results[$element->getAttribute($attribute)][] = $element;
00275 }
00276 }
00277 return $results;
00278 }
00279
00285 function getNextElement($element) {
00286 $parent = $element->parentNode;
00287 $next = false;
00288 foreach($parent->childNodes as $child) {
00289 if($next)
00290 return $child;
00291 if($child->isSameNode($element))
00292 $next = true;
00293 }
00294 return false;
00295 }
00296
00307 function propertyIsEqual($object, $property, $value, $trim = false, $lower = false) {
00308 if(!is_object($object)) {
00309 return false;
00310 }
00311 if(!property_exists($object, $property)) {
00312 return false;
00313 }
00314 $property_value = $object->$property;
00315 if($trim) {
00316 $property_value = trim($property_value);
00317 $value = trim($value);
00318 }
00319 if($lower) {
00320 $property_value = strtolower($property_value);
00321 $value = strtolower($value);
00322 }
00323 return ($property_value == $value);
00324 }
00325
00336 function getParent($element, $tag_name, $limiter) {
00337 while($element) {
00338 if($element->tagName == $tag_name)
00339 return $element;
00340 if($element->tagName == $limiter)
00341 return false;
00342 $element = $element->parentNode;
00343 }
00344 return false;
00345 }
00346
00351 function imageIsAnimated($filename) {
00352 if(!($fh = @fopen($filename, 'rb')))
00353 return false;
00354 $count = 0;
00355
00356
00357
00358
00359
00360
00361
00362
00363 while(!feof($fh) && $count < 2)
00364 $chunk = fread($fh, 1024 * 100);
00365 $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00\x2C#s', $chunk, $matches);
00366
00367 fclose($fh);
00368 return $count > 1;
00369 }
00370
00377 function elementContainsReadableText($element) {
00378 if(is_a($element, 'DOMText')) {
00379 if(trim($element->wholeText) != '') {
00380 return true;
00381 }
00382 }
00383 else {
00384 if(trim($element->nodeValue) != '' ||
00385 ($element->hasAttribute('alt') && trim($element->getAttribute('alt')) != '')) {
00386 return true;
00387 }
00388 if(method_exists($element, 'hasChildNodes') && $element->hasChildNodes()) {
00389 foreach($element->childNodes as $child) {
00390 if($this->elementContainsReadableText($child)) {
00391 return true;
00392 }
00393 }
00394 }
00395 }
00396 return false;
00397 }
00398
00399 }
00400
00409 class quailTagTest extends quailTest {
00410
00414 var $tag = '';
00415
00420 function check() {
00421 foreach($this->getAllElements($this->tag) as $element) {
00422 $this->addReport($element);
00423 }
00424 }
00425 }
00426
00431 class quailHeaderTest extends quailTest {
00432
00436 var $tag = '';
00437
00441 var $headers = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6');
00442
00447 function check() {
00448 $tag_number = substr($this->tag, -1, 1);
00449 $doc_headers = array();
00450 $first_header = $this->dom->getElementsByTagName($this->tag);
00451 if($first_header->item(0)) {
00452 $current = $first_header->item(0);
00453 $previous_number = intval(substr($current->tagName, -1, 1));
00454 while($current) {
00455
00456 if(property_exists($current, 'tagName') && in_array($current->tagName, $this->headers)) {
00457 $current_number = intval(substr($current->tagName, -1, 1));
00458 if($current_number > ($previous_number + 1))
00459 $this->addReport($current);
00460 $previous_number = intval(substr($current->tagName, -1, 1));
00461 }
00462 $current = $current->nextSibling;
00463
00464 }
00465
00466 }
00467
00468 }
00469 }
00470
00474 class quailTableTest extends quailTest {
00475
00483 function getTable($table) {
00484 $rows = 0;
00485 $columns = 0;
00486 $first_row = true;
00487 if($table->tagName != 'table')
00488 return false;
00489 foreach($table->childNodes as $child) {
00490 if(property_exists($child, 'tagName') && $child->tagName == 'tr') {
00491 $rows++;
00492 if($first_row) {
00493 foreach($child->childNodes as $column_child) {
00494 if($column_child->tagName == 'th' || $column_child->tagName == 'td')
00495 $columns++;
00496 }
00497 $first_row = false;
00498 }
00499 }
00500 }
00501
00502 return array('rows' => $rows, 'columns' => $columns);
00503 }
00504
00512 function isData($table) {
00513 if($table->tagName != 'table')
00514 return false;
00515 foreach($table->childNodes as $child) {
00516 if(property_exists($child, 'tagName') && $child->tagName == 'tr') {
00517 foreach($child->childNodes as $row_child) {
00518 if(property_exists($row_child, 'tagName') && $row_child->tagName == 'th')
00519 return true;
00520 }
00521 }
00522 if(property_exists($child, 'tagName') && $child->tagName == 'thead')
00523 return true;
00524 }
00525 return false;
00526 }
00527
00528 }
00529
00536 class inputHasLabel extends quailTest {
00537
00541 var $tag = 'input';
00542
00546 var $type = 'text';
00547
00551 var $no_type = false;
00552
00557 function check() {
00558 foreach($this->getAllElements('label') as $label) {
00559 if($label->hasAttribute('for'))
00560 $labels[$label->getAttribute('for')] = $label;
00561 else {
00562 foreach($label->childNodes as $child) {
00563 if(property_exists($child, 'tagName') &&
00564 $child->tagName == $this->tag &&
00565 ($child->getAttribute('type') == $this->type || $this->no_type)) {
00566 $input_in_label[$child->getAttribute('name')] = $child;
00567 }
00568 }
00569 }
00570 }
00571 foreach($this->getAllElements($this->tag) as $input) {
00572 if($input->getAttribute('type') == $this->type || $this->no_type) {
00573 if(!$input->hasAttribute('title')) {
00574 if(!isset($input_in_label[$input->getAttribute('name')])) {
00575 if(!isset($labels[$input->getAttribute('id')]) || trim($labels[$input->getAttribute('id')]->nodeValue) == '')
00576 $this->addReport($input);
00577 }
00578
00579 }
00580 }
00581 }
00582 }
00583
00584 }
00585
00589 class inputTabIndex extends quailTest {
00590
00594 var $tag;
00595
00599 var $type;
00600
00604 var $no_type = false;
00605
00610 function check() {
00611 foreach($this->getAllElements($this->tag) as $element) {
00612 if(($element->getAttribute('type') == $this->type)
00613 && (!($element->hasAttribute('tabindex'))
00614 || !is_numeric($element->getAttribute('tabindex'))))
00615 $this->addReport($element);
00616 }
00617 }
00618 }
00619
00623 class quailColorTest extends quailTest {
00624
00625 var $color_names = array(
00626 'aliceblue' => 'f0f8ff',
00627 'antiquewhite' => 'faebd7',
00628 'aqua' => '00ffff',
00629 'aquamarine' => '7fffd4',
00630 'azure' => 'f0ffff',
00631 'beige' => 'f5f5dc',
00632 'bisque' => 'ffe4c4',
00633 'black' => '000000',
00634 'blanchedalmond' => 'ffebcd',
00635 'blue' => '0000ff',
00636 'blueviolet' => '8a2be2',
00637 'brown' => 'a52a2a',
00638 'burlywood' => 'deb887',
00639 'cadetblue' => '5f9ea0',
00640 'chartreuse' => '7fff00',
00641 'chocolate' => 'd2691e',
00642 'coral' => 'ff7f50',
00643 'cornflowerblue' => '6495ed',
00644 'cornsilk' => 'fff8dc',
00645 'crimson' => 'dc143c',
00646 'cyan' => '00ffff',
00647 'darkblue' => '00008b',
00648 'darkcyan' => '008b8b',
00649 'darkgoldenrod' => 'b8860b',
00650 'darkgray' => 'a9a9a9',
00651 'darkgreen' => '006400',
00652 'darkkhaki' => 'bdb76b',
00653 'darkmagenta' => '8b008b',
00654 'darkolivegreen' => '556b2f',
00655 'darkorange' => 'ff8c00',
00656 'darkorchid' => '9932cc',
00657 'darkred' => '8b0000',
00658 'darksalmon' => 'e9967a',
00659 'darkseagreen' => '8fbc8f',
00660 'darkslateblue' => '483d8b',
00661 'darkslategray' => '2f4f4f',
00662 'darkturquoise' => '00ced1',
00663 'darkviolet' => '9400d3',
00664 'deeppink' => 'ff1493',
00665 'deepskyblue' => '00bfff',
00666 'dimgray' => '696969',
00667 'dodgerblue' => '1e90ff',
00668 'firebrick' => 'b22222',
00669 'floralwhite' => 'fffaf0',
00670 'forestgreen' => '228b22',
00671 'fuchsia' => 'ff00ff',
00672 'gainsboro' => 'dcdcdc',
00673 'ghostwhite' => 'f8f8ff',
00674 'gold' => 'ffd700',
00675 'goldenrod' => 'daa520',
00676 'gray' => '808080',
00677 'green' => '008000',
00678 'greenyellow' => 'adff2f',
00679 'honeydew' => 'f0fff0',
00680 'hotpink' => 'ff69b4',
00681 'indianred' => 'cd5c5c',
00682 'indigo' => '4b0082',
00683 'ivory' => 'fffff0',
00684 'khaki' => 'f0e68c',
00685 'lavender' => 'e6e6fa',
00686 'lavenderblush' => 'fff0f5',
00687 'lawngreen' => '7cfc00',
00688 'lemonchiffon' => 'fffacd',
00689 'lightblue' => 'add8e6',
00690 'lightcoral' => 'f08080',
00691 'lightcyan' => 'e0ffff',
00692 'lightgoldenrodyellow' => 'fafad2',
00693 'lightgrey' => 'd3d3d3',
00694 'lightgreen' => '90ee90',
00695 'lightpink' => 'ffb6c1',
00696 'lightsalmon' => 'ffa07a',
00697 'lightseagreen' => '20b2aa',
00698 'lightskyblue' => '87cefa',
00699 'lightslategray' => '778899',
00700 'lightsteelblue' => 'b0c4de',
00701 'lightyellow' => 'ffffe0',
00702 'lime' => '00ff00',
00703 'limegreen' => '32cd32',
00704 'linen' => 'faf0e6',
00705 'magenta' => 'ff00ff',
00706 'maroon' => '800000',
00707 'mediumaquamarine' => '66cdaa',
00708 'mediumblue' => '0000cd',
00709 'mediumorchid' => 'ba55d3',
00710 'mediumpurple' => '9370d8',
00711 'mediumseagreen' => '3cb371',
00712 'mediumslateblue' => '7b68ee',
00713 'mediumspringgreen' => '00fa9a',
00714 'mediumturquoise' => '48d1cc',
00715 'mediumvioletred' => 'c71585',
00716 'midnightblue' => '191970',
00717 'mintcream' => 'f5fffa',
00718 'mistyrose' => 'ffe4e1',
00719 'moccasin' => 'ffe4b5',
00720 'navajowhite' => 'ffdead',
00721 'navy' => '000080',
00722 'oldlace' => 'fdf5e6',
00723 'olive' => '808000',
00724 'olivedrab' => '6b8e23',
00725 'orange' => 'ffa500',
00726 'orangered' => 'ff4500',
00727 'orchid' => 'da70d6',
00728 'palegoldenrod' => 'eee8aa',
00729 'palegreen' => '98fb98',
00730 'paleturquoise' => 'afeeee',
00731 'palevioletred' => 'd87093',
00732 'papayawhip' => 'ffefd5',
00733 'peachpuff' => 'ffdab9',
00734 'peru' => 'cd853f',
00735 'pink' => 'ffc0cb',
00736 'plum' => 'dda0dd',
00737 'powderblue' => 'b0e0e6',
00738 'purple' => '800080',
00739 'red' => 'ff0000',
00740 'rosybrown' => 'bc8f8f',
00741 'royalblue' => '4169e1',
00742 'saddlebrown' => '8b4513',
00743 'salmon' => 'fa8072',
00744 'sandybrown' => 'f4a460',
00745 'seagreen' => '2e8b57',
00746 'seashell' => 'fff5ee',
00747 'sienna' => 'a0522d',
00748 'silver' => 'c0c0c0',
00749 'skyblue' => '87ceeb',
00750 'slateblue' => '6a5acd',
00751 'slategray' => '708090',
00752 'snow' => 'fffafa',
00753 'springgreen' => '00ff7f',
00754 'steelblue' => '4682b4',
00755 'tan' => 'd2b48c',
00756 'teal' => '008080',
00757 'thistle' => 'd8bfd8',
00758 'tomato' => 'ff6347',
00759 'turquoise' => '40e0d0',
00760 'violet' => 'ee82ee',
00761 'wheat' => 'f5deb3',
00762 'white' => 'ffffff',
00763 'whitesmoke' => 'f5f5f5',
00764 'yellow' => 'ffff00',
00765 'yellowgreen' => '9acd32'
00766 );
00767
00775 function getLuminosity($foreground, $background) {
00776 if($foreground == $background) return 0;
00777 $fore_rgb = $this->getRGB($foreground);
00778 $back_rgb = $this->getRGB($background);
00779 return $this->luminosity($fore_rgb['r'], $back_rgb['r'],
00780 $fore_rgb['g'], $back_rgb['g'],
00781 $fore_rgb['b'], $back_rgb['b']);
00782 }
00783
00794 function luminosity($r,$r2,$g,$g2,$b,$b2) {
00795 $RsRGB = $r/255;
00796 $GsRGB = $g/255;
00797 $BsRGB = $b/255;
00798 $R = ($RsRGB <= 0.03928) ? $RsRGB/12.92 : pow(($RsRGB+0.055)/1.055, 2.4);
00799 $G = ($GsRGB <= 0.03928) ? $GsRGB/12.92 : pow(($GsRGB+0.055)/1.055, 2.4);
00800 $B = ($BsRGB <= 0.03928) ? $BsRGB/12.92 : pow(($BsRGB+0.055)/1.055, 2.4);
00801
00802 $RsRGB2 = $r2/255;
00803 $GsRGB2 = $g2/255;
00804 $BsRGB2 = $b2/255;
00805 $R2 = ($RsRGB2 <= 0.03928) ? $RsRGB2/12.92 : pow(($RsRGB2+0.055)/1.055, 2.4);
00806 $G2 = ($GsRGB2 <= 0.03928) ? $GsRGB2/12.92 : pow(($GsRGB2+0.055)/1.055, 2.4);
00807 $B2 = ($BsRGB2 <= 0.03928) ? $BsRGB2/12.92 : pow(($BsRGB2+0.055)/1.055, 2.4);
00808
00809 if ($r+$g+$b <= $r2+$g2+$b2) {
00810 $l2 = (.2126 * $R + 0.7152 * $G + 0.0722 * $B);
00811 $l1 = (.2126 * $R2 + 0.7152 * $G2 + 0.0722 * $B2);
00812 } else {
00813 $l1 = (.2126 * $R + 0.7152 * $G + 0.0722 * $B);
00814 $l2 = (.2126 * $R2 + 0.7152 * $G2 + 0.0722 * $B2);
00815 }
00816
00817 $luminosity = round(($l1 + 0.05)/($l2 + 0.05),2);
00818 return $luminosity;
00819 }
00820
00821
00827 function getRGB($color) {
00828 $color = $this->convertColor($color);
00829 $c = str_split($color, 2);
00830 if(count($c) != 3) {
00831 return false;
00832 }
00833 $results = array('r' => hexdec($c[0]), 'g' => hexdec($c[1]), 'b' => hexdec($c[2]));
00834 return $results;
00835 }
00836
00842 function convertColor($color) {
00843 $color = trim($color);
00844 if(strpos($color, ' ') !== false) {
00845 $colors = explode(' ', $color);
00846 foreach($colors as $background_part) {
00847 if(substr(trim($background_part), 0, 1) == '#' ||
00848 in_array(trim($background_part), array_keys($this->color_names)) ||
00849 strtolower(substr(trim($background_part), 0, 3)) == 'rgb') {
00850 $color = $background_part;
00851 }
00852 }
00853 }
00854
00855 if(substr($color, 0, 1) == '#') {
00856 if(strlen($color) == 7) {
00857 return str_replace('#', '', $color);
00858 }
00859 elseif (strlen($color == 4)) {
00860 return substr($color, 1, 1).substr($color, 1, 1).
00861 substr($color, 2, 1).substr($color, 2, 1).
00862 substr($color, 3, 1).substr($color, 3, 1);
00863 }
00864 }
00865
00866 if(in_array($color, array_keys($this->color_names))) {
00867 return $this->color_names[$color];
00868 }
00869
00870 if(strtolower(substr($color, 0, 3)) == 'rgb') {
00871 $colors = explode(',', trim(str_replace('rgb(', '', $color), '()'));
00872 if(!count($colors) != 3) {
00873 return false;
00874 }
00875 $r = intval($colors[0]);
00876 $g = intval($colors[1]);
00877 $b = intval($colors[2]);
00878
00879 $r = dechex($r<0?0:($r>255?255:$r));
00880 $g = dechex($g<0?0:($g>255?255:$g));
00881 $b = dechex($b<0?0:($b>255?255:$b));
00882
00883 $color = (strlen($r) < 2?'0':'').$r;
00884 $color .= (strlen($g) < 2?'0':'').$g;
00885 $color .= (strlen($b) < 2?'0':'').$b;
00886 return $color;
00887 }
00888 }
00889
00894 function getWaiErtContrast($foreground, $background) {
00895 $fore_rgb = $this->getRGB($foreground);
00896 $back_rgb = $this->getRGB($background);
00897 $diffs = $this->getWaiDiffs($fore_rgb, $back_rgb);
00898
00899 return $diffs['red'] + $diffs['green'] + $diffs['blue'];
00900 }
00901
00906 function getWaiErtBrightness($foreground, $background) {
00907 $fore_rgb = $this->getRGB($foreground);
00908 $back_rgb = $this->getRGB($background);
00909 $color = $this->getWaiDiffs($fore_rgb, $back_rgb);
00910 return (($color['red'] * 299) + ($color['green'] * 587) + ($color['blue'] * 114)) / 1000;
00911 }
00912
00913 function getWaiDiffs($fore_rgb, $back_rgb) {
00914 $red_diff = ($fore_rgb['r'] > $back_rgb['r'])
00915 ? $fore_rgb['r'] - $back_rgb['r']
00916 : $back_rgb['r'] - $fore_rgb['r'];
00917 $green_diff = ($fore_rgb['g'] > $back_rgb['g'])
00918 ? $fore_rgb['g'] - $back_rgb['g']
00919 : $back_rgb['g'] - $fore_rgb['g'];
00920
00921 $blue_diff = ($fore_rgb['b'] > $back_rgb['b'])
00922 ? $fore_rgb['b'] - $back_rgb['b']
00923 : $back_rgb['b'] - $fore_rgb['b'];
00924 return array('red' => $red_diff, 'green' => $green_diff, 'blue' => $blue_diff);
00925 }
00926 }
00927
00932 class bodyWaiErtColorContrast extends quailColorTest {
00933
00937 var $background = 'bgcolor';
00938
00942 var $foreground = 'text';
00943
00947 function check() {
00948 $body = $this->getAllElements('body');
00949 if(!$body)
00950 return false;
00951 $body = $body[0];
00952 if($body->hasAttribute($this->foreground) && $body->hasAttribute($this->background))
00953 if( $this->getWaiErtContrast($body->getAttribute($this->foreground), $body->getAttribute($this->background)) < 500)
00954 $this->addReport(null, null, false);
00955 elseif($this->getWaiErtBrightness($body->getAttribute($this->foreground), $body->getAttribute($this->background)) < 125)
00956 $this->addReport(null, null, false);
00957
00958 }
00959
00960 }
00961
00966 class bodyColorContrast extends quailColorTest {
00967
00971 var $background = 'bgcolor';
00972
00976 var $foreground = 'text';
00977
00981 function check() {
00982 $body = $this->getAllElements('body');
00983 if(!$body)
00984 return false;
00985 $body = $body[0];
00986 if($body->hasAttribute($this->foreground) && $body->hasAttribute($this->background))
00987 if( $this->getLuminosity($body->getAttribute($this->foreground), $body->getAttribute($this->background)) < 5)
00988 $this->addReport(null, null, false);
00989 }
00990 }