diff --git a/Classes/CommandArgumentFilter.php b/Classes/CommandArgumentFilter.php new file mode 100644 index 0000000..1ffa45e --- /dev/null +++ b/Classes/CommandArgumentFilter.php @@ -0,0 +1,343 @@ + + */ + +namespace Clapp; + +/** + * Filters an array and extracts and validates command line arguments + * + * @author Patrick Forget + */ +class CommandArgumentFilter +{ + /** + * Command line arguments + * @var array + */ + private $arguments = array(); + + /** + * Definition of allowed parameters + * @var \Clapp\CommandLineArgumentDefinition + */ + private $definitions = null; + + /** + * Flag if arguments have been parsed in to params + * @var boolean + */ + private $parsed = false; + + /** + * Parsed params + * @var array + */ + private $params = array(); + + /** + * Trailing values + * @var string + */ + private $trailingValues = ""; + + /** + * program name + * @var string + */ + private $programName = ""; + + /** + * class constructor + * + * @author Patrick Forget + * + * @param \Clapp\CommandLineDefinition $definitions contains list of allowed parameters + * @param array $args list of arguments to filter. + */ + public function __construct(\Clapp\CommandLineArgumentDefinition $definitions, $args) + { + if (is_array($args)) { + $this->arguments = $args; + } //if + + $this->definitions = $definitions; + } // __construct() + + /** + * returns parameter matching provided name + * + * @author Patrick Forget + * + * @param string name of the paramter to retreive + * + * @return mixed if param the param appears only once the method will + * return 1 if the parameter doesn't take a value. The specified value + * for that param will returned if it does take value. + * + * If many occurence of the param appear the number of occurences will + * be returned for params that do not take values. An array of values + * will be returned for the parameters that do take values. + * + * If the parameter is not present null if it takes a value and false if + * it's not present and doesn't allow values + */ + public function getParam($name) + { + if (!$this->parsed) { + $this->parseParams(); + } //if + + $longName = strlen($name) === 1 ? $this->definitions->getLongName($name) : $name; + if (isset($this->params[$longName])) { + return $this->params[$longName]; + } else { + if ($this->definitions->allowsValue($longName)) { + return null; + } else { + return false; + } //if + } //if + + } // getParam() + + /** + * retreive the program name + * + * @author Patrick Forget + */ + public function getProgramName() + { + if (!$this->parsed) { + $this->parseParams(); + } //if + + return $this->programName; + } // getProgramName() + + /** + * retreive the trailing values + * + * @author Patrick Forget + */ + public function getTrailingValues() + { + if (!$this->parsed) { + $this->parseParams(); + } //if + + return $this->trailingValues; + } // getTrailingValues() + + /** + * extracts params from arguments + * + * @author Patrick Forget + */ + protected function parseParams() + { + + $argumentStack = $this->arguments; + + $expectingValue = false; + $currentLongName = null; + $currentValue = null; + $trailingValues = ""; + $endOfDashedArguments = false; + $addParam = false; + $argumentsLeft = sizeof($argumentStack); + $multiShortParams = array(); + + $this->programName = array_shift($argumentStack); // remove first argument which is the program name + + while ($currentArgument = array_shift($argumentStack)) { + $argumentsLeft--; + $currentArgumentLength = strlen($currentArgument); + + // arguments that don't start with a dash + if (substr($currentArgument, 0, 1) !== '-') { + if ($expectingValue) { + $currentValue = $currentArgument; + $addParam = true; + } else { + $trailingValues .= " ". $currentArgument; + $endOfDashedArguments = true; + } //if + + // double dash detected + } elseif (substr($currentArgument, 1, 1) === '-') { + if ($expectingValue) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + + /* stop parsing arguments if double dash + only param is encountered */ + if ($currentArgumentLength == 2) { + if ($trailingValues !== "") { + throw new \UnexpectedValueException("Trailing values must appear after double dash"); + } //if + + $trailingValues = " ". implode(" ", $argumentStack); + $argumentStack = array(); + $endOfDashedArguments = true; + break; + } //if + + $longNameParts = explode("=", substr($currentArgument, 2), 2); + + $currentLongName = $longNameParts[0]; + + if (sizeof($longNameParts) > 1) { + $currentValue = $longNameParts[1]; + $addParam = true; + } elseif ($this->definitions->allowsValue($currentLongName)) { + $expectingValue = true; + } else { + $addParam = true; + } //if + + // single dash + } else { + if ($expectingValue) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + + $shortNameParts = explode("=", substr($currentArgument, 1), 2); + + $shortName = $shortNameParts[0]; + + if (strlen($shortName) <= 1) { + $currentLongName = $this->definitions->getLongName($shortName); + + if ($currentLongName === null) { + throw new \InvalidArgumentException("Unable to find name with ". + "provided parameter ({$shortName})"); + } //if + + if (sizeof($shortNameParts) > 1) { + $currentValue = $shortNameParts[1]; + $addParam = true; + } elseif ($this->definitions->allowsValue($currentLongName)) { + $expectingValue = true; + } else { + $addParam = true; + } //if + + } else { + $multiShortParams = str_split($shortName); + + /* process the last one (which is the only one that can have a value) */ + $lastParam = array_pop($multiShortParams); + $currentLongName = $this->definitions->getLongName($lastParam); + if (sizeof($shortNameParts) > 1) { + $currentValue = $shortNameParts[1]; + $addParam = true; + } elseif ($this->definitions->allowsValue($lastParam)) { + $expectingValue = true; + } else { + $addParam = true; + } //if + + } //if + + } //if + + if ($addParam) { + if ($endOfDashedArguments) { + throw new \UnexpectedValueException("Unexpected argument after undashed values"); + } //if + + /* Not sure how this could happen */ + // @codeCoverageIgnoreStart + if ($currentLongName === false || $currentLongName === null) { + throw new \UnexpectedValueException("Missing argument name"); + } //if + // @codeCoverageIgnoreEnd + + if (!$this->definitions->paramExists($currentLongName)) { + throw new \InvalidArgumentException("Invalid argument name"); + } //if + + $allowsMultiple = $this->definitions->allowsMultiple($currentLongName); + $allowsValue = $this->definitions->allowsValue($currentLongName); + + if (isset($this->params[$currentLongName]) && !$allowsMultiple) { + throw new \UnexpectedValueException("Multiple instace of parameter {$currentLongName} not allowed"); + } //if + + if ($allowsValue) { + /* Missing values should always be detected before addParam is true */ + // @codeCoverageIgnoreStart + if ($currentValue === null) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + // @codeCoverageIgnoreEnd + + } elseif ($currentValue !== null) { + throw new \UnexpectedValueException("Parameter {$currentLongName} does not accept values"); + + } else { + $currentValue = true; + } //if + + if ($allowsMultiple) { + if ($allowsValue) { + if (!isset($this->params[$currentLongName])) { + $this->params[$currentLongName] = array(); + } //if + + $this->params[$currentLongName][] = $currentValue; + + } else { + if (!isset($this->params[$currentLongName])) { + $this->params[$currentLongName] = 0; + } //if + + $this->params[$currentLongName]++; + + } //if + + } else { + $this->params[$currentLongName] = $currentValue; + } //if + + foreach ($multiShortParams as $shortName) { + $argumentStack[] = "-{$shortName}"; + $argumentsLeft++; + } //foreach + + /* reset stuff for next param */ + $expectingValue = false; + $currentLongName = null; + $currentValue = null; + $addParam = false; + $multiShortParams = array(); + + } //if + + } //while + + if ($expectingValue !== false) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + + /* Not sure how this could happen */ + // @codeCoverageIgnoreStart + if ($currentLongName !== null || + $addParam !== false || + $currentValue !== null || + sizeof($multiShortParams) !== 0) { + throw new \UnexpectedValueException("Unable to process some parameters"); + } //if + // @codeCoverageIgnoreEnd + + if ($trailingValues !== "") { + $this->trailingValues = substr($trailingValues, 1); // remove extra space at the begging + } //if + + $this->parsed = true; + } // parseParams() +} diff --git a/Classes/CommandArgumentFilter.php b/Classes/CommandArgumentFilter.php new file mode 100644 index 0000000..1ffa45e --- /dev/null +++ b/Classes/CommandArgumentFilter.php @@ -0,0 +1,343 @@ + + */ + +namespace Clapp; + +/** + * Filters an array and extracts and validates command line arguments + * + * @author Patrick Forget + */ +class CommandArgumentFilter +{ + /** + * Command line arguments + * @var array + */ + private $arguments = array(); + + /** + * Definition of allowed parameters + * @var \Clapp\CommandLineArgumentDefinition + */ + private $definitions = null; + + /** + * Flag if arguments have been parsed in to params + * @var boolean + */ + private $parsed = false; + + /** + * Parsed params + * @var array + */ + private $params = array(); + + /** + * Trailing values + * @var string + */ + private $trailingValues = ""; + + /** + * program name + * @var string + */ + private $programName = ""; + + /** + * class constructor + * + * @author Patrick Forget + * + * @param \Clapp\CommandLineDefinition $definitions contains list of allowed parameters + * @param array $args list of arguments to filter. + */ + public function __construct(\Clapp\CommandLineArgumentDefinition $definitions, $args) + { + if (is_array($args)) { + $this->arguments = $args; + } //if + + $this->definitions = $definitions; + } // __construct() + + /** + * returns parameter matching provided name + * + * @author Patrick Forget + * + * @param string name of the paramter to retreive + * + * @return mixed if param the param appears only once the method will + * return 1 if the parameter doesn't take a value. The specified value + * for that param will returned if it does take value. + * + * If many occurence of the param appear the number of occurences will + * be returned for params that do not take values. An array of values + * will be returned for the parameters that do take values. + * + * If the parameter is not present null if it takes a value and false if + * it's not present and doesn't allow values + */ + public function getParam($name) + { + if (!$this->parsed) { + $this->parseParams(); + } //if + + $longName = strlen($name) === 1 ? $this->definitions->getLongName($name) : $name; + if (isset($this->params[$longName])) { + return $this->params[$longName]; + } else { + if ($this->definitions->allowsValue($longName)) { + return null; + } else { + return false; + } //if + } //if + + } // getParam() + + /** + * retreive the program name + * + * @author Patrick Forget + */ + public function getProgramName() + { + if (!$this->parsed) { + $this->parseParams(); + } //if + + return $this->programName; + } // getProgramName() + + /** + * retreive the trailing values + * + * @author Patrick Forget + */ + public function getTrailingValues() + { + if (!$this->parsed) { + $this->parseParams(); + } //if + + return $this->trailingValues; + } // getTrailingValues() + + /** + * extracts params from arguments + * + * @author Patrick Forget + */ + protected function parseParams() + { + + $argumentStack = $this->arguments; + + $expectingValue = false; + $currentLongName = null; + $currentValue = null; + $trailingValues = ""; + $endOfDashedArguments = false; + $addParam = false; + $argumentsLeft = sizeof($argumentStack); + $multiShortParams = array(); + + $this->programName = array_shift($argumentStack); // remove first argument which is the program name + + while ($currentArgument = array_shift($argumentStack)) { + $argumentsLeft--; + $currentArgumentLength = strlen($currentArgument); + + // arguments that don't start with a dash + if (substr($currentArgument, 0, 1) !== '-') { + if ($expectingValue) { + $currentValue = $currentArgument; + $addParam = true; + } else { + $trailingValues .= " ". $currentArgument; + $endOfDashedArguments = true; + } //if + + // double dash detected + } elseif (substr($currentArgument, 1, 1) === '-') { + if ($expectingValue) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + + /* stop parsing arguments if double dash + only param is encountered */ + if ($currentArgumentLength == 2) { + if ($trailingValues !== "") { + throw new \UnexpectedValueException("Trailing values must appear after double dash"); + } //if + + $trailingValues = " ". implode(" ", $argumentStack); + $argumentStack = array(); + $endOfDashedArguments = true; + break; + } //if + + $longNameParts = explode("=", substr($currentArgument, 2), 2); + + $currentLongName = $longNameParts[0]; + + if (sizeof($longNameParts) > 1) { + $currentValue = $longNameParts[1]; + $addParam = true; + } elseif ($this->definitions->allowsValue($currentLongName)) { + $expectingValue = true; + } else { + $addParam = true; + } //if + + // single dash + } else { + if ($expectingValue) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + + $shortNameParts = explode("=", substr($currentArgument, 1), 2); + + $shortName = $shortNameParts[0]; + + if (strlen($shortName) <= 1) { + $currentLongName = $this->definitions->getLongName($shortName); + + if ($currentLongName === null) { + throw new \InvalidArgumentException("Unable to find name with ". + "provided parameter ({$shortName})"); + } //if + + if (sizeof($shortNameParts) > 1) { + $currentValue = $shortNameParts[1]; + $addParam = true; + } elseif ($this->definitions->allowsValue($currentLongName)) { + $expectingValue = true; + } else { + $addParam = true; + } //if + + } else { + $multiShortParams = str_split($shortName); + + /* process the last one (which is the only one that can have a value) */ + $lastParam = array_pop($multiShortParams); + $currentLongName = $this->definitions->getLongName($lastParam); + if (sizeof($shortNameParts) > 1) { + $currentValue = $shortNameParts[1]; + $addParam = true; + } elseif ($this->definitions->allowsValue($lastParam)) { + $expectingValue = true; + } else { + $addParam = true; + } //if + + } //if + + } //if + + if ($addParam) { + if ($endOfDashedArguments) { + throw new \UnexpectedValueException("Unexpected argument after undashed values"); + } //if + + /* Not sure how this could happen */ + // @codeCoverageIgnoreStart + if ($currentLongName === false || $currentLongName === null) { + throw new \UnexpectedValueException("Missing argument name"); + } //if + // @codeCoverageIgnoreEnd + + if (!$this->definitions->paramExists($currentLongName)) { + throw new \InvalidArgumentException("Invalid argument name"); + } //if + + $allowsMultiple = $this->definitions->allowsMultiple($currentLongName); + $allowsValue = $this->definitions->allowsValue($currentLongName); + + if (isset($this->params[$currentLongName]) && !$allowsMultiple) { + throw new \UnexpectedValueException("Multiple instace of parameter {$currentLongName} not allowed"); + } //if + + if ($allowsValue) { + /* Missing values should always be detected before addParam is true */ + // @codeCoverageIgnoreStart + if ($currentValue === null) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + // @codeCoverageIgnoreEnd + + } elseif ($currentValue !== null) { + throw new \UnexpectedValueException("Parameter {$currentLongName} does not accept values"); + + } else { + $currentValue = true; + } //if + + if ($allowsMultiple) { + if ($allowsValue) { + if (!isset($this->params[$currentLongName])) { + $this->params[$currentLongName] = array(); + } //if + + $this->params[$currentLongName][] = $currentValue; + + } else { + if (!isset($this->params[$currentLongName])) { + $this->params[$currentLongName] = 0; + } //if + + $this->params[$currentLongName]++; + + } //if + + } else { + $this->params[$currentLongName] = $currentValue; + } //if + + foreach ($multiShortParams as $shortName) { + $argumentStack[] = "-{$shortName}"; + $argumentsLeft++; + } //foreach + + /* reset stuff for next param */ + $expectingValue = false; + $currentLongName = null; + $currentValue = null; + $addParam = false; + $multiShortParams = array(); + + } //if + + } //while + + if ($expectingValue !== false) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + + /* Not sure how this could happen */ + // @codeCoverageIgnoreStart + if ($currentLongName !== null || + $addParam !== false || + $currentValue !== null || + sizeof($multiShortParams) !== 0) { + throw new \UnexpectedValueException("Unable to process some parameters"); + } //if + // @codeCoverageIgnoreEnd + + if ($trailingValues !== "") { + $this->trailingValues = substr($trailingValues, 1); // remove extra space at the begging + } //if + + $this->parsed = true; + } // parseParams() +} diff --git a/Classes/CommandLineArgumentDefinition.php b/Classes/CommandLineArgumentDefinition.php new file mode 100644 index 0000000..2b2e8fa --- /dev/null +++ b/Classes/CommandLineArgumentDefinition.php @@ -0,0 +1,389 @@ + + */ + +namespace Clapp; + +/** + * Defines list and formats of command line arguments + * + * @author Patrick Forget + */ +class CommandLineArgumentDefinition +{ + + /** + * @var array + */ + private $definitions = array(); + + /** + * long names as keys and array of properties as values + * + * properties are as follows + * * string "shortName" one letter char to the corresponding short name + * * boolean "isMultipleAllowed" true if mutliple instances of the param are allowed + * * mixed "parameterType" false if paramters are not alloweda value, + * otherwise a string with the value "integer" or "string" + * * string "description" description of the parameter + * @var array + */ + private $longNames = array(); + + /** + * list of short names as keys and their long name equivalent as values + * @var array + */ + private $shortNames = array(); + + /** + * Flag if arguments have been parsed in to params + * @var boolean + */ + private $isParsed = false; + + /** + * class constructor + * + * @author Patrick Forget + * + * @param array $definitions contains list of allowed parameters + * the key is the long name of the parameter followed by a pipe (|) + * then a single character specifying the short name. + * + * If the parameter allows for arguments then an equal sign (=) + * follows and then the type of paramter. + * + * Allowed types are either i, int or integer for integer types + * and s, str or string for string types. + * + * If a parameter can appear more than once the last character of + * the key should be a plus character (+). + * + * The value of the entry is the definition of what the paramter + * does. + */ + public function __construct($definitions) + { + if (is_array($definitions)) { + $this->definitions = $definitions; + } //if + } // __construct() + + /** + * checks if parameter is allowed + * + * @author Patrick Forget + * + * @param string $name either short or long name of the parameter to check + * + * @return boolean true if definition exisits, false otherwise + */ + public function paramExists($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + if (strlen($name) == 1) { + return isset($this->shortNames[$name]); + } else { + return isset($this->longNames[$name]); + } //if + } // paramExists($name) + + + /** + * checks if parameter allows a value if so what type + * + * @author Patrick Forget + * + * @param string $name either short or long name of the parameter to check + * + * @return boolean|string false doesn't allow value, The value "string" or "integer" depending which type it allows + */ + public function allowsValue($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + $longName = (strlen($name) == 1 ? ( isset($this->shortNames[$name]) ? $this->shortNames[$name] : '') : $name); + + if (isset($this->longNames[$longName])) { + return $this->longNames[$longName]['parameterType'] !== false ? true : false; + } else { + return false; + } //if + } // allowsValue() + + /** + * returns the type of value allowed + * + * @author Patrick Forget + */ + public function getValueType($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + $longName = (strlen($name) == 1 ? ( isset($this->shortNames[$name]) ? $this->shortNames[$name] : '') : $name); + + if (isset($this->longNames[$longName]['parameterType']) + && $this->longNames[$longName]['parameterType'] !== false) { + return $this->longNames[$longName]['parameterType']; + } else { + return ''; + } //if + } // getValueType() + + + /** + * checks if pamultiple instance of parameter are allowed + * + * @author Patrick Forget + * + * @param string $name either short or long name of the parameter to check + * + * @return boolean false if parameter doesn't allow multiple values, true if it does + */ + public function allowsMultiple($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + $longName = (strlen($name) == 1 ? ( isset($this->shortNames[$name]) ? $this->shortNames[$name] : '') : $name); + + if (isset($this->longNames[$longName])) { + return $this->longNames[$longName]['isMultipleAllowed']; + } else { + return false; + } //if + } // allowsMultiple() + + /** + * retreive short name of a parameter using its long name + * + * @author Patrick Forget + * + * @param string $name long name of the parameter to check + * + * @return string character of the short name or null if it doesn't exist + */ + public function getShortName($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + if (isset($this->longNames[$name])) { + return $this->longNames[$name]['shortName']; + } else { + return null; + } //if + } // getShortName($name) + + /** + * retreive long name of a parameter using its short name + * + * @author Patrick Forget + * + * @param string $name short name of the parameter to check + * + * @return string long name or null if it doesn't exist + */ + public function getLongName($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + if (isset($this->shortNames[$name])) { + return $this->shortNames[$name]; + } else { + return null; + } //if + } // getLongName($name) + + /** + * retreive description of a paramter + * + * @author Patrick Forget + * + * @param string $name either short or long name of the parameter to check + * + * @return string description or null if it doesn't exist + */ + public function getDescription($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + $longName = (strlen($name) == 1 ? ( isset($this->shortNames[$name]) ? $this->shortNames[$name] : '') : $name); + + if (isset($this->longNames[$longName])) { + return $this->longNames[$longName]['description']; + } else { + return null; + } //if + } // getDescription() + + /** + * builds a usage definition based on definition of params + * + * @author Patrick Forget + */ + public function getUsage() + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + /* build list of argument names and calculate + the first column width so we can pad to + align definitions */ + $firstCol = array(); + $longestDef = 0; + foreach (array_keys($this->longNames) as $longName) { + ob_start(); + echo "--{$longName}|-{$this->getShortName($longName)}"; + + if ($this->allowsValue($longName)) { + echo "={$this->getValueType($longName)}"; + } //if + + if ($this->allowsMultiple($longName)) { + echo "+"; + } //if + + $defLength = ob_get_length(); + + $longestDef = max($longestDef, $defLength); + + $firstCol[$longName] = ob_get_contents(); + ob_end_clean(); + + } //foreach + + $firstColMaxWidth = $longestDef + 4; + + ob_start(); + + foreach ($firstCol as $longName => $def) { + $currentDefLength = strlen($def); + + $padding = str_repeat(" ", $firstColMaxWidth - $currentDefLength); + + echo "{$def}{$padding}{$this->getDescription($longName)}", PHP_EOL; + } //foreach + + echo PHP_EOL; + + $usage = ob_get_contents(); + ob_end_clean(); + + return $usage; + + } // getUsage() + + + /** + * parses the definitions + * + * @author Patrick Forget + */ + protected function parseDefinitions() + { + foreach ($this->definitions as $nameDef => $description) { + $nameParts = explode("|", $nameDef); + + if (sizeof($nameParts) !== 2) { + throw new \UnexpectedValueException("Unexpected argument name definition expecting \"longName|char\""); + } //if + + $longName = $nameParts[0]; + $isMulti = false; + $parameterType = false; + + $shortNameLength = strlen($nameParts[1]); + + if ($shortNameLength == 1) { + $shortName = $nameParts[1]; + } else { + $secondChar = substr($nameParts[1], 1, 1); + + switch ($secondChar) { + case '=': + $shortNameParts = explode("=", $nameParts[1]); + + $shortName = $shortNameParts[0]; + $parameterTypeString = $shortNameParts[1]; + + if (substr($parameterTypeString, -1) === '+') { + $isMulti = true; + $parameterTypeString = substr($parameterTypeString, 0, -1); // remove trailing + + } //if + + switch ($parameterTypeString) { + case 'i': + case 'int': + case 'integer': + $parameterType = 'integer'; + break; + case 's': + case 'str': + case 'string': + $parameterType = 'string'; + break; + default: + throw new \UnexpectedValueException("Expecting parameter type". + " to be either integer or string"); + break; + } //switch + + break; + case '+': + if ($shortNameLength > 2) { + throw new \UnexpectedValueException("Multiple flag charachter (+)". + " should be last character in definition"); + } //if + + $shortName = substr($nameParts[1], 0, 1); + $isMulti = true; + + break; + default: + throw new \UnexpectedValueException("Expecting short name definition to be a single char"); + break; + } // switch + + } //if + + if (isset($this->longNames[$longName])) { + throw new \UnexpectedValueException("Cannot redefine long name {$longName}"); + } //if + + if (isset($this->shortNames[$shortName])) { + throw new \UnexpectedValueException("Cannot redefine short name {$shortName}"); + } //if + + $this->longNames[$longName] = array( + 'shortName' => $shortName, + 'isMultipleAllowed' => $isMulti, + 'parameterType' => $parameterType, + 'description' => $description + ); + + $this->shortNames[$shortName] = $longName; + + } //foreach + + $this->isParsed = true; + } // parseDefinitions() +} diff --git a/Classes/CommandArgumentFilter.php b/Classes/CommandArgumentFilter.php new file mode 100644 index 0000000..1ffa45e --- /dev/null +++ b/Classes/CommandArgumentFilter.php @@ -0,0 +1,343 @@ + + */ + +namespace Clapp; + +/** + * Filters an array and extracts and validates command line arguments + * + * @author Patrick Forget + */ +class CommandArgumentFilter +{ + /** + * Command line arguments + * @var array + */ + private $arguments = array(); + + /** + * Definition of allowed parameters + * @var \Clapp\CommandLineArgumentDefinition + */ + private $definitions = null; + + /** + * Flag if arguments have been parsed in to params + * @var boolean + */ + private $parsed = false; + + /** + * Parsed params + * @var array + */ + private $params = array(); + + /** + * Trailing values + * @var string + */ + private $trailingValues = ""; + + /** + * program name + * @var string + */ + private $programName = ""; + + /** + * class constructor + * + * @author Patrick Forget + * + * @param \Clapp\CommandLineDefinition $definitions contains list of allowed parameters + * @param array $args list of arguments to filter. + */ + public function __construct(\Clapp\CommandLineArgumentDefinition $definitions, $args) + { + if (is_array($args)) { + $this->arguments = $args; + } //if + + $this->definitions = $definitions; + } // __construct() + + /** + * returns parameter matching provided name + * + * @author Patrick Forget + * + * @param string name of the paramter to retreive + * + * @return mixed if param the param appears only once the method will + * return 1 if the parameter doesn't take a value. The specified value + * for that param will returned if it does take value. + * + * If many occurence of the param appear the number of occurences will + * be returned for params that do not take values. An array of values + * will be returned for the parameters that do take values. + * + * If the parameter is not present null if it takes a value and false if + * it's not present and doesn't allow values + */ + public function getParam($name) + { + if (!$this->parsed) { + $this->parseParams(); + } //if + + $longName = strlen($name) === 1 ? $this->definitions->getLongName($name) : $name; + if (isset($this->params[$longName])) { + return $this->params[$longName]; + } else { + if ($this->definitions->allowsValue($longName)) { + return null; + } else { + return false; + } //if + } //if + + } // getParam() + + /** + * retreive the program name + * + * @author Patrick Forget + */ + public function getProgramName() + { + if (!$this->parsed) { + $this->parseParams(); + } //if + + return $this->programName; + } // getProgramName() + + /** + * retreive the trailing values + * + * @author Patrick Forget + */ + public function getTrailingValues() + { + if (!$this->parsed) { + $this->parseParams(); + } //if + + return $this->trailingValues; + } // getTrailingValues() + + /** + * extracts params from arguments + * + * @author Patrick Forget + */ + protected function parseParams() + { + + $argumentStack = $this->arguments; + + $expectingValue = false; + $currentLongName = null; + $currentValue = null; + $trailingValues = ""; + $endOfDashedArguments = false; + $addParam = false; + $argumentsLeft = sizeof($argumentStack); + $multiShortParams = array(); + + $this->programName = array_shift($argumentStack); // remove first argument which is the program name + + while ($currentArgument = array_shift($argumentStack)) { + $argumentsLeft--; + $currentArgumentLength = strlen($currentArgument); + + // arguments that don't start with a dash + if (substr($currentArgument, 0, 1) !== '-') { + if ($expectingValue) { + $currentValue = $currentArgument; + $addParam = true; + } else { + $trailingValues .= " ". $currentArgument; + $endOfDashedArguments = true; + } //if + + // double dash detected + } elseif (substr($currentArgument, 1, 1) === '-') { + if ($expectingValue) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + + /* stop parsing arguments if double dash + only param is encountered */ + if ($currentArgumentLength == 2) { + if ($trailingValues !== "") { + throw new \UnexpectedValueException("Trailing values must appear after double dash"); + } //if + + $trailingValues = " ". implode(" ", $argumentStack); + $argumentStack = array(); + $endOfDashedArguments = true; + break; + } //if + + $longNameParts = explode("=", substr($currentArgument, 2), 2); + + $currentLongName = $longNameParts[0]; + + if (sizeof($longNameParts) > 1) { + $currentValue = $longNameParts[1]; + $addParam = true; + } elseif ($this->definitions->allowsValue($currentLongName)) { + $expectingValue = true; + } else { + $addParam = true; + } //if + + // single dash + } else { + if ($expectingValue) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + + $shortNameParts = explode("=", substr($currentArgument, 1), 2); + + $shortName = $shortNameParts[0]; + + if (strlen($shortName) <= 1) { + $currentLongName = $this->definitions->getLongName($shortName); + + if ($currentLongName === null) { + throw new \InvalidArgumentException("Unable to find name with ". + "provided parameter ({$shortName})"); + } //if + + if (sizeof($shortNameParts) > 1) { + $currentValue = $shortNameParts[1]; + $addParam = true; + } elseif ($this->definitions->allowsValue($currentLongName)) { + $expectingValue = true; + } else { + $addParam = true; + } //if + + } else { + $multiShortParams = str_split($shortName); + + /* process the last one (which is the only one that can have a value) */ + $lastParam = array_pop($multiShortParams); + $currentLongName = $this->definitions->getLongName($lastParam); + if (sizeof($shortNameParts) > 1) { + $currentValue = $shortNameParts[1]; + $addParam = true; + } elseif ($this->definitions->allowsValue($lastParam)) { + $expectingValue = true; + } else { + $addParam = true; + } //if + + } //if + + } //if + + if ($addParam) { + if ($endOfDashedArguments) { + throw new \UnexpectedValueException("Unexpected argument after undashed values"); + } //if + + /* Not sure how this could happen */ + // @codeCoverageIgnoreStart + if ($currentLongName === false || $currentLongName === null) { + throw new \UnexpectedValueException("Missing argument name"); + } //if + // @codeCoverageIgnoreEnd + + if (!$this->definitions->paramExists($currentLongName)) { + throw new \InvalidArgumentException("Invalid argument name"); + } //if + + $allowsMultiple = $this->definitions->allowsMultiple($currentLongName); + $allowsValue = $this->definitions->allowsValue($currentLongName); + + if (isset($this->params[$currentLongName]) && !$allowsMultiple) { + throw new \UnexpectedValueException("Multiple instace of parameter {$currentLongName} not allowed"); + } //if + + if ($allowsValue) { + /* Missing values should always be detected before addParam is true */ + // @codeCoverageIgnoreStart + if ($currentValue === null) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + // @codeCoverageIgnoreEnd + + } elseif ($currentValue !== null) { + throw new \UnexpectedValueException("Parameter {$currentLongName} does not accept values"); + + } else { + $currentValue = true; + } //if + + if ($allowsMultiple) { + if ($allowsValue) { + if (!isset($this->params[$currentLongName])) { + $this->params[$currentLongName] = array(); + } //if + + $this->params[$currentLongName][] = $currentValue; + + } else { + if (!isset($this->params[$currentLongName])) { + $this->params[$currentLongName] = 0; + } //if + + $this->params[$currentLongName]++; + + } //if + + } else { + $this->params[$currentLongName] = $currentValue; + } //if + + foreach ($multiShortParams as $shortName) { + $argumentStack[] = "-{$shortName}"; + $argumentsLeft++; + } //foreach + + /* reset stuff for next param */ + $expectingValue = false; + $currentLongName = null; + $currentValue = null; + $addParam = false; + $multiShortParams = array(); + + } //if + + } //while + + if ($expectingValue !== false) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + + /* Not sure how this could happen */ + // @codeCoverageIgnoreStart + if ($currentLongName !== null || + $addParam !== false || + $currentValue !== null || + sizeof($multiShortParams) !== 0) { + throw new \UnexpectedValueException("Unable to process some parameters"); + } //if + // @codeCoverageIgnoreEnd + + if ($trailingValues !== "") { + $this->trailingValues = substr($trailingValues, 1); // remove extra space at the begging + } //if + + $this->parsed = true; + } // parseParams() +} diff --git a/Classes/CommandLineArgumentDefinition.php b/Classes/CommandLineArgumentDefinition.php new file mode 100644 index 0000000..2b2e8fa --- /dev/null +++ b/Classes/CommandLineArgumentDefinition.php @@ -0,0 +1,389 @@ + + */ + +namespace Clapp; + +/** + * Defines list and formats of command line arguments + * + * @author Patrick Forget + */ +class CommandLineArgumentDefinition +{ + + /** + * @var array + */ + private $definitions = array(); + + /** + * long names as keys and array of properties as values + * + * properties are as follows + * * string "shortName" one letter char to the corresponding short name + * * boolean "isMultipleAllowed" true if mutliple instances of the param are allowed + * * mixed "parameterType" false if paramters are not alloweda value, + * otherwise a string with the value "integer" or "string" + * * string "description" description of the parameter + * @var array + */ + private $longNames = array(); + + /** + * list of short names as keys and their long name equivalent as values + * @var array + */ + private $shortNames = array(); + + /** + * Flag if arguments have been parsed in to params + * @var boolean + */ + private $isParsed = false; + + /** + * class constructor + * + * @author Patrick Forget + * + * @param array $definitions contains list of allowed parameters + * the key is the long name of the parameter followed by a pipe (|) + * then a single character specifying the short name. + * + * If the parameter allows for arguments then an equal sign (=) + * follows and then the type of paramter. + * + * Allowed types are either i, int or integer for integer types + * and s, str or string for string types. + * + * If a parameter can appear more than once the last character of + * the key should be a plus character (+). + * + * The value of the entry is the definition of what the paramter + * does. + */ + public function __construct($definitions) + { + if (is_array($definitions)) { + $this->definitions = $definitions; + } //if + } // __construct() + + /** + * checks if parameter is allowed + * + * @author Patrick Forget + * + * @param string $name either short or long name of the parameter to check + * + * @return boolean true if definition exisits, false otherwise + */ + public function paramExists($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + if (strlen($name) == 1) { + return isset($this->shortNames[$name]); + } else { + return isset($this->longNames[$name]); + } //if + } // paramExists($name) + + + /** + * checks if parameter allows a value if so what type + * + * @author Patrick Forget + * + * @param string $name either short or long name of the parameter to check + * + * @return boolean|string false doesn't allow value, The value "string" or "integer" depending which type it allows + */ + public function allowsValue($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + $longName = (strlen($name) == 1 ? ( isset($this->shortNames[$name]) ? $this->shortNames[$name] : '') : $name); + + if (isset($this->longNames[$longName])) { + return $this->longNames[$longName]['parameterType'] !== false ? true : false; + } else { + return false; + } //if + } // allowsValue() + + /** + * returns the type of value allowed + * + * @author Patrick Forget + */ + public function getValueType($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + $longName = (strlen($name) == 1 ? ( isset($this->shortNames[$name]) ? $this->shortNames[$name] : '') : $name); + + if (isset($this->longNames[$longName]['parameterType']) + && $this->longNames[$longName]['parameterType'] !== false) { + return $this->longNames[$longName]['parameterType']; + } else { + return ''; + } //if + } // getValueType() + + + /** + * checks if pamultiple instance of parameter are allowed + * + * @author Patrick Forget + * + * @param string $name either short or long name of the parameter to check + * + * @return boolean false if parameter doesn't allow multiple values, true if it does + */ + public function allowsMultiple($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + $longName = (strlen($name) == 1 ? ( isset($this->shortNames[$name]) ? $this->shortNames[$name] : '') : $name); + + if (isset($this->longNames[$longName])) { + return $this->longNames[$longName]['isMultipleAllowed']; + } else { + return false; + } //if + } // allowsMultiple() + + /** + * retreive short name of a parameter using its long name + * + * @author Patrick Forget + * + * @param string $name long name of the parameter to check + * + * @return string character of the short name or null if it doesn't exist + */ + public function getShortName($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + if (isset($this->longNames[$name])) { + return $this->longNames[$name]['shortName']; + } else { + return null; + } //if + } // getShortName($name) + + /** + * retreive long name of a parameter using its short name + * + * @author Patrick Forget + * + * @param string $name short name of the parameter to check + * + * @return string long name or null if it doesn't exist + */ + public function getLongName($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + if (isset($this->shortNames[$name])) { + return $this->shortNames[$name]; + } else { + return null; + } //if + } // getLongName($name) + + /** + * retreive description of a paramter + * + * @author Patrick Forget + * + * @param string $name either short or long name of the parameter to check + * + * @return string description or null if it doesn't exist + */ + public function getDescription($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + $longName = (strlen($name) == 1 ? ( isset($this->shortNames[$name]) ? $this->shortNames[$name] : '') : $name); + + if (isset($this->longNames[$longName])) { + return $this->longNames[$longName]['description']; + } else { + return null; + } //if + } // getDescription() + + /** + * builds a usage definition based on definition of params + * + * @author Patrick Forget + */ + public function getUsage() + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + /* build list of argument names and calculate + the first column width so we can pad to + align definitions */ + $firstCol = array(); + $longestDef = 0; + foreach (array_keys($this->longNames) as $longName) { + ob_start(); + echo "--{$longName}|-{$this->getShortName($longName)}"; + + if ($this->allowsValue($longName)) { + echo "={$this->getValueType($longName)}"; + } //if + + if ($this->allowsMultiple($longName)) { + echo "+"; + } //if + + $defLength = ob_get_length(); + + $longestDef = max($longestDef, $defLength); + + $firstCol[$longName] = ob_get_contents(); + ob_end_clean(); + + } //foreach + + $firstColMaxWidth = $longestDef + 4; + + ob_start(); + + foreach ($firstCol as $longName => $def) { + $currentDefLength = strlen($def); + + $padding = str_repeat(" ", $firstColMaxWidth - $currentDefLength); + + echo "{$def}{$padding}{$this->getDescription($longName)}", PHP_EOL; + } //foreach + + echo PHP_EOL; + + $usage = ob_get_contents(); + ob_end_clean(); + + return $usage; + + } // getUsage() + + + /** + * parses the definitions + * + * @author Patrick Forget + */ + protected function parseDefinitions() + { + foreach ($this->definitions as $nameDef => $description) { + $nameParts = explode("|", $nameDef); + + if (sizeof($nameParts) !== 2) { + throw new \UnexpectedValueException("Unexpected argument name definition expecting \"longName|char\""); + } //if + + $longName = $nameParts[0]; + $isMulti = false; + $parameterType = false; + + $shortNameLength = strlen($nameParts[1]); + + if ($shortNameLength == 1) { + $shortName = $nameParts[1]; + } else { + $secondChar = substr($nameParts[1], 1, 1); + + switch ($secondChar) { + case '=': + $shortNameParts = explode("=", $nameParts[1]); + + $shortName = $shortNameParts[0]; + $parameterTypeString = $shortNameParts[1]; + + if (substr($parameterTypeString, -1) === '+') { + $isMulti = true; + $parameterTypeString = substr($parameterTypeString, 0, -1); // remove trailing + + } //if + + switch ($parameterTypeString) { + case 'i': + case 'int': + case 'integer': + $parameterType = 'integer'; + break; + case 's': + case 'str': + case 'string': + $parameterType = 'string'; + break; + default: + throw new \UnexpectedValueException("Expecting parameter type". + " to be either integer or string"); + break; + } //switch + + break; + case '+': + if ($shortNameLength > 2) { + throw new \UnexpectedValueException("Multiple flag charachter (+)". + " should be last character in definition"); + } //if + + $shortName = substr($nameParts[1], 0, 1); + $isMulti = true; + + break; + default: + throw new \UnexpectedValueException("Expecting short name definition to be a single char"); + break; + } // switch + + } //if + + if (isset($this->longNames[$longName])) { + throw new \UnexpectedValueException("Cannot redefine long name {$longName}"); + } //if + + if (isset($this->shortNames[$shortName])) { + throw new \UnexpectedValueException("Cannot redefine short name {$shortName}"); + } //if + + $this->longNames[$longName] = array( + 'shortName' => $shortName, + 'isMultipleAllowed' => $isMulti, + 'parameterType' => $parameterType, + 'description' => $description + ); + + $this->shortNames[$shortName] = $longName; + + } //foreach + + $this->isParsed = true; + } // parseDefinitions() +} diff --git a/Classes/Curl.php b/Classes/Curl.php new file mode 100644 index 0000000..45a70c7 --- /dev/null +++ b/Classes/Curl.php @@ -0,0 +1,23 @@ +url); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, True); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + //curl_setopt($ch, CURLOPT_VERBOSE, true); // verbose mode for debugging + + $json = curl_exec($ch); + + curl_close($ch); + + $array = json_decode($json, true); + return $array; + } +} +?> \ No newline at end of file diff --git a/Classes/CommandArgumentFilter.php b/Classes/CommandArgumentFilter.php new file mode 100644 index 0000000..1ffa45e --- /dev/null +++ b/Classes/CommandArgumentFilter.php @@ -0,0 +1,343 @@ + + */ + +namespace Clapp; + +/** + * Filters an array and extracts and validates command line arguments + * + * @author Patrick Forget + */ +class CommandArgumentFilter +{ + /** + * Command line arguments + * @var array + */ + private $arguments = array(); + + /** + * Definition of allowed parameters + * @var \Clapp\CommandLineArgumentDefinition + */ + private $definitions = null; + + /** + * Flag if arguments have been parsed in to params + * @var boolean + */ + private $parsed = false; + + /** + * Parsed params + * @var array + */ + private $params = array(); + + /** + * Trailing values + * @var string + */ + private $trailingValues = ""; + + /** + * program name + * @var string + */ + private $programName = ""; + + /** + * class constructor + * + * @author Patrick Forget + * + * @param \Clapp\CommandLineDefinition $definitions contains list of allowed parameters + * @param array $args list of arguments to filter. + */ + public function __construct(\Clapp\CommandLineArgumentDefinition $definitions, $args) + { + if (is_array($args)) { + $this->arguments = $args; + } //if + + $this->definitions = $definitions; + } // __construct() + + /** + * returns parameter matching provided name + * + * @author Patrick Forget + * + * @param string name of the paramter to retreive + * + * @return mixed if param the param appears only once the method will + * return 1 if the parameter doesn't take a value. The specified value + * for that param will returned if it does take value. + * + * If many occurence of the param appear the number of occurences will + * be returned for params that do not take values. An array of values + * will be returned for the parameters that do take values. + * + * If the parameter is not present null if it takes a value and false if + * it's not present and doesn't allow values + */ + public function getParam($name) + { + if (!$this->parsed) { + $this->parseParams(); + } //if + + $longName = strlen($name) === 1 ? $this->definitions->getLongName($name) : $name; + if (isset($this->params[$longName])) { + return $this->params[$longName]; + } else { + if ($this->definitions->allowsValue($longName)) { + return null; + } else { + return false; + } //if + } //if + + } // getParam() + + /** + * retreive the program name + * + * @author Patrick Forget + */ + public function getProgramName() + { + if (!$this->parsed) { + $this->parseParams(); + } //if + + return $this->programName; + } // getProgramName() + + /** + * retreive the trailing values + * + * @author Patrick Forget + */ + public function getTrailingValues() + { + if (!$this->parsed) { + $this->parseParams(); + } //if + + return $this->trailingValues; + } // getTrailingValues() + + /** + * extracts params from arguments + * + * @author Patrick Forget + */ + protected function parseParams() + { + + $argumentStack = $this->arguments; + + $expectingValue = false; + $currentLongName = null; + $currentValue = null; + $trailingValues = ""; + $endOfDashedArguments = false; + $addParam = false; + $argumentsLeft = sizeof($argumentStack); + $multiShortParams = array(); + + $this->programName = array_shift($argumentStack); // remove first argument which is the program name + + while ($currentArgument = array_shift($argumentStack)) { + $argumentsLeft--; + $currentArgumentLength = strlen($currentArgument); + + // arguments that don't start with a dash + if (substr($currentArgument, 0, 1) !== '-') { + if ($expectingValue) { + $currentValue = $currentArgument; + $addParam = true; + } else { + $trailingValues .= " ". $currentArgument; + $endOfDashedArguments = true; + } //if + + // double dash detected + } elseif (substr($currentArgument, 1, 1) === '-') { + if ($expectingValue) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + + /* stop parsing arguments if double dash + only param is encountered */ + if ($currentArgumentLength == 2) { + if ($trailingValues !== "") { + throw new \UnexpectedValueException("Trailing values must appear after double dash"); + } //if + + $trailingValues = " ". implode(" ", $argumentStack); + $argumentStack = array(); + $endOfDashedArguments = true; + break; + } //if + + $longNameParts = explode("=", substr($currentArgument, 2), 2); + + $currentLongName = $longNameParts[0]; + + if (sizeof($longNameParts) > 1) { + $currentValue = $longNameParts[1]; + $addParam = true; + } elseif ($this->definitions->allowsValue($currentLongName)) { + $expectingValue = true; + } else { + $addParam = true; + } //if + + // single dash + } else { + if ($expectingValue) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + + $shortNameParts = explode("=", substr($currentArgument, 1), 2); + + $shortName = $shortNameParts[0]; + + if (strlen($shortName) <= 1) { + $currentLongName = $this->definitions->getLongName($shortName); + + if ($currentLongName === null) { + throw new \InvalidArgumentException("Unable to find name with ". + "provided parameter ({$shortName})"); + } //if + + if (sizeof($shortNameParts) > 1) { + $currentValue = $shortNameParts[1]; + $addParam = true; + } elseif ($this->definitions->allowsValue($currentLongName)) { + $expectingValue = true; + } else { + $addParam = true; + } //if + + } else { + $multiShortParams = str_split($shortName); + + /* process the last one (which is the only one that can have a value) */ + $lastParam = array_pop($multiShortParams); + $currentLongName = $this->definitions->getLongName($lastParam); + if (sizeof($shortNameParts) > 1) { + $currentValue = $shortNameParts[1]; + $addParam = true; + } elseif ($this->definitions->allowsValue($lastParam)) { + $expectingValue = true; + } else { + $addParam = true; + } //if + + } //if + + } //if + + if ($addParam) { + if ($endOfDashedArguments) { + throw new \UnexpectedValueException("Unexpected argument after undashed values"); + } //if + + /* Not sure how this could happen */ + // @codeCoverageIgnoreStart + if ($currentLongName === false || $currentLongName === null) { + throw new \UnexpectedValueException("Missing argument name"); + } //if + // @codeCoverageIgnoreEnd + + if (!$this->definitions->paramExists($currentLongName)) { + throw new \InvalidArgumentException("Invalid argument name"); + } //if + + $allowsMultiple = $this->definitions->allowsMultiple($currentLongName); + $allowsValue = $this->definitions->allowsValue($currentLongName); + + if (isset($this->params[$currentLongName]) && !$allowsMultiple) { + throw new \UnexpectedValueException("Multiple instace of parameter {$currentLongName} not allowed"); + } //if + + if ($allowsValue) { + /* Missing values should always be detected before addParam is true */ + // @codeCoverageIgnoreStart + if ($currentValue === null) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + // @codeCoverageIgnoreEnd + + } elseif ($currentValue !== null) { + throw new \UnexpectedValueException("Parameter {$currentLongName} does not accept values"); + + } else { + $currentValue = true; + } //if + + if ($allowsMultiple) { + if ($allowsValue) { + if (!isset($this->params[$currentLongName])) { + $this->params[$currentLongName] = array(); + } //if + + $this->params[$currentLongName][] = $currentValue; + + } else { + if (!isset($this->params[$currentLongName])) { + $this->params[$currentLongName] = 0; + } //if + + $this->params[$currentLongName]++; + + } //if + + } else { + $this->params[$currentLongName] = $currentValue; + } //if + + foreach ($multiShortParams as $shortName) { + $argumentStack[] = "-{$shortName}"; + $argumentsLeft++; + } //foreach + + /* reset stuff for next param */ + $expectingValue = false; + $currentLongName = null; + $currentValue = null; + $addParam = false; + $multiShortParams = array(); + + } //if + + } //while + + if ($expectingValue !== false) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + + /* Not sure how this could happen */ + // @codeCoverageIgnoreStart + if ($currentLongName !== null || + $addParam !== false || + $currentValue !== null || + sizeof($multiShortParams) !== 0) { + throw new \UnexpectedValueException("Unable to process some parameters"); + } //if + // @codeCoverageIgnoreEnd + + if ($trailingValues !== "") { + $this->trailingValues = substr($trailingValues, 1); // remove extra space at the begging + } //if + + $this->parsed = true; + } // parseParams() +} diff --git a/Classes/CommandLineArgumentDefinition.php b/Classes/CommandLineArgumentDefinition.php new file mode 100644 index 0000000..2b2e8fa --- /dev/null +++ b/Classes/CommandLineArgumentDefinition.php @@ -0,0 +1,389 @@ + + */ + +namespace Clapp; + +/** + * Defines list and formats of command line arguments + * + * @author Patrick Forget + */ +class CommandLineArgumentDefinition +{ + + /** + * @var array + */ + private $definitions = array(); + + /** + * long names as keys and array of properties as values + * + * properties are as follows + * * string "shortName" one letter char to the corresponding short name + * * boolean "isMultipleAllowed" true if mutliple instances of the param are allowed + * * mixed "parameterType" false if paramters are not alloweda value, + * otherwise a string with the value "integer" or "string" + * * string "description" description of the parameter + * @var array + */ + private $longNames = array(); + + /** + * list of short names as keys and their long name equivalent as values + * @var array + */ + private $shortNames = array(); + + /** + * Flag if arguments have been parsed in to params + * @var boolean + */ + private $isParsed = false; + + /** + * class constructor + * + * @author Patrick Forget + * + * @param array $definitions contains list of allowed parameters + * the key is the long name of the parameter followed by a pipe (|) + * then a single character specifying the short name. + * + * If the parameter allows for arguments then an equal sign (=) + * follows and then the type of paramter. + * + * Allowed types are either i, int or integer for integer types + * and s, str or string for string types. + * + * If a parameter can appear more than once the last character of + * the key should be a plus character (+). + * + * The value of the entry is the definition of what the paramter + * does. + */ + public function __construct($definitions) + { + if (is_array($definitions)) { + $this->definitions = $definitions; + } //if + } // __construct() + + /** + * checks if parameter is allowed + * + * @author Patrick Forget + * + * @param string $name either short or long name of the parameter to check + * + * @return boolean true if definition exisits, false otherwise + */ + public function paramExists($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + if (strlen($name) == 1) { + return isset($this->shortNames[$name]); + } else { + return isset($this->longNames[$name]); + } //if + } // paramExists($name) + + + /** + * checks if parameter allows a value if so what type + * + * @author Patrick Forget + * + * @param string $name either short or long name of the parameter to check + * + * @return boolean|string false doesn't allow value, The value "string" or "integer" depending which type it allows + */ + public function allowsValue($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + $longName = (strlen($name) == 1 ? ( isset($this->shortNames[$name]) ? $this->shortNames[$name] : '') : $name); + + if (isset($this->longNames[$longName])) { + return $this->longNames[$longName]['parameterType'] !== false ? true : false; + } else { + return false; + } //if + } // allowsValue() + + /** + * returns the type of value allowed + * + * @author Patrick Forget + */ + public function getValueType($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + $longName = (strlen($name) == 1 ? ( isset($this->shortNames[$name]) ? $this->shortNames[$name] : '') : $name); + + if (isset($this->longNames[$longName]['parameterType']) + && $this->longNames[$longName]['parameterType'] !== false) { + return $this->longNames[$longName]['parameterType']; + } else { + return ''; + } //if + } // getValueType() + + + /** + * checks if pamultiple instance of parameter are allowed + * + * @author Patrick Forget + * + * @param string $name either short or long name of the parameter to check + * + * @return boolean false if parameter doesn't allow multiple values, true if it does + */ + public function allowsMultiple($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + $longName = (strlen($name) == 1 ? ( isset($this->shortNames[$name]) ? $this->shortNames[$name] : '') : $name); + + if (isset($this->longNames[$longName])) { + return $this->longNames[$longName]['isMultipleAllowed']; + } else { + return false; + } //if + } // allowsMultiple() + + /** + * retreive short name of a parameter using its long name + * + * @author Patrick Forget + * + * @param string $name long name of the parameter to check + * + * @return string character of the short name or null if it doesn't exist + */ + public function getShortName($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + if (isset($this->longNames[$name])) { + return $this->longNames[$name]['shortName']; + } else { + return null; + } //if + } // getShortName($name) + + /** + * retreive long name of a parameter using its short name + * + * @author Patrick Forget + * + * @param string $name short name of the parameter to check + * + * @return string long name or null if it doesn't exist + */ + public function getLongName($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + if (isset($this->shortNames[$name])) { + return $this->shortNames[$name]; + } else { + return null; + } //if + } // getLongName($name) + + /** + * retreive description of a paramter + * + * @author Patrick Forget + * + * @param string $name either short or long name of the parameter to check + * + * @return string description or null if it doesn't exist + */ + public function getDescription($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + $longName = (strlen($name) == 1 ? ( isset($this->shortNames[$name]) ? $this->shortNames[$name] : '') : $name); + + if (isset($this->longNames[$longName])) { + return $this->longNames[$longName]['description']; + } else { + return null; + } //if + } // getDescription() + + /** + * builds a usage definition based on definition of params + * + * @author Patrick Forget + */ + public function getUsage() + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + /* build list of argument names and calculate + the first column width so we can pad to + align definitions */ + $firstCol = array(); + $longestDef = 0; + foreach (array_keys($this->longNames) as $longName) { + ob_start(); + echo "--{$longName}|-{$this->getShortName($longName)}"; + + if ($this->allowsValue($longName)) { + echo "={$this->getValueType($longName)}"; + } //if + + if ($this->allowsMultiple($longName)) { + echo "+"; + } //if + + $defLength = ob_get_length(); + + $longestDef = max($longestDef, $defLength); + + $firstCol[$longName] = ob_get_contents(); + ob_end_clean(); + + } //foreach + + $firstColMaxWidth = $longestDef + 4; + + ob_start(); + + foreach ($firstCol as $longName => $def) { + $currentDefLength = strlen($def); + + $padding = str_repeat(" ", $firstColMaxWidth - $currentDefLength); + + echo "{$def}{$padding}{$this->getDescription($longName)}", PHP_EOL; + } //foreach + + echo PHP_EOL; + + $usage = ob_get_contents(); + ob_end_clean(); + + return $usage; + + } // getUsage() + + + /** + * parses the definitions + * + * @author Patrick Forget + */ + protected function parseDefinitions() + { + foreach ($this->definitions as $nameDef => $description) { + $nameParts = explode("|", $nameDef); + + if (sizeof($nameParts) !== 2) { + throw new \UnexpectedValueException("Unexpected argument name definition expecting \"longName|char\""); + } //if + + $longName = $nameParts[0]; + $isMulti = false; + $parameterType = false; + + $shortNameLength = strlen($nameParts[1]); + + if ($shortNameLength == 1) { + $shortName = $nameParts[1]; + } else { + $secondChar = substr($nameParts[1], 1, 1); + + switch ($secondChar) { + case '=': + $shortNameParts = explode("=", $nameParts[1]); + + $shortName = $shortNameParts[0]; + $parameterTypeString = $shortNameParts[1]; + + if (substr($parameterTypeString, -1) === '+') { + $isMulti = true; + $parameterTypeString = substr($parameterTypeString, 0, -1); // remove trailing + + } //if + + switch ($parameterTypeString) { + case 'i': + case 'int': + case 'integer': + $parameterType = 'integer'; + break; + case 's': + case 'str': + case 'string': + $parameterType = 'string'; + break; + default: + throw new \UnexpectedValueException("Expecting parameter type". + " to be either integer or string"); + break; + } //switch + + break; + case '+': + if ($shortNameLength > 2) { + throw new \UnexpectedValueException("Multiple flag charachter (+)". + " should be last character in definition"); + } //if + + $shortName = substr($nameParts[1], 0, 1); + $isMulti = true; + + break; + default: + throw new \UnexpectedValueException("Expecting short name definition to be a single char"); + break; + } // switch + + } //if + + if (isset($this->longNames[$longName])) { + throw new \UnexpectedValueException("Cannot redefine long name {$longName}"); + } //if + + if (isset($this->shortNames[$shortName])) { + throw new \UnexpectedValueException("Cannot redefine short name {$shortName}"); + } //if + + $this->longNames[$longName] = array( + 'shortName' => $shortName, + 'isMultipleAllowed' => $isMulti, + 'parameterType' => $parameterType, + 'description' => $description + ); + + $this->shortNames[$shortName] = $longName; + + } //foreach + + $this->isParsed = true; + } // parseDefinitions() +} diff --git a/Classes/Curl.php b/Classes/Curl.php new file mode 100644 index 0000000..45a70c7 --- /dev/null +++ b/Classes/Curl.php @@ -0,0 +1,23 @@ +url); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, True); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + //curl_setopt($ch, CURLOPT_VERBOSE, true); // verbose mode for debugging + + $json = curl_exec($ch); + + curl_close($ch); + + $array = json_decode($json, true); + return $array; + } +} +?> \ No newline at end of file diff --git a/GoStats.php b/GoStats.php new file mode 100755 index 0000000..c1ba2c6 --- /dev/null +++ b/GoStats.php @@ -0,0 +1,401 @@ +#!/usr/bin/php + "Shows help message", + "list|l" => "List campaigns and their ID's", + "campaign|c=i" => "Get campaign by id", + "dump|d=s" => "Dump user:pass list to ", + "training|t=s" => "Dump list of users requiring training ", + "all|a" => "All of the below options", + "ips|i" => "Top 10 IP's", + "useragent|u" => "Top 10 user agents", + "attempts|m" => "Top 10 attempts to log in", + "active|o" => "Active times", + "speed|e" => "Clickthrough speed", + "stats|s" => "Victim statistics", + "pass|p" => "Password analysis with pwdlyser", + ) +); + +$filter = new \Clapp\CommandArgumentFilter($definitions, $argv); + +if ($filter->getParam('h') === true || $argc < 2) { + fwrite(STDERR, $definitions->getUsage()); + exit(0); +} + +/* Get list of campaigns */ +if ($filter->getParam("list") !== false) { + echo "[+] Getting data from server\n"; + $curl = new curl(); + $curl->url = "$url/api/campaigns/?api_key=$key"; + $list = $curl->curlQuery(); + + if(isset($list->message) && $list->message == "Invalid API Key"){ + echo "[!] Invalid API key\n"; + exit(0); + }else{ + echo "[id] -campaign name-\n"; + foreach($list as $id) + echo "[".$id['id']."] ".$id['name']."\n"; + } + exit(0); +} + +/* Get campaign data */ +$campid = $filter->getParam('c'); +if ($campid == null || !is_numeric($campid)) { + echo "[!] Campaign ID not set\nn"; + exit(0); +}else{ + echo "[+] Getting data from server\n"; + $curl = new curl(); + $curl->url = "$url/api/campaigns/$campid?api_key=$key"; + $list = $curl->curlQuery(); + if(isset($list->message) && $list->message == "Invalid API Key"){ + echo "[!] Invalid API key\n"; + exit(0); + }else{ + /* all data got correctly time to do stuff! */ + echo "[$campid] ".$list['name']."\n"; + echo "\n--- Notable times ---\n"; + + if(isset($list['launch_date']) && $list['launch_date'] <> ""){ + $time = date('d-m-Y H:i', $datetime = strtotime(substr($list['launch_date'], 0, 10) . ' ' . substr($list['launch_date'], 11, 8 ))); + echo "Campaign launched: $time\n"; + } + + foreach($list['timeline'] as $record){ + if($record['message'] == "Email Sent"){ + $time = date('d-m-Y H:i', $datetime = strtotime(substr($record['time'], 0, 10) . ' ' . substr($record['time'], 11, 8 ))); + echo "First email sent: $time\n"; + break; + } + } + foreach($list['timeline'] as $record){ + if($record['message'] == "Email Sent"){ + $time = date('d-m-Y H:i', $datetime = strtotime(substr($record['time'], 0, 10) . ' ' . substr($record['time'], 11, 8 ))); + } + } + echo "Last email sent:: $time\n"; + foreach($list['timeline'] as $record){ + if($record['message'] == "Clicked Link"){ + $time = date('d-m-Y H:i', $datetime = strtotime(substr($record['time'], 0, 10) . ' ' . substr($record['time'], 11, 8 ))); + echo "First email opened: $time\n"; + break; + } + } + foreach($list['timeline'] as $record){ + if($record['message'] == "Clicked Link"){ + $time = date('d-m-Y H:i', $datetime = strtotime(substr($record['time'], 0, 10) . ' ' . substr($record['time'], 11, 8 ))); + echo "First page view: $time\n"; + break; + } + } + foreach($list['timeline'] as $record){ + if($record['message'] == "Submitted Data"){ + $time = date('d-m-Y H:i', $datetime = strtotime(substr($record['time'], 0, 10) . ' ' . substr($record['time'], 11, 8 ))); + echo "First credentials submitted: $time\n"; + break; + } + } + if(isset($list['completed_date']) && $list['completed_date'] <> ""){ + $time = date('d-m-Y H:i', $datetime = strtotime(substr($list['completed_date'], 0, 10) . ' ' . substr($list['completed_date'], 11, 8 ))); + echo "Campaign finished: $time\n"; + } + } +} + +/* Top 10 IP's */ +if ($filter->getParam("ips") !== false || $filter->getParam("all") !== false) { + $ips = array(); + foreach($list['timeline'] as $item){ + if($item['details'] <> ""){ + $details = json_decode($item['details'], true); + if($details['browser']['address'] !== "unknown") + $ips[] = (string)$details['browser']['address']; + } + } + $ips = array_count_values($ips); + arsort($ips); + $ips = array_slice($ips,0,10,true); + echo "\n--- Top 10 IP's ---\n"; + foreach($ips as $ip=>$no){ + $geoip_details = ""; + if($geoip == true){ + $geojson = file_get_contents("http://freegeoip.net/json/$ip"); + $geodetails = json_decode($geojson, true); + $geoip_details = "- ".$geodetails['country_name'].", ".$geodetails['city']; + } + echo "[$no] $ip $geoip_details\n"; + } +} + +/* Top 10 user agent's */ +if ($filter->getParam("useragent") !== false || $filter->getParam("all") !== false) { + $agents = array(); + foreach($list['timeline'] as $item){ + if($item['details'] <> "" && $item['message'] == "Clicked Link"){ // only people who visited site, not email user agent + $details = json_decode($item['details'], true); + if($details['browser']['user-agent'] !== "unknown" && $details['browser']['user-agent'] !== "") + $agents[] = (string)$details['browser']['user-agent']; + } + } + $agents = array_count_values($agents); + arsort($agents); + $agents = array_slice($agents,0,10,true); + echo "\n--- Top 10 User Agents ---\n"; + foreach($agents as $ua=>$no){ + echo "[$no] $ua\n"; + } +} + +/* Top 10 attempts to log in */ +if($filter->getParam("attempts") !== false || $filter->getParam("all") !== false) { + $userids = array(); + foreach($list['results'] as $item){ + $userids[$item['id']] = $item['email']; + } + + $attemptrids = array(); + foreach($list['timeline'] as $item){ + if($item['details'] <> ""){ + $details = json_decode($item['details'], true); + if(isset($details['payload']['password'][0]) && $details['payload']['password'][0] <> ""){ + $attemptrids[$details['payload']['rid'][0]] += 1; + } + } + } + arsort($attemptrids); + $attemptrids = array_slice($attemptrids,0,10,true); + echo "\n--- Top 10 Login Attempts ---\n"; + foreach($attemptrids as $id=>$amount){ + $newemail= preg_replace('/(?:^|.@).\K|.\.[^@]*$(*SKIP)(*F)|.(?=.*?\.)/', '*', $userids[$id]); + echo "[$amount] $newemail\n"; + } +} + +/* Active times */ +if($filter->getParam("active") !== false || $filter->getParam("all") !== false) { + $active_count = array(); + $active_percent = array(); + $total = 0; + echo "\n--- Active times (hour, actions & percent) ---\n"; + foreach($list['timeline'] as $item){ + if($item['message'] != "Campaign Created" && $item['message'] != "Email Sent" ){ + $hour = (int)substr($item['time'], 11, 2); + $active_count[$hour]++; + $total++; + } + + } + foreach($active_count as $id => $count) // populate percentages + $active_percent[$id] = ($count / $total) * 100; + + for($i = 0; $i <= 12; $i++){ + $iDsp = str_pad($i, 2, " ", STR_PAD_LEFT); + $j = $i+12; + $user1 = str_pad($active_count[$i], 4, " ", STR_PAD_LEFT); + $user2 = str_pad($active_count[$j], 4, " ", STR_PAD_LEFT); + $percent1 = number_format($active_percent[$i], 2, '.', ''); + $percent1 = str_pad($percent1, 5, " ", STR_PAD_LEFT); + $percent2 = number_format($active_percent[$j], 2, '.', ''); + $percent2 = str_pad($percent2, 5, " ", STR_PAD_LEFT); + + echo "$iDsp - $user1 = $percent1% | $j - $user2 = $percent2% \n"; + } +} + +/* Clickthrough speed */ +if ($filter->getParam("speed") !== false || $filter->getParam("all") !== false) { + $speed_opened = array(); + $speed_visited = array(); + $speed_offset = array(); + echo "\n--- Clickthrough Speed ---\n"; + foreach($list['timeline'] as $item){ + if($item['message'] == "Email Opened"){ + $details = json_decode($item['details'], true); + $check_rid = $details['payload']['rid'][0]; + + $current_time = strtotime(substr($item['time'], 0, 10) . ' ' . substr($item['time'], 11, 8 )); + $existing_time = strtotime(substr($speed_opened[$check_rid], 0, 10) . ' ' . substr($speed_opened[$check_rid], 11, 8 )); + + if(!isset($speed_opened[$check_rid]) || $existing_time > $current_time) + $speed_opened[$check_rid] = $item['time']; + } + if($item['message'] == "Clicked Link"){ + $details = json_decode($item['details'], true); + $check_rid = $details['payload']['rid'][0]; + if(!isset($speed_visited[$check_rid])) + $speed_visited[$check_rid] = $item['time']; + } + } + foreach($speed_opened as $id=>$val){ // remove all the ones that didn't visit site + if(!isset($speed_visited[$id])) + unset($speed_opened[$id]); + } + foreach($speed_visited as $id=>$val){ // remove all the ones that didn't load email tracking image + if(!isset($speed_opened[$id])) + unset($speed_visited[$id]); + } + foreach($speed_opened as $id=>$val){ //calculate speed between reading email and clicking link + $date_opened = substr($val, 0, 10); + $time_opened = substr($val, 11, 8 ); + $time_opened_stamp = strtotime($date_opened." ".$time_opened); + + $date_visited = substr($speed_visited[$id], 0, 10); + $time_visited = substr($speed_visited[$id], 11, 8 ); + $time_visited_stamp = strtotime($date_visited." ".$time_visited); + + $diff = $time_visited_stamp - $time_opened_stamp; + if($diff > 0) + $speed_offset[$id] = $diff; + } + + unset($speed_opened); // check me out doing memory management and cleaning up! :D + unset($speed_visited); + + $quickest = min($speed_offset); + echo "Quickest click: $quickest sec\n"; + $longest = max($speed_offset); + $longest = floor(($longest / 60) % 60); + echo "Longest click: $longest min\n"; + $sec_5 = array_reduce($speed_offset, function ($a, $b){ + return ($b <= 5) ? ++$a : $a; }); + echo "Users clicked < 5 sec: $sec_5 \n"; + $sec_30 = array_reduce($speed_offset, function ($a, $b){ + return ($b <= 30) ? ++$a : $a; }); + echo "Users clicked < 30 sec: $sec_30 \n"; + $sec_60 = array_reduce($speed_offset, function ($a, $b){ + return ($b <= 60) ? ++$a : $a; }); + echo "Users clicked < 1 min: $sec_60 \n"; +} + +/* Victim statistics */ +if ($filter->getParam("stats") !== false || $filter->getParam("all") !== false) { + $status = array(); + foreach($list['results'] as $item){ + if($item['status'] <> ""){ + $status[] = $item['status']; + } + } + echo "\n--- Victim Statistics ---\n"; + $statusall = count($status); + $counts = array_count_values($status); + echo "Targets: ".$statusall."\n"; + $openedpercent = ($counts['Email Opened'] / $statusall) * 100; + echo "Email opened: ".$counts['Email Opened']." (".round($openedpercent, 2)."%)\n"; + $linkpercent = ($counts['Clicked Link'] / $statusall) * 100; + echo "Visited link: ".$counts['Clicked Link']." (".round($linkpercent, 2)."%)\n"; + $subpercent = ($counts['Submitted Data'] / $statusall) * 100; + echo "Submitted data: ".$counts['Submitted Data']." (".round($subpercent, 2)."%)\n"; + foreach($list['timeline'] as $item){ + if($item['details'] <> ""){ + $details = json_decode($item['details'], true); + if($details['payload']['password'][0] <> "") + $totalLoginAttempts++; + } + } + echo "Total login attempts: $totalLoginAttempts\n"; +} + +/* Pwdlyzer */ +if ($filter->getParam("pass") !== false || $filter->getParam("all") !== false) { + $username = array(); + $password = array(); + echo "\n--- Password Statistics ---\n"; + foreach($list['timeline'] as $item){ + if($item['details'] <> ""){ + $details = json_decode($item['details'], true); + if($details['payload']['password'][0] <> ""){ + $username[] = $details['payload']['username'][0]; + $password[] = $details['payload']['password'][0]; + } + } + } + $tmpfname = tempnam("/tmp", "GoStats-"); + $pwdfname = tempnam("/tmp", "GoStats-"); + $handle = fopen($tmpfname, "w"); + foreach($username as $id=>$user){ + fwrite($handle, "$user:".$password[$id]."\n"); + } + fclose($handle); + echo "[+] Launching pwdlyzer\n"; + exec("cd $pwd && ./pwdlyser.py -p $tmpfname --all > $pwdfname"); + unlink($tmpfname); + echo "[+] pwdlyzer results at: $pwdfname\n"; +} + +/* dump username:password list to file */ +$dumpfile = $filter->getParam('dump'); +if(file_exists($dumpfile)){ + echo "[!] File already exists ($dumpfile)\n"; + exit(0); +} +if(!file_exists($dumpfile) && isset($dumpfile)){ + $username = array(); + $password = array(); + echo "\n--- Dumping username:password to file ---\n"; + foreach($list['timeline'] as $item){ + if($item['details'] <> ""){ + $details = json_decode($item['details'], true); + if($details['payload']['password'][0] <> ""){ + $username[] = $details['payload']['username'][0]; + $password[] = $details['payload']['password'][0]; + } + } + } + $handle = fopen($dumpfile, "w"); + foreach($username as $id=>$user){ + fwrite($handle, "$user:".$password[$id]."\n"); + } + fclose($handle); + echo "[+] File created: $dumpfile\n"; +} + +/* dump list of users requiring training */ +$dumpfile2 = $filter->getParam('training'); +if(file_exists($dumpfile2)){ + echo "[!] File already exists ($dumpfile)\n"; + exit(0); +} +if(!file_exists($dumpfile2) && isset($dumpfile2)){ + $tusername = array(); + $temail = array(); + $tstatus = array(); + echo "\n--- Dumping list of users requiring training ---\n"; + foreach($list['results'] as $item){ + if($item['status'] == "Submitted Data" || $item['status'] == "Clicked Link"){ + $tusername[] = $item['first_name']." ".$item['last_name']; + $temail[] = $item['email']; + $tstatus[] = $item['status']; + } + } + $handle = fopen($dumpfile2, "w"); + foreach($tusername as $id=>$user){ + fwrite($handle, "$user, ".$temail[$id].", ".$tstatus[$id]."\n"); + } + fclose($handle); + echo "[+] File created: $dumpfile2\n"; +} + +?> diff --git a/Classes/CommandArgumentFilter.php b/Classes/CommandArgumentFilter.php new file mode 100644 index 0000000..1ffa45e --- /dev/null +++ b/Classes/CommandArgumentFilter.php @@ -0,0 +1,343 @@ + + */ + +namespace Clapp; + +/** + * Filters an array and extracts and validates command line arguments + * + * @author Patrick Forget + */ +class CommandArgumentFilter +{ + /** + * Command line arguments + * @var array + */ + private $arguments = array(); + + /** + * Definition of allowed parameters + * @var \Clapp\CommandLineArgumentDefinition + */ + private $definitions = null; + + /** + * Flag if arguments have been parsed in to params + * @var boolean + */ + private $parsed = false; + + /** + * Parsed params + * @var array + */ + private $params = array(); + + /** + * Trailing values + * @var string + */ + private $trailingValues = ""; + + /** + * program name + * @var string + */ + private $programName = ""; + + /** + * class constructor + * + * @author Patrick Forget + * + * @param \Clapp\CommandLineDefinition $definitions contains list of allowed parameters + * @param array $args list of arguments to filter. + */ + public function __construct(\Clapp\CommandLineArgumentDefinition $definitions, $args) + { + if (is_array($args)) { + $this->arguments = $args; + } //if + + $this->definitions = $definitions; + } // __construct() + + /** + * returns parameter matching provided name + * + * @author Patrick Forget + * + * @param string name of the paramter to retreive + * + * @return mixed if param the param appears only once the method will + * return 1 if the parameter doesn't take a value. The specified value + * for that param will returned if it does take value. + * + * If many occurence of the param appear the number of occurences will + * be returned for params that do not take values. An array of values + * will be returned for the parameters that do take values. + * + * If the parameter is not present null if it takes a value and false if + * it's not present and doesn't allow values + */ + public function getParam($name) + { + if (!$this->parsed) { + $this->parseParams(); + } //if + + $longName = strlen($name) === 1 ? $this->definitions->getLongName($name) : $name; + if (isset($this->params[$longName])) { + return $this->params[$longName]; + } else { + if ($this->definitions->allowsValue($longName)) { + return null; + } else { + return false; + } //if + } //if + + } // getParam() + + /** + * retreive the program name + * + * @author Patrick Forget + */ + public function getProgramName() + { + if (!$this->parsed) { + $this->parseParams(); + } //if + + return $this->programName; + } // getProgramName() + + /** + * retreive the trailing values + * + * @author Patrick Forget + */ + public function getTrailingValues() + { + if (!$this->parsed) { + $this->parseParams(); + } //if + + return $this->trailingValues; + } // getTrailingValues() + + /** + * extracts params from arguments + * + * @author Patrick Forget + */ + protected function parseParams() + { + + $argumentStack = $this->arguments; + + $expectingValue = false; + $currentLongName = null; + $currentValue = null; + $trailingValues = ""; + $endOfDashedArguments = false; + $addParam = false; + $argumentsLeft = sizeof($argumentStack); + $multiShortParams = array(); + + $this->programName = array_shift($argumentStack); // remove first argument which is the program name + + while ($currentArgument = array_shift($argumentStack)) { + $argumentsLeft--; + $currentArgumentLength = strlen($currentArgument); + + // arguments that don't start with a dash + if (substr($currentArgument, 0, 1) !== '-') { + if ($expectingValue) { + $currentValue = $currentArgument; + $addParam = true; + } else { + $trailingValues .= " ". $currentArgument; + $endOfDashedArguments = true; + } //if + + // double dash detected + } elseif (substr($currentArgument, 1, 1) === '-') { + if ($expectingValue) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + + /* stop parsing arguments if double dash + only param is encountered */ + if ($currentArgumentLength == 2) { + if ($trailingValues !== "") { + throw new \UnexpectedValueException("Trailing values must appear after double dash"); + } //if + + $trailingValues = " ". implode(" ", $argumentStack); + $argumentStack = array(); + $endOfDashedArguments = true; + break; + } //if + + $longNameParts = explode("=", substr($currentArgument, 2), 2); + + $currentLongName = $longNameParts[0]; + + if (sizeof($longNameParts) > 1) { + $currentValue = $longNameParts[1]; + $addParam = true; + } elseif ($this->definitions->allowsValue($currentLongName)) { + $expectingValue = true; + } else { + $addParam = true; + } //if + + // single dash + } else { + if ($expectingValue) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + + $shortNameParts = explode("=", substr($currentArgument, 1), 2); + + $shortName = $shortNameParts[0]; + + if (strlen($shortName) <= 1) { + $currentLongName = $this->definitions->getLongName($shortName); + + if ($currentLongName === null) { + throw new \InvalidArgumentException("Unable to find name with ". + "provided parameter ({$shortName})"); + } //if + + if (sizeof($shortNameParts) > 1) { + $currentValue = $shortNameParts[1]; + $addParam = true; + } elseif ($this->definitions->allowsValue($currentLongName)) { + $expectingValue = true; + } else { + $addParam = true; + } //if + + } else { + $multiShortParams = str_split($shortName); + + /* process the last one (which is the only one that can have a value) */ + $lastParam = array_pop($multiShortParams); + $currentLongName = $this->definitions->getLongName($lastParam); + if (sizeof($shortNameParts) > 1) { + $currentValue = $shortNameParts[1]; + $addParam = true; + } elseif ($this->definitions->allowsValue($lastParam)) { + $expectingValue = true; + } else { + $addParam = true; + } //if + + } //if + + } //if + + if ($addParam) { + if ($endOfDashedArguments) { + throw new \UnexpectedValueException("Unexpected argument after undashed values"); + } //if + + /* Not sure how this could happen */ + // @codeCoverageIgnoreStart + if ($currentLongName === false || $currentLongName === null) { + throw new \UnexpectedValueException("Missing argument name"); + } //if + // @codeCoverageIgnoreEnd + + if (!$this->definitions->paramExists($currentLongName)) { + throw new \InvalidArgumentException("Invalid argument name"); + } //if + + $allowsMultiple = $this->definitions->allowsMultiple($currentLongName); + $allowsValue = $this->definitions->allowsValue($currentLongName); + + if (isset($this->params[$currentLongName]) && !$allowsMultiple) { + throw new \UnexpectedValueException("Multiple instace of parameter {$currentLongName} not allowed"); + } //if + + if ($allowsValue) { + /* Missing values should always be detected before addParam is true */ + // @codeCoverageIgnoreStart + if ($currentValue === null) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + // @codeCoverageIgnoreEnd + + } elseif ($currentValue !== null) { + throw new \UnexpectedValueException("Parameter {$currentLongName} does not accept values"); + + } else { + $currentValue = true; + } //if + + if ($allowsMultiple) { + if ($allowsValue) { + if (!isset($this->params[$currentLongName])) { + $this->params[$currentLongName] = array(); + } //if + + $this->params[$currentLongName][] = $currentValue; + + } else { + if (!isset($this->params[$currentLongName])) { + $this->params[$currentLongName] = 0; + } //if + + $this->params[$currentLongName]++; + + } //if + + } else { + $this->params[$currentLongName] = $currentValue; + } //if + + foreach ($multiShortParams as $shortName) { + $argumentStack[] = "-{$shortName}"; + $argumentsLeft++; + } //foreach + + /* reset stuff for next param */ + $expectingValue = false; + $currentLongName = null; + $currentValue = null; + $addParam = false; + $multiShortParams = array(); + + } //if + + } //while + + if ($expectingValue !== false) { + throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values"); + } //if + + /* Not sure how this could happen */ + // @codeCoverageIgnoreStart + if ($currentLongName !== null || + $addParam !== false || + $currentValue !== null || + sizeof($multiShortParams) !== 0) { + throw new \UnexpectedValueException("Unable to process some parameters"); + } //if + // @codeCoverageIgnoreEnd + + if ($trailingValues !== "") { + $this->trailingValues = substr($trailingValues, 1); // remove extra space at the begging + } //if + + $this->parsed = true; + } // parseParams() +} diff --git a/Classes/CommandLineArgumentDefinition.php b/Classes/CommandLineArgumentDefinition.php new file mode 100644 index 0000000..2b2e8fa --- /dev/null +++ b/Classes/CommandLineArgumentDefinition.php @@ -0,0 +1,389 @@ + + */ + +namespace Clapp; + +/** + * Defines list and formats of command line arguments + * + * @author Patrick Forget + */ +class CommandLineArgumentDefinition +{ + + /** + * @var array + */ + private $definitions = array(); + + /** + * long names as keys and array of properties as values + * + * properties are as follows + * * string "shortName" one letter char to the corresponding short name + * * boolean "isMultipleAllowed" true if mutliple instances of the param are allowed + * * mixed "parameterType" false if paramters are not alloweda value, + * otherwise a string with the value "integer" or "string" + * * string "description" description of the parameter + * @var array + */ + private $longNames = array(); + + /** + * list of short names as keys and their long name equivalent as values + * @var array + */ + private $shortNames = array(); + + /** + * Flag if arguments have been parsed in to params + * @var boolean + */ + private $isParsed = false; + + /** + * class constructor + * + * @author Patrick Forget + * + * @param array $definitions contains list of allowed parameters + * the key is the long name of the parameter followed by a pipe (|) + * then a single character specifying the short name. + * + * If the parameter allows for arguments then an equal sign (=) + * follows and then the type of paramter. + * + * Allowed types are either i, int or integer for integer types + * and s, str or string for string types. + * + * If a parameter can appear more than once the last character of + * the key should be a plus character (+). + * + * The value of the entry is the definition of what the paramter + * does. + */ + public function __construct($definitions) + { + if (is_array($definitions)) { + $this->definitions = $definitions; + } //if + } // __construct() + + /** + * checks if parameter is allowed + * + * @author Patrick Forget + * + * @param string $name either short or long name of the parameter to check + * + * @return boolean true if definition exisits, false otherwise + */ + public function paramExists($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + if (strlen($name) == 1) { + return isset($this->shortNames[$name]); + } else { + return isset($this->longNames[$name]); + } //if + } // paramExists($name) + + + /** + * checks if parameter allows a value if so what type + * + * @author Patrick Forget + * + * @param string $name either short or long name of the parameter to check + * + * @return boolean|string false doesn't allow value, The value "string" or "integer" depending which type it allows + */ + public function allowsValue($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + $longName = (strlen($name) == 1 ? ( isset($this->shortNames[$name]) ? $this->shortNames[$name] : '') : $name); + + if (isset($this->longNames[$longName])) { + return $this->longNames[$longName]['parameterType'] !== false ? true : false; + } else { + return false; + } //if + } // allowsValue() + + /** + * returns the type of value allowed + * + * @author Patrick Forget + */ + public function getValueType($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + $longName = (strlen($name) == 1 ? ( isset($this->shortNames[$name]) ? $this->shortNames[$name] : '') : $name); + + if (isset($this->longNames[$longName]['parameterType']) + && $this->longNames[$longName]['parameterType'] !== false) { + return $this->longNames[$longName]['parameterType']; + } else { + return ''; + } //if + } // getValueType() + + + /** + * checks if pamultiple instance of parameter are allowed + * + * @author Patrick Forget + * + * @param string $name either short or long name of the parameter to check + * + * @return boolean false if parameter doesn't allow multiple values, true if it does + */ + public function allowsMultiple($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + $longName = (strlen($name) == 1 ? ( isset($this->shortNames[$name]) ? $this->shortNames[$name] : '') : $name); + + if (isset($this->longNames[$longName])) { + return $this->longNames[$longName]['isMultipleAllowed']; + } else { + return false; + } //if + } // allowsMultiple() + + /** + * retreive short name of a parameter using its long name + * + * @author Patrick Forget + * + * @param string $name long name of the parameter to check + * + * @return string character of the short name or null if it doesn't exist + */ + public function getShortName($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + if (isset($this->longNames[$name])) { + return $this->longNames[$name]['shortName']; + } else { + return null; + } //if + } // getShortName($name) + + /** + * retreive long name of a parameter using its short name + * + * @author Patrick Forget + * + * @param string $name short name of the parameter to check + * + * @return string long name or null if it doesn't exist + */ + public function getLongName($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + if (isset($this->shortNames[$name])) { + return $this->shortNames[$name]; + } else { + return null; + } //if + } // getLongName($name) + + /** + * retreive description of a paramter + * + * @author Patrick Forget + * + * @param string $name either short or long name of the parameter to check + * + * @return string description or null if it doesn't exist + */ + public function getDescription($name) + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + $longName = (strlen($name) == 1 ? ( isset($this->shortNames[$name]) ? $this->shortNames[$name] : '') : $name); + + if (isset($this->longNames[$longName])) { + return $this->longNames[$longName]['description']; + } else { + return null; + } //if + } // getDescription() + + /** + * builds a usage definition based on definition of params + * + * @author Patrick Forget + */ + public function getUsage() + { + if (!$this->isParsed) { + $this->parseDefinitions(); + } //if + + /* build list of argument names and calculate + the first column width so we can pad to + align definitions */ + $firstCol = array(); + $longestDef = 0; + foreach (array_keys($this->longNames) as $longName) { + ob_start(); + echo "--{$longName}|-{$this->getShortName($longName)}"; + + if ($this->allowsValue($longName)) { + echo "={$this->getValueType($longName)}"; + } //if + + if ($this->allowsMultiple($longName)) { + echo "+"; + } //if + + $defLength = ob_get_length(); + + $longestDef = max($longestDef, $defLength); + + $firstCol[$longName] = ob_get_contents(); + ob_end_clean(); + + } //foreach + + $firstColMaxWidth = $longestDef + 4; + + ob_start(); + + foreach ($firstCol as $longName => $def) { + $currentDefLength = strlen($def); + + $padding = str_repeat(" ", $firstColMaxWidth - $currentDefLength); + + echo "{$def}{$padding}{$this->getDescription($longName)}", PHP_EOL; + } //foreach + + echo PHP_EOL; + + $usage = ob_get_contents(); + ob_end_clean(); + + return $usage; + + } // getUsage() + + + /** + * parses the definitions + * + * @author Patrick Forget + */ + protected function parseDefinitions() + { + foreach ($this->definitions as $nameDef => $description) { + $nameParts = explode("|", $nameDef); + + if (sizeof($nameParts) !== 2) { + throw new \UnexpectedValueException("Unexpected argument name definition expecting \"longName|char\""); + } //if + + $longName = $nameParts[0]; + $isMulti = false; + $parameterType = false; + + $shortNameLength = strlen($nameParts[1]); + + if ($shortNameLength == 1) { + $shortName = $nameParts[1]; + } else { + $secondChar = substr($nameParts[1], 1, 1); + + switch ($secondChar) { + case '=': + $shortNameParts = explode("=", $nameParts[1]); + + $shortName = $shortNameParts[0]; + $parameterTypeString = $shortNameParts[1]; + + if (substr($parameterTypeString, -1) === '+') { + $isMulti = true; + $parameterTypeString = substr($parameterTypeString, 0, -1); // remove trailing + + } //if + + switch ($parameterTypeString) { + case 'i': + case 'int': + case 'integer': + $parameterType = 'integer'; + break; + case 's': + case 'str': + case 'string': + $parameterType = 'string'; + break; + default: + throw new \UnexpectedValueException("Expecting parameter type". + " to be either integer or string"); + break; + } //switch + + break; + case '+': + if ($shortNameLength > 2) { + throw new \UnexpectedValueException("Multiple flag charachter (+)". + " should be last character in definition"); + } //if + + $shortName = substr($nameParts[1], 0, 1); + $isMulti = true; + + break; + default: + throw new \UnexpectedValueException("Expecting short name definition to be a single char"); + break; + } // switch + + } //if + + if (isset($this->longNames[$longName])) { + throw new \UnexpectedValueException("Cannot redefine long name {$longName}"); + } //if + + if (isset($this->shortNames[$shortName])) { + throw new \UnexpectedValueException("Cannot redefine short name {$shortName}"); + } //if + + $this->longNames[$longName] = array( + 'shortName' => $shortName, + 'isMultipleAllowed' => $isMulti, + 'parameterType' => $parameterType, + 'description' => $description + ); + + $this->shortNames[$shortName] = $longName; + + } //foreach + + $this->isParsed = true; + } // parseDefinitions() +} diff --git a/Classes/Curl.php b/Classes/Curl.php new file mode 100644 index 0000000..45a70c7 --- /dev/null +++ b/Classes/Curl.php @@ -0,0 +1,23 @@ +url); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, True); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + //curl_setopt($ch, CURLOPT_VERBOSE, true); // verbose mode for debugging + + $json = curl_exec($ch); + + curl_close($ch); + + $array = json_decode($json, true); + return $array; + } +} +?> \ No newline at end of file diff --git a/GoStats.php b/GoStats.php new file mode 100755 index 0000000..c1ba2c6 --- /dev/null +++ b/GoStats.php @@ -0,0 +1,401 @@ +#!/usr/bin/php + "Shows help message", + "list|l" => "List campaigns and their ID's", + "campaign|c=i" => "Get campaign by id", + "dump|d=s" => "Dump user:pass list to ", + "training|t=s" => "Dump list of users requiring training ", + "all|a" => "All of the below options", + "ips|i" => "Top 10 IP's", + "useragent|u" => "Top 10 user agents", + "attempts|m" => "Top 10 attempts to log in", + "active|o" => "Active times", + "speed|e" => "Clickthrough speed", + "stats|s" => "Victim statistics", + "pass|p" => "Password analysis with pwdlyser", + ) +); + +$filter = new \Clapp\CommandArgumentFilter($definitions, $argv); + +if ($filter->getParam('h') === true || $argc < 2) { + fwrite(STDERR, $definitions->getUsage()); + exit(0); +} + +/* Get list of campaigns */ +if ($filter->getParam("list") !== false) { + echo "[+] Getting data from server\n"; + $curl = new curl(); + $curl->url = "$url/api/campaigns/?api_key=$key"; + $list = $curl->curlQuery(); + + if(isset($list->message) && $list->message == "Invalid API Key"){ + echo "[!] Invalid API key\n"; + exit(0); + }else{ + echo "[id] -campaign name-\n"; + foreach($list as $id) + echo "[".$id['id']."] ".$id['name']."\n"; + } + exit(0); +} + +/* Get campaign data */ +$campid = $filter->getParam('c'); +if ($campid == null || !is_numeric($campid)) { + echo "[!] Campaign ID not set\nn"; + exit(0); +}else{ + echo "[+] Getting data from server\n"; + $curl = new curl(); + $curl->url = "$url/api/campaigns/$campid?api_key=$key"; + $list = $curl->curlQuery(); + if(isset($list->message) && $list->message == "Invalid API Key"){ + echo "[!] Invalid API key\n"; + exit(0); + }else{ + /* all data got correctly time to do stuff! */ + echo "[$campid] ".$list['name']."\n"; + echo "\n--- Notable times ---\n"; + + if(isset($list['launch_date']) && $list['launch_date'] <> ""){ + $time = date('d-m-Y H:i', $datetime = strtotime(substr($list['launch_date'], 0, 10) . ' ' . substr($list['launch_date'], 11, 8 ))); + echo "Campaign launched: $time\n"; + } + + foreach($list['timeline'] as $record){ + if($record['message'] == "Email Sent"){ + $time = date('d-m-Y H:i', $datetime = strtotime(substr($record['time'], 0, 10) . ' ' . substr($record['time'], 11, 8 ))); + echo "First email sent: $time\n"; + break; + } + } + foreach($list['timeline'] as $record){ + if($record['message'] == "Email Sent"){ + $time = date('d-m-Y H:i', $datetime = strtotime(substr($record['time'], 0, 10) . ' ' . substr($record['time'], 11, 8 ))); + } + } + echo "Last email sent:: $time\n"; + foreach($list['timeline'] as $record){ + if($record['message'] == "Clicked Link"){ + $time = date('d-m-Y H:i', $datetime = strtotime(substr($record['time'], 0, 10) . ' ' . substr($record['time'], 11, 8 ))); + echo "First email opened: $time\n"; + break; + } + } + foreach($list['timeline'] as $record){ + if($record['message'] == "Clicked Link"){ + $time = date('d-m-Y H:i', $datetime = strtotime(substr($record['time'], 0, 10) . ' ' . substr($record['time'], 11, 8 ))); + echo "First page view: $time\n"; + break; + } + } + foreach($list['timeline'] as $record){ + if($record['message'] == "Submitted Data"){ + $time = date('d-m-Y H:i', $datetime = strtotime(substr($record['time'], 0, 10) . ' ' . substr($record['time'], 11, 8 ))); + echo "First credentials submitted: $time\n"; + break; + } + } + if(isset($list['completed_date']) && $list['completed_date'] <> ""){ + $time = date('d-m-Y H:i', $datetime = strtotime(substr($list['completed_date'], 0, 10) . ' ' . substr($list['completed_date'], 11, 8 ))); + echo "Campaign finished: $time\n"; + } + } +} + +/* Top 10 IP's */ +if ($filter->getParam("ips") !== false || $filter->getParam("all") !== false) { + $ips = array(); + foreach($list['timeline'] as $item){ + if($item['details'] <> ""){ + $details = json_decode($item['details'], true); + if($details['browser']['address'] !== "unknown") + $ips[] = (string)$details['browser']['address']; + } + } + $ips = array_count_values($ips); + arsort($ips); + $ips = array_slice($ips,0,10,true); + echo "\n--- Top 10 IP's ---\n"; + foreach($ips as $ip=>$no){ + $geoip_details = ""; + if($geoip == true){ + $geojson = file_get_contents("http://freegeoip.net/json/$ip"); + $geodetails = json_decode($geojson, true); + $geoip_details = "- ".$geodetails['country_name'].", ".$geodetails['city']; + } + echo "[$no] $ip $geoip_details\n"; + } +} + +/* Top 10 user agent's */ +if ($filter->getParam("useragent") !== false || $filter->getParam("all") !== false) { + $agents = array(); + foreach($list['timeline'] as $item){ + if($item['details'] <> "" && $item['message'] == "Clicked Link"){ // only people who visited site, not email user agent + $details = json_decode($item['details'], true); + if($details['browser']['user-agent'] !== "unknown" && $details['browser']['user-agent'] !== "") + $agents[] = (string)$details['browser']['user-agent']; + } + } + $agents = array_count_values($agents); + arsort($agents); + $agents = array_slice($agents,0,10,true); + echo "\n--- Top 10 User Agents ---\n"; + foreach($agents as $ua=>$no){ + echo "[$no] $ua\n"; + } +} + +/* Top 10 attempts to log in */ +if($filter->getParam("attempts") !== false || $filter->getParam("all") !== false) { + $userids = array(); + foreach($list['results'] as $item){ + $userids[$item['id']] = $item['email']; + } + + $attemptrids = array(); + foreach($list['timeline'] as $item){ + if($item['details'] <> ""){ + $details = json_decode($item['details'], true); + if(isset($details['payload']['password'][0]) && $details['payload']['password'][0] <> ""){ + $attemptrids[$details['payload']['rid'][0]] += 1; + } + } + } + arsort($attemptrids); + $attemptrids = array_slice($attemptrids,0,10,true); + echo "\n--- Top 10 Login Attempts ---\n"; + foreach($attemptrids as $id=>$amount){ + $newemail= preg_replace('/(?:^|.@).\K|.\.[^@]*$(*SKIP)(*F)|.(?=.*?\.)/', '*', $userids[$id]); + echo "[$amount] $newemail\n"; + } +} + +/* Active times */ +if($filter->getParam("active") !== false || $filter->getParam("all") !== false) { + $active_count = array(); + $active_percent = array(); + $total = 0; + echo "\n--- Active times (hour, actions & percent) ---\n"; + foreach($list['timeline'] as $item){ + if($item['message'] != "Campaign Created" && $item['message'] != "Email Sent" ){ + $hour = (int)substr($item['time'], 11, 2); + $active_count[$hour]++; + $total++; + } + + } + foreach($active_count as $id => $count) // populate percentages + $active_percent[$id] = ($count / $total) * 100; + + for($i = 0; $i <= 12; $i++){ + $iDsp = str_pad($i, 2, " ", STR_PAD_LEFT); + $j = $i+12; + $user1 = str_pad($active_count[$i], 4, " ", STR_PAD_LEFT); + $user2 = str_pad($active_count[$j], 4, " ", STR_PAD_LEFT); + $percent1 = number_format($active_percent[$i], 2, '.', ''); + $percent1 = str_pad($percent1, 5, " ", STR_PAD_LEFT); + $percent2 = number_format($active_percent[$j], 2, '.', ''); + $percent2 = str_pad($percent2, 5, " ", STR_PAD_LEFT); + + echo "$iDsp - $user1 = $percent1% | $j - $user2 = $percent2% \n"; + } +} + +/* Clickthrough speed */ +if ($filter->getParam("speed") !== false || $filter->getParam("all") !== false) { + $speed_opened = array(); + $speed_visited = array(); + $speed_offset = array(); + echo "\n--- Clickthrough Speed ---\n"; + foreach($list['timeline'] as $item){ + if($item['message'] == "Email Opened"){ + $details = json_decode($item['details'], true); + $check_rid = $details['payload']['rid'][0]; + + $current_time = strtotime(substr($item['time'], 0, 10) . ' ' . substr($item['time'], 11, 8 )); + $existing_time = strtotime(substr($speed_opened[$check_rid], 0, 10) . ' ' . substr($speed_opened[$check_rid], 11, 8 )); + + if(!isset($speed_opened[$check_rid]) || $existing_time > $current_time) + $speed_opened[$check_rid] = $item['time']; + } + if($item['message'] == "Clicked Link"){ + $details = json_decode($item['details'], true); + $check_rid = $details['payload']['rid'][0]; + if(!isset($speed_visited[$check_rid])) + $speed_visited[$check_rid] = $item['time']; + } + } + foreach($speed_opened as $id=>$val){ // remove all the ones that didn't visit site + if(!isset($speed_visited[$id])) + unset($speed_opened[$id]); + } + foreach($speed_visited as $id=>$val){ // remove all the ones that didn't load email tracking image + if(!isset($speed_opened[$id])) + unset($speed_visited[$id]); + } + foreach($speed_opened as $id=>$val){ //calculate speed between reading email and clicking link + $date_opened = substr($val, 0, 10); + $time_opened = substr($val, 11, 8 ); + $time_opened_stamp = strtotime($date_opened." ".$time_opened); + + $date_visited = substr($speed_visited[$id], 0, 10); + $time_visited = substr($speed_visited[$id], 11, 8 ); + $time_visited_stamp = strtotime($date_visited." ".$time_visited); + + $diff = $time_visited_stamp - $time_opened_stamp; + if($diff > 0) + $speed_offset[$id] = $diff; + } + + unset($speed_opened); // check me out doing memory management and cleaning up! :D + unset($speed_visited); + + $quickest = min($speed_offset); + echo "Quickest click: $quickest sec\n"; + $longest = max($speed_offset); + $longest = floor(($longest / 60) % 60); + echo "Longest click: $longest min\n"; + $sec_5 = array_reduce($speed_offset, function ($a, $b){ + return ($b <= 5) ? ++$a : $a; }); + echo "Users clicked < 5 sec: $sec_5 \n"; + $sec_30 = array_reduce($speed_offset, function ($a, $b){ + return ($b <= 30) ? ++$a : $a; }); + echo "Users clicked < 30 sec: $sec_30 \n"; + $sec_60 = array_reduce($speed_offset, function ($a, $b){ + return ($b <= 60) ? ++$a : $a; }); + echo "Users clicked < 1 min: $sec_60 \n"; +} + +/* Victim statistics */ +if ($filter->getParam("stats") !== false || $filter->getParam("all") !== false) { + $status = array(); + foreach($list['results'] as $item){ + if($item['status'] <> ""){ + $status[] = $item['status']; + } + } + echo "\n--- Victim Statistics ---\n"; + $statusall = count($status); + $counts = array_count_values($status); + echo "Targets: ".$statusall."\n"; + $openedpercent = ($counts['Email Opened'] / $statusall) * 100; + echo "Email opened: ".$counts['Email Opened']." (".round($openedpercent, 2)."%)\n"; + $linkpercent = ($counts['Clicked Link'] / $statusall) * 100; + echo "Visited link: ".$counts['Clicked Link']." (".round($linkpercent, 2)."%)\n"; + $subpercent = ($counts['Submitted Data'] / $statusall) * 100; + echo "Submitted data: ".$counts['Submitted Data']." (".round($subpercent, 2)."%)\n"; + foreach($list['timeline'] as $item){ + if($item['details'] <> ""){ + $details = json_decode($item['details'], true); + if($details['payload']['password'][0] <> "") + $totalLoginAttempts++; + } + } + echo "Total login attempts: $totalLoginAttempts\n"; +} + +/* Pwdlyzer */ +if ($filter->getParam("pass") !== false || $filter->getParam("all") !== false) { + $username = array(); + $password = array(); + echo "\n--- Password Statistics ---\n"; + foreach($list['timeline'] as $item){ + if($item['details'] <> ""){ + $details = json_decode($item['details'], true); + if($details['payload']['password'][0] <> ""){ + $username[] = $details['payload']['username'][0]; + $password[] = $details['payload']['password'][0]; + } + } + } + $tmpfname = tempnam("/tmp", "GoStats-"); + $pwdfname = tempnam("/tmp", "GoStats-"); + $handle = fopen($tmpfname, "w"); + foreach($username as $id=>$user){ + fwrite($handle, "$user:".$password[$id]."\n"); + } + fclose($handle); + echo "[+] Launching pwdlyzer\n"; + exec("cd $pwd && ./pwdlyser.py -p $tmpfname --all > $pwdfname"); + unlink($tmpfname); + echo "[+] pwdlyzer results at: $pwdfname\n"; +} + +/* dump username:password list to file */ +$dumpfile = $filter->getParam('dump'); +if(file_exists($dumpfile)){ + echo "[!] File already exists ($dumpfile)\n"; + exit(0); +} +if(!file_exists($dumpfile) && isset($dumpfile)){ + $username = array(); + $password = array(); + echo "\n--- Dumping username:password to file ---\n"; + foreach($list['timeline'] as $item){ + if($item['details'] <> ""){ + $details = json_decode($item['details'], true); + if($details['payload']['password'][0] <> ""){ + $username[] = $details['payload']['username'][0]; + $password[] = $details['payload']['password'][0]; + } + } + } + $handle = fopen($dumpfile, "w"); + foreach($username as $id=>$user){ + fwrite($handle, "$user:".$password[$id]."\n"); + } + fclose($handle); + echo "[+] File created: $dumpfile\n"; +} + +/* dump list of users requiring training */ +$dumpfile2 = $filter->getParam('training'); +if(file_exists($dumpfile2)){ + echo "[!] File already exists ($dumpfile)\n"; + exit(0); +} +if(!file_exists($dumpfile2) && isset($dumpfile2)){ + $tusername = array(); + $temail = array(); + $tstatus = array(); + echo "\n--- Dumping list of users requiring training ---\n"; + foreach($list['results'] as $item){ + if($item['status'] == "Submitted Data" || $item['status'] == "Clicked Link"){ + $tusername[] = $item['first_name']." ".$item['last_name']; + $temail[] = $item['email']; + $tstatus[] = $item['status']; + } + } + $handle = fopen($dumpfile2, "w"); + foreach($tusername as $id=>$user){ + fwrite($handle, "$user, ".$temail[$id].", ".$tstatus[$id]."\n"); + } + fclose($handle); + echo "[+] File created: $dumpfile2\n"; +} + +?> diff --git a/README.md b/README.md index 07e3b17..01f7b48 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,133 @@ GoStats =============== -Get statistics from GoPhish campaign \ No newline at end of file +Get statistics from GoPhish campaigns + +Requirements +=============== +Pwdlyser - [https://github.com/ins1gn1a/Pwdlyser](https://github.com/ins1gn1a/Pwdlyser) + +Installation and Usage +=============== +git clone the repo + + chmod +x ./GoStats.php + +Modify **GoStats.php** to contain your gophish URL, API key and path to pwdlyser + + ./GoStats.php + +Example Output +=============== + + root[/opt/GoStats]: ./GoStats.php + ╔═╗┌─┐╔═╗┌┬┐┌─┐┌┬┐┌─┐ + ║ ╦│ │╚═╗ │ ├─┤ │ └─┐ v1.0 + ╚═╝└─┘╚═╝ ┴ ┴ ┴ ┴ └─┘ + --help|-h Shows help message + --list|-l List campaigns and their ID's + --campaign|-c=integer Get campaign by id + --dump|-d=string Dump user:pass list to + --training|-t=string Dump list of users requiring training + --all|-a All of the below options + --ips|-i Top 10 IP's + --useragent|-u Top 10 user agents + --attempts|-m Top 10 attempts to log in + --active|-o Active times + --speed|-e Clickthrough speed + --stats|-s Victim statistics + --pass|-p Password analysis with pwdlyser + + root[/opt/GoStats]: ./GoStats.php -l + ╔═╗┌─┐╔═╗┌┬┐┌─┐┌┬┐┌─┐ + ║ ╦│ │╚═╗ │ ├─┤ │ └─┐ v1.0 + ╚═╝└─┘╚═╝ ┴ ┴ ┴ ┴ └─┘ + [+] Getting data from server + [id] -campaign name- + [33] Campaign_01 + [60] Campaign_02 + + root[/opt/GoStats]: ./GoStats.php -c 60 -a + ╔═╗┌─┐╔═╗┌┬┐┌─┐┌┬┐┌─┐ + ║ ╦│ │╚═╗ │ ├─┤ │ └─┐ v1.0 + ╚═╝└─┘╚═╝ ┴ ┴ ┴ ┴ └─┘ + [+] Getting data from server + [60] Campaign_02 + + [+] Notable times + Campaign launched: 16-10-2017 09:28 + First email sent: 16-10-2017 10:20 + Last email sent: 16-10-2017 10:25 + First email opened: 16-10-2017 10:28 + First page view: 16-10-2017 10:28 + First credentials submitted: 16-10-2017 10:29 + Campaign finished: 21-10-2017 10:09 + + [+] Top 10 IPs + [177] 130.***.**.50 - United Kingdom, London + [96] 212.***.**.69 - France, + [41] 86.**.**.2 - United Kingdom, Edgware + [32] 193.***.**.190 - United Kingdom, + [28] 205.***.**.189 - United States, Chesterfield + [19] 86.***.***.47 - United Kingdom, Gillingham + [15] 82.**.**.34 - United Kingdom, Bradford-on-Avon + [14] 24.**.***.62 - United States, New York + [9] 66.**.**.130 - United States, West Jordan + [7] 2.**.**.183 - United Kingdom, London + + [+] Top 10 User Agents + [60] WebClient/1.0 + [32] Mozilla/4.0 (redacted details) + [30] Mozilla/4.0 (redacted details) + [29] Mozilla/4.0 (redacted details) + [26] Mozilla/4.0 (redacted details) + [26] Mozilla/4.0 (redacted details) + [22] Mozilla/4.0 (redacted details) + [20] Mozilla/5.0 (redacted details) Chrome/61.0.3163.100 Safari/537.36 + [18] Mozilla/4.0 (redacted details) + [16] Mozilla/4.0 (redacted details) AppleWebKit/603.3.8 + + [+] Top 10 Login Attempts + [19] b**********s@w***n.com + [12] b***************o@w***n.com + [8] n*************e@g*********e.com + [7] c****a@w***n.com + [7] s***************d@g*********e.com + [7] e**********r@w***n.com + [6] c*********m@w***n.com + [5] d******j@w***n.com + [3] j********d@a********l.com + [3] c***********n@a********l.com + + [+] Active times (hour, actions & percent) + 0 - = 0.00% | 12 - 28 = 5.47% + 1 - = 0.00% | 13 - 22 = 4.30% + 2 - = 0.00% | 14 - 31 = 6.05% + 3 - = 0.00% | 15 - 25 = 4.88% + 4 - = 0.00% | 16 - 53 = 10.35% + 5 - 1 = 0.20% | 17 - 2 = 0.39% + 6 - 5 = 0.98% | 18 - 18 = 3.52% + 7 - 9 = 1.76% | 19 - 8 = 1.56% + 8 - 21 = 4.10% | 20 - = 0.00% + 9 - 134 = 26.17% | 21 - 4 = 0.78% + 10 - 125 = 24.41% | 22 - 1 = 0.20% + 11 - 24 = 4.69% | 23 - 1 = 0.20% + 12 - 28 = 5.47% | 24 - = 0.00% + + [+] Clickthrough Speed + Quickest click: 2 sec + Longest click: 18 min + Users clicked < 5 sec: 7 + Users clicked < 30 sec: 11 + Users clicked < 1 min: 16 + + [+] Victim Statistics + Targets: 136 + Email opened: 15 (11.03%) + Visited link: 20 (14.71%) + Submitted data: 20 (14.71%) + Total login attempts: 96 + + [+] Password Statistics + [+] Launching pwdlyzer + [!] pwdlyzer results at: /tmp/GoStats-VpGHth \ No newline at end of file