Unpack Mainframe packed Decimal (BCD) with PHP - php

I got an data file from a mainframe. I handled already the EBCDIC conversion to latin1 with PHP. But now are this packed decimal fields left.
For examle the number 12345 is packed into 3 Bytes and looks like: x'12345C'
Negative would be like: x'12345D'
So the right half Byte telling the sign. Is there a way to do this easily with PHP?
Now I do that like:
$bin = "\x12\x34\x5C";
var_dump(
unpack("H*", $bin)
);
It results in:
array(1) {
[1]=>
string(4) "123c"
}
Now I could check if last sign is C or D and do all by hand. But maybe there is a nicer solution?

As Bill said, get the mainframe people to convert the file to Text on the Mainframe and send the Text file, utilities like sort etc can do this on the mainframe. Also is it just packed decimal in the file or do you have either binary or Zoned Decimal as well ???
If you insist on doing it in PHP, you need to do the Packed Decimal conversion before you do the EBCDIC conversion because for a Packed-decimal like x'400c'
the EBCDIC converter will look at the x'40' and say that is a space and convert it to x'20', so your x'400c' becomes x'200c'.
Also the final nyble in a Packed-decimal can be f - unsigned as well as c and d.
Finally if you have the Cobol Copybook, my project JRecord has Cobol to Csv && Cobol to Xml conversion programs (Written in java). See
Cobol to Csv notes
Cobol To Xml notes

Ok, because I did not find any nicer solution, I made a php-class for handle a record from this dataset:
<?php
namespace Mainframe;
/**
* Mainframe main function
*
* #author vp1zag4
*
*/
class Mainframe
{
/**
* Data string for reading
*
* #var string | null
*/
protected $data = null;
/**
* Default ouput charset
*
* #var string
*/
const OUTPUT_CHARSET = 'latin1';
/**
* Record length of dataset
*
* #var integer
*/
protected $recordLength = 10;
/**
* Inits the
*
* #param unknown $data
*/
public function __construct($data = null)
{
if (! is_null($data)) {
$this->setData($data);
}
}
/**
* Sets the data string and validates
*
* #param unknown $data
* #throws \LengthException
*/
public function setData($data)
{
if (strlen($data) != $this->recordLength) {
throw new \LengthException('Given data does not fit to dataset record length');
}
$this->data = $data;
}
/**
* Unpack packed decimal (BCD) from mainframe format to integer
*
* #param unknown $str
* #return number
*/
public static function unpackBCD($str)
{
$num = unpack('H*', $str);
$num = array_shift($num);
$sign = strtoupper(substr($num, - 1));
$num = (int) substr($num, 0, - 1);
if ($sign == 'D') {
$num = $num * - 1;
}
return (int) $num;
}
/**
* convert EBCDIC to default output charset
*
* #param string $str
* #return string
*/
public static function conv($str, $optionalCharset = null)
{
$charset = (is_string($optionalCharset)) ? $optionalCharset : self::OUTPUT_CHARSET;
return iconv('IBM037', $charset, $str);
}
/**
* Reads part of data string and converts or unpacks
*
* #param integer $start
* #param integer $length
* #param bool $unpack
* #param bool | string $conv
*/
public function read($start, $length, $unpack = false, $conv = true)
{
if (empty($this->data)) {
return null;
}
$result = substr($this->data, $start, $length);
if($unpack) {
return self::unpackBCD($result);
}
if ($conv) {
return self::conv($result, $conv);
}
return $result;
}
}
With $class->read(1, 3, True) it is possible to read part of the data and convert/unpack it on same time.
Maybe it will help anybody anytime, too.
But of course I will try to setup some Job which will do that for me directly on mainframe with some JSON data as output.

Related

PHP range - cidr conversion

I am looking for a function in PHP, which makes the same as Perl's Net::CIDR::range2cidr function.
I found some solutions in Google:
tutorialspots - ip2cidr (Not working correctly for ['10.0.0.0', '10.255.255.255'])
tutorialspots - cidr2ip (Not working correctly for "192.168.1.15/24")
flygoast/range2cidr (Must be installed)
Is there an easier way to convert CIDR to range and back ?
(Maybe, did I just miss an existing function in PHP ?)
Solved it using the s1lentium/iptools composer package and the following code:
/**
* Perl's "Net::CIDR::range2cidr()" function.
*
* #param string $from
* #param string $to
*
* #return array
*/
function range2cidr($from, $to) {
$networks = IPTools\Range::parse("{$from}-{$to}")->getNetworks();
return array_map(function(IPTools\Network $network) {
return (string)$network;
}, $networks);
}
/**
* #param string $cidr
*
* #return array
*/
function cidr2range($cidr) {
/** #var IPTools\Range $range */
$range = IPTools\Network::parse($cidr)->hosts;
return [
(string)$range->getFirstIP(),
(string)$range->getLastIP(),
];
}

How to replicate MySQL's aes-256-cbc in PHP

Using this article as a guide I was able to successfully replicate MySQL's aes-128-ecb in PHP:
final class Encryption
{
// The key
const KEY = '36F3D40A7A41A827968BE75A87D60950';
/**
* Encrypt a string
*
* #access public
* #static
* #param string $string
* #return string
*/
public static function encrypt($string)
{
return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, self::getMySQLKey(self::KEY), self::getPaddedString($string), MCRYPT_MODE_ECB);
}
/**
* Decrypt a string
*
* #access public
* #static
* #param string $string
* #return string
*/
public static function decrypt($string)
{
return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_128, self::getMySQLKey(self::KEY), $string, MCRYPT_MODE_ECB), "\x00..\x10");
}
/**
* Get MySQL key
*
* #access public
* #static
* #param string $key
* #return string
*/
public static function getMySQLKey($key)
{
// The new key
$new_key = str_repeat(chr(0), 16);
// Iterate over the key and XOR
for ($i = 0, $l = strlen($key); $i < $l; ++$i)
{
$new_key[$i % 16] = $new_key[$i % 16] ^ $key[$i];
}
// Return the new key
return $new_key;
}
/**
* Get padded string
*
* #access public
* #static
* #param string $string
* #return string
*/
public static function getPaddedString($string)
{
return str_pad($string, (16 * (floor(strlen($string) / 16) + 1)), chr(16 - (strlen($string) % 16)));
}
}
As an example:
// PHP, gives CJI+zJyviQI7GgSCLGMNsqsXq2MDKC3a9FIG3wDrE8Y=
base64_encode(Encryption::encrypt('michael#example.com'))
// MySQL, gives CJI+zJyviQI7GgSCLGMNsqsXq2MDKC3a9FIG3wDrE8Y=
SELECT TO_BASE64(AES_ENCRYPT('michael#example.com', '36F3D40A7A41A827968BE75A87D60950'));
However, I want to update to using aes-256-cbc but am having difficulties. I started by replacing MCRYPT_RIJNDAEL_128 with MCRYPT_RIJNDAEL_256 and MCRYPT_MODE_ECB with MCRYPT_MODE_CBC and used the KEY constant as the initialization vector:
/**
* Encrypt a string
*
* #access public
* #static
* #param string $string
* #return string
*/
public static function encrypt($string)
{
return mcrypt_encrypt(MCRYPT_RIJNDAEL_256, self::getMySQLKey(self::KEY), self::getPaddedString($string), MCRYPT_MODE_CBC, self::KEY);
}
/**
* Decrypt a string
*
* #access public
* #static
* #param string $string
* #return string
*/
public static function decrypt($string)
{
return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, self::getMySQLKey(self::KEY), $string, MCRYPT_MODE_CBC, self::KEY), "\x00..\x10");
}
The problem is that I'm now getting different values from PHP and MySQL:
// PHP, gives XSRfnrl05CE7JIHCvfhq6D67O0mAW2ayrFv2YkjFVYI=
base64_encode(Encryption::encrypt('michael#example.com'))
// MySQL, gives lTLT4MRXcHnOAsYjlwUX4WVPHgYvyi6nKC4/3us/VF4=
SELECT TO_BASE64(AES_ENCRYPT('michael#example.com', '36F3D40A7A41A827968BE75A87D60950', '36F3D40A7A41A827968BE75A87D60950'));
I'm unsure where to proceed from here so any help would be appreciated.
And just to confirm that MySQL is indeed using the correct encryption method:
// aes-256-cbc
SELECT ##session.block_encryption_mode
AES is a subset of Rijndael so to use it for AES one must select a block size of 128-bits and a key size of 128, 192 or 256 bits.
MCRYPT_RIJNDAEL_256 specifies a block size of 256-bits, not the key size, similarly MCRYPT_RIJNDAEL_128 specifies a block size of 128-bits, not the key size. This is a common confusion.
This implementation of Rijndael auto selects a key size based on the passed key so it is important to use a key of exactly the correct desired size.
Also note that the iv length is the same as the block size so for AES it is 128-bits in length.
This is a good example of how poor naming can have unfortunate consequences.
As neither eggyal nor Artjom B. have chosen to provide an answer with their solution, I'll do so on their behalf.
The first issue is that AES-256 still uses MCRYPT_RIJNDAEL_128 rather than MCRYPT_RIJNDAEL_256, and the second is that AES-256 is used over AES-128 by providing a 32 byte key rather than a 16 byte key.
The below provides a correct implementation:
final class AESEncrypter
{
// The key
const KEY = 'F40E2A9E22150793C6D0CA9E316FEA42';
// The IV
const IV = '5C354934224F698E';
/**
* Encrypt a string
*
* #access public
* #static
* #param string $string
* #return string
*/
public static function encrypt($string)
{
return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, self::getMySQLKey(self::KEY), self::getPaddedString($string), MCRYPT_MODE_CBC, self::IV);
}
/**
* Decrypt a string
*
* #access public
* #static
* #param string $string
* #return string
*/
public static function decrypt($string)
{
return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_128, self::getMySQLKey(self::KEY), $string, MCRYPT_MODE_CBC, self::IV), "\x00..\x10");
}
/**
* Get MySQL key
*
* #access public
* #static
* #param string $key
* #return string
*/
public static function getMySQLKey($key)
{
// The new key
$new_key = str_repeat(chr(0), 32);
// Iterate over the key and XOR
for ($i = 0, $l = strlen($key); $i < $l; ++$i)
{
$new_key[$i % 32] = $new_key[$i % 32] ^ $key[$i];
}
// Return the new key
return $new_key;
}
/**
* Get padded string
*
* #access public
* #static
* #param string $string
* #return string
*/
public static function getPaddedString($string)
{
return str_pad($string, (16 * (floor(strlen($string) / 16) + 1)), chr(16 - (strlen($string) % 16)));
}
}
As an example:
// PHP, gives mwVraDh/7jG3BvPJyYqgxY6Ca8CTRN5JHvwPGeV8Vd0=
base64_encode(AESEncrypter::encrypt('michael#example.com'))
// MySQL, gives mwVraDh/7jG3BvPJyYqgxY6Ca8CTRN5JHvwPGeV8Vd0=
SELECT TO_BASE64(AES_ENCRYPT('michael#example.com', 'F40E2A9E22150793C6D0CA9E316FEA42', '5C354934224F698E'))

PHP Pear Number_Words

)
I start to use PEAR Number_Word class, and it works greate! It convert numbers to a string by this way:
<?php
include_once 'Numbers/Words.php';
$number = 100254;
$nw = new Numbers_Words();
$numw = $nw->toWords($number);
print $numw;
?>
But now I need to convert the same number to a different languages at the same time.
For exemple: I have number 100 and I need to convert it in the same time to English and French. I was trying to pass in the argument an array of two languages, but it's not work:
/**
* Default Locale name
* #var string
* #access public
*/
public $locale = 'en_US'; //__her I was try to pass an array, but no result__
/**
* Default decimal mark
* #var string
* #access public
*/
public $decimalPoint = '.';
// }}}
// {{{ toWords()
/**
* Converts a number to its word representation
*
* #param integer $num An integer between -infinity and infinity inclusive :)
* that should be converted to a words representation
* #param string $locale Language name abbreviation. Optional. Defaults to
* current loaded driver or en_US if any.
* #param array $options Specific driver options
*
* #access public
* #author Piotr Klaban <makler#man.torun.pl>
* #since PHP 4.2.3
* #return string The corresponding word representation
*/
function toWords($num, $locale = '', $options = array())
{
if (empty($locale) && isset($this) && $this instanceof Numbers_Words) {
$locale = $this->locale;
}
if (empty($locale)) {
$locale = 'en_US';
}
$classname = self::loadLocale($locale, '_toWords');
$obj = new $classname;
if (!is_int($num)) {
$num = $obj->normalizeNumber($num);
// cast (sanitize) to int without losing precision
$num = preg_replace('/(.*?)('.preg_quote($obj->decimalPoint).'.*?)?$/', '$1', $num);
}
if (empty($options)) {
return trim($obj->_toWords($num));
}
return trim($obj->_toWords($num, $options));
}
Please, halp me if you know how! Thank you in advance!
Numbers_Words does not support multiple languages at once.
You need to create a new Numbers_Words instance for every language.

PHP strpos on string returned from ldap_search

ok, so here's the problem:
I do a search for the userparameters attribute with ldap_search.
I needed to get a couple of values out, being "CtxWFHomeDirDrive", "CtxWFHomeDir" and "CtxWFProfilePath"
The string I got was complete jibberish, so after an entire day of trying out every single character encoding conversion I could find, this one did half the trick:
$pUserParams = iconv('UTF-8', 'UTF-32', $entry_value)
For some reason the individual hex numbers following the 3 values I needed to extract were inversed (so 5C, which is backslash, came out as C5. Don't ask how I figured that one out :-P )
Knowing this, I could convert the hex values to display the actual citrix homedirdrive, profilepath, etc.
However, I still need to filter out a lot of unneeded characters from the final result string, but the following always returns false for some reason:
if (strpos($pUserParams,"CtxWFProfilePath") !== false) {}
When I echo the $pUserParams variable, it does display the whole string with the above three ctx parameters in it.
I suspected it must have something to do with special characters in the result string, so I tried removing line breaks, EOL's, unserializing the string (which produces an error at offset 0), etc etc
Nothing seems to work... Does anyone have an idea?
Thanks,
Vincent
original string run through hex2bin:
20202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202050071a080143747843666750726573656e74e394b5e694b1e688b0e381a2180801437478436667466c61677331e380b0e381a6e380b2e380b9120801437478536861646f77e384b0e380b0e380b0e380b02a02014374784d696e456e6372797074696f6e4c6576656ce384b02206014374785746486f6d654469724472697665e3a0b4e684b3e380b018c282014374785746486f6d65446972e68cb5e68cb5e394b7e38cb7e39cb6e388b6e394b6e398b6e3a4b6e68cb6e394b6e380b3e380b3e384b3e694b2e394b7e38cb7e39cb6e380b7e394b6e698b6e380b7e68cb6e394b6e388b6e394b6e694b2e3a4b6e694b6e390b7e68cb5e394b7e38cb7e394b6e388b7e3a0b6e698b6e690b6e394b6e390b6e388b7e3a4b6e398b7e394b6e390b2e68cb5e38cb7e390b7e3a4b6e684b6e694b6e694b2e688b6e698b6e688b6e688b6e394b6e68cb6e394b6e694b6e388b6e394b6e388b7e39cb6e380b020c28001437478574650726f66696c6550617468e68cb5e68cb5e384b6e38cb7e380b7e694b6e394b6e390b7e384b6e380b7e380b7e380b3e380b3e384b3e694b2e384b6e38cb7e380b7e694b2e3a4b6e694b6e390b7e68cb5e380b7e388b7e698b6e398b6e3a4b6e68cb6e394b6e38cb7e698b5e388b6e394b6e68cb6e39cb6e3a4b6e394b7e690b6e390b2e68cb5e38cb5e38cb5e38cb4e68cb5e38cb7e390b7e3a4b6e684b6e694b6e694b2e688b6e698b6e688b6e688b6e394b6e68cb6e394b6e694b6e388b6e394b6e388b7e39cb6e380b0e380b0
��
To build on the work already done by Tenzian, I ended up creating a few self-contained classes that can be used to safely decode/encode a userParameters blob to extract and modify/view/create all TS properties. This does make a few assumptions:
PHP 5.6+ (Feel free to edit to suit your needs/work with a lower version)
Assumes all of the classes are defined in the same namespace.
For multi-byte string support you have the mbstring extension loaded.
Any TS property for time is represented in terms of minutes.
Anyway, these are the classes you need:
TSPropertyArray
/**
* Represents TSPropertyArray data that contains individual TSProperty structures in a userParameters value.
*
* #see https://msdn.microsoft.com/en-us/library/ff635189.aspx
* #author Chad Sikorra <Chad.Sikorra#gmail.com>
*/
class TSPropertyArray
{
/**
* Represents that the TSPropertyArray data is valid.
*/
const VALID_SIGNATURE = 'P';
/**
* #var array The default values for the TSPropertyArray structure.
*/
const DEFAULTS = [
'CtxCfgPresent' => 2953518677,
'CtxWFProfilePath' => '',
'CtxWFProfilePathW' => '',
'CtxWFHomeDir' => '',
'CtxWFHomeDirW' => '',
'CtxWFHomeDirDrive' => '',
'CtxWFHomeDirDriveW' => '',
'CtxShadow' => 1,
'CtxMaxDisconnectionTime' => 0,
'CtxMaxConnectionTime' => 0,
'CtxMaxIdleTime' => 0,
'CtxWorkDirectory' => '',
'CtxWorkDirectoryW' => '',
'CtxCfgFlags1' => 2418077696,
'CtxInitialProgram' => '',
'CtxInitialProgramW' => '',
];
/**
* #var string The default data that occurs before the TSPropertyArray (CtxCfgPresent with a bunch of spaces...?)
*/
protected $defaultPreBinary = '43747843666750726573656e742020202020202020202020202020202020202020202020202020202020202020202020';
/**
* #var TSProperty[]
*/
protected $tsProperty = [];
/**
* #var string
*/
protected $signature = self::VALID_SIGNATURE;
/**
* #var string Binary data that occurs before the TSPropertyArray data in userParameters.
*/
protected $preBinary = '';
/**
* #var string Binary data that occurs after the TSPropertyArray data in userParameters.
*/
protected $postBinary = '';
/**
* Construct in one of the following ways:
*
* - Pass an array of TSProperty key => value pairs (See DEFAULTS constant).
* - Pass the userParameters binary value. The object representation of that will be decoded and constructed.
* - Pass nothing and a default set of TSProperty key => value pairs will be used (See DEFAULTS constant).
*
* #param mixed $tsPropertyArray
*/
public function __construct($tsPropertyArray = null)
{
$this->preBinary = hex2bin($this->defaultPreBinary);
if (is_null($tsPropertyArray) || is_array($tsPropertyArray)) {
$tsPropertyArray = $tsPropertyArray ?: self::DEFAULTS;
foreach ($tsPropertyArray as $key => $value) {
$tsProperty = new TSProperty();
$tsProperty->setName($key);
$tsProperty->setValue($value);
$this->tsProperty[$key] = $tsProperty;
}
} else {
$this->decodeUserParameters($tsPropertyArray);
}
}
/**
* Check if a specific TSProperty exists by its property name.
*
* #param string $propName
* #return bool
*/
public function has($propName)
{
return array_key_exists(strtolower($propName), array_change_key_case($this->tsProperty));
}
/**
* Get a TSProperty object by its property name (ie. CtxWFProfilePath).
*
* #param string $propName
* #return TSProperty
*/
public function get($propName)
{
$this->validateProp($propName);
return $this->getTsPropObj($propName)->getValue();
}
/**
* Add a TSProperty object. If it already exists, it will be overwritten.
*
* #param TSProperty $tsProperty
* #return $this
*/
public function add(TSProperty $tsProperty)
{
$this->tsProperty[$tsProperty->getName()] = $tsProperty;
return $this;
}
/**
* Remove a TSProperty by its property name (ie. CtxMinEncryptionLevel).
*
* #param string $propName
* #return $this
*/
public function remove($propName)
{
foreach (array_keys($this->tsProperty) as $property) {
if (strtolower($propName) == strtolower($property)) {
unset($this->tsProperty[$property]);
}
}
return $this;
}
/**
* Set the value for a specific TSProperty by its name.
*
* #param string $propName
* #param mixed $propValue
* #return $this
*/
public function set($propName, $propValue)
{
$this->validateProp($propName);
$this->getTsPropObj($propName)->setValue($propValue);
return $this;
}
/**
* Get the full binary representation of the userParameters containing the TSPropertyArray data.
*
* #return string
*/
public function toBinary()
{
$binary = $this->preBinary;
$binary .= hex2bin(str_pad(dechex(MBString::ord($this->signature)), 2, 0, STR_PAD_LEFT));
$binary .= hex2bin(str_pad(dechex(count($this->tsProperty)), 2, 0, STR_PAD_LEFT));
foreach ($this->tsProperty as $tsProperty) {
$binary .= $tsProperty->toBinary();
}
return $binary.$this->postBinary;
}
/**
* Get a simple associative array containing of all TSProperty names and values.
*
* #return array
*/
public function toArray()
{
$userParameters = [];
foreach ($this->tsProperty as $property => $tsPropObj) {
$userParameters[$property] = $tsPropObj->getValue();
}
return $userParameters;
}
/**
* Get all TSProperty objects.
*
* #return TSProperty[]
*/
public function getTSProperties()
{
return $this->tsProperty;
}
/**
* #param string $propName
*/
protected function validateProp($propName)
{
if (!$this->has($propName)) {
throw new \InvalidArgumentException(sprintf('TSProperty for "%s" does not exist.', $propName));
}
}
/**
* #param string $propName
* #return TSProperty
*/
protected function getTsPropObj($propName)
{
return array_change_key_case($this->tsProperty)[strtolower($propName)];
}
/**
* Get an associative array with all of the userParameters property names and values.
*
* #param string $userParameters
* #return array
*/
protected function decodeUserParameters($userParameters)
{
$userParameters = bin2hex($userParameters);
// Save the 96-byte array of reserved data, so as to not ruin anything that may be stored there.
$this->preBinary = hex2bin(substr($userParameters, 0, 96));
// The signature is a 2-byte unicode character at the front
$this->signature = MBString::chr(hexdec(substr($userParameters, 96, 2)));
// This asserts the validity of the tsPropertyArray data. For some reason 'P' means valid...
if ($this->signature != self::VALID_SIGNATURE) {
throw new \InvalidArgumentException('Invalid TSPropertyArray data');
}
// The property count is a 2-byte unsigned integer indicating the number of elements for the tsPropertyArray
// It starts at position 98. The actual variable data begins at position 100.
$length = $this->addTSPropData(substr($userParameters, 100), hexdec(substr($userParameters, 98, 2)));
// Reserved data length + (count and sig length == 4) + the added lengths of the TSPropertyArray
// This saves anything after that variable TSPropertyArray data, so as to not squash anything stored there
if (strlen($userParameters) > (96 + 4 + $length)) {
$this->postBinary = hex2bin(substr($userParameters, (96 + 4 + $length)));
}
}
/**
* Given the start of TSPropertyArray hex data, and the count for the number of TSProperty structures in contains,
* parse and split out the individual TSProperty structures. Return the full length of the TSPropertyArray data.
*
* #param string $tsPropertyArray
* #param int $tsPropCount
* #return int The length of the data in the TSPropertyArray
*/
protected function addTSPropData($tsPropertyArray, $tsPropCount)
{
$length = 0;
for ($i = 0; $i < $tsPropCount; $i++) {
// Prop length = name length + value length + type length + the space for the length data.
$propLength = hexdec(substr($tsPropertyArray, $length, 2)) + (hexdec(substr($tsPropertyArray, $length + 2, 2)) * 3) + 6;
$tsProperty = new TSProperty(hex2bin(substr($tsPropertyArray, $length, $propLength)));
$this->tsProperty[$tsProperty->getName()] = $tsProperty;
$length += $propLength;
}
return $length;
}
}
TSProperty
/**
* Represents a TSProperty structure in a TSPropertyArray of a userParameters binary value.
*
* #see https://msdn.microsoft.com/en-us/library/ff635169.aspx
* #see http://daduke.org/linux/userparameters.html
* #author Chad Sikorra <Chad.Sikorra#gmail.com>
*/
class TSProperty
{
/**
* Nibble control values. The first value for each is if the nibble is <= 9, otherwise the second value is used.
*/
const NIBBLE_CONTROL = [
'X' => ['001011', '011010'],
'Y' => ['001110', '011010'],
];
/**
* The nibble header.
*/
const NIBBLE_HEADER = '1110';
/**
* Conversion factor needed for time values in the TSPropertyArray (stored in microseconds).
*/
const TIME_CONVERSION = 60 * 1000;
/**
* A simple map to help determine how the property needs to be decoded/encoded from/to its binary value.
*
* There are some names that are simple repeats but have 'W' at the end. Not sure as to what that signifies. I
* cannot find any information on them in Microsoft documentation. However, their values appear to stay in sync with
* their non 'W' counterparts. But not doing so when manipulating the data manually does not seem to affect anything.
* This probably needs more investigation.
*
* #var array
*/
protected $propTypes = [
'string' => [
'CtxWFHomeDir',
'CtxWFHomeDirW',
'CtxWFHomeDirDrive',
'CtxWFHomeDirDriveW',
'CtxInitialProgram',
'CtxInitialProgramW',
'CtxWFProfilePath',
'CtxWFProfilePathW',
'CtxWorkDirectory',
'CtxWorkDirectoryW',
'CtxCallbackNumber',
],
'time' => [
'CtxMaxDisconnectionTime',
'CtxMaxConnectionTime',
'CtxMaxIdleTime',
],
'int' => [
'CtxCfgFlags1',
'CtxCfgPresent',
'CtxKeyboardLayout',
'CtxMinEncryptionLevel',
'CtxNWLogonServer',
'CtxShadow',
],
];
/**
* #var string The property name.
*/
protected $name;
/**
* #var string|int The property value.
*/
protected $value;
/**
* #var int The property value type.
*/
protected $valueType = 1;
/**
* #param string|null $value Pass binary TSProperty data to construct its object representation.
*/
public function __construct($value = null)
{
if ($value) {
$this->decode(bin2hex($value));
}
}
/**
* Set the name for the TSProperty.
*
* #param string $name
*/
public function setName($name)
{
$this->name = $name;
}
/**
* Get the name for the TSProperty.
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set the value for the TSProperty.
*
* #param string|int $value
*/
public function setValue($value)
{
$this->value = $value;
}
/**
* Get the value for the TSProperty.
*
* #return string|int
*/
public function getValue()
{
return $this->value;
}
/**
* Convert the TSProperty name/value back to its binary representation for the userParameters blob.
*
* #return string
*/
public function toBinary()
{
$name = bin2hex($this->name);
$binValue = $this->getEncodedValueForProp($this->name, $this->value);
$valueLen = strlen(bin2hex($binValue)) / 3;
$binary = hex2bin(
$this->dec2hex(strlen($name))
.$this->dec2hex($valueLen)
.$this->dec2hex($this->valueType)
.$name
);
return $binary.$binValue;
}
/**
* Given a TSProperty blob, decode the name/value/type/etc.
*
* #param string $tsProperty
*/
protected function decode($tsProperty)
{
$nameLength = hexdec(substr($tsProperty, 0, 2));
# 1 data byte is 3 encoded bytes
$valueLength = hexdec(substr($tsProperty, 2, 2)) * 3;
$this->valueType = hexdec(substr($tsProperty, 4, 2));
$this->name = pack('H*', substr($tsProperty, 6, $nameLength));
$this->value = $this->getDecodedValueForProp($this->name, substr($tsProperty, 6 + $nameLength, $valueLength));
}
/**
* Based on the property name/value in question, get its encoded form.
*
* #param string $propName
* #param string|int $propValue
* #return string
*/
protected function getEncodedValueForProp($propName, $propValue)
{
if (in_array($propName, $this->propTypes['string'])) {
# Simple strings are null terminated. Unsure if this is needed or simply a product of how ADUC does stuff?
$value = $this->encodePropValue($propValue."\0", true);
} elseif (in_array($propName, $this->propTypes['time'])) {
# Needs to be in microseconds (assuming it is in minute format)...
$value = $this->encodePropValue($propValue * self::TIME_CONVERSION);
} else {
$value = $this->encodePropValue($propValue);
}
return $value;
}
/**
* Based on the property name in question, get its actual value from the binary blob value.
*
* #param string $propName
* #param string $propValue
* #return string|int
*/
protected function getDecodedValueForProp($propName, $propValue)
{
if (in_array($propName, $this->propTypes['string'])) {
// Strip away null terminators. I think this should be desired, otherwise it just ends in confusion.
$value = str_replace("\0", '', $this->decodePropValue($propValue, true));
} elseif (in_array($propName, $this->propTypes['time'])) {
// Convert from microseconds to minutes (how ADUC displays it anyway, and seems the most practical).
$value = hexdec($this->decodePropValue($propValue)) / self::TIME_CONVERSION;
} elseif (in_array($propName, $this->propTypes['int'])) {
$value = hexdec($this->decodePropValue($propValue));
} else {
$value = $this->decodePropValue($propValue);
}
return $value;
}
/**
* Decode the property by inspecting the nibbles of each blob, checking the control, and adding up the results into
* a final value.
*
* #param string $hex
* #param bool $string Whether or not this is simple string data.
* #return string
*/
protected function decodePropValue($hex, $string = false)
{
$decodePropValue = '';
$blobs = str_split($hex, 6);
foreach ($blobs as $blob) {
$bin = decbin(hexdec($blob));
$controlY = substr($bin, 4, 6);
$nibbleY = substr($bin, 10, 4);
$controlX = substr($bin, 14, 6);
$nibbleX = substr($bin, 20, 4);
$byte = $this->nibbleControl($nibbleX, $controlX).$this->nibbleControl($nibbleY, $controlY);
if ($string) {
$decodePropValue .= MBString::chr(bindec($byte));
} else {
$decodePropValue = $this->dec2hex(bindec($byte)).$decodePropValue;
}
}
return $decodePropValue;
}
/**
* Get the encoded property value as a binary blob.
*
* #param string $value
* #param bool $string
* #return string
*/
protected function encodePropValue($value, $string = false)
{
// An int must be properly padded. (then split and reversed). For a string, we just split the chars. This seems
// to be the easiest way to handle UTF-8 characters instead of trying to work with their hex values.
$chars = $string ? MBString::str_split($value) : array_reverse(str_split($this->dec2hex($value, 8), 2));
$encoded = '';
foreach ($chars as $char) {
// Get the bits for the char. Using this method to ensure it is fully padded.
$bits = sprintf('%08b', $string ? MBString::ord($char) : hexdec($char));
$nibbleX = substr($bits, 0, 4);
$nibbleY = substr($bits, 4, 4);
// Construct the value with the header, high nibble, then low nibble.
$value = self::NIBBLE_HEADER;
foreach (['Y' => $nibbleY, 'X' => $nibbleX] as $nibbleType => $nibble) {
$value .= $this->getNibbleWithControl($nibbleType, $nibble);
}
// Convert it back to a binary bit stream
foreach ([0, 8, 16] as $start) {
$encoded .= $this->packBitString(substr($value, $start, 8), 8);
}
}
return $encoded;
}
/**
* PHP's pack() function has no 'b' or 'B' template. This is a workaround that turns a literal bit-string into a
* packed byte-string with 8 bits per byte.
*
* #param string $bits
* #param bool $len
* #return string
*/
protected function packBitString($bits, $len)
{
$bits = substr($bits, 0, $len);
// Pad input with zeros to next multiple of 4 above $len
$bits = str_pad($bits, 4 * (int) (($len + 3) / 4), '0');
// Split input into chunks of 4 bits, convert each to hex and pack them
$nibbles = str_split($bits, 4);
foreach ($nibbles as $i => $nibble) {
$nibbles[$i] = base_convert($nibble, 2, 16);
}
return pack('H*', implode('', $nibbles));
}
/**
* Based on the control, adjust the nibble accordingly.
*
* #param string $nibble
* #param string $control
* #return string
*/
protected function nibbleControl($nibble, $control)
{
// This control stays constant for the low/high nibbles, so it doesn't matter which we compare to
if ($control == self::NIBBLE_CONTROL['X'][1]) {
$dec = bindec($nibble);
$dec += 9;
$nibble = str_pad(decbin($dec), 4, '0', STR_PAD_LEFT);
}
return $nibble;
}
/**
* Get the nibble value with the control prefixed.
*
* If the nibble dec is <= 9, the control X equals 001011 and Y equals 001110, otherwise if the nibble dec is > 9
* the control for X or Y equals 011010. Additionally, if the dec value of the nibble is > 9, then the nibble value
* must be subtracted by 9 before the final value is constructed.
*
* #param string $nibbleType Either X or Y
* #param $nibble
* #return string
*/
protected function getNibbleWithControl($nibbleType, $nibble)
{
$dec = bindec($nibble);
if ($dec > 9) {
$dec -= 9;
$control = self::NIBBLE_CONTROL[$nibbleType][1];
} else {
$control = self::NIBBLE_CONTROL[$nibbleType][0];
}
return $control.sprintf('%04d', decbin($dec));
}
/**
* Need to make sure hex values are always an even length, so pad as needed.
*
* #param int $int
* #param int $padLength The hex string must be padded to this length (with zeros).
* #return string
*/
protected function dec2hex($int, $padLength = 2)
{
return str_pad(dechex($int), $padLength, 0, STR_PAD_LEFT);
}
}
MBString
/**
* Some utility functions to handle multi-byte strings properly, as support is lacking/inconsistent for most PHP string
* functions. This provides a wrapper for various workarounds and falls back to normal functions if needed.
*
* #author Chad Sikorra <Chad.Sikorra#gmail.com>
*/
class MBString
{
/**
* Get the integer value of a specific character.
*
* #param $string
* #return int
*/
public static function ord($string)
{
if (self::isMbstringLoaded()) {
$result = unpack('N', mb_convert_encoding($string, 'UCS-4BE', 'UTF-8'));
if (is_array($result) === true) {
return $result[1];
}
}
return ord($string);
}
/**
* Get the character for a specific integer value.
*
* #param $int
* #return string
*/
public static function chr($int)
{
if (self::isMbstringLoaded()) {
return mb_convert_encoding(pack('n', $int), 'UTF-8', 'UTF-16BE');
}
return chr($int);
}
/**
* Split a string into its individual characters and return it as an array.
*
* #param string $value
* #return string[]
*/
public static function str_split($value)
{
return preg_split('/(?<!^)(?!$)/u', $value);
}
/**
* Simple check for the mbstring extension.
*
* #return bool
*/
protected static function isMbstringLoaded()
{
return extension_loaded('mbstring');
}
}
Displaying userParameters Values
// Assuming $value is the binary value from userParameters.
$tsPropArray = new TSPropertyArray($value);
// Prints out each TS property name and value for the user.
foreach($tsPropArray->toArray() as $prop => $value) {
echo "$prop => $value".PHP_EOL;
}
// Print a single value
echo $tsPropArray->get('CtxWFProfilePath');
Modifying/Creating userParameters Values
// Creates a new set of values for userParameters (default values).
$tsPropArray = new TSPropertyArray();
// Set a max session idle time of 30 minutes.
$tsPropArray->set('CtxMaxIdleTime', 30);
// Get the binary value to save to LDAP
$binary = $tsPropArray->toBinary();
// Load binary userParameters data from a user
$tsPropArray = new TSPropertyArray($binary);
// Replace their user profile location...
$tsPropArray->set('CtxWFProfilePath', '\\some\path');
// Get the new binary value, then save it as needed back to LDAP...
$binary = $tsPropArray->toBinary();
A Few Additional Notes
The code above will handle multi-byte chars, so if there are UTF8 chars in a value it should be fine. It also respects other binary data within the userParameters binary blob. So it is not destructive, that data will be preserved when you are modifying an existing value.
I also noticed that there are some TS properties in userParameters that end in 'W' and are duplicates of other properties (even their values are duplicated). I could not find any information on this in MSDN or elsewhere, so I'm not sure what their significance is.
I know it's been a while since the original question was asked, but this is the only page that comes up in a search for "CtxWFProfilePath" and PHP, and it's where I started from when I was trying to work out how to get the values out of userParameters.
It turns out that the userParameters blob has possibly the most arcane and unnecessary encoding ever invented. I have no idea what Microsoft were thinking when this was dreamt up, but they fact that a CtxCfgPresent value of 0xB00B1E55 indicates valid data may go some way to explaining it...
(Big thanks to http://daduke.org/linux/userparameters.html for figuring out the encoding of the TSProperty structure.)
Here's my solution:
<?php
function userParameters($userParameters){
/*
userParameters data structure described at: http://msdn.microsoft.com/en-us/library/ff635189.aspx
TSProperty data structure described at: http://msdn.microsoft.com/en-us/library/ff635169.aspx
Input: userParameters blob returned from ldap_search
Output: associative array of user parameters
*/
$parameters = array();
$userParameters = bin2hex($userParameters);
$userParameters = substr($userParameters,96);
$Signature = chr(hexdec(substr($userParameters,0,2)));
$userParameters = substr($userParameters,2);
if ($Signature != 'P'){
return false;
}
$TSPropertyCount = hexdec(substr($userParameters,0,2));
$userParameters = substr($userParameters,2);
for ($i = 0; $i < $TSPropertyCount; $i++){
$NameLength = hexdec(substr($userParameters,0,2));
$userParameters = substr($userParameters,2);
$ValueLength = hexdec(substr($userParameters,0,2)) * 3; // 1 data byte = 3 encoded bytes
$userParameters = substr($userParameters,2);
$Type = substr($userParameters,0,2);
$userParameters = substr($userParameters,2);
$PropName = substr($userParameters,0,$NameLength);
$PropName = hex2str($PropName);
$userParameters = substr($userParameters,$NameLength);
$PropValue = substr($userParameters,0,$ValueLength);
$userParameters = substr($userParameters,$ValueLength);
switch ($PropName) {
case 'CtxWFHomeDir':
case 'CtxWFHomeDirDrive':
case 'CtxInitialProgram':
case 'CtxWFProfilePath':
case 'CtxWorkDirectory':
case 'CtxCallbackNumber':
$parameters[$PropName] = decode_PropValue($PropValue,true);
break;
case 'CtxCfgFlags1':
$parameters[$PropName] = parse_CtxCfgFlags1(decode_PropValue($PropValue));
break;
case 'CtxShadow':
$parameters[$PropName] = parse_CtxShadow(decode_PropValue($PropValue));
break;
default:
$parameters[$PropName] = decode_PropValue($PropValue);
}
}
return $parameters;
}
function hex2str($hex) {
$str = '';
for($i = 0; $i < strlen($hex); $i += 2){
$str .= chr(hexdec(substr($hex,$i,2)));
}
return $str;
}
function decode_PropValue($hex,$ascii=false){
/*
Encoding described at: http://daduke.org/linux/userparameters.html
for each character you want to encode, do:
- split the character's byte into nibbles xxxx and yyyy
- have a look at xxxx. If it's <= 9, control x (XXXXXX) equals 001011, otherwise it's 011010
- have a look at yyyy. Here the bit patterns for control y (YYYYYY) are 001110 (yyyy <= 9), 011010 otherwise
- if xxxx > 9: xxxx -= 9
- if yyyy > 9: yyyy -= 9
- take the prefix (1110), control y, yyyy, control x and xxxx and glue them all together to yield a 24 bit string
- convert this bit stream to three bytes: 1110 YYYY YYyy yyXX XXXX xxxx
*/
$decode_PropValue = '';
$blobs = str_split($hex,6);
foreach ($blobs as $blob){
$bin = decbin(hexdec($blob));
$control_y = substr($bin,4,6);
$nibble_y = substr($bin,10,4);
$control_x = substr($bin,14,6);
$nibble_x = substr($bin,20,4);
$byte = nibble_control($nibble_x,$control_x).nibble_control($nibble_y,$control_y);
if ($ascii){
$decode_PropValue .= chr(bindec($byte));
}
else {
$decode_PropValue = str_pad(dechex(bindec($byte)),2,'0',STR_PAD_LEFT).$decode_PropValue;
}
}
return $decode_PropValue;
}
function nibble_control($nibble,$control){
if ($control == '011010'){
$dec = bindec($nibble);
$dec += 9;
return str_pad(decbin($dec),4,'0',STR_PAD_LEFT);
}
return $nibble;
}
function parse_CtxCfgFlags1($CtxCfgFlags1) {
/* Flag bit mask values from: http://msdn.microsoft.com/en-us/library/ff635169.aspx */
$parse_CtxCfgFlags1 = array();
$CtxCfgFlags1 = hexdec($CtxCfgFlags1);
$flags = array(
'F1MSK_INHERITINITIALPROGRAM' => 268435456,
'F1MSK_INHERITCALLBACK' => 134217728,
'F1MSK_INHERITCALLBACKNUMBER' => 67108864,
'F1MSK_INHERITSHADOW' => 33554432,
'F1MSK_INHERITMAXSESSIONTIME' => 16777216,
'F1MSK_INHERITMAXDISCONNECTIONTIME' => 8388608,
'F1MSK_INHERITMAXIDLETIME' => 4194304,
'F1MSK_INHERITAUTOCLIENT' => 2097152,
'F1MSK_INHERITSECURITY' => 1048576,
'F1MSK_PROMPTFORPASSWORD' => 524288,
'F1MSK_RESETBROKEN' => 262144,
'F1MSK_RECONNECTSAME' => 131072,
'F1MSK_LOGONDISABLED' => 65536,
'F1MSK_AUTOCLIENTDRIVES' => 32768,
'F1MSK_AUTOCLIENTLPTS' => 16384,
'F1MSK_FORCECLIENTLPTDEF' => 8192,
'F1MSK_DISABLEENCRYPTION' => 4096,
'F1MSK_HOMEDIRECTORYMAPROOT' => 2048,
'F1MSK_USEDEFAULTGINA' => 1024,
'F1MSK_DISABLECPM' => 512,
'F1MSK_DISABLECDM' => 256,
'F1MSK_DISABLECCM' => 128,
'F1MSK_DISABLELPT' => 64,
'F1MSK_DISABLECLIP' => 32,
'F1MSK_DISABLEEXE' => 16,
'F1MSK_WALLPAPERDISABLED' => 8,
'F1MSK_DISABLECAM' => 4
);
foreach($flags as $flag => $bit) {
if ($CtxCfgFlags1 & $bit) {
$parse_CtxCfgFlags1[] = $flag;
}
}
return($parse_CtxCfgFlags1);
}
function parse_CtxShadow($CtxShadow) {
/* Flag values from: http://msdn.microsoft.com/en-us/library/ff635169.aspx */
$CtxShadow = hexdec($CtxShadow);
$flags = array('Disable','EnableInputNotify','EnableInputNoNotify','EnableNoInputNotify','EnableNoInputNoNotify');
if ($CtxShadow < 0 || $CtxShadow > 4) {
return false;
}
return $flags[$CtxShadow];
}
?>
removing all special characters with
preg_replace('/[^a-zA-Z0-9_ %[].()%&-]/s', '', $piConverted);
solved it :-)
Now, if I could only find a more elegant conversion so I don't have to "manually" reverse the hex code

write a PHPUnit Test for some classes i am new PHPUnit user

i want to write phpunit test for this classes
and sure I know how to execute phpunit
1000 thanks for helper :)
IRandomGenerator.php
<?php
/**
* Random Generator interface
*
* #package GNS
*/
interface IRandomGenerator
{
/**
* Get Random Byte
*
* #return int 0..255 range integer
* #throws NoMoreBytesException
*/
public function getByte();
}
/**
* No more bytes can be get exception
*
* It can be useful if generator depends on external source
*/
class NoMoreBytesException extends Exception {}
?>
ARandomGenerator.class.php
<?php
/**
* Base class for random generators
*/
require_once 'IRandomGenerator.php';
abstract class ARandomGenerator implements IRandomGenerator
{
/**
* Get Bytes
*
* #param int $number
* #return array Array of 0..255 range integers
* #throws NoMoreBytesException
*/
public function getBytes($number = 1)
{
$bytes = array();
for ($i = 0; $i < $number; $i++) $bytes[] = $this->getByte();
return $bytes;
}
/**
* Get Word
*
* #return int 0..65535 range integer
* #throws NoMoreBytesException
*/
public function getWord()
{
return $this->getByte() << 8 | $this->getByte();
}
/**
* Get Words
*
* #param int $number
* #return array Array of 0..65535 range integers
* #throws NoMoreBytesException
*/
public function getWords($number = 1)
{
$words = array();
for ($i = 0; $i < $number; $i++) $words[] = $this->getWord();
return $words;
}
/**
* Get Long
*
* #return 4-byte integer
* #throws NoMoreBytesException
*/
public function getLong()
{
return $this->getWord() << 16 | $this->getWord();
}
/**
* Get Longs
*
* #param int $number
* #return array Array of 4-byte integers
* #throws NoMoreBytesException
*/
public function getLongs($number = 1)
{
$longs = array();
for ($i = 0; $i < $number; $i++) $longs[] = $this->getLong();
return $longs;
}
}
?>
FileRandomGenerator.class
<?php
/**
* File random generator (reading data from file)
*
* Take care of your file holds really random data
* and contains it enough for task. Don't use the same
* data twice
*/
require_once 'ARandomGenerator.class.php';
class FileRandomGenerator extends ARandomGenerator
{
/** #const int Chunk to read from file */
const CHUNKSIZE = 128;
/** #var array $pool of data */
private $pool = array();
/** #var resourse Open file descriptor */
private $fd;
/**
* Constructor
*
* #param string $fname File name with random data
*/
public function __construct($fname)
{
$this->fd = fopen($fname, 'rb');
if ($this->fd === false) throw new NoMoreBytesException('Cannot open file: ' . $fname);
}
/**
* Get Random Byte
*
* #return int 0..255 range integer
* #throws NoMoreBytesException
*/
public function getByte()
{
// reading to pool
if (count($this->pool) === 0)
{
if (($data = fread($this->fd, self::CHUNKSIZE)) !== false)
$this->pool = unpack('C*', $data);
else
throw new NoMoreBytesException('No more data in file left');
}
return array_pop($this->pool);
}
}
?>
and how the anyone to know what the function from phpunit framework must to use for test each class ?
One easy way to get started is to use phpunit-skelgen. This utility will quickly set up a bare-bones test for your class.
You will want to read the manual, but a "tl;dr" is:
$ mkdir -pv tests/unit
$ phpunit-skelgen --test -- FileRandomGenerator FileRandomGenerator.class.php \
FileRandomGeneratorTest tests/unit/FileRandomGeneratorTest.php
HTH

Categories