diff --git a/src/Rule/BrokenLink.php b/src/Rule/BrokenLink.php new file mode 100644 index 0000000..c0c0475 --- /dev/null +++ b/src/Rule/BrokenLink.php @@ -0,0 +1,64 @@ +getAllElements('a') as $a) { + $href = $a->getAttribute('href'); + if ($href) { + $links[$href] = $a; + } + } + $this->checkLink($links); + + return count($this->issues); + } + + private function checkLink($links) { + $curls = array(); + $mcurl = curl_multi_init(); + foreach (array_keys($links) as $i => $link) { + $curls[$i] = curl_init(); + curl_setopt($curls[$i], CURLOPT_URL, $link); + curl_setopt($curls[$i], CURLOPT_HEADER, true); + curl_setopt($curls[$i], CURLOPT_NOBODY, true); + curl_setopt($curls[$i], CURLOPT_REFERER, true); + curl_setopt($curls[$i], CURLOPT_TIMEOUT, 2); + curl_setopt($curls[$i], CURLOPT_TIMEOUT, 2); + curl_setopt($curls[$i], CURLOPT_AUTOREFERER, true); + curl_setopt($curls[$i], CURLOPT_RETURNTRANSFER, true); + curl_setopt($curls[$i], CURLOPT_FOLLOWLOCATION, true); + curl_multi_add_handle($mcurl, $curls[$i]); + } + $running = null; + do { + curl_multi_exec($mcurl, $running); + } while ($running > 0); + foreach (array_keys($links) as $i => $link) { + $status = curl_getinfo($curls[$i], CURLINFO_RESPONSE_CODE); + // If the status is greater than or equal to 400 the link is broken. + if ($status >= 400) { + $this->setIssue($links[$link]); + } + curl_multi_remove_handle($mcurl, $curls[$i]); + } + curl_multi_close($mcurl); + } +} diff --git a/src/Rule/RedirectedLink.php b/src/Rule/RedirectedLink.php new file mode 100644 index 0000000..63e493c --- /dev/null +++ b/src/Rule/RedirectedLink.php @@ -0,0 +1,114 @@ +getAllElements('a') as $a) { + $href = $a->getAttribute('href'); + if ($href) { + $links[$href] = $a; + } + } + $this->checkLink($links); + + return count($this->issues); + } + + private function checkLink($links) { + $curls = array(); + $mcurl = curl_multi_init(); + foreach (array_keys($links) as $i => $link) { + $curls[$i] = curl_init(); + curl_setopt($curls[$i], CURLOPT_URL, $link); + curl_setopt($curls[$i], CURLOPT_HEADER, true); + curl_setopt($curls[$i], CURLOPT_NOBODY, true); + curl_setopt($curls[$i], CURLOPT_REFERER, true); + curl_setopt($curls[$i], CURLOPT_TIMEOUT, 2); + curl_setopt($curls[$i], CURLOPT_TIMEOUT, 2); + curl_setopt($curls[$i], CURLOPT_AUTOREFERER, true); + curl_setopt($curls[$i], CURLOPT_RETURNTRANSFER, true); + curl_setopt($curls[$i], CURLOPT_FOLLOWLOCATION, true); + curl_multi_add_handle($mcurl, $curls[$i]); + } + $running = null; + do { + curl_multi_exec($mcurl, $running); + } while ($running > 0); + foreach (array_keys($links) as $i => $link) { + $status = curl_getinfo($curls[$i], CURLINFO_RESPONSE_CODE); + // If the status is 400 or greater the link is broken so dont bother checking. + if ($status < 400) { + $this->checkRedirect($links[$link]); + } + curl_multi_remove_handle($mcurl, $curls[$i]); + } + curl_multi_close($mcurl); + } + + private function checkRedirect($original) { + $link = $original->getAttribute('href'); + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $link); + curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLOPT_NOBODY, true); + curl_setopt($curl, CURLOPT_REFERER, true); + curl_setopt($curl, CURLOPT_TIMEOUT, 2); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + + curl_exec($curl); + $redirect = curl_getinfo($curl, CURLINFO_REDIRECT_URL); + $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); + curl_close($curl); + + // Only permanent redirections are a problem + if ($status === 301 || $status === 308) { + $this->followPermanentRedirects($original, $redirect); + } + } + + private function followPermanentRedirects($original, $link, $maxRedirects = 20) { + // Avoid infinite calls. 20 is chrome and firefox redirect limit. + if ($maxRedirects < 1) { + $this->setIssue($original, null, json_encode(array('redirect_url' => $link))); + return; + } + + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $link); + curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLOPT_NOBODY, true); + curl_setopt($curl, CURLOPT_REFERER, true); + curl_setopt($curl, CURLOPT_TIMEOUT, 2); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + + curl_exec($curl); + $redirect = curl_getinfo($curl, CURLINFO_REDIRECT_URL); + $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); + curl_close($curl); + + // Continue until we run out of permanent redirects + if ($status === 301 || $status === 308) { + $this->followPermanentRedirects($original, $redirect, $maxRedirects - 1); + } else { + $this->setIssue($original, null, json_encode(array('redirect_url' => $link))); + } + } +} + diff --git a/src/rules.json b/src/rules.json index 9802b43..514369c 100644 --- a/src/rules.json +++ b/src/rules.json @@ -5,6 +5,7 @@ "CidiLabs\\PhpAlly\\Rule\\AnchorSuspiciousLinkText", "CidiLabs\\PhpAlly\\Rule\\BaseFontIsNotUsed", "CidiLabs\\PhpAlly\\Rule\\BlinkIsNotUsed", + "CidiLabs\\PhpAlly\\Rule\\BrokenLink", "CidiLabs\\PhpAlly\\Rule\\ContentTooLong", "CidiLabs\\PhpAlly\\Rule\\CssTextHasContrast", "CidiLabs\\PhpAlly\\Rule\\CssTextStyleEmphasize", @@ -29,6 +30,7 @@ "CidiLabs\\PhpAlly\\Rule\\ObjectTagDetected", "CidiLabs\\PhpAlly\\Rule\\ParagraphNotUsedAsHeader", "CidiLabs\\PhpAlly\\Rule\\PreShouldNotBeUsedForTabularValues", + "CidiLabs\\PhpAlly\\Rule\\RedirectedLink", "CidiLabs\\PhpAlly\\Rule\\TableDataShouldHaveTableHeader", "CidiLabs\\PhpAlly\\Rule\\TableHeaderShouldHaveScope", "CidiLabs\\PhpAlly\\Rule\\VideoCaptionsMatchCourseLanguage", diff --git a/tests/BrokenLinkTest.php b/tests/BrokenLinkTest.php new file mode 100644 index 0000000..4a42807 --- /dev/null +++ b/tests/BrokenLinkTest.php @@ -0,0 +1,35 @@ +I am a link.
'; + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->loadHTML($html); + $rule = new BrokenLink($dom); + + $this->assertEquals(0, $rule->check(), 'BrokenLink should have no issue.'); + } + + public function testCheckBroken400() + { + $html = '
I am a link.
'; + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->loadHTML($html); + $rule = new BrokenLink($dom); + + $this->assertEquals(1, $rule->check(), 'BrokenLink should have one issue.'); + } + + public function testCheckBroken404() + { + $html = '
I am a link.
'; + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->loadHTML($html); + $rule = new BrokenLink($dom); + + $this->assertEquals(1, $rule->check(), 'BrokenLink should have one issue.'); + } +} diff --git a/tests/PhpAllyTest.php b/tests/PhpAllyTest.php index 50ddc13..c8d8b08 100644 --- a/tests/PhpAllyTest.php +++ b/tests/PhpAllyTest.php @@ -29,7 +29,7 @@ public function testCheckOne() $this->phpAllyReportTest($report); } - public function testCheckMany() + public function testCheckMany() { $ally = new PhpAlly(); $options = [ @@ -41,17 +41,17 @@ public function testCheckMany() $report = $ally->checkMany($this->getManyHtml(), $ally->getRuleIds(), $options); $issues = $report->getIssues(); $issue = reset($issues); - - $this->assertCount(6, $issues, 'Total report should have 5 issues.'); + + $this->assertCount(6, $issues, 'Total report should have 6 issues.'); $this->phpAllyIssueTest($issue); $this->phpAllyReportTest($report); } - + protected function phpAllyReportTest($report) { $issues = $report->getIssues(); - + foreach($issues as $issue) { $this->phpAllyIssueTest($issue); } @@ -64,4 +64,4 @@ protected function phpAllyIssueTest(PhpAllyIssue $issue) $this->assertEquals(DOMElement::class, get_class($issue->getPreviewElement()), 'Issue return DomElement for getPreviewElement()'); } -} \ No newline at end of file +} diff --git a/tests/PhpAllyTestCase.php b/tests/PhpAllyTestCase.php index 913bc5b..0f5154e 100644 --- a/tests/PhpAllyTestCase.php +++ b/tests/PhpAllyTestCase.php @@ -18,10 +18,10 @@ protected function getManyHtml() { return '

Paragraph text is here.

- - Click Here - - + + Click Here + +

Paragraph text does have enough contrast.

Paragraph text has enough contrast.

@@ -129,4 +129,4 @@ protected function getImageHtml() { return ''; } -} \ No newline at end of file +} diff --git a/tests/RedirectedLinkTest.php b/tests/RedirectedLinkTest.php new file mode 100644 index 0000000..fa9df98 --- /dev/null +++ b/tests/RedirectedLinkTest.php @@ -0,0 +1,42 @@ +I am a link.
'; + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->loadHTML($html); + $rule = new RedirectedLink($dom); + + $this->assertEquals(0, $rule->check(), 'RedirectedLink should have no issue.'); + } + + public function testCheckRedirected() + { + $html = '
I am a link.
'; + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->loadHTML($html); + $rule = new RedirectedLink($dom); + + $this->assertEquals(1, $rule->check(), 'RedirectedLink should have one issue.'); + } + + public function testCheckRedirectedAndMetadata() + { + $html = '
I am a link.
'; + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->loadHTML($html); + $rule = new RedirectedLink($dom); + + // Check if metadata is present with a new link + $result = $rule->check(); + if ($rule->getIssues() && count($rule->getIssues()) == 1) { + $meta = $rule->getIssues()[0]->getMetadata(); + $result = 1 + $result; + } + + $this->assertEquals(2, $result, 'RedirectedLink should have one issue.'); + } +}