<?php
/*
* This file includes:
*
* 1. "PHP SVN CLIENT" class
* Copyright (C) 2007-2008 by Sixdegrees <cesar@sixdegrees.com.br>
* Cesar D. Rodas
* https://code.google.com/archive/p/phpsvnclient/
* License: BSD License
* CHANGES by Daniel Marschall, ViaThinkSoft in 2019:
* - The class has been customized and contains specific changes for the software "OIDplus"
* - Functions which are not used in the "SVN checkout" were removed.
* The only important functions are getVersion() and updateWorkingCopy()
* - The dependency class xml2array was converted from a class into a function and
* included into this class
* - Added "revision log/comment" functionality
*
* 2. "xml2array" class
* Taken from http://www.php.net/manual/en/function.xml-parse.php#52567
* Modified by Martin Guppy <http://www.deadpan110.com/>
* CHANGES by Daniel Marschall, ViaThinkSoft in 2019:
* - Converted class into a single function and added that function into the phpsvnclient class
*/
// TODO: Translate (OIDplus _L function)
/**
* PHP SVN CLIENT
*
* This class is an SVN client. It can perform read operations
* to an SVN server (over Web-DAV).
* It can get directory files, file contents, logs. All the operaration
* could be done for a specific version or for the last version.
*
* @author Cesar D. Rodas <cesar@sixdegrees.com.br>
* @license BSD License
*/
class phpsvnclient {
/*protected*/ const PHPSVN_NORMAL_REQUEST = '<?xml version="1.0" encoding="utf-8"?><propfind xmlns="DAV:"><prop><getlastmodified xmlns="DAV:"/> <checked-in xmlns="DAV:"/><version-name xmlns="DAV:"/><version-controlled-configuration xmlns="DAV:"/><resourcetype xmlns="DAV:"/><baseline-relative-path xmlns="http://subversion.tigris.org/xmlns/dav/"/><repository-uuid xmlns="http://subversion.tigris.org/xmlns/dav/"/></prop></propfind>';
/*protected*/ const PHPSVN_VERSION_REQUEST = '<?xml version="1.0" encoding="utf-8"?><propfind xmlns="DAV:"><prop><checked-in xmlns="DAV:"/></prop></propfind>';
/*protected*/ const PHPSVN_LOGS_REQUEST = '<?xml version="1.0" encoding="utf-8"?> <S:log-report xmlns:S="svn:"> <S:start-revision>%d</S:start-revision><S:end-revision>%d</S:end-revision><S:path></S:path><S:discover-changed-paths/></S:log-report>';
/*protected*/ const NO_ERROR = 1;
/*protected*/ const NOT_FOUND = 2;
/*protected*/ const AUTH_REQUIRED = 3;
/*protected*/ const UNKNOWN_ERROR = 4;
/**
* SVN Repository URL
*
* @var string
* @access private
*/
private $_url;
/**
* HTTP Client object
*
* @var object
* @access private
*/
private $_http;
/**
* Respository Version.
*
* @access private
* @var int
*/
private $_repVersion;
/**
* Last error number
*
* Possible values are NOT_ERROR, NOT_FOUND, AUTH_REQUIRED, UNKOWN_ERROR
*
* @access public
* @var integer
*/
public $errNro;
/**
* Number of actual revision local repository.
* @var Integer, Long
*/
private $actVersion;
private $storeDirectoryFiles = array();
private $lastDirectoryFiles;
private $file_size;
private $file_size_founded = false;
public function __construct($url)
{
$http =& $this->_http;
$http = new http_class;
$http->user_agent = "phpsvnclient (https://code.google.com/archive/p/phpsvnclient/)";
$this->_url = $url;
$this->actVersion = $this->getVersion();
}
/**
* Function for creating directories.
* @param $path (string) The path to the directory that will be created.
*/
private function createDirs($path)
{
foreach ($dirs as $dir) {
if ($dir != "") {
}
}
}
/**
* Function for the recursive removal of directories.
* @param $path (string) The path to the directory to be deleted.
* @return (string) Returns the status of a function or function rmdir unlink.
*/
private function removeDirs($path)
{
if ($entries === false) {
}
foreach ($entries as $entry) {
if ($entry != '.' && $entry != '..') {
$this->removeDirs($path . '/' . $entry);
}
}
} else {
}
}
/**
* Public Functions
*/
/**
* Updates a working copy
* @param $from_revision (string) Either a revision number or a text file with the
* contents "Revision ..." (if it is a file,
* the file revision will be updated if everything
* was successful)
* @param $folder (string) SVN remote folder
* @param $outpath (string) Local path of the working copy
* @param $preview (bool) Only simulate, do not write to files
**/
public function updateWorkingCopy($from_revision='version.txt', $folder = '/trunk/', $outPath = '.', $preview = false)
{
echo "ERROR: Local path $outPath not existing\n";
return false;
}
$version_file = $from_revision;
$from_revision = -1;
echo "ERROR: $version_file missing\n";
return false;
} else {
//Obtain the number of current version number of the local copy.
echo "ERROR: $version_file unknown format\n";
return false;
}
$from_revision = $m[1];
echo "Found $version_file with revision information $from_revision\n";
}
} else {
$version_file = '';
}
$errors_happened = false;
if ($webbrowser_update) {
// First, do some read/write test (even if we are in preview mode, because we want to detect errors before it is too late)
$file = $outPath . '/dummy_'.uniqid().'.tmp';
echo (!$preview ? "ERROR" : "WARNING").": Cannot write test file $file ! An update through the web browser will NOT be possible.\n";
if (!$preview) return false;
}
echo (!$preview ? "ERROR" : "WARNING").": Cannot delete test file $file ! An update through the web browser will NOT be possible.\n";
if (!$preview) return false;
}
}
//Get a list of objects to be updated.
$objects_list = $this->getLogsForUpdate($folder, $from_revision + 1);
// Output version information
foreach ($objects_list['revisions'] as $revision) {
$comment = empty($revision['comment']) ?
'No comment' : $revision['comment'];
$tex = "New revision ".$revision['versionName']." by ".$revision['creator']." (".date('Y-m-d H:i:s', strtotime($revision['date'])).") ";
echo "\n";
}
// Add dirs
sort($objects_list['dirs']); // <-- added by Daniel Marschall: Sort folder list, so that directories will be created in the correct hierarchical order
foreach ($objects_list['dirs'] as $file) {
if ($file != '') {
$localPath = rtrim($outPath,DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($localPath,DIRECTORY_SEPARATOR);
echo "Added or modified directory: $file\n";
if (!$preview) {
$this->createDirs($localPath);
$errors_happened = true;
echo "=> FAILED\n";
}
}
}
}
// Add files
sort($objects_list['files']); // <-- added by Daniel Marschall: Sort list, just for cosmetic improvement
foreach ($objects_list['files'] as $file) {
if ($file != '') {
$localFile = rtrim($outPath,DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($localFile,DIRECTORY_SEPARATOR);
echo "Added or modified file: $file\n";
if (!$preview) {
$contents = $this->getFile($file);
$errors_happened = true;
echo "=> FAILED\n";
}
}
}
}
// Remove files
sort($objects_list['filesDelete']); // <-- added by Daniel Marschall: Sort list, just for cosmetic improvement
foreach ($objects_list['filesDelete'] as $file) {
if ($file != '') {
$localFile = rtrim($outPath,DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($localFile,DIRECTORY_SEPARATOR);
echo "Removed file: $file\n";
if (!$preview) {
$errors_happened = true;
echo "=> FAILED\n";
}
}
}
}
// Remove dirs
// Changed by Daniel Marschall: moved this to the end, because "add/update" requests for this directory might happen before the directory gets removed
rsort($objects_list['dirsDelete']); // <-- added by Daniel Marschall: Sort list in reverse order, so that directories get deleted in the correct hierarchical order
foreach ($objects_list['dirsDelete'] as $file) {
if ($file != '') {
$localPath = rtrim($outPath,DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($localPath,DIRECTORY_SEPARATOR);
echo "Removed directory: $file\n";
if (!$preview) {
$this->removeDirs($localPath);
$errors_happened = true;
echo "=> FAILED\n";
}
}
}
}
// Update version file
// Changed by Daniel Marschall: Added $errors_happened
if (!$preview && !empty($version_file)) {
if (!$errors_happened) {
if (@file_put_contents($version_file, "Revision " . $this->actVersion . "\n") === false) {
echo "ERROR: Could not set the revision\n";
return false;
} else {
echo "Set revision to " . $this->actVersion . "\n";
return true;
}
} else {
echo "Revision NOT set to " . $this->actVersion . " because some files/dirs could not be updated. Please try again.\n";
return false;
}
} else {
return true;
}
}
}
/**
* rawDirectoryDump
*
* Dumps SVN data for $folder in the version $version of the repository.
*
* @param string $folder Folder to get data
* @param integer $version Repository version, -1 means actual
* @return array SVN data dump.
*/
private function rawDirectoryDump($folder = '/trunk/', $version = -1)
{
if ($version == -1 || $version > $this->actVersion) {
$version = $this->actVersion;
}
$url = $this->cleanURL($this->_url . "/!svn/bc/" . $version . "/" . $folder . "/");
$this->initQuery($args, "PROPFIND", $url);
$args['Body'] = self::PHPSVN_NORMAL_REQUEST;
$args['Headers']['Content-Length'] = strlen(self::PHPSVN_NORMAL_REQUEST);
$body = '';
if (!$this->Request($args, $headers, $body))
throw new OIDplusException("Cannot get rawDirectoryDump (Request failed)");
return self::xmlParse($body);
}
/**
* getDirectoryFiles
*
* Returns all the files in $folder in the version $version of
* the repository.
*
* @param string $folder Folder to get files
* @param integer $version Repository version, -1 means actual
* @return array List of files.
*/
private function getDirectoryFiles($folder = '/trunk/', $version = -1)
{
if ($arrOutput = $this->rawDirectoryDump($folder, $version)) {
foreach ($arrOutput['children'] as $key => $value) {
function ($item, $key) {
if ($key == 'name') {
if (($item == 'D:HREF') || ($item == 'LP1:GETLASTMODIFIED') || ($item == 'LP1:VERSION-NAME') || ($item == 'LP2:BASELINE-RELATIVE-PATH') || ($item == 'LP3:BASELINE-RELATIVE-PATH') || ($item == 'D:STATUS')) {
$this->lastDirectoryFiles = $item;
}
} elseif (($key == 'tagData') && ($this->lastDirectoryFiles != '')) {
// Unsure if the 1st of two D:HREF's always returns the result we want, but for now...
if (($this->lastDirectoryFiles == 'D:HREF') && (isset($this->storeDirectoryFiles['type'])))
return;
// Dump into the array
switch ($this->lastDirectoryFiles) {
case 'D:HREF':
$var = 'type';
break;
case 'LP1:VERSION-NAME':
$var = 'version';
break;
case 'LP1:GETLASTMODIFIED':
$var = 'last-mod';
break;
case 'LP2:BASELINE-RELATIVE-PATH':
case 'LP3:BASELINE-RELATIVE-PATH':
$var = 'path';
break;
case 'D:STATUS':
$var = 'status';
break;
}
$this->storeDirectoryFiles[$var] = $item;
$this->lastDirectoryFiles = '';
// Detect 'type' as either a 'directory' or 'file'
if ((isset($this->storeDirectoryFiles['type'])) && (isset($this->storeDirectoryFiles['last-mod'])) && (isset($this->storeDirectoryFiles['path'])) && (isset($this->storeDirectoryFiles['status']))) {
$this->storeDirectoryFiles['path'] = str_replace(' ', '%20', $this->storeDirectoryFiles['path']); //Hack to make filenames with spaces work.
$len = strlen($this->storeDirectoryFiles['path']);
if (substr($this->storeDirectoryFiles['type'], strlen($this->storeDirectoryFiles['type']) - $len) == $this->storeDirectoryFiles['path']) {
$this->storeDirectoryFiles['type'] = 'file';
} else {
$this->storeDirectoryFiles['type'] = 'directory';
}
}
} else {
$this->lastDirectoryFiles = '';
}
}
);
unset($this->storeDirectoryFiles);
}
return $files;
}
return false;
}
private static function dirToArray($dir, &$result) {
foreach ($cdir as $key => $value) {
if (is_dir($dir . DIRECTORY_SEPARATOR . $value)) {
$result[] = $dir.DIRECTORY_SEPARATOR.$value.DIRECTORY_SEPARATOR;
self::dirToArray($dir.DIRECTORY_SEPARATOR.$value, $result);
} else {
$result[] = $dir.DIRECTORY_SEPARATOR.$value;
}
}
}
}
public function compareToDirectory($local_folder, $svn_folder='/trunk/', $version=-1) {
self::dirToArray($local_folder, $local_cont);
foreach ($local_cont as $key => &$c) {
if ($c === '') unset($local_cont[$key]);
if (strpos($c,'.svn/') === 0) unset($local_cont[$key]);
if ((strpos($c,'userdata/') === 0) && ($c !== 'userdata/info.txt') && ($c !== 'userdata/.htaccess') && ($c !== 'userdata/index.html') && (substr($c,-1) !== '/')) unset($local_cont[$key]);
}
$contents = $this->getDirectoryTree($svn_folder, $version, true);
foreach ($contents as $cont) {
if ($cont['type'] == 'directory') {
$svn_cont[] = '/'.urldecode($cont['path']).'/';
} else if ($cont['type'] == 'file') {
}
}
foreach ($svn_cont as $key => &$c) {
if ($c === '') unset($svn_cont[$key]);
if ((strpos($c,'userdata/') === 0) && ($c !== 'userdata/info.txt') && ($c !== 'userdata/.htaccess') && ($c !== 'userdata/index.html') && (substr($c,-1) !== '/')) unset($svn_cont[$key]);
}
return array($svn_cont, $local_cont);
}
/**
* getDirectoryTree
*
* Returns the complete tree of files and directories in $folder from the
* version $version of the repository. Can also be used to get the info
* for a single file or directory.
*
* @param string $folder Folder to get tree
* @param integer $version Repository version, -1 means current
* @param boolean $recursive Whether to get the tree recursively, or just
* the specified directory/file.
*
* @return array List of files and directories.
*/
private function getDirectoryTree($folder = '/trunk/', $version = -1, $recursive = true)
{
$directoryTree = array();
if (!($arrOutput = $this->getDirectoryFiles($folder, $version)))
return false;
if (!$recursive)
return $arrOutput[0];
if (trim($array['path'], '/') == trim($folder, '/'))
continue;
if ($array['type'] == 'directory') {
$walk = $this->getDirectoryFiles($array['path'], $version);
foreach ($walk as $step) {
}
}
}
return $directoryTree;
}
/**
* Returns file contents
*
* @param string $file File pathname
* @param integer $version File Version
* @return string File content and information, false on error, or if a
* directory is requested
*/
private function getFile($file, $version = -1)
{
if ($version == -1 || $version > $this->actVersion) {
$version = $this->actVersion;
}
// check if this is a directory... if so, return false, otherwise we
// get the HTML output of the directory listing from the SVN server.
// This is maybe a bit heavy since it makes another connection to the
// SVN server. Maybe add this as an option/parameter? ES 23/06/08
$fileInfo = $this->getDirectoryTree($file, $version, false);
if ($fileInfo["type"] == "directory")
return false;
$url = $this->cleanURL($this->_url . "/!svn/bc/" . $version . "/" . $file . "/");
$this->initQuery($args, "GET", $url);
$body = '';
if (!$this->Request($args, $headers, $body))
throw new OIDplusException("Cannot call getFile (Request failed)");
return $body;
}
private function getLogsForUpdate($file, $vini = 0, $vend = -1)
{
if ($vend == -1) {
$vend = $this->actVersion;
}
if ($vini < 0)
$vini = 0;
if ($vini > $vend) {
$vini = $vend;
echo "Nothing updated\n";
return null;
}
$url = $this->cleanURL($this->_url . "/!svn/bc/" . $this->actVersion . "/" . $file . "/");
$this->initQuery($args, "REPORT", $url);
$args['Body'] = sprintf(self::PHPSVN_LOGS_REQUEST, $vini, $vend);
$args['Headers']['Content-Length'] = strlen($args['Body']);
$args['Headers']['Depth'] = 1;
$body = '';
if (!$this->Request($args, $headers, $body))
throw new OIDplusException("Cannot call getLogsForUpdate (Request failed)");
$arrOutput = self::xmlParse($body);
if (!isset($arrOutput['children'])) $arrOutput['children'] = array();
foreach ($arrOutput['children'] as $value) {
/*
<S:log-item>
<D:version-name>164</D:version-name>
<S:date>2019-08-13T13:12:13.915920Z</S:date>
<D:comment>Update assistant bugfix</D:comment>
<D:creator-displayname>daniel-marschall</D:creator-displayname>
<S:modified-path node-kind="file" text-mods="true" prop-mods="false">/trunk/update/index.php</S:modified-path>
<S:modified-path node-kind="file" text-mods="true" prop-mods="false">/trunk/update/phpsvnclient.inc.php</S:modified-path>
</S:log-item>
*/
$versionName = '';
$date = '';
$comment = '';
foreach ($value['children'] as $entry) {
if (($entry['name'] == 'S:ADDED-PATH') || ($entry['name'] == 'S:MODIFIED-PATH') || ($entry['name'] == 'S:DELETED-PATH')) {
if ($entry['attrs']['NODE-KIND'] == "file") {
$array['objects'][] = array(
'object_name' => $entry['tagData'],
'action' => $entry['name'],
'type' => 'file'
);
} else if ($entry['attrs']['NODE-KIND'] == "dir") {
$array['objects'][] = array(
'object_name' => $entry['tagData'],
'action' => $entry['name'],
'type' => 'dir'
);
}
} else if ($entry['name'] == 'D:VERSION-NAME') {
$versionName = isset($entry['tagData']) ?
$entry['tagData'] : '';
} else if ($entry['name'] == 'S:DATE') {
$date = isset($entry['tagData']) ?
$entry['tagData'] : '';
} else if ($entry['name'] == 'D:COMMENT') {
$comment = isset($entry['tagData']) ?
$entry['tagData'] : '';
} else if ($entry['name'] == 'D:CREATOR-DISPLAYNAME') {
$creator = isset($entry['tagData']) ?
$entry['tagData'] : '';
}
}
$revlogs[] = array('versionName' => $versionName,
'date' => $date,
'comment' => $comment,
'creator' => $creator);
}
if (!isset($array['objects'])) $array['objects'] = array();
foreach ($array['objects'] as $objects) {
// This section was completely changed by Daniel Marschall
if ($objects['type'] == "file") {
if ($objects['action'] == "S:ADDED-PATH" || $objects['action'] == "S:MODIFIED-PATH") {
self::xarray_add($objects['object_name'], $files);
self::xarray_remove($objects['object_name'], $filesDelete);
}
if ($objects['action'] == "S:DELETED-PATH") {
self::xarray_add($objects['object_name'], $filesDelete);
self::xarray_remove($objects['object_name'], $files);
}
}
if ($objects['type'] == "dir") {
if ($objects['action'] == "S:ADDED-PATH") {
self::xarray_add($objects['object_name'], $dirs);
self::xarray_add($objects['object_name'], $dirsNew);
self::xarray_remove($objects['object_name'], $dirsDelete);
}
if ($objects['action'] == "S:MODIFIED-PATH") {
self::xarray_add($objects['object_name'], $dirs);
self::xarray_add($objects['object_name'], $dirsMod);
self::xarray_remove($objects['object_name'], $dirsDelete);
}
if ($objects['action'] == "S:DELETED-PATH") {
// Delete files from filelist
$files_copy = $files;
foreach ($files_copy as $file) {
if (strpos($file, $objects['object_name'].'/') === 0) self::xarray_remove($file, $files);
}
// END OF Delete files from filelist
// Delete dirs from dirslist
self::xarray_add($objects['object_name'], $dirsDelete);
self::xarray_remove($objects['object_name'], $dirs);
self::xarray_remove($objects['object_name'], $dirsMod);
self::xarray_remove($objects['object_name'], $dirsNew);
// END OF Delete dirs from dirslist
}
}
}
foreach ($dirsNew as $dir) {
// For new directories, also download all its contents
try {
$contents = $this->getDirectoryTree($dir, $vend, true);
} catch (Exception $e) {
// This can happen when you update from a very old version and a directory was new which is not existing in the newest ($vend) version
// In this case, we don't need it and can ignore the error
}
foreach ($contents as $cont) {
if ($cont['type'] == 'directory') {
self::xarray_add($dirname, $dirs);
self::xarray_remove($dirname, $dirsDelete);
} else if ($cont['type'] == 'file') {
self::xarray_add($filename, $files);
self::xarray_remove($filename, $filesDelete);
}
}
}
$out['files'] = $files;
$out['filesDelete'] = $filesDelete;
$out['dirs'] = $dirs;
$out['dirsDelete'] = $dirsDelete;
$out['revisions'] = $revlogs;
return $out;
}
/**
* Returns the repository version
*
* @return integer Repository version
* @access public
*/
public function getVersion()
{
if ($this->_repVersion > 0)
return $this->_repVersion;
$this->_repVersion = -1;
$this->initQuery($args, "PROPFIND", $this->cleanURL($this->_url . "/!svn/vcc/default"));
$args['Body'] = self::PHPSVN_VERSION_REQUEST;
$args['Headers']['Content-Length'] = strlen(self::PHPSVN_NORMAL_REQUEST);
$args['Headers']['Depth'] = 0;
$body = '';
if (!$this->Request($args, $tmp, $body))
throw new OIDplusException("Cannot get repository revision (Request failed)");
$this->_repVersion = null;
if (preg_match('@/(\d+)\s*</D:href>@ismU', $body, $m)) {
$this->_repVersion = $m[1];
} else {
throw new OIDplusException("Cannot get repository revision (RegEx failed)");
}
return $this->_repVersion;
}
/**
* Private Functions
*/
/**
* Prepare HTTP CLIENT object
*
* @param array &$arguments Byreferences variable.
* @param string $method Method for the request (GET,POST,PROPFIND, REPORT,ETC).
* @param string $url URL for the action.
* @access private
*/
private function initQuery(&$arguments, $method, $url)
{
$http =& $this->_http;
$http->GetRequestArguments($url, $arguments);
$arguments["RequestMethod"] = $method;
$arguments["Headers"]["Content-Type"] = "text/xml";
$arguments["Headers"]["Depth"] = 1;
}
/**
* Open a connection, send request, read header
* and body.
*
* @param Array $args Connetion's argument
* @param Array &$headers Array with the header response.
* @param string &$body Body response.
* @return boolean True is query success
* @access private
*/
private function Request($args, &$headers, &$body)
{
$args['RequestURI'] = str_replace(' ', '%20', $args['RequestURI']); //Hack to make filenames with spaces work.
$http =& $this->_http;
$http->Open($args);
$http->SendRequest($args);
$http->ReadReplyHeaders($headers);
if ($http->response_status[0] != 2) {
switch ($http->response_status) {
case 404:
$this->errNro = self::NOT_FOUND;
break;
case 401:
$this->errNro = self::AUTH_REQUIRED;
break;
default:
$this->errNro = self::UNKNOWN_ERROR;
break;
}
// trigger_error("request to $args[RequestURI] failed: $http->response_status
//Error: $http->error");
$http->close();
return false;
}
$this->errNro = self::NO_ERROR;
$body = '';
$tbody = '';
for (;;) {
$error = $http->ReadReplyBody($tbody, 1000);
if ($error != "" || strlen($tbody) == 0) {
break;
}
$body .= ($tbody);
}
$http->close();
return true;
}
/**
* Returns $url stripped of '//'
*
* Delete "//" on URL requests.
*
* @param string $url URL
* @return string New cleaned URL.
* @access private
*/
private function cleanURL($url)
{
}
/*
Taken from http://www.php.net/manual/en/function.xml-parse.php#52567
Modified by Martin Guppy <http://www.deadpan110.com/>
Usage
Grab some XML data, either from a file, URL, etc. however you want.
Assume storage in $strYourXML;
Converted "class" into a single function by Daniel Marschall, ViaThinkSoft
*/
private static function xmlParse($strInputXML) {
function /*tagOpen*/($parser, $name, $attrs) use (&$arrOutput) {
$tag = array("name" => $name, "attrs" => $attrs);
},
function /*tagClosed*/($parser, $name) use (&$arrOutput) {
$arrOutput[count($arrOutput) - 2]['children'][] = $arrOutput[count($arrOutput) - 1];
}
);
function /*tagData*/($parser, $tagData) use (&$arrOutput) {
if (isset($arrOutput[count($arrOutput) - 1]['tagData'])) {
$arrOutput[count($arrOutput) - 1]['tagData'] .= $tagData;
} else {
$arrOutput[count($arrOutput) - 1]['tagData'] = $tagData;
}
}
}
);
}
return $arrOutput[0];
}
/*
Small helper functions
*/
private static function xarray_add($needle, &$array) {
if ($key === false) {
$array[] = $needle;
}
}
private static function xarray_remove($needle, &$array) {
while (true) {
if ($key === false) break;
}
}
}