#! /usr/bin/env php
<?php

/**
 * mixed-content-scan - A CLI Script to crawl+scan HTTPS-enabled websites for Mixed Content.
 * @author Bramus! <bramus@bram.us>
 */

// Error settings
error_reporting(E_ERROR);
ini_set('display_errors', 'on');

// Check if we're at the CLI
if (php_sapi_name() != 'cli') exit('Please run this file on the command line. E.g. `mixed-content-scan $url`' . PHP_EOL);

// Require autoloader
if (file_exists(__DIR__ . '/../vendor/autoload.php')) { // Installed Locally
    require __DIR__ . '/../vendor/autoload.php';
} elseif (file_exists(__DIR__ . '/../../../autoload.php')) { // Installed Globally
    require __DIR__ . '/../../../autoload.php';
} else {
    exit('Make sure you run `composer install` first, before running this scanner');
}

// Define CLI Options/Arguments
$cli = new \Garden\Cli\Cli();

$cli->description('Scan your HTTPS-enabled website for Mixed Content.')
    ->opt('loglevel', 'The Monolog loglevel to log at. Defaults to 200.', false)
    ->opt('output', 'Stream to write to. Defaults to `php://stdout`', false)
    ->opt('format', 'Output format to use. Allowed values: `ansi`, `no-ansi`, or `json`. Defaults to `ansi`', false)
    ->opt('no-crawl', 'Don\'t crawl scanned pages for new pages.', false)
    ->opt('no-check-certificate', 'Don\'t check the certificate for validity.', false)
    ->opt('timeout', 'How long to wait for each request to complete. Defaults to 10000ms.', false, 'integer')
    ->opt('input', 'Specify a file containing a list of links as the source, instead of parsing the passed in URL. Automatically enables `--no-crawl`', false)
    ->opt('ignore', 'File containing URL patterns to ignore. See readme shipping with release on how to build this file.', false)
    ->opt('delay', 'Time to delay between requests (seconds), for rate-limiting', false, 'integer')
    ->opt('user-agent', 'Set the user agent to be used when crawling', false)
    ->arg('rootUrl', 'The URL to start scanning at', false);

// Parse and return cli options
$opts = $cli->parse($argv, true)->getOpts();
$args = $cli->parse($argv, true)->getArgs();

// Determine numerical log level
if (isset($opts['loglevel']) && !is_int($opts['loglevel'])) {
    $levels = \Monolog\Logger::getLevels();
    if (array_key_exists(strtoupper($opts['loglevel']), $levels)) {
        $opts['loglevel'] = $levels[ strtoupper($opts['loglevel']) ];
    }
}
$loglevel = isset($opts['loglevel']) ? (int) $opts['loglevel'] : 200;
$delay = isset($opts['delay']) ? (int) $opts['delay'] : 0;

// Create logger writing to the specified output
$logger = new \Monolog\Logger('MCS');
$handler = new \Monolog\Handler\StreamHandler((isset($opts['output']) ? $opts['output'] : 'php://stdout'), $loglevel);

// Define formatter to use
if (!isset($opts['format'])) $opts['format'] = 'ansi';

switch($opts['format']) {

    case 'no-ansi':
        $formatter = new \Monolog\Formatter\LineFormatter(null, null, false, true);
        break;

    case 'json':
        $formatter = new \Monolog\Formatter\JsonFormatter();
        break;

    case 'ansi':
    default:
        $formatter = new \Bramus\Monolog\Formatter\ColoredLineFormatter(null, null, null, false, true);
        break;

}

// Link formatter to logger
$handler->setFormatter($formatter);
$logger->pushHandler($handler);

// Define the rootURL and/or the list of links to scan
$urlsToQueue = [];
if (isset($opts['input'])) {

    // Set the rootUrl to the wildcard
    $rootUrl = '*';

    // Open the file and make sure it's readable
    try {
        $fi = new \SplFileObject($opts['input']);
    } catch(\Exception $e) {
        $logger->addError('Please make sure the file containing the list of links passed in via `--input` exists and is readable.');
        exit();
    }
    if (!$fi->isFile() || !$fi->isReadable()) {
        $logger->addError('Please make sure the file containing the list of links passed in via `--input` exists and is readable.');
        exit();
    }

    // Loop the contents and queue all URLs
    foreach ($fi as $link) {
        if (parse_url(trim($link)) && (trim($link) != '')) $urlsToQueue[] = trim($link);
    }

    // Make sure `--no-crawl` is set when working with `--input-file`
    $opts['no-crawl'] = true;

    // Give a notice if we have ignored any passed in rootUrl
    if (isset($args['rootUrl'])) $logger->addNotice('Using an input-file as source. Ignoring the passed in $rootUrl');

} else {

    if (!isset($args['rootUrl']) || !parse_url($args['rootUrl'])) {
        $cli->writeHelp();
        // $logger->addError('Please pass the URL to scan (rootUrl) as the 1st argument to this script. E.g. `mixed-content-scan $url`');
        exit();
    }

    $rootUrl = $args['rootUrl'];

}

// Define the ignore patterns
$ignorePatterns = [];
if (isset($opts['ignore'])) {

    // Open the file and make sure it's readable
    try {
        $fi = new \SplFileObject($opts['ignore']);
    } catch(\Exception $e) {
        $logger->addError('Please make sure the file containing the ignore patterns passed in via `--ignore` exists and is readable.');
        exit();
    }
    if (!$fi->isFile() || !$fi->isReadable()) {
        $logger->addError('Please make sure the file containing the ignore patterns passed in via `--ignore` exists and is readable.');
        exit();
    }

    // Loop the contents and extract all patterns
    foreach ($fi as $pattern) {
        if ((strlen(trim($pattern)) > 0) && (substr($pattern, 0, 1) != '#')) $ignorePatterns[] = trim($pattern);
    }
}

// Do we need to crawl or not?
if (isset($opts['no-crawl'])) {
    $crawl = false;
} else {
    $crawl = true;
}

// Do we need to check the certificate or not?
if (isset($opts['no-check-certificate'])) {
    $checkCertificate = false;
} else {
    $checkCertificate = true;
}

// Set the timeout value for each request
if (isset($opts['timeout'])) {
    $timeout = $opts['timeout'];
    if (!(is_numeric($timeout) && $timeout > 0 && $timeout == round($timeout, 0))) {
        $timeout = 10000;
        $logger->addNotice('Invalid timeout value specified. Using default value of 10000ms.');
    }
} else {
    $timeout = 10000;
}

// Set the user agent to use when crawling
if (isset($opts['user-agent'])) {
    $userAgent = $opts['user-agent'] .' mixed-content-scan';
} else {
    $userAgent = 'mixed-content-scan';
}

// Go for it!
try {
    $scanner = new \Bramus\MCS\Scanner($rootUrl, $logger, (array) $ignorePatterns);
    $scanner->setCrawl($crawl);
    $scanner->setTimeout($timeout);
    $scanner->setDelay($delay);
    $scanner->setCheckCertificate($checkCertificate);
    $scanner->setUserAgent($userAgent);
    if (sizeof($urlsToQueue) > 0) $scanner->queueUrls($urlsToQueue);
    $scanner->scan();
} catch(\Exception $e) {
    exit(1);
}

// EOF
