0, 'timeout' => 10, 'sslverifypeer' => false, ]; protected $validLocationUrlCodes = [301, 302, 200]; protected $warningLocationUrlCodes = [301]; protected $index; protected $line; protected $columns = [ 'line' => [ 'title' => 'Line', 'width' => 6, 'align' => 'center', ], 'testType' => [ 'title' => 'Test Type', 'width' => 15, ], 'url' => [ 'title' => 'URLs', 'width' => 122, ], 'message' => [ 'title' => 'Message', 'width' => 42, ], 'status' => [ 'title' => 'Status', 'width' => 9, ], ]; /** * @var ConsoleColorText */ protected $text; protected $_user; protected $_password; protected $format = self::FORMAT_CONSOLE; protected $noSuccess = false; protected $filterLines; function __construct($options = []) { $this->client = new Client(null, array_merge($this->clientDefaultOptions, $options)); $this->client->getRequest()->setMethod(Request::METHOD_HEAD); $this->text = new ConsoleColorText(); } public function getFormat() { return $this->format; } public function setFormat($format) { $this->format = $format; return $this; } /** * @return boolean */ public function isNoSuccess() { return $this->noSuccess; } /** * @param boolean $noSuccess * @return $this */ public function setNoSuccess($noSuccess) { $this->noSuccess = $noSuccess; return $this; } /** * @return mixed */ public function getFilterLines() { return $this->filterLines; } /** * @param mixed $filterLines * @return $this */ public function setFilterLines($filterLines) { $this->filterLines = $this->parseFilterLines($filterLines); return $this; } protected function parseFilterLines($filterLines) { $matches = []; if (!preg_match('/^(\d+)(\:)?(\d+)?$/', $filterLines, $matches)) { return null; } $start = $matches[1]; $end = isset($matches[3]) ? $matches[3] : null; return [$start, isset($matches[2]) ? $end : $start]; } public function head($url) { $this->_initCredentials(); return $this->client->setUri($url)->send(); } public function validateRedirect($sourceUrl, $destinationUrl, array $flags = []) { if (empty($flags['R'])) { $this->logError(self::TYPE_DST_URL, $destinationUrl, 'R flag is undefined'); return false; } $response = $this->head($sourceUrl); // Validate Source URL Redirect Code if ($flags['R'] != ($code = $response->getStatusCode())) { $this->logError( self::TYPE_SRC_URL, $this->_formatUrl($sourceUrl), 'HTTP ' . $code . ' (' . $flags['R'] . ' expected)' ); return false; } $this->logSuccess(self::TYPE_SRC_URL, $this->_formatUrl($sourceUrl), 'HTTP ' . $code); if ($this->areUrlEqual($sourceUrl, $destinationUrl)) { $this->logError(self::TYPE_DST_URL, $destinationUrl, 'infinite loop'); return false; } /** @var $location Zend\Http\Header\Location */ if (!($location = $response->getHeaders()->get('Location')) || !($locationUrl = $location->getFieldValue())) { $this->logError(self::TYPE_SRC_URL, $this->_formatUrl($sourceUrl), '"Location" header is undefined'); return false; } // Validate Location $locationResponse = $this->head($locationUrl); if (!($isValidLocationCode = in_array($locationResponse->getStatusCode(), $this->validLocationUrlCodes))) { $this->logError( self::TYPE_DST_URL, ' --> ' . $this->_formatUrl($locationUrl), 'unexpected HTTP code ' . $locationResponse->getStatusCode() ); } else { $message = 'HTTP ' . $locationResponse->getStatusCode(); if (in_array($locationResponse->getStatusCode(), $this->warningLocationUrlCodes)) { $this->logWarning(self::TYPE_DST_URL, ' --> ' . $this->_formatUrl($locationUrl), $message); } else { $this->logSuccess(self::TYPE_DST_URL, ' --> ' . $this->_formatUrl($locationUrl), $message); } } if (!$this->areUrlEqual($locationUrl, $destinationUrl)) { $this->logError(self::TYPE_DST_URL_MATCH, ' --> ' . $this->_formatUrl($destinationUrl)); } else { $this->logSuccess(self::TYPE_DST_URL_MATCH, ' --> ' . $this->_formatUrl($destinationUrl)); } return true; } protected function logWarning($testType, $url, $message = null) { return $this->log($testType, $url, $this->_formatWarning($message), $this->_formatWarning(self::STATUS_WARNING)); } protected function logError($testType, $url, $message = null) { return $this->log($testType, $url, $this->_formatError($message), $this->_formatError(self::STATUS_FAILED)); } protected function logSuccess($testType, $url, $message = null) { if ($this->isNoSuccess()) { return $this; } return $this->log($testType, $url, $this->_formatSuccess($message), $this->_formatSuccess(self::STATUS_SUCCESS)); } protected function _formatError($text) { switch ($this->getFormat()) { case self::FORMAT_YOUTRACK: return '{color:red}' . $text . '{color}'; case self::FORMAT_CONSOLE: return $this->text->paint($text, 'red'); case self::FORMAT_HTML: return '' . htmlspecialchars($text) . ''; } return new Exception('Undefined Format: ' . $this->getFormat()); } protected function _formatWarning($text) { switch ($this->getFormat()) { case self::FORMAT_YOUTRACK: return '{color:orange}' . $text . '{color}'; case self::FORMAT_CONSOLE: return $this->text->paint($text, 'orange'); case self::FORMAT_HTML: return '' . htmlspecialchars($text) . ''; } return new Exception('Undefined Format: ' . $this->getFormat()); } protected function _formatSuccess($text) { switch ($this->getFormat()) { case self::FORMAT_YOUTRACK: return '{color:green}' . $text . '{color}'; case self::FORMAT_CONSOLE: return $this->text->paint($text, 'green'); case self::FORMAT_HTML: return '' . htmlspecialchars($text) . ''; } return new Exception('Undefined Format: ' . $this->getFormat()); } protected function areUrlEqual($locationUrl, $destinationUrl) { if ('?' == substr($destinationUrl, -1)) { $destinationUrl = substr($destinationUrl, 0, -1); } return rawurldecode($locationUrl) == rawurldecode($destinationUrl); } public function log($testType, $url, $message, $status) { $line = $this->line + 1; $values = compact('line', 'testType', 'url', 'message', 'status'); $columns = []; foreach ($this->columns as $name => $column) { $align = array_key_exists('align', $column) ? $column['align'] : null; $value = $values[$name]; switch ($this->getFormat()) { case self::FORMAT_YOUTRACK: case self::FORMAT_CONSOLE: $columns[] = $this->sprint($value, $column['width'], $align); break; case self::FORMAT_HTML: $columns[] = $value; break; default: return new Exception('Undefined Format: ' . $this->getFormat()); } } switch ($this->getFormat()) { case self::FORMAT_YOUTRACK: echo implode('||', $columns) . PHP_EOL; break; case self::FORMAT_CONSOLE: echo implode('|', $columns) . PHP_EOL; break; case self::FORMAT_HTML: echo '' . implode('', $columns) . '' . PHP_EOL; break; default: return new Exception('Undefined Format: ' . $this->getFormat()); } return $this; } protected function _formatUrl($url) { switch ($this->getFormat()) { case self::FORMAT_YOUTRACK: if (strlen($url) > 80) { $text = substr($url, 0, 10) . '...' . substr($url, -68); return '[' . $url . ' ' . $text . ']'; } return $url; case self::FORMAT_CONSOLE: return $url; case self::FORMAT_HTML: return '' . htmlspecialchars($url) . ''; } return new Exception('Undefined Format: ' . $this->getFormat()); } protected function renderHeaders() { $columns = []; foreach ($this->columns as $column) { $align = array_key_exists('align', $column) ? $column['align'] : null; switch ($this->getFormat()) { case self::FORMAT_YOUTRACK: $columns[] = $this->sprint($column['title'], $column['width'], $align); break; case self::FORMAT_CONSOLE: $columns[] = $this->sprint($column['title'], $column['width'], $align); break; case self::FORMAT_HTML: $columns[] = htmlspecialchars($column['title']); break; default: return new Exception('Undefined Format: ' . $this->getFormat()); } } switch ($this->getFormat()) { case self::FORMAT_YOUTRACK: return implode('||', $columns) . PHP_EOL; break; case self::FORMAT_CONSOLE: return implode('|', $columns) . PHP_EOL; break; case self::FORMAT_HTML: return '' . '' . PHP_EOL; break; default: return new Exception('Undefined Format: ' . $this->getFormat()); } } protected function sprint($text, $width, $align = null) { if (false !== (strpos($text, ConsoleColorText::START))) { $plainText = preg_replace("/\033\\[[\\d;]+m/", '', $text); $length = strlen($plainText); } else { $length = strlen($text); } switch ($align) { case 'center': $paddingLeft = floor(max($width - $length, 0) / 2); $paddingRight = ceil(max($width - $length, 0) / 2); break; case 'right': $paddingRight = ($width - $length > 1) ? 1 : 0; $paddingLeft = floor(max($width - $length - $paddingRight, 0)); break; case null: // break was intentionally omitted case 'left': // break was intentionally omitted default: $paddingLeft = ($width - $length > 1) ? 1 : 0; $paddingRight = max($width - $length - $paddingLeft, 0); } return str_repeat(' ', $paddingLeft) . $text . str_repeat(' ', $paddingRight); } public function run($file) { $srcUrls = []; $lines = file($file); if (($filterLines = $this->getFilterLines())) { list($start, $end) = $filterLines; if (null === $end) { $end = count($lines) - 1; } while (($start - 1) && array_key_exists($start - 1, $lines) && array_key_exists($start, $lines) && preg_match(self::RULE_REGEX, $lines[$start]) && 0 !== strpos($lines[$start - 1], '#') ) { $start--; } while (($start - 1) && array_key_exists($start - 1, $lines) && 0 === strpos($lines[$start - 1], '#')) { $start--; } while (array_key_exists($end, $lines) && trim($lines[$end])) { $end++; } if (array_key_exists($start, $lines)) { $lines = array_slice($lines, $start, $end - $start + 1, true); } else { $lines = []; } } $prevLine = null; echo $this->renderHeaders(); $this->index = 0; foreach ($lines as $this->line => $line) { $line = trim($line); if (empty($line)) { $srcUrls = []; continue; } if ($line[0] == '#' && ($url = trim(ltrim($line, '# '), "\n")) && 0 === strpos($url, 'http')) { $srcUrls[] = str_replace(' ', '%20', $url); } if (empty($srcUrls) || 0 !== strncmp($line, 'RewriteRule', 11)) { continue; } $matches = []; if (!preg_match(self::RULE_REGEX, $line, $matches)) { continue; } list(, , $substitution, $flags) = $matches; $this->index++; foreach ($srcUrls as $sourceUrl) { $this->validateRedirect($sourceUrl, $substitution, $this->_parseFlags($flags)); } $srcUrls = []; } if (self::FORMAT_HTML == $this->getFormat()) { echo '
' . implode('', $columns) . '
'; } } protected function _parseFlags($value) { $fields = []; $parts = explode(',', $value); foreach ($parts as $part) { $list = explode('=', $part); $fields[$list[0]] = isset($list[1]) ? $list[1] : null; } return $fields; } protected function _initCredentials() { if (!empty($this->_user) && !empty($this->_password)) { $this->client->setAuth($this->_user, $this->_password); } return $this; } public function setCredentials($user, $password) { $this->_user = $user; $this->_password = $password; return $this; } }