PHP to Matching lines between files - php

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

Related

Laravel 5.2 Builder insert replace multiple rows

I have this code, can insert an array at first time, but when trying replace and update, it returns error
Integrity constraint violation: 1062 Duplicate entry for composite key ['periodo_id','asociado_id']
Model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Lectura extends Model
{
protected $primaryKey = array('periodo_id', 'asociado_id');
public $timestamps = false;
public $incrementing = false;
}
And controller:
$rows = DB::table('lecturas_temp')->get();
$arr = array();
foreach ($rows as $row) {
$a = [
'asociado_id' => $row->asociado_id,
'periodo_id' => $request->periodo_id,
'nombre' => $row->nombre,
];
array_push($arr, $a);
}
DB::table('lecturas')->insert($arr);
Any alternatives to line DB::table('lecturas')->insert($arr)?
I tried Eloquent Lectura::insert($arr) and updateOrCreate but same results;
The error message is crystal clear. You are using a composite key ['periodo_id', 'asociado_id'], which means you cannot insert the same data twice because you defined it as your primary key.
If you are expecting to have duplicate composite key, then please, remove it from your model as the primary key. However, if you want the data to be unique, you should use updateOrCreate().
$rows = DB::table('lecturas_temp')->get();
$arr = array();
foreach ($rows as $row) {
Lectura::updateOrCreate(
[
'asociado_id' => $row->asociado_id,
'periodo_id' => $request->periodo_id
],
[
'nombre' => $row->nombre
]
);
}
As you can see, updateOrCreate() takes 2 arrays as arguments. The first array is the elements used to verify if it already exists. Also, unfortunately you will have to do it one by one instead all in one go as you were doing.
If you want to stick to the query builder, you can use DB::updateOrInsert(), with the same call signature (passing 2 arrays).
Using this code I did solved: https://github.com/yadakhov/insert-on-duplicate-key
class Lectura extends Model
{
protected $primaryKey = array('periodo_id', 'asociado_id');
public $timestamps = false;
public $incrementing = false;
public static function insertOnDuplicateKey(array $data, array $updateColumns = null)
{
if (empty($data)) {
return false;
}
// Case where $data is not an array of arrays.
if (!isset($data[0])) {
$data = [$data];
}
$sql = static::buildInsertOnDuplicateSql($data, $updateColumns);
$data = static::inLineArray($data);
return self::getModelConnectionName()->affectingStatement($sql, $data);
}
/**
* Insert using mysql INSERT IGNORE INTO.
*
* #param array $data
*
* #return int 0 if row is ignored, 1 if row is inserted
*/
public static function insertIgnore(array $data)
{
if (empty($data)) {
return false;
}
// Case where $data is not an array of arrays.
if (!isset($data[0])) {
$data = [$data];
}
$sql = static::buildInsertIgnoreSql($data);
$data = static::inLineArray($data);
return self::getModelConnectionName()->affectingStatement($sql, $data);
}
/**
* Insert using mysql REPLACE INTO.
*
* #param array $data
*
* #return int 1 if row is inserted without replacements, greater than 1 if rows were replaced
*/
public static function replace(array $data)
{
if (empty($data)) {
return false;
}
// Case where $data is not an array of arrays.
if (!isset($data[0])) {
$data = [$data];
}
$sql = static::buildReplaceSql($data);
$data = static::inLineArray($data);
return self::getModelConnectionName()->affectingStatement($sql, $data);
}
/**
* Static function for getting table name.
*
* #return string
*/
public static function getTableName()
{
$class = get_called_class();
return (new $class())->getTable();
}
/**
* Static function for getting connection name
*
* #return string
*/
public static function getModelConnectionName()
{
$class = get_called_class();
return (new $class())->getConnection();
}
/**
* Get the table prefix.
*
* #return string
*/
public static function getTablePrefix()
{
return self::getModelConnectionName()->getTablePrefix();
}
/**
* Static function for getting the primary key.
*
* #return string
*/
public static function getPrimaryKey()
{
$class = get_called_class();
return (new $class())->getKeyName();
}
/**
* Build the question mark placeholder. Helper function for insertOnDuplicateKeyUpdate().
* Helper function for insertOnDuplicateKeyUpdate().
*
* #param $data
*
* #return string
*/
protected static function buildQuestionMarks($data)
{
$lines = [];
foreach ($data as $row) {
$count = count($row);
$questions = [];
for ($i = 0; $i < $count; ++$i) {
$questions[] = '?';
}
$lines[] = '(' . implode(',', $questions) . ')';
}
return implode(', ', $lines);
}
/**
* Get the first row of the $data array.
*
* #param array $data
*
* #return mixed
*/
protected static function getFirstRow(array $data)
{
if (empty($data)) {
throw new \InvalidArgumentException('Empty data.');
}
list($first) = $data;
if (!is_array($first)) {
throw new \InvalidArgumentException('$data is not an array of array.');
}
return $first;
}
/**
* Build a value list.
*
* #param array $first
*
* #return string
*/
protected static function getColumnList(array $first)
{
if (empty($first)) {
throw new \InvalidArgumentException('Empty array.');
}
return '`' . implode('`,`', array_keys($first)) . '`';
}
/**
* Build a value list.
*
* #param array $first
*
* #return string
*/
protected static function buildValuesList(array $first)
{
$out = [];
foreach (array_keys($first) as $key) {
$out[] = sprintf('`%s` = VALUES(`%s`)', $key, $key);
}
return implode(', ', $out);
}
/**
* Inline a multiple dimensions array.
*
* #param $data
*
* #return array
*/
protected static function inLineArray(array $data)
{
return call_user_func_array('array_merge', array_map('array_values', $data));
}
/**
* Build the INSERT ON DUPLICATE KEY sql statement.
*
* #param array $data
* #param array $updateColumns
*
* #return string
*/
protected static function buildInsertOnDuplicateSql(array $data, array $updateColumns = null)
{
$first = static::getFirstRow($data);
$sql = 'INSERT INTO `' . static::getTablePrefix() . static::getTableName() . '`(' . static::getColumnList($first) . ') VALUES' . PHP_EOL;
$sql .= static::buildQuestionMarks($data) . PHP_EOL;
$sql .= 'ON DUPLICATE KEY UPDATE ';
if (empty($updateColumns)) {
$sql .= static::buildValuesList($first);
} else {
$sql .= static::buildValuesList(array_combine($updateColumns, $updateColumns));
}
return $sql;
}
/**
* Build the INSERT IGNORE sql statement.
*
* #param array $data
*
* #return string
*/
protected static function buildInsertIgnoreSql(array $data)
{
$first = static::getFirstRow($data);
$sql = 'INSERT IGNORE INTO `' . static::getTablePrefix() . static::getTableName() . '`(' . static::getColumnList($first) . ') VALUES' . PHP_EOL;
$sql .= static::buildQuestionMarks($data);
return $sql;
}
/**
* Build REPLACE sql statement.
*
* #param array $data
*
* #return string
*/
protected static function buildReplaceSql(array $data)
{
$first = static::getFirstRow($data);
$sql = 'REPLACE INTO `' . static::getTablePrefix() . static::getTableName() . '`(' . static::getColumnList($first) . ') VALUES' . PHP_EOL;
$sql .= static::buildQuestionMarks($data);
return $sql;
}
In Controller
Lectura::replace($arr);

How to add or update values in a file using PHP

I am working on a small class that will allow me to write queue data to a file. Similar idea to PHP $_SESSION.
I thought the following steps will do the trick
Open a file using fopen() in 'a+' mode
Lock the file using flock() with LOCK_EX type to prevent another process from using the same file
Read the file's existing content using fread(). Then put the data into array using json_decode(array, true)
Now the data is in array. If the key exists in the array update its value, otherwise insert the key to the array.
After create an array with all the data that need to go on the file, I truncate the file using ftruncate() and write the new data to the file using fwrite()
Unlock the file using flock() with LOCK_UN type to allow another process to use it.
Finally Close the file
I believe I wrote the code to satisfy the above step in my updateCache() method. But it does not seems to work properly. It does not keep track of the data.
Here is my class
<?php
namespace ICWS;
use \ICWS\showVar;
use \DirectoryIterator;
/**
* CacheHandler
*
* #package ICWS
*/
class CacheHandler {
/* Max time second allowed to keep a session File */
private $maxTimeAllowed = 300;
private $filename = '';
private $handle;
public function __construct( $filename )
{
$this->filename = $filename;
// 5% chance to collect garbage when the class is initialized.
if(mt_rand(1, 100) <= 5){
$this->collectGarbage();
}
}
private function print_me($a)
{
echo '<pre>';
print_r($a);
echo '</pre>';
}
/**
* Add/Update the cached array in the session
*
* #param string $key
* #param bigint $id
* #param array $field
* #return void
*/
public function updateCache($key, $id, array $field)
{
$currentVal = $field;
$this->openFile();
$this->lockFile();
$storage = $this->readFile();
//create a new if the $id does not exists in the cache
if( isset($storage[$key]) && array_key_exists($id, $storage[$key]) ){
$currentVal = $storage[$key][$id];
foreach($field as $k => $v){
$currentVal[$k] = $v; //update existing key or add a new one
}
}
$storage[$key][$id] = $currentVal;
$this->updateFile($storage);
$this->unlockFile();
$this->closeFile();
}
/**
* gets the current cache/session for a giving $key
*
* #param string $key. If null is passed it will return everything in the cache
* #return object or boolean
*/
public function getCache($key = null)
{
$value = false;
$this->openFile();
rewind($this->handle);
$storage = $this->readFile();
if(!is_null($key) && isset($storage[$key])){
$value = $storage[$key];
}
if(is_null($key)){
$value = $storage;
}
$this->closeFile();
return $value;
}
/**
* removes the $id from the cache/session
*
* #param string $key
* #param bigint $id
* #return boolean
*/
public function removeCache($key, $id)
{
$this->openFile();
$this->lockFile();
$storage = $this->readFile();
if( !isset($storage[$key][$id])){
$this->unlockFile();
$this->closeFile();
return false;
}
unset($storage[$key][$id]);
$this->updateFile($storage);
$this->unlockFile();
$this->closeFile();
return true;
}
/**
* unset a session
*
* #param argument $keys
* #return void
*/
public function truncateCache()
{
if(file_exists($this->filename)){
unlink($this->filename);
}
}
/**
* Open a file in a giving mode
*
* #params string $mode
* #return void
*/
private function openFile( $mode = "a+")
{
$this->handle = fopen($this->filename, $mode);
if(!$this->handle){
throw new exception('The File could not be opened!');
}
}
/**
* Close the file
* #return void
*/
private function closeFile()
{
fclose($this->handle);
$this->handle = null;
}
/**
* Update the file with array data
*
* #param array $data the array in that has the data
* #return void
*/
private function updateFile(array $data = array() )
{
$raw = json_encode($data);
$this->truncateFile();
fwrite($this->handle, $raw);
}
/**
* Delete the file content;
*
* #return void
*/
private function truncateFile( )
{
ftruncate($this->handle, 0);
rewind($this->handle);
}
/**
* Read the data from the opned file
*
* #params string $mode
* #return array of the data found in the file
*/
private function readFile()
{
$length = filesize($this->filename);
if($length > 0){
rewind($this->handle);
$raw = fread($this->handle, $length);
return json_decode($raw, true);
}
return array();
}
/**
* Lock the file
*
* #return void
*/
private function lockFile( $maxAttempts = 100, $lockType = LOCK_EX)
{
$i = 0;
while($i <= $maxAttempts){
// acquire an exclusive lock
if( flock($this->handle, LOCK_EX) ){
break;
}
++$i;
}
}
/**
* Unlock the file
*
* #return void
*/
private function unlockFile()
{
fflush($this->handle);
flock($this->handle, LOCK_UN);
}
/**
* Remove add cache files
*
* #return void
*/
private function collectGarbage()
{
$mydir = dirname($this->filename);
$dir = new DirectoryIterator( $mydir );
$time = strtotime("now");
foreach ($dir as $file) {
if ( !$file->isDot()
&& $file->isFile()
&& ($time - $file->getATime()) > $this->maxTimeAllowed
&& $file->getFilename() != 'index.html'
) {
unlink($file->getPathName());
}
}
}
function addSlashes($str)
{
return addslashes($str);
}
}
This is how I use it
<?php
require 'autoloader.php';
try {
$cache = new \ICWS\CacheHandler('cache/879');
$field = array('name' => 'Mike A', 'Address' => '123 S Main', 'phone' => '2152456245', 'ext' => 123);
$cache->updateCache('NewKey', 'first', $field);
$cache->updateCache('NewKey', 'second', $field);
$field = array('Address' => '987 S Main', 'phone' => '000000000000', 'ext' => 5555);
$cache->updateCache('NewKey', 'first', $field);
$field = array('locations' => array('S' => 'South', 'N' => 'North', 'E' => 'East'));
$cache->updateCache('NewKey', 'first', $field);
echo '<pre>';
print_r($cache->getCache());
echo '</pre>';
} catch (exception $e){
echo $e->getMessage();
}
?>
I am expecting the array to look like this
Array
(
[first] => stdClass Object
(
[name] => Mike A
[Address] => 987 S Main
[phone] => 000000000000
[ext] => 5555
[locations] => Array
(
[S] => South
[N] => North
[E] => East
)
)
[second] => stdClass Object
(
[name] => Mike A
[Address] => 123 S Main
[phone] => 2152456245
[ext] => 123
)
)
But after executing the first time, I get blank array. Then after I execute the script again I get the following array.
Array
(
[NewKey] => Array
(
[first] => Array
(
[locations] => Array
(
[S] => South
[N] => North
[E] => East
)
)
)
)
What could be causing the data not to update correctly?
I wrote my updateCache() using $_SESSION which gave me the correct output but I need to do this without sessions
Here is my sessions bases method
public function updateCache($key, $id, array $field)
{
$currentVal = (object) $field;
//create a new if the $id does not exists in the cache
if( isset($_SESSION[$key]) && array_key_exists($id, $_SESSION[$key]) ){
$currentVal = (object) $_SESSION[$key][$id];
foreach($field as $k => $v){
$currentVal->$k = $v; //update existing key or add a new one
}
}
$_SESSION[$key][$id] = $currentVal;
}
The problem lies here:
private function readFile()
{
$length = filesize($this->filename);
if($length > 0){
rewind($this->handle);
$raw = fread($this->handle, $length);
return json_decode($raw, true);
}
return array();
}
The documentation of filesize() states:
Note: The results of this function are cached. See clearstatcache() for more details.
Because the file is empty when you start, it would have cached that information and your branch is skipped entirely until the next script execution. This should fix it:
private function readFile()
{
rewind($this->handle);
$raw = stream_get_contents($this->handle);
return json_decode($raw, true) ?: [];
}

Why my file is always empty when trying to read it using PHP?

I am trying to write a class that will allow me to write data to a file and then read it. In some cases, I also lock the file for write, then unlock it once the writing is done.
The problem that I am having is when trying to get the content of the file using my getCache() method the file becomes empty. It seems that when the getCache() method is called the content of the file are deleted for some reason.
Here is my class
<?php
namespace ICWS;
use \ICWS\showVar;
use \DirectoryIterator;
/**
* CacheHandler
*
* #package ICWS
*/
class CacheHandler {
/* Max time second allowed to keep a session File */
private $maxTimeAllowed = 300;
private $filename = '';
private $handle;
public function __construct( $filename ){
$this->filename = $filename;
// 5% chance to collect garbage when the class is initialized.
if(rand(1, 100) <= 5){
$this->collectGarbage();
}
}
/**
* Add/Update the cached array in the session
*
* #param string $key
* #param bigint $id
* #param array $field
* #return void
*/
public function updateCache($key, $id, array $field)
{
$currentVal = (object) $field;
$this->openFile();
$this->lockFile();
$storage = $this->readFile();
//create a new if the $id does not exists in the cache
if( isset($storage[$key]) && array_key_exists($id, $storage[$key]) ){
$currentVal = (object) $storage[$key][$id];
foreach($field as $k => $v){
$currentVal->$k = $v; //update existing key or add a new one
}
}
$storage[$key][$id] = $currentVal;
new showVar($storage);
$this->updateFile($storage);
$this->unlockFile();
$this->closeFile();
}
/**
* gets the current cache/session for a giving $key
*
* #param string $key
* #return object or boolean
*/
public function getCache($key)
{
$value = false;
$this->openFile();
$storage = $this->readFile();
if(isset($storage[$key])){
$value = $storage[$key];
}
$this->closeFile();
return $value;
}
/**
* removes the $id from the cache/session
*
* #param string $key
* #param bigint $id
* #return boolean
*/
public function removeCache($key, $id)
{
$this->openFile();
$this->lockFile();
$storage = $this->readFile();
if( !isset($storage[$key][$id])){
$this->unlockFile();
$this->closeFile();
return false;
}
unset($storage[$key][$id]);
$this->updateFile($storage);
$this->unlockFile();
$this->closeFile();
return true;
}
/**
* unset a session
*
* #param argument $keys
* #return void
*/
public function truncateCache()
{
if(file_exists($this->filename)){
unlink($this->filename);
}
}
/**
* Open a file in a giving mode
*
* #params string $mode
* #return void
*/
private function openFile( $mode = "w+"){
$this->handle = fopen($this->filename, $mode);
if(!$this->handle){
throw new exception('The File could not be opened!');
}
}
/**
* Close the file
* #return void
*/
private function closeFile(){
fclose($this->handle);
$this->handle = null;
}
/**
* Update the file with array data
* #param array $data the array in that has the data
* #return void
*/
private function updateFile(array $data = array() ){
$raw = serialize($data);
fwrite($this->handle, $raw);
}
/**
* Read the data from the opned file
*
* #params string $mode
* #return array of the data found in the file
*/
private function readFile() {
$length = filesize($this->filename);
if($length > 0){
rewind($this->handle);
$raw = fread($this->handle, filesize($this->filename));
return unserialize($raw);
}
return array();
}
/**
* Lock the file
*
* #return void
*/
private function lockFile( $maxAttempts = 100, $lockType = LOCK_EX) {
$i = 0;
while($i <= $maxAttempts){
// acquire an exclusive lock
if( flock($this->handle, LOCK_EX) ){
break;
}
++$i;
}
}
/**
* Unlock the file
*
* #return void
*/
private function unlockFile() {
fflush($this->handle);
flock($this->handle, LOCK_UN);
}
/**
* Remove add cache files
*
* #return void
*/
private function collectGarbage(){
$mydir = dirname($this->filename);
$dir = new DirectoryIterator( $mydir );
$time = strtotime("now");
foreach ($dir as $file) {
if ( !$file->isDot()
&& $file->isFile()
&& ($time - $file->getATime()) > $this->maxTimeAllowed
&& isset($file->getFilename) && $file->getFilename != 'index.html'
) {
unlink($file->getPathName());
}
}
}
}
This is how I call my class
<?php
require 'autoloader.php';
try {
$cache = new \ICWS\CacheHandler('cache/12300000');
$field = array('name' => 'Mike A', 'Address' => '123 S Main', 'phone' => '2152456245', 'ext' => 123);
$cache->updateCache('NewKey', '123456', $field);
echo '<pre>';
print_r($cache->getCache('NewKey'));
echo '</pre>';
} catch (exception $e){
echo $e->getMessage();
}
?>
When commenting the line print_r($cache->getCache('NewKey')); The file 12300000 will have the data below as expected
a:1:{s:6:"NewKey";a:1:{i:123456;O:8:"stdClass":4:{s:4:"name";s:6:"Mike A";s:7:"Address";s:10:"123 S Main";s:5:"phone";s:10:"2152456245";s:3:"ext";i:123;}}}
However, when the method print_r($cache->getCache('NewKey')); is called the file becomes empty.
What am I doing wrong here? Why is my print_r($cache->getCache('NewKey')); method is emptying out the file?
openFile($mode = "w+")
The documentation for fopen() says:
'w+' Open for reading and writing; place the file pointer at the beginning of the file and truncate the file to zero length. If the file does not exist, attempt to create it.
So it resets the file, deleting everything in it.
Probably you want mode a+:
'a+' Open for reading and writing; place the file pointer at the end of the file. If the file does not exist, attempt to create it. In this mode, fseek()() only affects the reading position, writes are always appended.

Access array using dynamic path

I have an issue in accessing the array in php.
$path = "['a']['b']['c']";
$value = $array.$path;
In the above piece of code I have an multidimensional array named $array.
$path is a dynamic value which I would get from database.
Now I want to retrieve the value from $array using $path but I am not able to.
$value = $array.$path
returns me
Array['a']['b']['c']
rather than the value.
I hope I have explained my question properly.
You have two options. First (evil) if to use eval() function - i.e. interpret your string as code.
Second is to parse your path. That will be:
//$path = "['a']['b']['c']";
preg_match_all("/\['(.*?)'\]/", $path, $rgMatches);
$rgResult = $array;
foreach($rgMatches[1] as $sPath)
{
$rgResult=$rgResult[$sPath];
}
The Kohana framework "Arr" class (API) has a method (Arr::path) that does something similar to what you are requesting. It simply takes an array and a path (with a . as delimiter) and returns the value if found. You could modify this method to suit your needs.
public static function path($array, $path, $default = NULL, $delimiter = NULL)
{
if ( ! Arr::is_array($array))
{
// This is not an array!
return $default;
}
if (is_array($path))
{
// The path has already been separated into keys
$keys = $path;
}
else
{
if (array_key_exists($path, $array))
{
// No need to do extra processing
return $array[$path];
}
if ($delimiter === NULL)
{
// Use the default delimiter
$delimiter = Arr::$delimiter;
}
// Remove starting delimiters and spaces
$path = ltrim($path, "{$delimiter} ");
// Remove ending delimiters, spaces, and wildcards
$path = rtrim($path, "{$delimiter} *");
// Split the keys by delimiter
$keys = explode($delimiter, $path);
}
do
{
$key = array_shift($keys);
if (ctype_digit($key))
{
// Make the key an integer
$key = (int) $key;
}
if (isset($array[$key]))
{
if ($keys)
{
if (Arr::is_array($array[$key]))
{
// Dig down into the next part of the path
$array = $array[$key];
}
else
{
// Unable to dig deeper
break;
}
}
else
{
// Found the path requested
return $array[$key];
}
}
elseif ($key === '*')
{
// Handle wildcards
$values = array();
foreach ($array as $arr)
{
if ($value = Arr::path($arr, implode('.', $keys)))
{
$values[] = $value;
}
}
if ($values)
{
// Found the values requested
return $values;
}
else
{
// Unable to dig deeper
break;
}
}
else
{
// Unable to dig deeper
break;
}
}
while ($keys);
// Unable to find the value requested
return $default;
}
I was hoping to find an elegant solution to nested array access without throwing undefined index errors, and this post hits high on google. I'm late to the party, but I wanted to weigh in for future visitors.
A simple isset($array['a']['b']['c'] can safely check nested values, but you need to know the elements to access ahead of time. I like the dot notation for accessing multidimensional arrays, so I wrote a class of my own. It does require PHP 5.6.
This class parses a string path written in dot-notation and safely accesses the nested values of the array or array-like object (implements ArrayAccess). It will return the value or NULL if not set.
use ArrayAccess;
class SafeArrayGetter implements \JsonSerializable {
/**
* #var array
*/
private $data;
/**
* SafeArrayGetter constructor.
*
* #param array $data
*/
public function __construct( array $data )
{
$this->data = $data;
}
/**
* #param array $target
* #param array ...$indices
*
* #return array|mixed|null
*/
protected function safeGet( array $target, ...$indices )
{
$movingTarget = $target;
foreach ( $indices as $index )
{
$isArray = is_array( $movingTarget ) || $movingTarget instanceof ArrayAccess;
if ( ! $isArray || ! isset( $movingTarget[ $index ] ) ) return NULL;
$movingTarget = $movingTarget[ $index ];
}
return $movingTarget;
}
/**
* #param array ...$keys
*
* #return array|mixed|null
*/
public function getKeys( ...$keys )
{
return static::safeGet( $this->data, ...$keys );
}
/**
* <p>Access nested array index values by providing a dot notation access string.</p>
* <p>Example: $safeArrayGetter->get('customer.paymentInfo.ccToken') ==
* $array['customer']['paymentInfo']['ccToken']</p>
*
* #param $accessString
*
* #return array|mixed|null
*/
public function get( $accessString )
{
$keys = $this->parseDotNotation( $accessString );
return $this->getKeys( ...$keys );
}
/**
* #param $string
*
* #return array
*/
protected function parseDotNotation( $string )
{
return explode( '.', strval( $string ) );
}
/**
* #return array
*/
public function toArray()
{
return $this->data;
}
/**
* #param int $options
* #param int $depth
*
* #return string
*/
public function toJson( $options = 0, $depth = 512 )
{
return json_encode( $this, $options, $depth );
}
/**
* #param array $data
*
* #return static
*/
public static function newFromArray( array $data )
{
return new static( $data );
}
/**
* #param \stdClass $data
*
* #return static
*/
public static function newFromObject( \stdClass $data )
{
return new static( json_decode( json_encode( $data ), TRUE ) );
}
/**
* Specify data which should be serialized to JSON
* #link http://php.net/manual/en/jsonserializable.jsonserialize.php
* #return array data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* #since 5.4.0
*/
function jsonSerialize()
{
return $this->toArray();
}
}

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