6/12/2019

Populate Fritz Adressbuch from owncloud/nextcloud or any other vcard source

I am a heavy user of Fritz!Fax . I know this is an antique technology, but there a still some good reasons (legal) for using it.

Fax numbers can be used from Fritz Adressbuch, but I hate manually typing in fax numbers - you may call me lazy :-)

But I like coding, so I created a small command line application that can import my fax numbers from my owncloud address book.

Running this from time to time (or even on a schedule) ensures that I have the latest fax numbers from all my contacts at hand when I need them.

If you would like to use the tool, help yourself to it here. It requires .NET framework 4.6.1 or later. I do not provide an installer for such a simple tool, just download, extract, run.

When you run it without any parameters, it will show you an explanation how it works. No guarantees, but it works for me. You can use any vcard source (file or web) containing one or multiple vcards.

6/08/2019

Der Titel "Die Zerstörung der CDU" wird falsch interpretirert

Die Kommentare zu Rezos Video gehen merkwürdigerweise alle davon aus, dass der Titel des Videos dazu aufrufen soll, die CDU zu zerstören.

Rein grammatikalisch gibt der Titel diese Interpretation auch her.

Allerdings gibt es eine alternative Interpretation des Genitivs: es muss ja kein Genitivus Objectivus, sondern kann auch ein Genitivus Subjectivus sein, in welchem Fall die Interpretation lauten würde, dass es um die Zerstörung geht, die die CDU (unter anderem an unserer Umwelt) verübt.

Wer das Video gesehen hat, kann eigentlich nur zu letzterer Interpretation gelangen.

Aber natürlich brauchen Presse, Medien und Politiker Aufreger, und da passt die erste Interpretation besser ins Bild.

Ach und wenn ich schon beim Thema bin: Paul Ziemiak, CDU-Generalsekretär, spricht im Zusammenhang mit dem Video von "Pseudofakten". Ist das die deutsche Übersetzung von "fake news"? Ist Trump unter deutschen Politikern jetzt auch schon salonfähig geworden? Um es mit Trump zu sagen: "So sad!"

6/01/2019

Grandstream XML Address Book from OwnCloud

I have all my contacts stored in OwnCloud. Obviously it would be nice to have them available on my Grandstream phones.

I wrote the below script to read the contacts from OwnCloud (VCard format) and create an XML structure that Grandstream phones can process.

If you need something similar help yourself to the code below. I am sure it is far from perfect, but ir works for me. No warranty, of course.

If you want to use it, just replace the URL, username and password and put it on a web server runnung PHP.  Finally point your phone to the URL of the file and have it load your contacts.

This may or may not work with NextCloud too. My guess is that is will.

The nice thing about this approach is that it does not involve exporting and importing, it just grabs the latest information whenever you choose lo load the address book on your phone.

function geturl($url, $username, $password)
{
    $ch = curl_init();
    
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_HEADER, TRUE);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
    curl_setopt($ch, CURLOPT_VERBOSE, 0);
    curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
    curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
    $a = curl_exec($ch);


    if(false === $a)
    {
        var_dump(curl_error($ch), curl_errno($ch));
    }


    /*
    $info = curl_getinfo($ch);
    print_r($info)
    */
    
    return $a;
}


$data = geturl("https://owncloud.torabli.eu:444/remote.php/dav/addressbooks/users/kian/contacts/?export", "kian", "Omposele1");
//$data = fakeData();
//var_dump($data);


if(false === $data)
{
    echo 'No data';
    exit('No data');
}


header('Content-Type: application/xml; charset=utf-8');
echo '' . "\r\n";
echo '' . "\r\n";


$vcards = null;
preg_match_all("/BEGIN:VCARD.*?END:VCARD/s",
        $data, $vcards);
//echo "number of vcards: " . strval(count($vcards[0]));


foreach($vcards[0] as $vcard)
{
    processvcard($vcard);
}


function processvcard($vc)
{
    $vc = str_replace('\r\n', "\r\n", $vc);
    $names = null;
    preg_match_all('/^N:(.*?);(.*?);.*$/m',
        $vc, $names);
    $firstname = $names[2][0];
    $lastname = $names[1][0];


    $phones = null;
    preg_match_all('/^TEL;TYPE=(.*?)((,|;).*)?:(.*)\r?$/m',
        $vc, $phones);
    
    /*
    match first group using
    /^CATEGORIES:(.*?)(,.*)?$/m
    */
    
    foreach($phones[0] as $i => $phone)
    {
        $phonetype = strtolower($phones[1][$i]);
        $type = strtoupper(substr($phonetype, 0, 1));
        $phonenumber = my_replace($phones[4][$i]);
        
        if(in_array($phonetype, array("work", "cell", "home")) &&
            (trim($lastname) !== "" || trim($firstname) !== ""))
        {
            echo "\r\n";
            echo " $lastname ($type)
\r\n";
            echo " $firstname
\r\n";
            echo " \r\n";
            echo " $phonenumber
\r\n";
            echo " 1\r\n";
            echo "
\r\n";
            /*
            echo " \r\n";
            echo " Enter group ID here\r\n";
            echo "
\r\n";
            */
            echo "
\r\n";
        }
    }
}


echo '
' . "\r\n";

function my_replace($s)
{
    $r = str_replace("\n", '', $s);
    $r = str_replace("\r", '', $r);
    
    return $r;
}


?>


function fakeData()
{
return
'BEGIN:VCARD\r\n' .
'VERSION:3.0\r\n' .
'UID:02fe604a-ea63-49fb-8c28-47c37062cd2a\r\n' .
'FN:Heinz Maschke\r\n' .
'N:Maschke;Heinz;;;\r\n' .
'TEL;TYPE=work,pref:+49 8206 961993\r\n' .
'TEL;TYPE=fax,work:+49 8206 - 961994\r\n' .
'EMAIL;TYPE=home:bioexpress-hmaschke@t-online.de\r\n' .
'NOTE:Biokiste\r\n' .
'ADR;TYPE=work:;;Ulrichstr. 10 \n86492 Egling a.d. Paar;;;;\r\n' .
'LABEL;TYPE=work:Ulrichstr. 10 \n86492 Egling a.d. Paar\r\n' .
'PRODID:DAVdroid/1.0-beta (ez-vcard/0.9.3)\r\n' .
'REV:20170105T113243Z\r\n' .
'END:VCARD\r\n' .
'BEGIN:VCARD\r\n' .
'VERSION:4.0\r\n' .
'PRODID:+//IDN bitfire.at//DAVx5/2.2.3.1-ose ez-vcard/0.10.5\r\n' .
'UID:045fa2c0-03ce-47fe-a264-4bdd9301a193\r\n' .
'FN:Martin Schütz\r\n' .
'N:Schütz;Martin;;;\r\n' .
'TEL;TYPE=work:+49 151 12551804\r\n' .
'TEL;TYPE=cell:+49 176 72678207\r\n' .
'EMAIL;TYPE=home:schuetzmm@gmail.com\r\n' .
'URL;TYPE=x-homepage;VALUE=URI:http://www.google.com/profiles/10952715549372\r\n' .
' 9506703\r\n' .
'CATEGORIES:Family\r\n' .
'BDAY:--0301\r\n' .
'REV:20190302T104435Z\r\n' .
'END:VCARD\r\n' .
'BEGIN:VCARD\r\n' .
'VERSION:3.0\r\n' .
'UID:06011ab7-cdd1-4212-8339-eae8f77e1a7b\r\n' .
'FN:Crissi Schöpp\r\n' .
'N:Schöpp;Crissi;;;\r\n' .
'TEL;TYPE=home:+49 821 154562\r\n' .
'TEL;TYPE=cell:+49 1515 1256215\r\n' .
'NOTE:Mutter Magali\r\n' .
'PRODID:DAVdroid/1.0-beta (ez-vcard/0.9.3)\r\n' .
'REV:20170803T054608Z\r\n' .
'END:VCARD\r\n' .
'BEGIN:VCARD\r\n' .
'VERSION:3.0\r\n' .
'PRODID:DAVdroid/1.0-beta (ez-vcard/0.9.3)\r\n' .
'UID:0648bfc9-255d-4f7f-841e-be4c17ce12b3\r\n' .
'CATEGORIES:Egling,Weihnachtskarte\r\n' .
'FN:Familie Sießmeir\r\n' .
'N:Sießmeir;;;Familie;\r\n' .
'REV:2018-12-11T21:51:02Z\r\n' .
'TEL;TYPE=HOME:+49 (8206) 96 15 87\r\n' .
'ADR;TYPE=HOME:;;Hauptstr. 39;Egling 86492;;;\r\n' .
'NOTE:\nTanja\, Manni\r\n' .
'LABEL;TYPE=home:Hauptstr. 39\nEgling 86492\r\n' .
'END:VCARD\r\n'
;
}
?>

PHP Script to Merge Multiple XML Files

For the configuration of my IP phones (Grandstream) I needed a way to create a general configuration that yould apply to all phones, and more specific one pertaining to groups and individual phones.

The configuration is done via XML files, each phone pull a file with its MAC address in the name.

So I needed to merge the general XML with that of the group and that of the phone to create the final configuration for the phone.

Of course this could be done through a deployment management solution, but my approach gives me more flexibility, albeit also more manual work.

I was not able to find a program to merge XML files - I am sure there is one out there, but I couldn't find it, and I like this kind of work, so I created my own.

I usually would use C#, but my PBX does not run the .NET framework, but it does run PHP. I am sure that my preference of C# shows in my PHP code :-)

Anyway, if you are looking for a solution to merge XML files for whatever ends, you may copy the code below. No warranty, of course. The code and usage message are hopefully self-explaining.

It requires PHP 7.0 or later, because I like the type checking.


#!/usr/local/bin/php72


// convenience function
function findXpath(DOMDocument $dom, String $xpath) : DOMNodelist
{
$objXpath = new DOMXPath($dom);
return $objXpath->query($xpath);
}


/*
* Alternative to getNodePath.
* This one uses the ID column to uniquely identify nodes,
* if option is set.
*/
function getXpath(DOMNode $domNode) : String
{
switch(get_class($domNode))
{
case "DOMElement":
$idAttribute = $GLOBALS['idAttribute'];
if(strlen($idAttribute) > 0)
{
$attrValue = $domNode->getAttribute("$idAttribute");
}
$attr = ($attrValue !== "") ? "[@{$idAttribute}='{$attrValue}']" : "";
if(is_null($domNode->parentNode))
{
return "/" . $domNode->tagName . $attr;
}
else
{
return getXpath($domNode->parentNode) . "/" . $domNode->tagName . $attr;
}
break;
case "DOMDocument":
return "";
break;
case "DOMText":
return getXpath($domNode->parentNode) . "/text()";
break;
default:
echo "\nFAIL: getXPath class " . get_class($domNode) . " path " . $domNode->getNodePath() . "\n";
break;
}
}


/*
* Find a match for dom2Node, which belongs to dom2, in dom1
* A match is found, if the XPath of the node has exactly one
* match in both DOMs.
* Returns the matching node in dom1, or NULL if not match was found.
*/
function findMatch(DOMDocument $dom1, DOMDocument $dom2, DOMNode $dom2Node) : ?DOMNode
{
// todo: Jedes Element auf dem Pfad muss auf ID überprüft werden
$attrValue = "";
$idAttribute = $GLOBALS['idAttribute'];
if(strlen($idAttribute) > 0 && $dom2Node instanceof DOMElement)
{
$attrValue = $dom2Node->getAttribute($idAttribute);
}
$attr = ($attrValue !== "") ? "[@{$idAttribute}='{$attrValue}']" : "";
//$xpath = $dom2Element->getNodePath() . $attr;
// own solution using ID attribute if applicable
$xpath = getXpath($dom2Node);
$m1 = findXpath($dom1, $xpath);
$m2 = findXpath($dom2, $xpath);


if(count($m1) === 1 && count($m2) === 1 &&
$m1[0] !== null && $m2[0] !== null)
{
return $m1[0];
}
else
{
return null;
}
}


/*
* This is where the real work is done.
* Merges node addElement of DOM add into DOM dom.
* Manipulates dom.
*/
function merge(DOMDocument $dom, DOMDocument $add, DOMNode $addElement = null) : void
{
// starting point is the document itself
if(null === $addElement)
{
$addElement = $add->documentElement;
}


$classname = get_class($addElement);
//echo "Class {$classname}\n";
switch ($classname)
{
case ("DOMDocument"):
merge($dom, $add, $addElement->documentElement);
break;
case ("DOMElement"):
// do we have a delete attribute from options?
$delete = false;
$deleteAttribute = $GLOBALS['deleteAttribute'];
if(strlen($deleteAttribute) > 0 &&
$addElement->getAttribute($deleteAttribute) !== "")
{
$delete = true;
}
$xp = findMatch($dom, $add, $addElement);
if(null === $xp && !$delete)
{
// no match found, add whole element to dom
$node = findXpath($dom, getXpath($addElement->parentNode))[0];
$addElementClone = $dom->importNode($addElement, true);
$node->appendChild($addElementClone);
}
else // match found
{
if($delete)
{
$xp->parentNode->removeChild($xp);
}
else
{
// merge all children
for($i=0; $i < $addElement->childNodes->length; $i++)
{
$cn = $addElement->childNodes->item($i);
merge($dom, $add, $cn);
}
// merge all attributes
if ($addElement->hasAttributes())
{
foreach ($addElement->attributes as $attr)
{
merge($dom, $add, $attr);
}
}
}
}
break;
case ("DOMAttr"):
/*
* copy attribute to matching node in dom.
* there must be a match, since the element has already been matched.
* setting the attribute will create a new one or overwrite
* an existing one.
*/
$xp = findXpath($dom, getXpath($addElement->ownerElement))[0];
$xp->setAttribute($addElement->name, $addElement->value);
break;
case ("DOMNodeList"):
// calculated field, hence we store the value for performane
$n = $addElement->length;
//echo "dom node list has {$n} items\n";
for ($i = 0; $i < $n; $i++)
{
//echo "dom node list item #{$i}\n";
merge($dom, $add, $addElement->item($i));
}
break;
case ("DOMText"):
/*
* empty text will not be merged.
* if no match is found, the whole node will be copied over,
* otherwise we create a new text node and replace the old one,
* because you cannot set the text in an existing text node.
*/
$xp = findMatch($dom, $add, $addElement);
if(null === $xp)
{
if(trim($addElement->wholeText) !== "")
{
// add whole element to dom
$node = findXpath($dom, getXpath($addElement->parentNode))[0];
$addElementClone = $dom->importNode($addElement, true);
$node->appendChild($addElementClone);
}
}
else
{
$txt = trim($addElement->wholeText);
$newTextNode = $dom->createTextNode($txt);
$xp->parentNode->replaceChild($newTextNode, $xp);
}
break;
case ("DOMComment"):
// do not process comments
break;
default:
// should never happen
if($classname)
{
exit("Abort: instance of unknown class: $classname\n");
}
else
{
exit("Abort: no XML found\n");
}
}
}


/*
* Main Program
*/


if(count($argv) < 3)
{
echo "Usage: {$argv[0]} [-o] [inputXML3..N] \n
-o: overwrite existing output file
-id: specifies the id attribute to identify unique elements
-d: dpecified the attribute that indicated that an element should be deleted";
exit(-1);
}


// evaluate options
// options will be stored in global variables
$overwrite = false;
$idAttribute = "";
$deleteAttribute = "";
while(substr($argv[1], 0, 1) === "-")
{
if($argv[1] === "-o")
{
$overwrite = true;
array_shift($argv);
}


else if($argv[1] === "-id")
{
$idAttribute = $argv[2];
array_shift($argv);
array_shift($argv);
}


else if($argv[1] === "-d")
{
$deleteAttribute = $argv[2];
array_shift($argv);
array_shift($argv);
}


else
{
echo "Unknow option: {$argv[1]}\n";
exit(-10);
}
}


// test input files
for ($i = 1; $i < count($argv) - 1; $i++)
{
if(!file_exists($argv[$i]))
{
echo "Input file '{$argv[$i]}' does not exist.\n";
exit(-2);
}
}


// test outut file
if(!$overwrite && file_exists($argv[count($argv) - 1]))
{
echo "Output file '{$argv[count($argv) - 1]}' already exists.\n";
exit(-3);
}


//echo "Delete attribute: {$deleteAttribute}\n";
//echo "ID attribute: {$idAttribute}\n";


// load 1st XML
echo "Loading file {$argv[1]} ... ";
$mainDom = new DomDocument();
// pretty formatting later
$mainDom->preserveWhiteSpace = false;
$mainDom->formatOutput = true;
$mainDom->load($argv[1]);
echo "Done\n";


// load and merge all other files
for ($i = 2; $i < count($argv) - 1; $i++)
{
echo "Loading file {$argv[$i]} ... ";
$dom = new DomDocument();
    $dom->load($argv[$i]);
// merge dom into mainDom
echo "Merging ... ";
merge($mainDom, $dom);
echo "Done\n";
}


echo "Processed all files\n";


// write to file
echo "Writing output file {$argv[count($argv) - 1]} ... ";
/* does not work here
$mainDom->preserveWhiteSpace = false;
$mainDom->formatOutput = true;
*/
$mainDom->save($argv[count($argv) - 1]);
echo "Done\n";


exit(0);
?>
adaxas Web Directory