Skip to content

Commit

Permalink
Persist quickdial and vanity numbers on Fritz!Box internal memory (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
blacksenator authored and andig committed Sep 22, 2019
1 parent 7d055fc commit 564691a
Show file tree
Hide file tree
Showing 11 changed files with 555 additions and 267 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This is an entirely simplified version of https://github.com/jens-maus/carddav2f
* if more than nine phone numbers are included, the contact will be divided into a corresponding number of phonebook entries (any existing email addresses are assigned to the first set [there is no quantity limit!])
* phone numbers are sorted by type. The order of the conversion values ('phoneTypes') determines the order in the phone book entry
* the contact's UID of the CardDAV server is added to the phonebook entry (not visible in the FRITZ! Box GUI)
* automatically preserves QuickDial and Vanity attributes of phone numbers set in FRITZ!Box Web GUI. Works without config. These data are saved separately in the internal FRITZ!Box memory under `../FRITZ/mediabox/Atrributes.csv` from loss. The legacy way of configuring your CardDav server with X-FB-QUICKDIAL/X-FB-VANITY is no longer supported.
* generates an image with keypad and designated quickdial numbers (2-9), which can be uploaded to designated handhelds (see details below)

## Requirements
Expand Down Expand Up @@ -55,7 +56,7 @@ edit `config.example.php` and save as `config.php`

<img align="right" src="assets/fritzfon.png"/>

### Upload Fritz!FON background image
### Upload FRITZ!Fon background image

Using the `background-image` command it is possible to upload the quickdial numbers as background image to FRITZ!Fon (nothing else!)

Expand Down Expand Up @@ -94,7 +95,7 @@ afterwards).

Without a command, the container entrypoint will enter an endless loop,
repeatedly executing `carddav2fb run` in given intervals. This allows
automatic, regular updates of your FritzBox's phonebook.
automatic, regular updates of your FRITZ!Box's phonebook.


## License
Expand Down
13 changes: 11 additions & 2 deletions src/BackgroundCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,19 @@ protected function execute(InputInterface $input, OutputInterface $output)
$this->loadConfig($input);

// uploading background image
if (count($this->config['fritzbox']['fritzfons'])) {
$savedAttributes = [];
if (count($this->config['fritzbox']['fritzfons']) && $this->config['phonebook']['id'] == 0) {
error_log('Downloading FRITZ!Box phonebook');
$xmlPhonebook = downloadPhonebook($this->config['fritzbox'], $this->config['phonebook']);
uploadBackgroundImage($xmlPhonebook, $this->config['fritzbox']);
if (count($savedAttributes = uploadAttributes($xmlPhonebook, $this->config))) {
error_log('Numbers with special attributes saved' . PHP_EOL);
} else { // no attributes are set in the FRITZ!Box or lost
$savedAttributes = downloadAttributes($this->config['fritzbox']); // try to get last saved attributes
}
$xmlPhonebook = mergeAttributes($xmlPhonebook, $savedAttributes);
uploadBackgroundImage($xmlPhonebook, $savedAttributes, $this->config['fritzbox']);
} else {
error_log('No destination phones are defined and/or the first phone book is not selected!');
}
}
}
2 changes: 1 addition & 1 deletion src/FritzBox/BackgroundImage.php
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ public function uploadImage($quickdials, $config)
continue;
}

error_log(sprintf("Uploading background image to Fritz!Fon #%s", $phone));
error_log(sprintf("Uploading background image to FRITZ!Fon #%s", $phone));

$body = $this->getBody($fritz->getSID(), $phone, $backgroundImage);
$result = $fritz->postImage($body);
Expand Down
54 changes: 23 additions & 31 deletions src/FritzBox/Converter.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,28 @@ public function convert($card): array
return $contacts;
}

/**
* convert a phone number if conversions (phoneReplaceCharacters) are set
*
* @param string $number
* @return string $number
*/
public function convertPhonenumber($number)
{
// check if phone number is a SIP or internal number to avoid unwanted conversions
if (filter_var($number, FILTER_VALIDATE_EMAIL) || substr($number, 0, 2) == '**') {
return $number;
}
if (count($this->config['phoneReplaceCharacters'])) {
$number = str_replace("\xc2\xa0", "\x20", $number);
$number = strtr($number, $this->config['phoneReplaceCharacters']);
$number = trim(preg_replace('/\s+/', ' ', $number));
}

return $number;
}


/**
* Return a simple array depending on the order of phonetype conversions
* whose order should determine the sorting of the telephone numbers
Expand Down Expand Up @@ -156,16 +178,10 @@ private function getPhoneNumbers($card): array
}

$phoneNumbers = [];

$replaceCharacters = $this->config['phoneReplaceCharacters'] ?? [];
$phoneTypes = $this->config['phoneTypes'] ?? [];
foreach ($card->TEL as $key => $number) {
// format number
if (count($replaceCharacters)) {
$number = str_replace("\xc2\xa0", "\x20", $number);
$number = strtr($number, $replaceCharacters);
$number = trim(preg_replace('/\s+/', ' ', $number));
}
$number = $this->convertPhonenumber($number);
// get type
$type = 'other';
$telTypes = strtoupper($card->TEL[$key]->parameters['TYPE'] ?? '');
Expand All @@ -178,34 +194,10 @@ private function getPhoneNumbers($card): array
if (strpos($telTypes, 'FAX') !== false) {
$type = 'fax_work';
}

$addNumber = [
'type' => $type,
'number' => (string)$number,
];

/* Add quick dial and vanity numbers if card has xquickdial or xvanity attributes set
* A phone number with 'PREF' type is needed to activate the attribute.
* For quick dial numbers Fritz!Box will add the prefix **7 automatically.
* For vanity numbers Fritz!Box will add the prefix **8 automatically. */
foreach (['quickdial', 'vanity'] as $property) {
$attr = 'X-FB-' . strtoupper($property);
if (!isset($card->$attr)) {
continue;
}
if (strpos($telTypes, 'PREF') === false) {
continue;
}
$specialAttribute = (string)$card->$attr;
// number unique?
if (in_array($specialAttribute, $this->uniqueDials)) {
error_log(sprintf("The %s number >%s< has been assigned more than once (%s)!", $property, $specialAttribute, $number));
continue;
}
$this->uniqueDials[] = $specialAttribute; // keep list of unique numbers
$addNumber[$property] = $specialAttribute;
}

$phoneNumbers[] = $addNumber;
}

Expand Down
211 changes: 211 additions & 0 deletions src/FritzBox/Restorer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
<?php

namespace Andig\FritzBox;

use Andig\FritzBox\Converter;
use Sabre\VObject\Document;
use \SimpleXMLElement;

/**
* Copyright (c) 2019 Volker Püschel
* @license MIT
*/

class Restorer
{
const CSV_HEADER = 'uid,number,id,type,quickdial,vanity,prio,name';

private $collums = [];

public function __construct()
{
$this->collums = explode(',', self::CSV_HEADER);
}

/**
* get an empty associated array according to CSV_HEADER
* [ 'number' => '',
* 'id' => '',
* 'type' => '',
* 'quickdial' => '',
* 'vanity' => '',
* 'prio' => '',
* 'name' => '']
*
* @return array
*/
private function getPlainArray()
{
$csvHeader = explode(',', self::CSV_HEADER);
$dump = array_shift($csvHeader); // eliminate the first column header (uid)

return array_fill_keys($csvHeader, '');
}

/**
* Get quickdial and vanity special attributes and
* internal numbers ('**[n]' from given XML phone book
* return is an array according to CSV_HEADER:
* ['foo-bar' => [ // uid
* 'number' => '1',
* 'id' => '1',
* 'type' => 'foo',
* 'quickdial' => '1',
* 'vanity' => 'bar',
* 'prio' => '1',
* 'name' => 'baz']
* ],
*
* @param SimpleXMLElement $xmlPhonebook
* @return array an array of special attributes with CardDAV UID as key
*/
public function getPhonebookData(SimpleXMLElement $xmlPhonebook, array $conversions)
{
if (!property_exists($xmlPhonebook, "phonebook")) {
return [];
}

$converter = new Converter($conversions);
$phonebookData = [];

$numbers = $xmlPhonebook->xpath('//number[@quickdial or @vanity] | //number[starts-with(text(),"**")]');

foreach ($numbers as $number) {
$attributes = $this->getPlainArray(); // it´s easier to handle with the full set
// regardless of how the number was previously converted, the current config is applied here
$attributes['number'] = $converter->convertPhonenumber((string)$number);
// get all phone number attibutes
foreach ($number->attributes() as $key => $value) {
$attributes[(string)$key] = (string)$value;
}
// get the contacts header data (name and UID)
$contact = $number->xpath("./ancestor::contact");
$attributes['name'] = (string)$contact[0]->person->realName;
$uid = (string)$contact[0]->carddav_uid ?: uniqid();
$phonebookData[$uid] = $attributes;
}

return $phonebookData;
}

/**
* get a xml contact structure from saved internal numbers
*
* @param string $uid
* @param array $internalNumber
* @return SimpleXMLElement $contact
*/
private function getInternalContact(string $uid, array $internalNumber)
{
$contact = new SimpleXMLElement('<contact />');
$contact->addChild('carddav_uid', $uid);
$telephony = $contact->addChild('telephony');
$number = $telephony->addChild('number', $internalNumber['number']);
$number->addAttribute('id', $internalNumber['id']);
$number->addAttribute('type', $internalNumber['type']);
$person = $contact->addChild('person');
$person->addChild('realName', $internalNumber['name']);

return $contact;
}

/**
* Attach xml element to parent
* https://stackoverflow.com/questions/4778865/php-simplexml-addchild-with-another-simplexmlelement
*
* @param SimpleXMLElement $to
* @param SimpleXMLElement $from
* @return void
*/
public function xml_adopt(SimpleXMLElement $to, SimpleXMLElement $from)
{
$toDom = dom_import_simplexml($to);
$fromDom = dom_import_simplexml($from);
$toDom->appendChild($toDom->ownerDocument->importNode($fromDom, true));
}

/**
* Restore special attributes (quickdial, vanity) and internal phone numbers
* in given target phone book
*
* @param SimpleXMLElement $xmlTargetPhoneBook
* @param array $attributes array of special attributes
* @return SimpleXMLElement phonebook with restored special attributes
*/
public function setPhonebookData(SimpleXMLElement $xmlTargetPhoneBook, array $attributes)
{
$root = $xmlTargetPhoneBook->xpath('//phonebook')[0];

error_log('Restoring saved attributes (quickdial, vanity) and internal numbers');
foreach ($attributes as $key => $values) {
if (substr($values['number'], 0, 2) == '**') { // internal number
$contact = $this->getInternalContact($key, $values);
$this->xml_adopt($root, $contact); // add contact with internal number
}
if ($contact = $xmlTargetPhoneBook->xpath(sprintf('//contact[carddav_uid = "%s"]', $key))) {
if ($phone = $contact[0]->xpath(sprintf("telephony/number[text() = '%s']", $values['number']))) {
foreach (['quickdial', 'vanity'] as $attribute) {
if (!empty($values[$attribute])) {
$phone[0]->addAttribute($attribute, $values[$attribute]);
}
}
}
}
}

return $xmlTargetPhoneBook;
}

/**
* convert internal phonbook data (array of SimpleXMLElement) to string (rows of csv)
*
* @param array $phonebookData
* @return string $row csv
*/
public function phonebookDataToCSV($phonebookData)
{
$row = self::CSV_HEADER . PHP_EOL; // csv header row
foreach ($phonebookData as $uid => $values) {
$row .= $uid; // array key first collum
foreach ($values as $key => $value) {
if ($key == 'name') {
$value = '"' . $value . '"';
}
$row .= ',' . $value; // values => collums
}
if (next($phonebookData) == true) {
$row .= PHP_EOL;
}
}

return $row;
}

/**
* convert csv line to internal phonbook data
*
* @param array $csvRow
* @return array $phonebookData
*/
public function csvToPhonebookData($csvRow)
{
$rows = '';
$uid = '';
$phonebookData = [];

if (count($csvRow) <> count($this->collums)) {
throw new \Exception('The number of csv columns does not match the default!');
}
if ($csvRow <> $this->collums) { // values equal CSV_HEADER
foreach ($csvRow as $key => $value) {
if ($key == 0) {
$uid = $value;
} else {
$phonebookData[$uid][$this->collums[$key]] = $value;
}
}
}

return $phonebookData;
}
}
23 changes: 18 additions & 5 deletions src/RunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ protected function execute(InputInterface $input, OutputInterface $output)
$this->checkUploadImagePreconditions($this->config['fritzbox'], $this->config['phonebook']);
}

// download recent phonebook and save special attributes
$savedAttributes = [];
error_log("Downloading recent FRITZ!Box phonebook");
$recentPhonebook = downloadPhonebook($this->config['fritzbox'], $this->config['phonebook']);
if (count($savedAttributes = uploadAttributes($recentPhonebook, $this->config))) {
error_log('Phone numbers with special attributes saved');
} else { // no attributes are set in the FRITZ!Box or lost -> try to download them
$savedAttributes = downloadAttributes($this->config['fritzbox']); // try to get last saved attributes
}

// download from server or lokal files
$vcards = $this->downloadAllProviders($output, $input->getOption('image'));
error_log(sprintf("Downloaded %d vCard(s) in total", count($vcards)));

Expand Down Expand Up @@ -76,15 +87,17 @@ protected function execute(InputInterface $input, OutputInterface $output)
return null;
}

// upload
error_log("Uploading");
// write back saved attributes
$xmlPhonebook = mergeAttributes($xmlPhonebook, $savedAttributes);

// upload
error_log("Uploading new phonebook to FRITZ!Box");
uploadPhonebook($xmlPhonebook, $this->config);
error_log("Successful uploaded new Fritz!Box phonebook");
error_log("Successful uploaded new FRITZ!Box phonebook");

// uploading background image
if (count($this->config['fritzbox']['fritzfons'])) {
uploadBackgroundImage($xmlPhonebook, $this->config['fritzbox']);
if (count($this->config['fritzbox']['fritzfons']) && $this->config['phonebook']['id'] == 0) {
uploadBackgroundImage($xmlPhonebook, $savedAttributes, $this->config['fritzbox']);
}
}

Expand Down
Loading

0 comments on commit 564691a

Please sign in to comment.