ErrorException Array to string conversion-Laravel 8 - php

Am trying to upload files on my laravel project but getting an error
'ErrorException Array to string conversion'
Any help will be much appreciated. Below is the error
ErrorException Array to string conversion
C:\xampp\htdocs\laravel\filesUpload\vendor\laravel\framework\src\Illuminate\Support\Str.php:609
/**
* Replace a given value in the string sequentially with an array.
*
* #param string $search
* #param array<int|string, string> $replace
* #param string $subject
* #return string
*/
public static function replaceArray($search, array $replace, $subject)
{
$segments = explode($search, $subject);
$result = array_shift($segments);
foreach ($segments as $segment) {
$result .= (array_shift($replace) ?? $search).$segment; //line 609
}
return $result;
}

Related

PHP to Matching lines between files

Text File 1:
426684146543xxxx|xx|xxxx|xxx
407166210197xxxx|xx|xxxx|xxx
521307101305xxxx|xx|xxxx|xxx
521307101485xxxx|xx|xxxx|xxx
Text File 2:
521307
407166
If the lines in the 2nd text file exist in the 1st text file, I want it to show me all the matching lines from the 1st file
OUTPUT:
521307101485xxxx
521307101305xxxx
407166210197xxxx
This can be a tricky problem to solve. If you are dealing with large files, or don't know how large your files will be, you have to find a way to solve the problem without reading either file into memory all at once.
In order to do that, you need to create some sort of efficient structure for the data in file 1 that you can search for each ID in file 2, that also allows you to retrieve the full records for file 1 after you have determined the matches. This is exactly what trees are made for.
Here is a solution that reads in the data in file 1, creates a tree structure from the first column of each row, and keeps track of the byte offsets from the file where the strings appear. This allows you to search using any length of ID prefix (searching with "4" would return the first two lines, "40" only the second).
There are two classes, CharNode represents a single node in the tree, and IdTree, which manages the structure of nodes, handles ingesting the files, and searching.
<?php
class CharNode implements JsonSerializable
{
private string $char;
private array $byteOffsets = [];
private array $children = [];
/**
* CharNode constructor.
* #param string $char
* #param bool $terminal
*/
public function __construct(string $char)
{
$this->char = $char;
}
/**
* #return string
*/
public function getChar(): string
{
return $this->char;
}
/**
* #param string $char
*/
public function setChar(string $char): void
{
$this->char = $char;
}
/**
* #return array
*/
public function getChildren(): array
{
return $this->children;
}
/**
* #param array $children
*/
public function setChildren(array $children): void
{
$this->children = $children;
}
/**
* #return array
*/
public function getByteOffsets(): array
{
return $this->byteOffsets;
}
/**
* #param array $byteOffsets
*/
public function setByteOffsets(array $byteOffsets): void
{
$this->byteOffsets = $byteOffsets;
}
/**
* #param int $byteOffset
* #return void
*/
public function addByteOffset(int $byteOffset): void
{
$this->byteOffsets[] = $byteOffset;
}
/**
* #param array $charVector
* #param int $byteOffset
* #return void
*/
public function ingestCharVector(array $charVector, int $byteOffset)
{
$char = array_shift($charVector);
if (!array_key_exists($char, $this->children))
{
$newNode = new CharNode($char);
$this->children[$char] = $newNode;
}
$currChild = $this->children[$char];
$currChild->addByteOffset($byteOffset);
if (!empty($charVector))
{
$currChild->ingestCharVector($charVector, $byteOffset);
}
}
public function jsonSerialize()
{
return [
'char' => $this->char,
'byteOffsets' => $this->byteOffsets,
'children' => array_values($this->children)
];
}
}
class IdTree implements JsonSerializable
{
private array $tree = [];
private array $byteOffsetOutput = [];
private string $filePath;
/**
* #param string $filePath
* #param string $delimiter
* #throws Exception
*/
public function __construct(string $filePath, string $delimiter = '|')
{
$this->filePath = $filePath;
$fh = fopen($filePath, 'r');
if (!$fh)
{
throw new Exception('Could not open file ' . $filePath);
}
$currByteOffset = 0;
while (($currRow = fgetcsv($fh, null, $delimiter)))
{
$this->ingestWord($currRow[0], $currByteOffset);
$currByteOffset = ftell($fh);
}
fclose($fh);
}
/**
* #param string $idFilePath
* #return array
* #throws Exception
*/
public function getByteOffsetsForIdFile(string $idFilePath): array
{
$byteOffsets = [];
$fh = fopen($idFilePath, 'r');
if (!$fh)
{
throw new Exception('Could not open file ' . $idFilePath);
}
while (($currLine = fgets($fh)))
{
$currByteOffsets = $this->findByteOffsetsForId(trim($currLine));
$byteOffsets = array_merge($byteOffsets, $currByteOffsets);
}
fclose($fh);
asort($byteOffsets);
return $byteOffsets;
}
/**
* #param string $idFilePath
* #param bool $firstColumnOnly
* #return array
* #throws Exception
*/
public function getLinesMatchingIdFile(string $idFilePath, bool $firstColumnOnly = false): array
{
$byteOffsets = $this->getByteOffsetsForIdFile($idFilePath);
$fh = fopen($this->filePath, 'r');
$output = [];
foreach ($byteOffsets as $currOffset)
{
fseek($fh, $currOffset);
$currRow = fgetcsv($fh, null, '|');
$output[] = ($firstColumnOnly) ? $currRow[0] : $currRow;
}
return $output;
}
public function ingestWord(string $word, int $byteOffset): void
{
$word = $this->formatWord($word);
if (empty($word))
{
return;
}
$charVector = str_split($word, 1);
$this->ingestCharVector($charVector, $byteOffset);
}
/**
* #param array $charVector
* #param int $byteOffset
* #return void
*/
public function ingestCharVector(array $charVector, int $byteOffset): void
{
$char = array_shift($charVector);
if (!array_key_exists($char, $this->tree))
{
$this->tree[$char] = new CharNode($char);
}
$currChild = $this->tree[$char];
if (!empty($charVector))
{
$currChild->ingestCharVector($charVector, $byteOffset);
}
}
/**
* #param string $term
* #return array
*/
public function findByteOffsetsForId(string $term): array
{
// Reset state
$this->byteOffsetOutput = [];
$this->stringBuffer = [];
$word = $this->formatWord($term);
if (empty($word))
{
return [];
}
$charVector = str_split($word, 1);
$this->branchSearch($charVector, $this->tree);
return $this->byteOffsetOutput;
}
/**
* #param array $charVector
* #param array $charNodeSet
* #return void
*/
private function branchSearch(array $charVector, array $charNodeSet): void
{
if (empty($charNodeSet))
{
return;
}
if (!empty($charVector))
{
$currChar = array_shift($charVector);
if (!array_key_exists($currChar, $charNodeSet))
{
return;
}
/**
* #var $currCharNode CharNode
*/
$currCharNode = $charNodeSet[$currChar];
// If this is the end of the search term, set th eline numbers
if (empty($charVector))
{
$this->byteOffsetOutput = array_merge($this->byteOffsetOutput, $currCharNode->getByteOffsets());
}
$this->branchSearch($charVector, $currCharNode->getChildren());
}
}
/**
* #param string $word
* #return array|string|string[]|null
*/
private function formatWord(string $word)
{
$word = strtolower($word);
$word = preg_replace("/[^a-z0-9 ]/", '', $word);
return $word;
}
public function jsonSerialize()
{
return array_values($this->tree);
}
}
That looks like a lot of code, but most of it is fairly idiomatic tree logic.
Using it is dead simple:
// Instantiate and load our tree
$tree = new IdTree('file1.txt');
// Get all matching rows
$matchingRows = $tree->getLinesMatchingIdFile('file2.txt');
print_r($matchingRows);
Output:
Array
(
[0] => Array
(
[0] => 407166210197xxxx
[1] => xx
[2] => xxxx
[3] => xxx
)
[1] => Array
(
[0] => 521307101305xxxx
[1] => xx
[2] => xxxx
[3] => xxx
)
[2] => Array
(
[0] => 521307101485xxxx
[1] => xx
[2] => xxxx
[3] => xxx
)
)
It was not clear to me whether you wanted the entire row for each match, or just the first column, so I added a flag that allows that.
// Get only the first column of each line
$matchingIds = $tree->getLinesMatchingIdFile('file2.txt', true);
print_r($matchingIds);
Output:
Array
(
[0] => 407166210197xxxx
[1] => 521307101305xxxx
[2] => 521307101485xxxx
)
There is some extra stuff you may not need, like the JSON output, which is useful for visualizing how the structure works. You could also make this more efficient if you know your data will always be formatted in certain ways (if your search IDs will always be within certain lengths, etc). You could still run into memory problems if you are processing truly massive data files. This is just a basic example of how you can go about solving problems like this "for real".
You can try to use preg_match function to find all strings
$file1 = file_get_contents('text1.txt');
$file2 = file_get_contents('text2.txt');
$file2 = explode("\r\n", $file2);
foreach($file2 as $item){
preg_match_all('#'.$item.'.+#', $file1, $matches);
$result[] = $matches;
}
Result:
Array
(
[0] => Array
(
[0] => Array
(
[0] => 521307101305xxxx|xx|xxxx|xxx
[1] => 521307101485xxxx|xx|xxxx|xxx
)
)
[1] => Array
(
[0] => Array
(
[0] => 407166210197xxxx|xx|xxxx|xxx
)
)
)
but I think, what use preg_match to find a string, it's not best solution

How to understand this phpStan error (has parameter $astTreeData with no value type specified in iterable type array.)

Say I have a formatter function like this:
function prepareValue(mixed $value): string
{
...
return "{$value}";
}
/**
* #param array<int, array> $astTreeData
* #return string
*/
function makeFormattedDiff(array $astTreeData): string
{
$statusTree = [
'added' => function (string $path, array $node) {
{some code...}
return "Property '{$path}' was added with value: {$value}";
},
'deleted' => fn($path) => "Property '{$path}' was removed",
'nested' => function (string $path, array $node, callable $iterRender) {
{some code...}
return $iterRender($children, $path);
},
'changed' => function (string $path, array $node) {
{some code...}
return "Property '{$path}' was updated. From {$valueBefore} to {$valueAfter}";
},
'unchanged' => fn() => [],
];
/**
* #param array<int, array> $diff
* #return string
*/
$renderPlainDiff = function (array $diff, string|bool $pathComposition) use (&$renderPlainDiff, $statusTree) {
$diffCopy = $diff;
$lines = array_reduce(
$diffCopy,
/**
* #param array<int, string> $acc
* #param array<string, array> $node
* #param array|null $initial
* #return array
*/
function (array $acc, array $node, array|null $initial = []) use (
$renderPlainDiff,
$pathComposition,
$statusTree
) {
{some code...}
$diffTypeHandler = $statusTree[$status];
return flatten([$acc, $diffTypeHandler($newPath, $node, $renderPlainDiff)]);
},
[]
);
return implode("\n", $lines);
};
return $renderPlainDiff($astTreeData, false);
}
All parameters and return values are specified in detail with phpdoc and type hinting syntax but phpStan gives an error message phpstan: Function Differ\Formatters\plainFormatter\makeFormattedDiff() has parameter $astTreeData with no value type specified in iterable type array
What iterable type array values need to be described?
enter image description here

Spatie Media Library - Array to string conversion

I am trying to get Spatie Media Library to work, but I have this error:
I really don't know where it comes from, any suggestions?
I'll leave a copy of the portion of the code where the error happens
Str.php
/**
* Replace the given value in the given string.
*
* #param string|string[] $search
* #param string|string[] $replace
* #param string|string[] $subject
* #return string
*/
public static function replaceArray($search, array $replace, $subject)
{
$segments = explode($search, $subject);
$result = array_shift($segments);
foreach ($segments as $segment) {
$result .= (array_shift($replace) ?? $search).$segment;
}
return $result;
}

How to post Faker collection in Laravel testing?

I have a test case where the user will be allowed to create/post multiple items at once. Please take a look at my test:
/**
* Test multiple item creation.
*
* #return void
*/
public function testMultipleCreation()
{
$token = Test::generateToken();
$user = Test::getAuthenticatedUser();
$stall = factory(Stall::class)->make()->toArray();
$item = factory(Item::class, 5)->make()->toArray();
dump($item);
$user->addStall($stall);
$response = $this->withHeaders(['Authorization' => 'Bearer ' . $token]);
$response = $response->json('POST', route('items.store', $item));
$response->assertStatus(200);
// $this->assertDatabaseHas('items', $item);
}
and here is the controller:
/**
* Store a newly created resource in storage.
*
* #param \App\Http\Requests\StoreItem $request
* #return \App\Helpers\ResponseMessage
*/
public function store(StoreItem $request)
{
if (is_array($request)) {
$this->createMultiple($request);
}
$item = auth()->user()->addItem(
$request->validated()
);
return ResponseMessage::created('item', $item);
}
/**
* Create multiple items.
*
* #param array $items
* #return void
*/
protected function createMultiple($items)
{
$itemCollection = [];
foreach ($items as $item) {
$itemCollection[] = auth()->user()->addItem(
$item->validated()
);
}
return ResponseMessage::created('items', $itemCollection);
}
It works fine if I post it as $item = factory(Item::class)->make()->toArray();, but if I make it more than 1 factory item, it fails and throws this error:
1) Tests\Unit\ItemTest::testMultipleCreation
ErrorException: Array to string conversion
ERRORS!
Tests: 9, Assertions: 20, Errors: 1.
Problem is likely here
$response = $response->json('POST', route('items.store', $item));
Since $item is an Array, it doesn't know how to convert it.
Probably you meant
$response = $response->json('POST', route('items.store'), $item);

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

Categories