FWiePs Weblog2024-03-10T13:05:24+01:00Frans-Willem Posthttps://www.fwiep.nl/https://www.fwiep.nl/blog/Weblog van Frans-Willem Post (FWieP) met de nadruk op computer- & muziek gerelateerde know-how.https://www.fwiep.nl/favicon.icohttps://www.fwiep.nl/blog/php-met-xdebug-en-vscode-onder-fedoraPHP met Xdebug en VSCode onder Fedora2024-03-10T13:05:24+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Dezer dagen was het weer eens tijd om één van mijn computers te voorzien
van een blanco installatie met Fedora GNU/Linux. Uit ervaring weet ik dat
het daadwerkelijke installeren niet meer dan een half uur in beslag neemt.
Maar het inrichten en naar wens aanpassen van alle hoeken en gaten van
zo'n systeem; daar gaan dagen, soms zelfs weken overheen.</p>
<p>Zo kwam ik vandaag een stukje tegen dat ik nog niet naar behoren had
ingericht: het stap-voor-stap debuggen van PHP-code in mijn favoriete
ontikkelomgeving <a href="https://code.visualstudio.com/">Visual Studio Code</a>.</p>
<h2>Onderdelen</h2>
<p>Een van de bekendste PHP debug-hulpmiddelen is <a href="https://xdebug.org/">Xdebug</a>. Deze module
in PHP geeft je behoorlijk veel mogelijkheden om PHP-code te profileren,
optimaliseren en dus ook stap-voor-stap te inspecteren terwijl ze wordt
uitgevoerd. Een onmisbaar gereedschap voor een webontwikkelaar.</p>
<p>De installatie van Xdebug is dankzij de <a href="https://xdebug.org/wizard">installatiewizard</a> heel
gebruiksvriendelijk en voor alle gangbare platformen beschikbaar. In mijn
geval van Fedora 39 met PHP 8.3 waren het de volgende stappen:</p>
<pre><code class="bash"># Install xdebug
sudo dnf install php-pecl-xdebug3
# Add xdebug-settings to PHP's ini directives
sudo tee -a /etc/php.d/99-fwiep.ini << EOF
[Xdebug]
xdebug.mode = develop,debug,profile
xdebug.start_with_request = trigger
EOF
# Restart Apache and PHP to load added module
sudo systemctl restart php-fpm.service
sudo systemctl restart httpd.service
</code></pre>
<p>Hierna kun je met behulp van <code>phpinfo()</code> controleren of de module correct
geladen en actief is. Eenmaal in de browser kun je gebruik maken van
<strong>xdebug-helper</strong> (<a href="https://addons.mozilla.org/en-US/firefox/addon/xdebug-helper-for-firefox">Firefox</a>, <a href="https://chromewebstore.google.com/detail/xdebug-helper/eadndfjplgieldjbigjakmdgkmoaaaoc">Chrome</a>), een browser-extensie die
het debuggen heel comfortabel in- en uitschakelt. Ook kun je er gemakkelijk
mee kiezen tussen stap-voor-stap debuggen, profileren en traceren.</p>
<h2>3...2...1...Launch!</h2>
<p>In VSCode is de installatie van een extensie vereist: <a href="https://marketplace.visualstudio.com/items?itemName=xdebug.php-debug"><code>xdebug.php-debug</code></a>.
Daarnaast moet in elk project een zogenaamde <code>launch</code>-configuratie worden
toegevoegd. Hierin wordt onder andere de TCP-poort ingesteld waarop
Xdebug luistert, en ook een directe link gelegd tussen de bestanden en de
werkmap (sleutel <code>pathMappings</code>).</p>
<pre><code class="json">{
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9003,
"ignore": [
"**/vendor/**/*.php"
],
"pathMappings": {
"/path/to/project-folder": "${workspaceFolder}"
}
}
]
}
</code></pre>
<p>Na het starten van de debugger met <kbd>F5</kbd>, het zetten van een
breakpoint in de kantlijn van mijn code en het oproepen van de pagina
in mijn browser gebeurde er… niets. Ik was vergeten om met de
browser-extensie het debuggen in te schakelen. Klik, verversen en…
weer niets.</p>
<h2>Debuggen van het debuggen</h2>
<p>Zo'n beetje alle documentatie en tutorials op internet waar Xdebug en
VSCode worden beschreven stopten op dit punt; het zou zo moeten werken.
Xdebug geladen en actief? Ja. Xdebug-extensie geïnstalleerd in VSCode en
een launch-configuratie toegevoegd? Ja.</p>
<p>Nee dus. Uiteindelijk kwam ik een met <code>phpinfo()</code> vergelijkbare functie
genaamd <code>xdebug_info()</code> op het spoor. De output van deze functie geeft
alle mogelijke informatie van Xdebug; veel meer dan in de uitvoer van
<code>phpinfo()</code>. Hier zag ik de volgende foutmelding:</p>
<pre><code class="plain">"[Step Debug] Creating socket for 'localhost:9003', connect: Permission denied."
</code></pre>
<p>Permission denied, toegang geweigerd? Dat klinkt als een probleem met
SELinux! Onderzoek met behulp van <code>jounalctl</code> bracht me bij de volgende
melding in de logs:</p>
<pre><code>AVC avc: denied { name_connect } for pid=29691
comm="php-fpm" dest=9003
scontext=system_u:system_r:httpd_t:s0
tcontext=system_u:object_r:unreserved_port_t:s0
tclass=tcp_socket permissive=0
</code></pre>
<h2>Oplossing</h2>
<p>Het programma <code>setroubleshoot</code> bracht ten slotte met een aantal suggesties
de oplossing:</p>
<pre><code>SELinux is preventing php-fpm from name_connect access on the tcp_socket port 9003.
If you want to allow httpd to can network connect
Then you must tell SELinux about this by enabling the 'httpd_can_network_connect' boolean.
Do
setsebool -P httpd_can_network_connect 1
</code></pre>
<p>Wat blijkt? Bij het inrichten van mijn nieuwe systeem was ik vergeten om
dit commando uit te voeren; het stond wel degelijk in mijn aantekeningen.
Maar die zijn zo chaotisch dat ik het niet heb gezien. Nou kan dat
natuurlijk ook aan <a href="https://www.fwiep.nl/slechtziendheid">mijn slechtzienheid</a> liggen, maar toch… :)</p>
</div>2024-03-10T13:05:24+01:00https://www.fwiep.nl/blog/actuele-brandstofprijzen-met-comfortActuele brandstofprijzen met comfort2023-03-02T22:32:13+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Op lange termijn is het de bedoeling dat onze maatschappij qua mobiliteit de
duurzaamheid voorop gaat zetten; dat we minder eigen, persoonlijk vervoer
gebruiken, en daarvoor in de plaats veel meer delen met elkaar. Dat is niet
alleen veel beter voor het milieu, de aarde, het klimaat en dus de mensheid,
maar het maakt je als mens ook bewuster van wat je doet en waarom.</p>
<p>Tot zover het toekomstbeeld; terug naar de droevige werkelijkheid van 2023. We
verbruiken nog steeds miljoenen liters fossiele brandstoffen om onszelf te
verplaatsen. Totdat de gezinsauto wordt vervangen door een (elektrische) deelauto
blijft het zoeken naar de voordeligste manier om aan die brandstof te komen.</p>
<h2>Concurrentie</h2>
<p>Sommige leveranciers publiceren de brandstofprijzen online, anderen houden die
data achter gesloten deuren. Je moet dan bij het tankstation zelf gaan kijken
– en loopt het risico dat je daar, eenmaal aangekomeen, dan ook maar
meteen tankt; en onbewust meer betaalt dan nodig.</p>
<h2>Uitdaging</h2>
<p>Het is een behoorlijke uitdaging om actuele brandstofprijzen te verzamelen van
tankstations in de buurt; zeker in de grensstreek, waar je te maken hebt met
Nederlandse, Duitse en Belgische pompstations.</p>
<p>Dit project begon met een aantal aan mezelf gestuurde memo-berichtjes met URLs
van de verschillende websites. Als ik ze met elkaar wilde vergelijken, moest ik
elke pagina oproepen en de huidige prijs onthouden. Daarna kon ik ze met elkaar
vergelijken en een bewuste keuze maken. Dat moest toch te automatiseren zijn?</p>
<h2>Data verzamelen</h2>
<p>Aldus ging ik op zoek naar actuele prijsinformatie van de tankstations in onze
buurt: Nederlands Zuid-Limburg. Over de Duitse grens is <a href="https://mein.aral.de/tankstellenfinder/">Aral</a> een veel
voorkomend brandstofmerk; zij hebben voor elk station een eigen pagina die de
actuele prijzen in JSON-formaat ophaalt bij een openbare API op
<code>api.tankstelle.aral.de</code>. Dit was alvast één kandidaat die ik makkelijk met PHP
zou kunnen uitlezen!</p>
<p>Een aantal anderen (<a href="https://www.tango.nl/stations/">Tango</a>, <a href="https://www.lukoil.nl/go/">Lukoil</a> en <a href="https://www.argos.nl/tankstation/">Argos</a> in Nederland,
<a href="https://carbu.com/belgie/index.php">Carbu</a> in België) maakten het mij niet zo eenvoudig. De prijzen waren in
de HTML van de betreffende pagina's verweven en ik zou grondig moeten graven om
ze er uit te kunnen destileren. Aldus geschiedde :-).</p>
<h2>Aanpak</h2>
<p>De eerste stap van elke opvraging is een <code>cURL</code>-request naar de betreffende URL.
Daarna wordt het antwoord op deze request geanalyseerd op de daadwerkelijke
prijzen en een datum-tijd van laatste update. Elke implementatie is maatwerk;
geen enkele aanbieder gebruikt dezelfde opmaak of structuur. Ook het gebruik van
punt of komma als decimaal scheidingsteken kan per site verschillen.</p>
<pre><code class="php"><?php
$ch = curl_init();
curl_setopt_array(
$ch, [
CURLOPT_URL => 'https://...',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true
]
);
$output = curl_exec($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if ($info['http_code'] !== 200) {
return null;
}
// Do stuff with $output
</code></pre>
<h3>Aral (JSON)</h3>
<p>Binnen de response van de API heeft elke brandstof zijn eigen code. De prijzen
worden opgegeven in eurocenten. In mijn applicatie koos ik voor de codes <code>E5</code>
(Euro 98), <code>E10</code> (Euro 95) en <code>B7</code> (Diesel) en prijzen in Euro's. Ook de datum
en tijd van laatste update wordt door de API aangeleverd.</p>
<pre><code class="php"><?php
$json = json_decode($output);
if (json_last_error() !== JSON_ERROR_NONE) {
return null;
}
$o = [];
foreach ($json->data as $x) {
switch ($x->aral_id) {
case 'F00104': // Super E5
$o['E5'] = bcdiv($x->price->price, '100');
break;
case 'F00113': // Super E10
$o['E10'] = bcdiv($x->price->price, '100');
$o['datetime'] = $x->price->valid_from;
break;
case 'F00400': // Diesel
$o['B7'] = bcdiv($x->price->price, '100');
break;
}
}
// Do something with $o
</code></pre>
<h3>Tango (HTML)</h3>
<p>De tot nu toe succesvolste manier om de HTML te analyseren is het parsen met
behulp van <a href="https://github.com/Masterminds/html5-php">Masterminds HTML5</a>. Hieruit rolt dan een <code>\DOMDocument</code> dat je
kan doorzoeken met <code>\DOMXPath</code>. Soms staat de gewenste informatie in een element
met <code>id</code>-attribuut (<code>@id</code>), soms in een element met een bepaalde CSS-klasse
(<code>@class</code>).</p>
<p>Daarna wordt de tekstuele inhoud (<code>$node->textContent</code>) op één regel gepropt en
daarna met behulp van reguliere expressies ontleed. Tot slot worden de prijzen
met <code>bcmul()</code> vermenigvuldigd en de datumtijd geparst met behulp van PHP's
<a href="https://unicode-org.github.io/icu/userguide/format_parse/datetime/#date-field-symbol-table"><code>\IntlDateFormatter</code></a>.</p>
<details open>
<summary>Code in-/uitklappen</summary>
<pre><code class="php"><?php
use IntlDateFormatter as IDF;
use Masterminds\HTML5;
function _pluckForPrice(\DOMDocument &$dom, string $nodeId) : string
{
$xp = new \DOMXPath($dom);
$nodes = $xp->query("//*[contains(@id, '$nodeId')]");
if ($nodes->length == 0) {
return '';
}
// Compress all whitespaces into single spaces
$node = $nodes->item(0);
$txt = preg_replace('/\s+/u', ' ', $node->textContent);
$m = [];
$re = '!Pompprijs (?<PRICE>\d+\.\d+) EUR/L!i';
if (preg_match($re, $txt, $m) !== 1) {
return '';
}
return $m['PRICE'];
}
function _pluckForDatetime(\DOMDocument &$dom, string $c) : ?\DateTime
{
$xp = new \DOMXPath($dom);
$nodes = $xp->query(
"//*[contains(concat(' ', normalize-space(@class), ' '), ' $c ')]"
);
if ($nodes->length == 0) {
return null;
}
$node = $nodes->item(0);
$txt = preg_replace('/\s+/u', ' ', $node->textContent);
$m = [];
$re = '!(?<DATETIME>\d\d:\d\d uur, \d\d-\d\d-\d\d\d\d)!i';
if (preg_match($re, $txt, $m) !== 1) {
return null;
}
$tz = new \DateTimeZone('Europe/Amsterdam');
$dtfmt = new IDF(
'nl_NL', IDF::LONG, IDF::NONE,
$tz, IDF::GREGORIAN, 'HH:mm \'uur\', dd-MM-yyyy'
);
$dt = (new \DateTime('now', $tz))->setTimestamp(
$dtfmt->parse($m['DATETIME'])
);
return $dt;
}
$html5 = new HTML5();
$dom = $html5->loadHTML($output);
// Parse for fuel datetime
$c = 'field--name-price-last-changed'; // class denoting last update
$dt = self::_pluckForDatetime($dom, $c);
// Parse for fuel prices
$e5 = self::_pluckForPrice($dom, 'super98');
$e10 = self::_pluckForPrice($dom, 'euro95');
$b7 = self::_pluckForPrice($dom, 'diesel');
$o = [
'datetime' => $dt,
'E5' => bcmul($e5, '1'),
'E10' => bcmul($e10, '1'),
'B7' => bcmul($b7, '1')
];
// Do something with $o
</code></pre>
</details>
<h3>Carbu (HTML)</h3>
<p>Ook de HTML van het Belgische Carbu wordt op dezelfde manier geanalyseerd.
In dit geval staat bij elke prijs een eigen datum van laatste update.</p>
<details open>
<summary>Code in-/uitklappen</summary>
<pre><code class="php"><?php
function _pluck(array $m) : array
{
$tz = new \DateTimeZone('Europe/Amsterdam');
$price = bcmul(str_replace(',', '.', $m['PRICE']), '1');
$dtfmt = new IDF(
'nl_NL', IDF::LONG, IDF::NONE,
$tz, IDF::GREGORIAN, 'dd/MM/yy'
);
$dt = (new \DateTime('now', $tz))
->setTimestamp($dtfmt->parse($m['DATE']));
return ['PRICE' => $price, 'DATETIME' => $dt];
}
$html5 = new HTML5();
$dom = $html5->loadHTML($html);
$xp = new \DOMXPath($dom);
$c = 'carburants'; // CSS class that denotes area with fuel prices
$nodes = $xp->query(
"//*[contains(concat(' ', normalize-space(@class), ' '), ' $c ')]"
);
if ($nodes->length == 0) {
return null;
}
$m = [];
$node = $nodes->item(0);
// Compress all whitespaces into single spaces
$txt = preg_replace('/\s+/u', ' ', $node->textContent);
// Match different fuel prices and their datetimes
$reE5 = '!\(E5\) (?<PRICE>[^\\s]+) €/L (?<DATE>\d\d/\d\d/\d\d) !';
$reE10 = '!\(E10\) (?<PRICE>[^\\s]+) €/L (?<DATE>\d\d/\d\d/\d\d) !';
$reB7 = '!\(B7\) (?<PRICE>[^\\s]+) €/L (?<DATE>\d\d/\d\d/\d\d) !';
if (preg_match($reE5, $txt, $m) == 1) {
$pdt = self::_pluck($m);
$o['E5'] = $pdt['PRICE'];
}
if (preg_match($reE10, $txt, $m) == 1) {
$pdt = self::_pluck($m);
$o['E10'] = $pdt['PRICE'];
$o['datetime'] = $pdt['DATETIME'];
}
if (preg_match($reB7, $txt, $m) == 1) {
$pdt = self::_pluck($m);
$o['B7'] = $pdt['PRICE'];
}
// Do somthing with $o;
</code></pre>
</details>
<h3>Shell (HTML + JSON)</h3>
<p>Toen ik ook een tankstation van Shell wilde opnemen in mijn applicatie, bleek
dat er bij Nederlandse pompen geen prijzen staan genoemd (bijvoobeeld
<a href="https://find.shell.com/nl/fuel/10030618-shell-station-planetenbaan/nl_NL">in Utrecht</a>). Bij Duitse pompen staan daarentagen wél prijzen (bijvoorbeeld
<a href="https://find.shell.com/de/fuel/10025580-koln-messekreisel-1/nl_NL">in Keulen</a>). OK, dan nemen we een Duits station net over de grens ter
vergelijking.</p>
<pre><code class="html"><div
data-react-class="pages/LocationPage"
data-react-props="{&quot;config&quot;:{&quot;alwaysShowOpeningHours&quot;:false,..."
data-react-cache-id="pages/LocationPage-0">
</div>
</code></pre>
<p>Toen ik de rauwe HTML binnenhaalde met <code>cURL</code> bleek er slechts één <code><div></code>-element
te zijn met een aantal HTML attributen. Deze begonnen allemaal met <code>data-react-</code>;
mijn eerste kennismaking met <a href="https://reactjs.org/">React.js</a>. Alle data (en dus ook de prijzen
die ik zocht) zaten verstopt in het <code>data-react-props</code> attribuut. De inhoud leek
verdacht veel op JSON-data en dat bleek het ook te zijn!</p>
<details open>
<summary>Code in-/uitklappen</summary>
<pre><code class="php"><?php
$html5 = new HTML5();
$dom = $html5->loadHTML($html);
$xp = new DOMXPath($dom);
$tz = new \DateTimeZone('Europe/Amsterdam');
$o = [];
$nodes = $xp->query('//*/@data-react-props');
if ($nodes->length == 0) {
return null;
}
$props = $nodes->item(0);
$json = json_decode($props->nodeValue);
if (json_last_error() !== JSON_ERROR_NONE) {
return null;
}
if (!property_exists($json, 'location')) {
return null;
}
if (!property_exists($json->location, 'fuelPricing')) {
return null;
}
$pricing = $json->location->fuelPricing;
if (!property_exists($pricing, 'updated')) {
return null;
}
if (is_null($pricing->updated)) {
// Return empty datetime and prices
$o = [
'datetimee' => null,
'E5' => null,
'E10' => null,
'B7' => null
];
// Do something with $o
}
$dt = new \DateTime($pricing->updated, $tz);
if (!property_exists($pricing, 'prices')) {
return null;
}
$o['datetime'] = $dt;
$p = $pricing->prices;
if (property_exists($p, 'fuelsave_98')) {
$o['E5'] = bcmul((string)$p->fuelsave_98, '1');
}
if (property_exists($p, 'fuelsave_midgrade_gasoline')) {
$o['E10'] = bcmul((string)$p->fuelsave_midgrade_gasoline, '1');
}
if (property_exists($p, 'fuelsave_regular_diesel')) {
$o['B7'] = bcmul((string)$p->fuelsave_regular_diesel, '1');
}
// Do something with $o
</code></pre>
</details>
<h2>Buitenbeentje BP</h2>
<p>Tot slot wilde ik ook een tankstation van BP toevoegen, maar deze leverancier
heeft alleen maar <a href="https://www.bp.com/nl_nl/netherlands/home/producten-en-services/bp-brandstoffen/landelijke-adviesprijzen.html">generieke adviesprijzen</a> online staan. Die zijn
natuurlijk ook uit de HTML te destileren, maar deze zijn niet echt relevant
voor dit scenario. De daadwerkelijke (actuele) prijs aan een specifieke pomp kon
ik bij BP zelf niet vinden.</p>
<p>Een externe site, <a href="https://directlease.nl/tankservice/bp/">DirectLease</a>, beweerde wel de actuele prijzen te hebben.
Deze worden, waarschijnlijk om het geatomatiseerd 'lenen' van de data te
bemoeilijken, als PNG-afbeelding getoond bij elk station. Ook hier kwam de data
vanuit een API, hier genaamd <a href="https://tankservice.app-it-up.com/Tankservice/v2/places/11509.png?lang=nl"><code>tankservice.app-it-up.com</code></a>.</p>
<h3>OCR</h3>
<p>Aldus kwam het idee om met Optical Character Recognition (OCR) de afbeelding te
analyseren en daaruit de prijzen te destileren. Ik zocht en vond in
<a href="https://ocr.space/OCRAPI">Free OCR</a> een gratis API die een bestaande afbeelding (of een URL daarvan)
kon analyseren en een <code>JSON</code>-antwoord terugstuurde met de herkende tekst.</p>
<details open>
<summary>Code in-/uitklappen</summary>
<pre><code class="php"><?php
$url = 'https://api.ocr.space/parse/imageurl?' .
'url=https://tankservice.app-it-up.com/Tankservice/v2/places/' .
'11509.png?lang=nl&language=dut&filetype=PNG&OCREngine=3';
$curl = curl_init();
curl_setopt_array(
$curl, array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => array(
'apikey: <PUT_YOUR_API_KEY_HERE>'
),
)
);
$json = curl_exec($curl);
curl_close($curl);
$json = json_decode($json);
if (json_last_error() !== JSON_ERROR_NONE) {
return null;
}
if (!property_exists($json, 'ParsedResults')) {
return null;
}
if (!is_array($json->ParsedResults) || empty($json->ParsedResults)) {
return null;
}
if (!property_exists($json->ParsedResults[0], 'ParsedText')) {
return null;
}
$s = $json->ParsedResults[0]->ParsedText;
// Compress all whitespaces into single spaces
$s = preg_replace('/\s+/u', ' ', $s);
$m = [];
$re = '!(?:' .
'(?<E10>\d+\,\d+).*?' .
'(?<B7>\d+\,\d+).*?' .
'(?<E5>\d+\,\d+)' .
')' .
'!u';
if (preg_match($re, $s, $m) == 0) {
return null;
}
$o['E10'] = str_replace(',', '.', $m['E10']);
$o['B7'] = str_replace(',', '.', $m['B7']);
$o['E5'] = str_replace(',', '.', $m['E5']);
$o['datetime'] = new \DateTime('now', $tz);
// Do something with $o
</code></pre>
</details>
<h2>Conclusie</h2>
<p>Voor mijn applicatie heb ik bovenstaande stukken code gecombineerd met een
mini-webapplicatie. De opgehaalde prijzen worden opgeslagen in een database en
met een geautomatiseerde <em>cronjob</em> elk uur bijgewerkt. Ook kan met een druk op
de knop één specifieke prijs worden ververst, of alle prijzen tegelijk. De
prijzen worden automatisch gesorteerd zodat de goedkoopste leverancier bovenaan
staat.</p>
<p>In het dagelijks gebruik van deze applicatie blijkt dat sommige pompstations met
de prijzen stunten: in de ochtend is de prijs wel 20 cent hoger dan later op de
dag. Uiteindelijk heb ik het BP-station uit de lijst verwijderd; de prijs via de
externe site is al dagen niet ververst en de OCR-aanpak is niet echt betrouwbaar.
Dan maar geen Brits petroleum.</p>
<p>Het hele project was buitengewoon leerzaam. Ik kon lekker aan de slag met <code>cURL</code>,
<code>JSON</code>, <code>DOMDocument</code>s en reguliere expressies. Het toetje was toch zeker het
aanspannen van de OCR-API voor het analyseren van (waarschijnlijk) beschermde
data.</p>
</div>2023-03-02T22:32:13+01:00https://www.fwiep.nl/blog/whatsapp-export-met-sql-en-phpWhatsapp export met SQL en PHP2023-01-04T21:23:13+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Vooraanstaande berichtendiensten zoals Signal en Whatsapp maken gebruik
van punt-tot-punt versleuteling (end-to-end encryption, E2E). Hiermee
wordt alle data tussen de gesprekspartners versleuteld en kan niemand
anders meekijken - ook de aanbieder zelf niet. De back-ups van deze apps
zijn eveneens versleuteld, om ook offline de veiligheid van de berichten
en data te waarborgen.</p>
<p>Af en toe heb ik de behoefte om mijn chatgeschiedenis door te bladeren
en zo bepaalde gesprekken nog eens terug te lezen. Maar met die
versleutelde back-ups kon ik zo 1-2-3 niets beginnen. Ik zocht
<a href="https://blog.group-ib.com/whatsapp_forensic_artifacts">en vond</a> de informatie om via een omweg de onversleutelde
databases hiervoor te gebruiken. Maar écht comfortabel is het openen
van een SQLite database en het uitvoeren van talloze SQL queries
daarvoor niet. Dit moest toch elegant geautomatiseerd kunnen worden? Ja
dus! Het uiteindelijke resultaat is <a href="https://github.com/fwiep/wa-chat-export">op Github</a> te bewonderen.</p>
<h2>Update</h2>
<p>Januari 2023 - In een recente Whatsapp update is de databasestructuur veranderd.
Het script dat ik geschreven had, werkte niet meer; ik moest opnieuw de tabellen
doorzoeken naar de gegevens voor mijn export. Uiteindelijk heb ik
<a href="https://github.com/fwiep/wa-chat-export/commit/775ad28f53024ae3b711c0c53a6c01bac439d154">de aanpassingen</a> online verwerkt in versie 0.2.</p>
<details open>
<summary>Code in- en uitklappen</summary>
<pre><code class="plain">; WA Chat Export v0.1
; Creates a readable export of WhatsApp's chat history
;
; Export started: 2022-02-03 17:19:08 +01:00
; Group chat: no
;
; Participants in this chat:
; +31612345678: Alice
; +31687654321: Bob
[2016-07-12 08:16:32 +02:00] Alice: Hi Bob, how are you?
[2016-07-12 10:09:36 +02:00] Bob: Hi.
[2016-07-26 09:37:42 +02:00] Bob: I'm fine, how's your cat doing?
[2016-07-26 09:38:27 +02:00] Alice: (media file) Photo of my cat
[2016-07-26 09:39:25 +02:00] Bob: Waw!
...
</code></pre>
</details>
<h2>Prototype</h2>
<p>Gelukkig bood PHP, de scripttaal waar ik het beste in thuis ben, van
huis uit een mogelijkheid om met <a href="https://www.php.net/manual/en/class.sqlite3.php">SQLite-databases</a> te werken. Met
behulp van een 'normale' SQLite browser kon ik de tabellen en kolommen
die ik nodig zou hebben, makkelijk identificeren.</p>
<p>In het bestand <code>wa.db</code> staan de namen van alle bekende contactpersonen;
zeg maar, het adresboek van de applicatie. Alle andere data, daaronder
gegevens van de groepsapps en de afzonderlijke chatberichten, staan in
<code>msgstore.db</code>. De tabellen en kolommen die voor mijn script van belang
zijn, zijn:</p>
<details open>
<summary>Code in- en uitklappen</summary>
<pre><code class="sql">SELECT
`jid`, -- ID of the contact
`display_name`, -- name as set in the user's addressbook
`wa_name` -- name as set by the contact him/herself
FROM
`wa_contacts`; -- in wa.db
SELECT
`J`.`raw_string`, -- ID of the group chat
`C`.`subject` -- name of the group chat
FROM
`chat` AS `C` -- in msgstore.db
JOIN `jid` AS `J` ON `C`.`jid_row_id` = `J`.`_id`;
SELECT
`key_remote_jid`, -- ID of the remote contact, empty if sent by 'me'
`key_from_me`, -- boolean, 1 if sent by 'me', 0 otherwise
`data`, -- message body
`remote_resource`, -- in a group chat, the ID of sender, empty otherwise
`media_caption`, -- caption of a media file, if any
`forwarded`, -- boolean, 1 if forwarded to 'me', 0 otherwise
`timestamp` -- Unix timestamp * 1000, when sent/received
FROM
`messages` -- in msgstore.db
ORDER BY
`key_remote_jid` ASC,
`timestamp` ASC;
</code></pre>
</details>
<p>Elk <code>jid</code> in de verschillende tabellen is een unieke tekenreeks in de
vorm van een e-mailadres. Bij contactpersonen bestaat het uit het
internationale telefoonnummer, een <code>@</code> en het domein <code>s.whatsapp.net</code>.
Bijvoorbeeld: <code>31612345678@s.whatsapp.net</code>.</p>
<p>Bij een groepchat bestaat het uit het internationale telefoonnummer van
de gebruiker die de groep heeft aangemaakt, een minteken, de
Unix-timestamp van het moment dat de groep is aangemaakt, een <code>@</code> en
het domein <code>g.us</code>. Bijvoorbeeld: <code>31687654321-1576684071@g.us</code>.</p>
<h2>CLI handling</h2>
<p>Met behulp van PHP's <a href="https://www.php.net/manual/en/function.getopt.php"><code>getopt()</code></a> functie kun je argumenten op de
commandoprompt verwerken, ongeacht hun volgorde. Ook kun je zowel korte
argumenten (van één minteken met één letter) als lange (twee mintekens
en meerdere letters) ondersteunen. In dit geval koos ik voor beide:</p>
<pre><code class="plain">WA Chat Export v0.1
Chat-export tool for unencrypted WhatsApp databases
Usage: wa-chat-export.php OPTIONS
[ -h | --help ] show this help message and exit
[ -a | --addressdb= ] FILE path to WhatsApp addressbook file
[ -m | --messagedb= ] FILE path to WhatsApp database file
[ -n | --number= ] NUMBER phonenumber of the exporting user, e.g. +31612345678
[ -o | --outdir= ] DIRECTORY output folder, defaults to parent of database file
</code></pre>
<p>De code die de argumenten verwerkt, ziet er (ingekort) als volgt uit:</p>
<details open>
<summary>Code in- en uitklappen</summary>
<pre><code class="php">$validSQLite3mimes = ['application/x-sqlite3', 'application/vnd.sqlite3'];
$wacDBfile = null;
$msgDBfile = null;
$mePhone = null;
$outDir = null;
$opts = getopt(
'ha:m:n:o:',
['help', 'addressdb:', 'messagedb:', 'number:', 'outdir:']
);
if (array_intersect_key(['h' => 0, 'help' => 0], $opts)) {
print $outHeader; // print the program name and version
print $outUsage; // print the program usage info
exit(0);
}
if (array_intersect_key(['a' => 0, 'addressdb' => 0], $opts)) {
$wacDBfile = array_key_exists('a', $opts) ? $opts['a'] : $opts['addressdb'];
if (!file_exists($wacDBfile) || !is_readable($wacDBfile)) {
print $outHeader; // print program name and version to STDOUT
fwrite(
STDERR,
"The addressbook file does not exist, or could not be read.".
PHP_EOL."Exiting.".PHP_EOL
);
exit(1);
}
if (!in_array(mime_content_type($wacDBfile), $validSQLite3mimes)) {
print $outHeader; // print program name and version to STDOUT
fwrite(
STDERR,
"The addressbook file is not an SQLite3 database.".
PHP_EOL."Exiting.".PHP_EOL
);
exit(2);
}
}
// ...
if (array_intersect_key(['n' => 0, 'number' => 0], $opts)) {
$mePhone = array_key_exists('n', $opts) ? $opts['n'] : $opts['number'];
if (preg_match('!^\+?[0-9]+$!', $mePhone) != 1) {
print $outHeader; // print program name and version to STDOUT
fwrite(
STDERR,
"The phonenumber was not in the correct format, e.g. '+31612345678'.".
PHP_EOL."Exiting.".PHP_EOL
);
exit(5);
}
}
if (empty($wacDBfile) || empty($msgDBfile) || empty($mePhone)) {
print $outHeader; // print program name and version to STDOUT
print $outUsage; // print program usage info to STDOUT
fwrite(
STDERR,
"Not all required arguments were provided.".
PHP_EOL."Exiting.".PHP_EOL
);
exit(6);
}
if (array_intersect_key(['o' => 0, 'outdir' => 0], $opts)) {
$outDir = array_key_exists('o', $opts) ? $opts['o'] : $opts['outdir'];
} else {
$outDir = dirname($msgDBfile);
}
if (!is_dir($outDir) || !is_writable($outDir)) {
print $outHeader; // print program name and version to STDOUT
fwrite(
STDERR,
"The output directory does not exist, or isn't writable.".
PHP_EOL."Exiting.".PHP_EOL
);
exit(7);
}
</code></pre>
</details>
<h2>Klassen</h2>
<p>Het export script maakt gebruik van een aantal simpele klassen: <code>Contact</code>
met ID en naam, <code>Message</code> met de gegevens van de berichten zelf. Hieronder
zijn ze verkort weergegeven.</p>
<h3>Contact</h3>
<pre><code class="php">class Contact
{
private $_jID;
public function getJID() {
return $this->_jID;
}
public function setJID(string $_jID) {
$this->_jID = $_jID;
return $this;
}
private $_displayName;
public function getDisplayName() {
return $this->_displayName;
}
public function setDisplayName(string $_displayName) {
$this->_displayName = $_displayName;
return $this;
}
}
</code></pre>
<h3>Message</h3>
<pre><code class="php">class Message
{
private $_fromMe = false;
private $_forwarded = false;
private $_inGroup = false;
private $_remote; // Contact | NULL
private $_receivedAt; // DateTime
private $_text; // string
private $_group; // Contact | NULL
// ...
}
</code></pre>
<h3>ChatExporter</h3>
<p>De kern van het script huist in de klasse <code>ChatExporter</code>.</p>
<pre><code class="php">class ChatExporter
{
private $_addressBook = []; // Contact[]
private $_me; // Contact
private $_messages = []; // Message[]
private $_outputDirecory; // string
// ...
}
</code></pre>
<p>De argumenten die via de commandoprompt aan het script worden meegegeven
komen via de constructor in de ChatExporter terecht:</p>
<pre><code class="php">public function __construct(
string $abDbFilename, string $msgDbFilename, string $mePhoneNumber,
string $outputDirectory
) {
$meJID = trim($mePhoneNumber, "+")."@s.whatsapp.net";
$this->_outputDirecory = $outputDirectory;
// ...
}
</code></pre>
<p>Daarna wordt het adresboek geopend en de ID's en namen als contactpersonen
opgeslagen.</p>
<pre><code class="php">$wa = new \SQLite3($abDbFilename, SQLITE3_OPEN_READONLY);
$res = $wa->query(
"SELECT `jid`, `display_name`, `wa_name` FROM `wa_contacts`"
);
// Add contacts to local address book
while ($row = $res->fetchArray(SQLITE3_ASSOC)) {
if (!empty($row['display_name']) || !empty($row['wa_name'])) {
$n = $row['wa_name'] ? '('.$row['wa_name'].')' : null;
$c = (new Contact())
->setJID($row['jid'])
->setDisplayName($row['display_name'] ?? $n);
$this->addContactToAddressBook($c);
if ($row['jid'] == $meJID) {
$this->setMe($c);
}
}
}
$wa->close();
</code></pre>
<p>De gegevens van de groepschats worden uit de tabel <code>chat</code> ingelezen en
eveneens aan het adresboek toegevoegd.</p>
<pre><code class="php">$db = new \SQLite3($msgDbFilename, SQLITE3_OPEN_READONLY);
$res = $db->query(
"SELECT `J`.`raw_string`, `C`.`subject`
FROM `chat` AS `C` JOIN `jid` AS `J` ON `C`.`jid_row_id` = `J`.`_id`"
);
// Add group chats as separate contacts to the address book
while ($row = $res->fetchArray(SQLITE3_ASSOC)) {
if (!empty($row['raw_string']) && !empty($row['subject'])) {
$this->addContactToAddressBook(
(new Contact())
->setJID($row['raw_string'])
->setDisplayName($row['subject'])
);
}
}
// ...
$db->close();
</code></pre>
<p>Tot slot worden alle berichten, per contactpersoon van het adresboek,
toegevoegd aan de collectie.</p>
<details open>
<summary>Code in- en uitklappen</summary>
<pre><code class="php">// Loop through address book...
foreach ($this->_addressBook as $jID => $contact) {
$stmt = $db->prepare(
"SELECT
`key_remote_jid`, `key_from_me`, `data`, `remote_resource`,
`media_caption`, `forwarded`, `timestamp`
FROM
`messages`
WHERE
`key_remote_jid` == :jid
AND `status` != 6
ORDER BY
`key_remote_jid` ASC,
`timestamp` ASC"
);
$stmt->bindValue(':jid', $jID, SQLITE3_TEXT);
if ($res = $stmt->execute()) {
// ... and fetch this contact's messages
while ($row = $res->fetchArray(SQLITE3_ASSOC)) {
$msg = (new Message())
->setFromMe($row['key_from_me'] == 1)
->setForwarded($row['forwarded'] == 1)
->setInGroup(
(strpos($row['key_remote_jid'], '-') !== false)
);
$remoteJid = $row['key_remote_jid'];
$groupJid = null;
if ($msg->isInGroup()) {
$remoteJid = $row['remote_resource'];
$groupJid = $row['key_remote_jid'];
}
$remoteContact = null;
if (!$msg->isFromMe()) {
$remoteContact = $this->_findContact($remoteJid)
?? (new Contact())
->setJID($remoteJid)
->setDisplayName(self::_jidToNumber($remoteJid));
}
$groupContact = $this->_findContact($groupJid);
$dt = (new \DateTime())
->setTimestamp(intdiv($row['timestamp'], 1000));
$fw = ($msg->isForwarded() ? '(forwarded) ' : '');
$mediaCaption = ' '.($row['media_caption'] ?? '');
$text = $fw.($row['data'] ?? '(media file)'.$mediaCaption);
$msg
->setRemote($remoteContact)
->setGroup($groupContact)
->setReceivedAt($dt)
->setText($text);
// Add the message to this contact's chats
$this->addMessage($contact, $msg);
}
}
}
</code></pre>
</details>
<h2>Export</h2>
<p>Het daadwerkelijke exporteren begint met het doorlopen van het actuele
adresboek. Voor alle contactpersonen (dus ook groepchats, maar zonder de
eigen gebruiker) wordt een nieuw export-bestand gemaakt.</p>
<pre><code class="php">public function exportAllChats() : int
{
$x = 0;
foreach ($this->_addressBook as $jID => $contact) {
if ($contact !== $this->_me) {
$o = $this->exportSingleChat($jID);
if (!empty($o)) {
$ok = file_put_contents(
$this->_outputDirecory.DIRECTORY_SEPARATOR.$jID.'.txt',
$o
);
if ($ok !== false) {
$x++;
}
}
}
}
return $x;
}
</code></pre>
<p><code>exportSingleChat()</code> is de functie die voor één contactpersoon (lees: één
specifiek <code>jID</code>) de export uitvoert. Bovenaan het exportbestand staan
alle deelnemers aan het gesprek (<code>participants</code>). Die lijst begint met
de eigen gebruiker (<code>$this->_me</code>). In groepschats worden alle andere
deelnemers vanuit de berichten toegevoegd. In privéchats wordt met de
functie <code>_findContact()</code> de betreffende gesprekspartner opgezocht. Daarna
wordt de uiteindelijke lijst in tekst opgebouwd binnenin een <code>array_walk()</code>.</p>
<pre><code class="php">public function exportSingleChat(string $jID) : string
{
if (!array_key_exists($jID, $this->_messages)) {
return '';
}
$o = '';
$msgs = $this->_messages[$jID];
$top1msg = reset($msgs);
$participants = [];
$participants[$this->_me->getJID()] = $this->_me;
if ($top1msg->isInGroup()) {
foreach ($msgs as $m) {
if ($m->getRemote()) {
$participants[$m->getRemote()->getJID()] = $m->getRemote();
}
}
} else {
$participants[$jID] = $this->_findContact($jID);
}
uasort(
$participants, function (Contact $c1, Contact $c2) {
return strcmp($c1->getDisplayName(), $c2->getDisplayName());
}
);
$participantsString = '';
$maxDisplayNameLength = 0;
array_walk(
$participants,
function (Contact $c, string $jid) use (
&$participantsString, &$maxDisplayNameLength
) {
$participantsString .= sprintf(
'; %s: %s%s',
self::_jidToNumber($jid),
$c->getDisplayName(),
PHP_EOL
);
$senderStringLength = mb_strlen($c->getDisplayName());
if ($maxDisplayNameLength < $senderStringLength) {
$maxDisplayNameLength = $senderStringLength;
}
}
);
// ...
}
</code></pre>
<h2>Finale met hobbels</h2>
<p>Elk exportbestand begint met een header. Hierin wordt met <code>sprintf()</code> de
starttijd van de export, de eventuele groepchat-naam en de deelnemers
met hun telefoonnummers getoond. Voor het formatteren van datum en tijd
maak ik gebruik van PHP's <a href="https://www.php.net/manual/en/class.intldateformatter.php"><code>IntlDateFormatter</code></a>.</p>
<pre><code class="php">$exportFileHeader = <<<b8a38c2a67185f5d7f25
; WA Chat Export v0.1
; Creates a readable export of WhatsApp's chat history
;
; Export started: %s
; Group chat: %s
;
; Participants in this chat:
%s
b8a38c2a67185f5d7f25;
$locale = 'nl_NL.utf8';
$tz = new \DateTimeZone('Europe/Amsterdam');
$dtNow = new \DateTime('now', $tz);
$dtfmt = new \IntlDateFormatter(
$locale, \IntlDateFormatter::NONE, \IntlDateFormatter::NONE,
$tz, \IntlDateFormatter::GREGORIAN, "yyyy-MM-dd HH:mm:ss xxx"
);
$o .= sprintf(
$exportFileHeader,
$dtfmt->format(new \DateTime('now', $tz)),
($top1msg->isInGroup() ? $top1msg->getGroup()->getDisplayName() : 'no'),
$participantsString
);
</code></pre>
<p>Onder de header volgen de berichten, voorafgegaan door een timestamp. Om
het visueel aantrekkelijk maar vooral makkelijk leesbaar te maken, heb ik
er voor gekozen om de namen/telefoonnumers rechts uit te lijnen met
daarachter de berichten links uitgelijnd. Berichten met meerdere regels
worden eveneens zover ingesprongen, dat het gesprek optimaal leesbaar
wordt.</p>
<pre><code class="php">foreach ($msgs as $m) {
$indent = 29 + $maxDisplayNameLength + 2;
$indentSpaces = str_repeat(' ', $indent);
$o .= sprintf(
'[%s] %'.$maxDisplayNameLength.'s: %s%s',
$dtfmt->format($m->getReceivedAt()),
$m->isFromMe()
? $this->_me->getDisplayName()
: $m->getRemote()->getDisplayName(),
str_replace("\n", "\n".$indentSpaces, $m->getText()),
PHP_EOL
);
}
</code></pre>
<p>Had ik eindelijk de export af en bladerde ik steekproefsgewijs door de
tientallen bestanden, kwam ik een tekortkoming van PHP's <code>sprintf()</code> op
het spoor. Zo gauw een contactpersoon bijvoorbeeld een é of ë in zijn
naam had, klopte het aantal spaties voor het uitlijnen niet meer.</p>
<p>Dit bleek te worden veroorzaakt door PHP's kijk op strings. Elk karakter
wordt daar met één byte gecodeerd. Moderne encodings zoals UTF-8 hebben
echter vaak 2 of meer bytes per karakter. Voor <code>strlen()</code> en aanverwante
functies zijn daarvoor de <a href="https://www.php.net/manual/en/book.mbstring.php"><code>mb_...()</code> functies</a> uitgevonden. Maar voor
<code>sprintf()</code> is er geen vergelijkbare <code>mb_sprintf()</code> beschikbaar.
Gelukkig zijn er enthousiaste ontwikkelaars die dat gat
<a href="https://www.php.net/manual/en/function.sprintf.php#89020">vakkundig opvullen</a>. Ik mocht er zelf uiteindelijk nog één kleine
fix aan toevoegen; zie de volledige broncode <a href="https://github.com/fwiep/wa-chat-export/blob/37715e3c22584b4dfb817d7000ce910dba1d6968/src/Utils.php#L73">op Github</a>.</p>
<h2>Conclusie</h2>
<p>Het schrijven van dit script en de omliggende code was een leuke uitdaging. Ik
heb weer eens gestoeid met een nieuwe database en net zolang aan de export
geschaafd totdat ik helemaal tevreden was. Ook het documenteren voor Github en
het schrijven van dit artikel waren uiterst leerzaam.</p>
<p>Wel wist ik, nog vóór het publiceren van dit script, dat een waarschuwing op
zijn plaats zou zijn. Berichtdiensten zoals Signal en (in dit geval) Whatsapp
maken gebruik van end-to-end versleuteling en gaan tot het uiterste om de
communicatie te beveiligen en ook de offline back-ups te versleutelen.</p>
<p>Handmatig goochelen met onversleutelde databases en het exporteren van chats als
platte tekst staan haaks op deze inspanningen. Zie dit script dan ook alsjeblieft
als een mooi vrijetijdsproject van de programmeur en wellicht een interessante
stukje lectuur, maar niet méér dan dat. Dankjewel!</p>
</div>2022-02-04T18:33:57+01:00https://www.fwiep.nl/blog/motog6-met-android-12l-en-fm-radioMotoG6 met Android 12L en FM-radio2022-10-13T12:00:14+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Al geruime tijd maak ik dankbaar gebruik van LineageOS als besturingssysteem voor
mijn Android-apparaten. Bij aankoop en liefst daarvóór zoek ik uit of er officiële
ondersteuning is, of ten minste een actieve groep ontwikkelaars die een recente
versie van het alternatieve systeem op het betreffende apparaat draaiend heeft
gekregen.</p>
<p>Voor mij is het dan een uitdaging om bij een update van de LineageOS broncode
een nieuwe versie te bouwen met die aanpassingen ingebakken. Met name de maandelijkse
veiligheidspatches van Google zorgen er voor dat een Android apparaat nét dat
beetje veiliger gebruikt kan worden.</p>
<h2>TL;DR</h2>
<p>Mijn meest recente LineageOS 19.1 build (Android 12L) is te vinden op
<a href="https://forum.xda-developers.com/t/r.4480349/post-87287595">het XDA-forum</a>. Daarnaast voorzie ik ook elke maand in een
<a href="https://forum.xda-developers.com/t/r.4469945/">LineageOS 18.1 build</a> (Android 11). De downloads worden gehost op een
zelfgebouwde <a href="https://github.com/fwiep/fwiepdl">open-source download-service</a>.</p>
<h2>Dierentuin</h2>
<p>Tot zover de theorie en de algemene situatie. In mijn geval begon de leerweg met
de Ascend Y550 van Huawei, een Android 4.4 toestel uit 2014, waar uiteindelijk
LineageOS 14.1 (Android 7.1) op draaide. Google's laatste beveiligingsupdate voor
Android 7 kwam in juni 2021 – dus daar zal dit toestel voor altijd op blijven
steken.</p>
<p>Daarna volgde de Galaxy Tab S2, een Android 7 tablet van Samsung uit 2016. LineageOS
bood toendertijd officiële ondersteuning, tot en met versie 16.0 (Android 9).
Sinds januari 2022 zijn er geen beveiligingsupdates meer verschenen, dus ook dit
toestel is qua software jammer genoeg afgeschreven.</p>
<p>Met de Lenovo Tab M10, een Android 8 toestel uit 2017, volgde een tweede tablet,
waar geen specifiek ROM voor beschikbaar was, maar een zogenaamd Generic System
Image (<em>GSI</em>). Dit is een generiek, algemeen ROM waarbij alleen het
fabrikant-specifieke gedeelte (<em>vendor</em>) bij het apparaat moet passen en kan worden
hergebruikt uit de originele firmware. In dit geval was dat LineageOS 17.1
(Android 10) met de onderbouw van Lenovo (Android 9).</p>
<h2>Moto G6</h2>
<p>Na een eerdere ervaring met de G5 smartphone van Motorola, besloten we de opvolger
in huis te halen: een Motorola G6 (2018, Android 9). Dit was een betaalbaar toestel
met de boven genoemde vrijwillige, onofficiële ondersteuning voor het draaien van
LineageOS (17.1, Android 10). In de loop van de tijd werd deze ondersteuning
uitgebreid tot LineageOS 18.1 (Android 11). Op het moment van schrijven is zelfs
Android 12.1 (12L) beschikbaar.</p>
<p>Het bouwen van de maandelijkse ROMs is een leuke en vooral leerzame klus. Ik kwam
er door in contact met ontwikkelaars van over de hele wereld. In eerste instantie
vroeg ik hulp om het bouwen überhaupt te laten slagen. Daarna wilde ik méér en
probeerde een functie toe te voegen: FM-radio, waar met behulp van een bedraad
headset of hoofdtelefoon als antenne, de lokale ether wordt afgezocht naar de
zenders die op dat moment beschikbaar zijn.</p>
<p>Ik voegde de betreffende app toe aan het bouwmanifest en startte de build. Na
installatie wachtte ik vol spanning of de app zou starten en stelde vast…
dat ze dat niet deed. In het systeemlogboek vond ik de volgende melding:</p>
<pre><code class="plain">I android_hardware_fm: FM: loading FM-JNI
E android_hardware_fm: open_dev failed, [fd=-1] /dev/radio0
D android_hardware_fm: OpenFd, [ret=-1]
W android.fmradio: type=1400 audit(0.0:5661): avc: denied { read } for uid=1000 name="radio0" dev="tmpfs" ino=14169 scontext=u:r:system_app:s0 tcontext=u:object_r:fm_radio_device:s0 tclass=chr_file permissive=0
</code></pre>
<p>Ik vroeg mijn medestrijders om raad en de oorzaak bleek te liggen in het
ontbreken van een SELinux toestemming (<em>permission</em>) voor het openen van het
radio-apparaat. Toen ik deze toevoegde, was de FM-ondersteuning een feit!</p>
<pre><code class="bash">echo -e "\n\nallow system_app fm_radio_device:chr_file { ioctl open read };" >> \
device/motorola/msm8953-common/sepolicy/vendor/system_app.te
</code></pre>
<p>Uiteindelijk heeft de hoofdontwikkelaar van de groep
<a href="https://github.com/brunorolak/device_motorola_msm8953-common/commit/ebc3ec6bf0b9e6820ac8b424846d150ce053675a">mijn aanpassing overgenomen</a> in zijn code, waardoor nu alle builds die
zijn basis gebruiken, er van profiteren. <strong>Zo</strong> werkt open-source! :)</p>
</div>2022-10-13T12:00:14+02:00https://www.fwiep.nl/blog/youtube-download-in-delen-met-lijmYoutube download in delen, met lijm2022-06-16T21:55:17+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>In een nostalgische bui ging ik dezer dagen op zoek naar een televisieserie
uit mijn jeugd: <a href="https://www.imdb.com/title/tt0059968/">Batman (1966)</a>. Op Youtube vond ik een playlist met de
naam <a href="https://www.youtube.com/watch?v=_ccPgtFDdYw&list=PLOFSuAaSzTmVRZwsAktUZ8r3nfUsOngvX">"The Complete Series"</a>. Het waren 41 fragmenten van ongeveer 5 minuten
per stuk. Na het eerste fragment begon automatisch het tweede, dat daarop
aansloot: mooi!</p>
<p>Maar een hele reeks fragmenten kijken, met na elk fragment een scherm vol
Youtube-aanbevelingen; al dan niet met minuten lang reclame rondom? Nee, dat
gaan we anders doen…</p>
<h2>yt-dlp</h2>
<p>De eenvoudigste manier om video's van (onder andere) Youtube te downloaden is met
behulp van <a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a>, de opvolger van youtube-dl. Eerstgenoemde project wordt
actief doorontwikkeld en continu up-to-date gehouden. De bediening is nagenoeg
gelijk aan de voorganger. Met één simpel commando vonden de clips hun weg naar
mijn harde schijf:</p>
<pre><code class="bash">yt-dlp "https://www.youtube.com/watch?v=..."
</code></pre>
<h2>ffmpeg</h2>
<p>Toen wilde ik de fragmenten graag geautomatiseerd samenvoegen, achter elkaar plakken,
of in het Engels: <em>to concatenate</em>. Ik besloot mijn favoriete audio-video-tool
om hulp te vragen en zie daar; <code>ffmpeg</code> biedt een filter om precies deze functie
uit te voeren.</p>
<pre><code class="bash">ffmpeg -f concat -i list-of-files.txt output.webm
</code></pre>
<p>Het programma verwachtte dan wel een lijst van de samen te voegen video's
(<code>list-of-files.txt</code>) in een specifiek formaat:</p>
<pre><code class="plain">file 'video1.webm'
file 'video2.webm'
file 'video3.webm'
...
</code></pre>
<h2>Goochelen met bestandsnamen</h2>
<p>Natuurlijk is het leven niet zo eenvoudig als het op het eerste oog lijkt. In
deze specifieke playlist hadden de clips geen volgnummer, maar slechts een titel
die geen enkel verband hield met de volgorde in de reeks. Hoe zou ik nu het
lijstje voor <code>ffmpeg</code> kunnen samenstellen? De clips waren wel in volgorde
gedownload en op mijn harde schijf opgeslagen. <em><code>ls</code> to the rescue!</em>.</p>
<p>Maar er was nog een probleem: de apostrofs in de bestandsnamen; <code>ffmpeg</code> zou
daar beslist over gaan klagen bij het uitlezen van de lijst. Oké, dan gooien we
er nog wat <code>sed</code>-magie tegenaan:</p>
<pre><code class="sh"># list all webm-files, sort in reverse by birth time
# pipe to sed, escaping all apostrophes "'" with "'\''"
# prepend "file '" to all filenames
# append "'" to all filenames
ls --reverse --time=birth *.webm | sed \
-e "s/'/'\\\\''/g" \
-e "s/^/file '/" \
-e "s/$/'/" \
> list-of-files.txt
</code></pre>
<h2>Finale</h2>
<p>Met dit commando kreeg ik een mooi lijstje van bestandsnamen voor <code>ffmpeg</code>. Toen
ik het uitvoerde, klaagde het programma nog één laatste keer: de clips hadden
'onveilige' tekens in de bestandsnamen, met name het uitroepteken (<code>!</code>). Omdat
dat in dit geval geen bezwaar was, besloot ik de waarschuwing uit te schakelen
met de <code>-safe</code> optie – zie ook de <a href="https://ffmpeg.org/ffmpeg-formats.html#Options">ffmpeg documentatie</a>.</p>
<pre><code class="sh">ffmpeg -f concat -safe 0 -i list-of-files.txt -c copy output.webm;
</code></pre>
<p>Klaar!</p>
</div>2022-06-16T21:55:17+02:00https://www.fwiep.nl/blog/verloren-seconde-excel-vs-calcVerloren seconde: Excel vs. Calc2022-04-14T19:47:55+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Al enige tijd maak ik dankbaar gebruik van <a href="https://github.com/PHPOffice/PhpSpreadsheet">PhpSpreadsheet</a>, een project om
comfortabel te werken met spreadsheets in PHP. Het ondersteunt verschillende
bestandsformaten voor inlezen en uitvoer, zoals <code>.XLSX</code> (Microsoft) en <code>.ODS</code>
(OpenDocument).</p>
<p>Als fervent voorstander van opensource software, gebruik ik onder andere GNU/Linux
als besturingssysteem met <a href="https://nl.libreoffice.org/">LibreOffice</a> als kantoorpakket. Met laatstgenoemde
was echter iets bijzonders aan de hand: er leek in Calc (de spreadsheet-component)
één seconde verschil in weergave van een datum + tijd-kolom, ten opzichte van
dezelfde kolom in Microsoft Excel onder Windows of Android.</p>
<h2>Probleem</h2>
<p>In de door mijn code gegenereerde spreadsheet was één kolom gevuld met een
geformatteerde datum en tijd. Er stond bijvoorbeeld <code>zo 31-07-2022 10:59</code>. Maar
ik wist zeker dat in mijn applicatie (en de achterliggende database) een tijd
van <code>11:00</code> stond ingevoerd. Ook halve uren waren nét te kort: <code>9:30</code> in mijn
applicatie werd <code>09:29</code> in Calc. Wat was hier aan de hand?</p>
<h2>Oorzaak</h2>
<p>Aldus ging ik op zoek en verdiepte me in de <a href="https://www.myonlinetraininghub.com/excel-date-and-time">formattering van datum en tijd</a>
binnen Excel en Calc. Het vrije programma bleek zo goed als 100% compatibel met
het product uit Redmond, dus daar mocht het niet aan liggen. Pas toen ik ontdekte
hoe Excel achter de schermen met datums en tijden werkt, snapte ik wat er
gebeurde: ze worden opgeslagen als decimaal getal met de datum <em>vóór</em>, en de
tijd <em>achter</em> de komma.</p>
<pre><code class="plain">zo 24-07-2022 09:29 => 44766,395833333
</code></pre>
<p>Een dag heeft 24 uur, elk uur 60 minuten, elk uur 60 seconden. Eén dag heeft dus
<code>24</code> × <code>60</code> × <code>60</code> = <code>86400</code> seconden. Om in Excel (en Calc) een tijd
te formatteren met een preciesie van één seconde, moet je dus met eenheden van
(<code>1</code> / <code>86400</code>) = <code>0,00001157407407407410</code> werken. Voor een precisie
van één minuut zal dit (<code>1</code> / <code>1440</code>) = <code>0,00069444444444444400</code> zijn.
De tijd-kolom moet dus ook een minimum aantal decimalen hebben om correct te
kunnen formatteren.</p>
<p>Toen ik in dit voorbeeld van de laatste <code>3</code> een <code>4</code> maakte, versprong de
geformatteerde tijd (minuut) naar de gewenste waarde. Blijkbaar rondt Excel bij
het formateren van tijd naar boven af, en Calc niet.</p>
<pre><code class="plain">44766,395833334 => zo 24-07-2022 09:30
</code></pre>
<h2>Oplossing</h2>
<p>Mijn relatief eenvoudige oplossing was, om een tweede datum-tijd-kolom aan de
spreadsheet toe te voegen. Deze gaf ik dezelfde opmaak (formattering) als de
bestaande kolom: <code>DDD DD-MM-JJJJ UU:MM</code> met de Nederlandse regio-instellingen.
In de cel plaatste ik vervolgens deze formule (voorbeeld rij 2):</p>
<pre><code class="plain">=ROUNDUP(B2; 5)
</code></pre>
<p>Hierdoor werd elke tijd naar boven afgerond op 5 decimalen. Dit was voldoende om
de weergave op minuten te laten kloppen. Tot slot verborg ik nog de oude kolom
en klaar!</p>
<p>P.S: Ik had nog getwijfeld of ik de getoonde formule niet direct zou integreren
in mijn PHP code die de spreadsheet genereert. Maar dan zou die kolom alleen
deze formule en het decimale getal bevatten; niet bepaald handig om te kopiëren
en plakken om daarna aan te passen. Een mens rekent nou eenmaal niet zo
makkelijk met cijfers… :-)</p>
</div>2022-04-14T19:47:55+02:00https://www.fwiep.nl/blog/van-router-naar-baksteen-en-terugVan router naar baksteen en terug2021-09-18T17:13:59+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Al geruime tijd rolt de <a href="https://forum.kpn.com/kpncyclopedie-105/ipv6-461918#post773625">internetprovider KPN</a> IPv6 uit bij haar
klanten. Exacte cijfers over wanneer en hoeveel gebruikers al 'over'
zijn van IPv4 naar de nieuwe variant, zijn niet bekend. Maar afgelopen
week was het dan toch eindelijk de beurt aan onze internetaansluiting.
Het modem kreeg nieuwe firmware en daarbij ook een nieuw IPv4-adres.
Laten we nou net <em>die</em> week op vakantie zijn en buitenshuis
verblijven…</p>
<p>Tijdens die vakantie ontving ik ook een nieuwsbrief van <a href="https://openwrt.org/">OpenWrt</a>,
het open-source besturingssysteem voor onder andere routers. Vol trots
werd daarin de nieuwste release, versie <code>21.02</code>, aangekondigd. Ik besloot
die zo snel mogelijk te installeren als ik weer thuis was!</p>
<h2>Samenloop van omstandigheden</h2>
<p>Ik koos er voor om de twee ontwikkelingen te combineren en de router,
vanwege de nieuwe IPv6 adressen, volledig opnieuw te installeren. Zo
wist ik zeker dat eventuele oude (IPv6-)instellingen niet dwars zouden
liggen bij het out-of-the-box werken van een standaard OpenWrt-router.</p>
<p>Van eerdere updates was ik gewend dat ik het <a href="https://openwrt.org/toh/zyxel/nbg6716">bij OpenWrt</a> gedownloade
bestand zonder kleerscheuren kon uploaden in de grafische LuCI webinterface.
Dit keer kreeg ik, vlak voor het flashen, een waarschuwing te zien: dit
bestand was niet geschikt voor deze router. "Dat kan niet, de naam klopt
precies!" dacht ik – en flashte vrolijk verder. Even later was de
router niet meer te bereiken; ook niet na een volledige reset. De router
was dood; zoveel waard als een baksteen, oftewel een <em>brick</em>.</p>
<h2>Onderzoek (1)</h2>
<p>Uit eerdere experimenten met oude routers wist ik, dat de meeste exemplaren
minimaal één mogelijkheid van reanimatie bieden. Meestal in de vorm van
een seriële console die van buiten te bereiken is als je het apparaat
openschroeft. Gelukkig stond in de documentatie van mijn Zyxel router
een foto van zo'n <a href="https://openwrt.org/toh/zyxel/nbg6716#serial">provisorische verbinding</a>: <code>TX</code>, <code>RX</code> en <code>GND</code>. Bij
het aansluiten van zo'n seriële console moet je wel opletten dat de zender
van de ene kant (<code>TX</code>) aangesloten wordt op de ontvanger (<code>RX</code>) van de
andere kant; en vice versa. <code>GND</code> is de gemeenschappelijke massa.</p>
<p>Nadat ik de USB-naar-serieel converter met CP2102 chipset had aangesloten,
wilde ik met het commando <code>screen /dev/ttyUSB0 115200</code> de nieuwe seriële
poort openen, maar dat lukte niet. Wat blijkt? Onder Fedora moet de
gebruiker deel uitmaken van de groep <code>dialout</code> om modems (en dus ook
andere seriële verbindingen) te kunnen gebruiken:</p>
<pre><code class="bash"># Add fwiep to the dialout group, to use serial over USB
sudo usermod -aG dialout fwiep
</code></pre>
<p>Bij de zoveelste poging volgde ik opnieuw de instructies en zag helemaal
niets op mijn scherm. De router leek nog steeds zo dood als een pier.</p>
<h2>Eureka (1)</h2>
<p>Uit zowel frustratie als nieuwsgierigheid besloot ik om de <code>TX</code> en <code>RX</code>
eens om te draaien. Elektrisch kon er niets kapot gaan en misschien zou
er zo <strong>wel</strong> iets op het scherm verschijnen tijdens het opstarten van
de router. Eureka! Ik zag de volgende informatie, maar bij het lezen van
de laatste regels zakte de moed opnieuw in mijn schoenen…</p>
<pre><code class="plain">U-Boot 2009.11 (Sep 26 2014 - 18:07:51)
NBG6716 - Scorpion 1.0
DRAM: 32bit ddr2 256 MB
Flash: 16 MB
*** Warning *** : PCIe WLAN Module not found !!!
Net: eth0, eth1
NAND: Hynix NAND 128MiB 3,3V 8-bit [128MB]
ZyXEL zloader v1.31 (Sep 26 2014 - 18:33:33)
Multiboot clinent version: 1.2
could not establish link on eth0
eth_init failed!
### JFFS2 loading '/boot/vmlinux.lzma.uImage' to 0x80400000
Scanning JFFS2 FS: '/boot/vmlinux.lzma.uImage' found, Scanning whole partition done
Loading file: done
### JFFS2 load complete: 3520087 bytes loaded to 0x80400000
## Booting kernel from Legacy Image at 80400000 ...
Image Name: MIPS OpenWrt Linux-5.4.143
Created: 2021-08-31 22:20:08 UTC
Image Type: MIPS Linux Kernel Image (uncompressed)
Data Size: 6736290 Bytes = 6.4 MB
Load Address: 80060000
Entry Point: 80060000
Verifying Checksum ... Bad Data CRC
ERROR: can't get kernel image!
!!! Fail to booting kernel !!!
Reset your board! system halt...
</code></pre>
<h2>Onderzoek (2)</h2>
<p>Er moest toch een mogelijkheid zijn om de router opnieuw tot leven te
wekken? Jawel, de documentatie vermeldt het gebruik van een zogenaamde
TFTP-server die firmware aanbiedt, en dat de router die tijdens het
opstarten kan inladen en zelfstandig flashen. Maar hoe moest dat dan in
zijn werk gaan?</p>
<h2>TFTP</h2>
<p>De afkorting <code>TFTP</code> staat voor <em>trivial file transfer protocol</em>: een
heel eenvoudig protocol om bestanden over te dragen. Een server biedt
bestanden aan, een client downloadt die bestanden, klaar. Mijn Zyxel
router was zo ingesteld dat hij, als je bij het opstarten de <code>WPS</code>-knop
ingedrukt houdt, zelfstandig met zijn TFTP-client op zoek gaat naar het
bestand <code>ras.bin</code> op de server <code>192.168.1.33</code>.</p>
<p>Op dit moment (najaar 2021) is de originele firmware bij Zyxel niet (meer)
beschikbaar. Gelukkig kon ik haar wel nog uit mijn eigen archieven vissen
en stel ze nu als <a href="https://dl.fwiep.nl/GH3sXB" title="" rel="nofollow">download</a> ter beschikking op mijn zelfgebouwde
downloadportaal.</p>
<pre><code class="bash"># Install the TFTP-server
sudo dnf install -y tftp-server
sudo systemctl start tftp.service
</code></pre>
<p>Met bovenstaand commando wordt de TFTP-server geïnstalleerd en gestart.
Alle bestanden in <code>/var/lib/tftpboot</code> worden geserveerd aan TFTP-clients
die via het netwerk aankloppen.</p>
<p>Aldus probeerde ik opnieuw de router zijn firmware te laten downloaden
en hield de TFTP-server in het oog met <code>journalctl -b --follow</code>; als er
een overdracht plaatsvond, zou ik het hier zeker zien! Nee dus…</p>
<h2>Eureka (2)</h2>
<p>Na een nachtje slapen besloot ik dan toch maar hulp te vragen op het
forum van OpenWrt. Wat blijkt? Er was al iemand die <a href="https://forum.openwrt.org/t/zyxel-nbg6716-update-to-21-02-0-rc4/103638/15">dezelfde vraag</a>
stelde en antwoord kreeg. Hij gaf aan dat het gebruik van de TFTP-server
de oplossing was om de router terug leven in te blazen. Maar dat wilde
toch bij mij niet lukken?</p>
<p>Toen scrolde ik door de discussie onder zijn vraag en zag de hint naar
mijn uiteindelijke oplossing: de firewall van de server blokkeerde de
TFTP-verbinding! De volgende drie commando's maakten daar vakkundig een
einde aan:</p>
<pre><code class="bash">sudo firewall-cmd --add-service=tftp
sudo firewall-cmd --reload
sudo systemctl start tftp.service
</code></pre>
<p>Na opnieuw starten van de TFTP-server en de router (met ingedrukte
<code>WPS</code>-knop) kon ik in de nog steeds aangesloten seriële console het
flashen volgen. Na nog één keer opnieuw opstarten begroette me het
Zyxel loginscherm van de originele firmware op <code>http://192.168.1.1</code>.
Eindelijk!</p>
<h2>Finale</h2>
<p>Tot slot flashte ik de nieuwe firmware van OpenWrt door het bestand
te hernoemen naar <code>ras.bin</code> en in de map van de TFTP-server te
plaatsen. Ook dit werd zonder morren gedownload via het netwerk en in
het geheugen van de router weggeschreven. De router is dood, leve de
router!</p>
</div>2021-09-18T17:13:59+02:00https://www.fwiep.nl/blog/streamen-van-npo-nieuws-via-vlcStreamen van NPO Nieuws via VLC2021-09-12T21:21:18+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>De Nederlandse Publieke Omroepen stellen <a href="https://www.npostart.nl/live/npo-nieuws">een live stream</a> beschikbaar met 24
uur per dag het laatste nieuws. Deze pagina vereist echter onder Linux nog
steeds het gebruik van Adobe Flash. Deze plug-in staat bekend om zijn
<a href="https://krebsonsecurity.com/tag/adobe-flash-player/">talloze beveiligingslekken</a> op alle platformen.</p>
<p>Daarom wilde ik graag een manier om de livestream te bekijken in mijn favoriete
mediaspeler: <a href="https://www.videolan.org/vlc/">VLC</a>, buiten de site en lekke browser-plug-in om.</p>
<h2>Update (2)</h2>
<p>Vanaf begin 2019 is alle content van de NPO beveiligd met DRM-technologie.
Hierdoor is het wel in een browser of via hun eigen app te bekijken, maar
niet te voor offline gebruik te downloaden. Jammer, maar helaas.</p>
<h2>Update</h2>
<p>Het onderstaande script werkt niet meer naar behoren en is per mei 2017
achterhaald. Ik heb inmiddels een <a href="https://www.fwiep.nl/blog/download-npo-streams-zonder-browser">opvolger</a> geschreven die voor alle
NPO live-streams geschikt is.</p>
<h2>Onderzoek</h2>
<p>Aldus zocht ik een manier om de video-playlist die door de Flash plug-in wordt
afgespeeld zelfstandig op te roepen. Maar, wat blijkt? De URL van dit
<code>.m3u8</code>-bestand wordt dynamisch bepaald op het moment van opvragen. Dit werd
toch een stukje complexer dan een simpele URL kopiëren... Met behulp van
<a href="https://www.pcwdld.com/firebug-alternatives-javascript-debugging-tools">FireBug</a> onderzocht ik alle HTTP-verzoeken.</p>
<h2>Script</h2>
<p>In onderstaand script wordt in de variable <code>PLAYER</code> de aan te spreken
mediaspeler ingesteld (in dit geval <code>vlc</code>). Daarna wordt gecontroleerd of de
mediaspeler en <a href="https://curl.se/">cURL</a> geïnstalleerd en toegankelijk zijn. Dan volgt de rest
van het script in vier stappen:</p>
<ol>
<li>Als eerste wordt de stream-pagina opgeroepen</li>
<li>Daarna wordt in het antwoord gezocht naar een URL met de string <code>callback=?</code></li>
<li>Deze callback-URL wordt opgeroepen</li>
<li>In het antwoord bevindt zich de uiteindlijke <code>.m3u8</code>-URL</li>
</ol>
<p>De <code>cURL</code>-headers, zoals <code>Host</code>, <code>User-Agent</code>, <code>Referer</code> en <code>Cookie</code> zijn
rechtstreeks overgenomen van mijn eerste HTTP-verzoek met FireFox en FireBug.
Let met name op de <code>-d</code> parameter waarin de data wordt meegestuurd (stap 1).</p>
<p>Uiteindelijk wordt de ingestelde mediaspeler gestart met de gevonden URL als
argument. De livestream verschijnt nu, zonder gebruik van een internetbrowser
of plug-in, desgewenst in volledig scherm.</p>
<pre><code class="bash">#!/bin/bash
# Set prefered video player
PLAYER="vlc";
# Check whether the video player is installed
if ! hash "${PLAYER}" 2>/dev/null; then
echo "${PLAYER} is needed for playing back the downloaded playlist. Please
install it, or add it to your PATH.";
exit 1;
fi;
# Check whether cURL is installed
if ! hash "curl" 2>/dev/null; then
echo "cURL is needed for downloading the playlist. Please install it, or add
it to your PATH.";
exit 1;
fi;
# Step 1, request page
STEP1="$(
curl -X POST --silent \
-H "Host: www-ipv4.nos.nl" \
-H "User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:44.0)
Gecko/20100101 Firefox/44.0" \
-H "Accept: */*" \
-H "Accept-Language: en-US,en;q=0.5" \
-H "Accept-Encoding: gzip, deflate" \
-H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \
-H "Referer: http://nos.nl/livestream/npo-nieuws.html" \
-H "Content-Length: 73" \
-H "Origin: http://nos.nl" \
-H "Connection: keep-alive" \
-d '{"stream":"/live/npo/thematv/journaal24/journaal24.isml/journaal24.m3u8"}' \
--compressed \
\
http://www-ipv4.nos.nl/livestream/resolve/ \
)";
# Step 2, filter response from step 1
STEP2="$(
echo -n ""${STEP1}"" | grep -oP 'http.*?callback=\?' | sed -e 's/\\\//\//g'
)";
# Step 3, request new URL
STEP3="$(
curl -X GET --silent \
-b "balancer%3A%2F%2Flive2cluster=balancer.live2b" \
-H "Host: livestreams.omroep.nl" \
-H "User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:44.0)
Gecko/20100101 Firefox/44.0" \
-H "Accept: */*" \
-H "Accept-Language: en-US,en;q=0.5" \
-H "Accept-Encoding: gzip, deflate" \
-H "Referer: http://nos.nl/livestream/npo-nieuws.html" \
-H "Cookie: balancer://live2cluster=balancer.live2b" \
-H "Connection: keep-alive" \
-d '' \
\
""${STEP2}""
)";
# Step 4, filter response from step 3
STEP4="$(
echo -n ""${STEP3}"" | grep -oP 'http.*?\.m3u8' | sed -e 's/\\\//\//g'
)";
# Start player with the final .m3u8-URL
eval "${PLAYER} ${STEP4} &";
# Exit normally
exit 0;
</code></pre>
</div>2016-12-27T15:44:12+01:00https://www.fwiep.nl/blog/mijn-braille-leesregel-en-brlttyMijn braille-leesregel en BrlTTY2021-09-08T23:04:53+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Sinds mijn revalidatie bij Visio 't Loo Erf in Apeldoorn (zie
<a href="https://www.fwiep.nl/slechtziendheid">slechtziendheid</a>) lees ik Braille. Het duurt weken voordat je je eerste
woordjes voelt en je de patronen van de verschillende letters in je hoofd hebt.
Maar als je die periode hebt gehad, heb je een nieuwe manier van informatie
opnemen tot je beschikking: je kunt lezen zonder te kijken!</p>
<h2>Braille</h2>
<p>Brailleschrift op papier bestaat uit zes puntjes, op de computer zijn dat er
acht. Omdat iedere taal zijn eigen tekens kan hebben, heeft bijna elke taal een
eigen brailletabel. Daarin wordt een bepaald teken gekoppeld aan een bepaald
braille-patroon. Bij het leren lezen kies je meestal voor één bepaalde tabel. Ik
koos de Europese - een gemene deler van alle west-europese talen.</p>
<h2>BrlTTY</h2>
<p>Het standaard stuurprogramma voor braille-leesregels onder GNU/Linux is
<a href="https://brltty.app/">BrlTTY</a>. Het kent bijna alle gangbare merken en types en is goed
gedocumenteerd. In mijn geval werd de <a href="http://www.freedomscientific.com/Products/Blindness/Focus40BrailleDisplay">Focus 40 blue</a> van Freedom Scientific
meteen herkend en verschenen de eerste tekens op het brailledisplay. Ik had niet
lang nodig om te ontdekken dat BrlTTY standaard gebruik maakt van de Amerikaanse
brailletabel.</p>
<p>Aldus ging ik op zoek naar een mogelijkheid om gebruik te maken van de
Europese tabel. De documentatie stelde dat in <code>/etc/brltty.conf</code> alle
instellingen konden worden aangepast. Ja, er stonden maar liefst 86
verschillende tabellen ter beschikking - maar niet de Europese. Ik moest op zoek
naar een andere manier.</p>
<h2>De Europese tabel</h2>
<p>Met de hulp van de mensen van Visio en <a href="http://www.braille.ch/eb-erl-g.htm">www.braille.ch</a> kon ik handmatig alle
tekens en patronen verzamelen en zelf een eigen texttable (<code>.ttb</code>) samenstellen.
Voor de liefhebbers staat zij ook als <a href="https://www.fwiep.nl/download/1295939c-4a98-45c7-b5ab-f6cfbd3b5c6d/fwiep.ttb">download</a> beschikbaar. (Een oplettende
lezer zal opmerken dat mijn texttable is gegenereerd met behulp van een script.
Wellicht wijd ik daar nog eens een andere blogpost aan. Wie weet :-) )</p>
<h2>Persoonlijke voorkeuren</h2>
<p>Voor de persoonlijke instellingen zoals het wel-of-niet knipperen van de
braille-cursor en de hardheid van de braille-puntjes maakt BrlTTY gebruik van
een menu dat via de leesregel zelf wordt opgeroepen en bediend. Druk daartoe (op
de leesregel): <kbd>Spatie</kbd> + <kbd>1</kbd> + <kbd>2</kbd> + <kbd>3</kbd> +
<kbd>4</kbd> (Spatie + P). Er verschijnt kort 'Preferences menu', daarna het
eerste menu-item.</p>
<p>Mijn eigen voorkeursinstellingen zijn de volgende:</p>
<ul>
<li>hoofdmenu
<ul>
<li>item Save On Exit: Yes</li>
</ul></li>
<li>menu Braille Presentation
<ul>
<li>item Braille Firmness: High</li>
</ul></li>
<li>menu Text Indications:
<ul>
<li>item Blinking Screen Cursor: Yes</li>
</ul></li>
</ul>
<p>Zie hier een link naar de <a href="https://brltty.app/doc/KeyBindings/brl-fs-focus40.html">specifieke commando's en sneltoetsen</a> voor BrlTTY
en de Focus 40.</p>
<h2>Update</h2>
<p>Bij het inrichten van mijn leesregel op een <a href="https://www.fwiep.nl/blog/nieuwe-uitdaging-fedora-met-selinux">recente Fedora</a> GNU/Linux,
bleek dat ik in mijn documentatie een aantal zaken niet had genoteerd. Zo
moet de systeemgebruiker deel uitmaken van de grooep <code>brlapi</code> om de
leesregel op de grafische desktop te gebruiken. Dat kan bijvoorbeeld met
het volgende commando:</p>
<pre><code class="sh"># Add fwiep to the brlapi group, to use BrlTTY in GUI
sudo usermod -aG brlapi fwiep
</code></pre>
<p>Daarnaast wordt de BrlTTY service niet standaard ingeschakeld of opgestart
bij het aansluiten van een leesregel. Dat kan alsnog met deze commando's:</p>
<pre><code class="sh"># Enable the BrlTTY service upon next system start
sudo systemctl enable brltty.service
# Immediately start the BrlTTY service
sudo systemctl start brltty.service
# Chech the current status of the BrlTTY service
sudo systemctl start brltty.service
</code></pre>
<p>Tot slot wilde de leesregel nog steeds niet correct starten; alle puntjes
van alle cellen kwamen tegelijk omhoog. Ik ging met laatstgenoemd
commando op zoek in de statusmeldingen van BrlTTY. Daar werd geklaagd
over een onvindbaar bestand <code>spaces.tti</code>.</p>
<p>Toen ik in 2016 mijn eigen tabel samenstelde, was het gebruikelijk om
alle mogelijke vormen van spaties op de leesregel samen te vatten als
een simpele spatie. Dit gebeurde met een <code>alias</code>-instructie in een
bestand dat in de tabel werd ingesloten (<code>include</code>). Dit bestand is
tegenwoordig verdwenen; het is verplaatst naar de map <code>Contraction</code> en
wordt als zodanig automatisch afgehandeld. Je hoeft hier dus in je tabel
geen rekening meer mee te houden. Mooi!</p>
</div>2017-01-09T13:18:38+01:00https://www.fwiep.nl/blog/romeinse-cijfers-in-phpRomeinse cijfers in PHP2021-08-09T17:46:03+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Romeinse cijfers worden niet vaak gebruikt, maar ik vond het deze week
eens tijd om de logica van dat systeem in PHP te gieten. Kabouter Wesley
zou terecht vragen: <a href="https://rubix.nl/de-kabouter-wesley-methode/">"Wat is daar 't praktisch nut van?"</a>. Een antwoord
blijf ik dit keer schuldig.</p>
<p>Eén en ander begint met de gebruikte symbolen, waarbij specifieke
letters staan voor een bepaalde waarde – ongeacht hun positie in
het getal. Hierbij valt meteen op dat er geen symbool is voor het cijfer
<code>0</code> en dat negatieve getallen niet kunnen worden genoteerd.</p>
<pre><code class="php"><?php
$symbols = [
1000 => 'M',
900 => 'CM',
500 => 'D',
400 => 'CD',
100 => 'C',
90 => 'XC',
50 => 'L',
40 => 'XL',
10 => 'X',
9 => 'IX',
5 => 'V',
4 => 'IV',
1 => 'I'
];
</code></pre>
<h2>Logica</h2>
<p>Aldus begon ik met de conversie van normale, decimale cijfers (positieve
gehele getallen) naar Romeinse notatie. Om geen extreem lange, onleesbare
getallen te krijgen, beperkte ik de code van <code>1</code> tot <code>4999</code>. Als jaartallen
kom je daarmee al een heel eind.</p>
<pre><code class="php"><?php
function dec2roman(int $i) : string
{
if ($i <= 0 || $i > 4999) {
return '?';
}
global $symbols;
$symbolKeys = array_keys($symbols);
$symbolValues = array_values($symbols);
$o = '';
foreach ($symbolKeys as $ix => $k) {
if ($i >= $k) {
$d = intdiv($i, $k);
$o .= str_repeat($symbolValues[$ix], $d);
$i -= ($d * $k);
}
}
return $o;
}
</code></pre>
<p>De functie loopt door de bekende symbolen en kijkt of de waarde van het
huidige symbool in het getal past. Zo ja, dan wordt dat aantal keer het
symbool toegevoegd aan de output. De input wordt verminderd met diezelfde
factor.</p>
<p>Ik moet eerlijk toegeven: de volgorde en volledigheid van de lijst met
symbolen was wel bepalend voor het succes van deze functie. Ik had even
nogig om te beseffen dat de lijst van groot (<code>M</code> = 1000) naar klein (<code>I</code>
= 1) gesorteerd moest worden. En dat de buitenbeentjes (<code>CM</code> = 900, <code>CD</code>
= 400, <code>XC</code> = 90, <code>XL</code> = 40, <code>IX</code> = 9 en <code>IV</code> = 4) <em>in</em> deze lijst moesten
staan.</p>
<h2>Andersom</h2>
<p>Daarna volgde natuurlijk ook de omgekeerde berekening: van een Romeins
cijfer terug naar een decimaal getal. Hier maakte ik opnieuw gebruik van
de lijst met symbolen en hun waarden. Ik zocht mijn toevlucht tot een
uitgebreide <a href="http://regex.info/book.html">reguliere expressie</a>:</p>
<pre><code class="php">$romanNumeralRegexp
= "!^ # anchor the regex's start
(?<m>M*) # zero or more 'M's in group 'm'
(?<cm>CM)? # zero or one 'CM' in group 'cm'
(?:(?<d>D)|(?<cd>CD))? # either 'D' or 'CD' in separate groups
(?:(?<c>C{0,3})|(?<xc>XC))? # zero to three 'C's, or 'XC' in separate groups
(?:(?<l>L)|(?<xl>XL))? # either 'L' or 'XL' in separate groups
(?:(?<x>X{0,3})|(?<ix>IX))? # zero to three 'X's, or 'IX' in separate groups
(?:(?<v>V)|(?<iv>IV))? # either 'V' or 'IV' in separate groups
(?<i>I{0,3})? # zero to three 'I's in group 'i'
$!x"; // anchor the regex's end
</code></pre>
<p>Hiermee kon ik niet alleen een ingevoerd Romeins cijfer valideren, maar
ook meteen de verschillende onderdelen (symbolen) extraheren voor later
gebruik.</p>
<pre><code class="php">function roman2dec(string $s) : int
{
if (empty($s)) {
return -1;
}
global $symbols, romanNumeralRegexp;
$m = [];
if (!preg_match($romanNumeralRegexp, $s, $m)) {
return -1;
}
$o = 0;
foreach ($symbols as $v => $c) {
$factor = 1;
if (in_array($v, [1000, 100, 10, 1])) {
$factor = strlen($m[strtolower($c)]);
}
if (!empty($m[strtolower($c)])) {
$o += ($factor * $v);
}
}
return $o;
}
</code></pre>
<p>Als de reguliere expressie niet matcht, is er geen geldig cijfer ingevoerd;
de functie geeft dan <code>-1</code> terug. Daarna wordt de lijst van symbolen
doorlopen en bepaald hoeveel van dezelfde symbolen achter elkaar staan
(bij <code>M</code>, <code>C</code>, <code>X</code> en <code>I</code>). Tot slot volgt waar het daadwerkelijk om
draait: als de betreffende regex groep gevuld is, dan moet de waarde van
dat symbool (vermenigvuldigd met de factor) meetellen in de output.</p>
<h2>Conclusie</h2>
<p>Dit project was weer eens ouderwets code kloppen en logisch nadenken; een
welkome uitdaging. Met dank aan <a href="https://www.rapidtables.com/math/symbols/roman_numerals.html">rapidtables.com</a> voor een handige tabel
en zelfs een <a href="https://www.rapidtables.com/convert/number/roman-numerals-converter.html">online converter</a> ter controle.</p>
</div>2021-08-09T17:46:03+02:00https://www.fwiep.nl/blog/binaire-puzzels-oplossen-in-phpBinaire puzzels oplossen in PHP2021-05-18T22:11:33+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Afgelopen feestdagen kreeg ik een puzzelboekje cadeau met binaire puzzels. Al
jaren maakte ik soortgelijke, maar kleinere, puzzels in de wekelijkse tv-gids.
Nu kon ik zeker een aantal maanden vooruit!</p>
<p>Toen mijn helpende hand en ik eenmaal de vier mogelijke strategiën onder de knie
hadden, waren de meeste puzzels binnen een kwartier compleet. Maar soms zagen we
simpelweg niet waar de volgende stap zou zijn. De puzzel bleef dan voor alsnog
onopgelost.</p>
<p>Voor dát geval wilde ik een stuk code schrijven dat mij een hint zou geven met
welke strategie en in welke cel zich de volgende stap zou kunnen afspelen.
Hieronder zal ik het proces van de totstandkoming bespreken, gevolgd door een
korte analyse van de code zelf. Het eindresultaat is op <a href="https://www.fwiep.nl/binair/">FWieP.nl</a> te
bewonderen. De volledige broncode is sinds mei 2021 op <a href="https://github.com/fwiep/binary-solver">Github</a> beschikbaar.</p>
<h2>Update</h2>
<p>Meer dan drie jaar nadat ik dit artikel plaatste, nam John Segers met mij
contact op en meldde dat mijn oplosser zich niet aan zijn eigen regels hield.
Bij het oplossen van de door mij ingestelde voorbeeldpuzzel, bevatte de
uiteindelijke puzzel drie verticale enen onder elkaar – en dat is
absoluut niet toegestaan!</p>
<p>Met deze feedback ging ik op zoek in de code en vond de boosdoener. Na het
succesvol toepassen van een strategie op een rij of kolom werd niet
gecontroleerd of door deze stap de kruisende kolom of rij misschien ongeldig
werd. Toen ik deze check inbouwde, bleek mijn voorbeeldpuzzel onoplosbaar!
Dankjewel, John :).</p>
<p>Toen ik de code toch eenmaal onder handen had, besloot ik haar meteen
<a href="https://pear.php.net/manual/en/standards.php">PEAR-compatibel</a> te maken, zoals al mijn huidige projecten. Tot slot
gooide ik de volgorde van het toepassen van de verschillende strategiën om:</p>
<ul>
<li>eerst met strategie 1 alle rijen, dan kolommen</li>
<li>dan met strategie 2 alle rijen, dan kolommen</li>
<li>etc…</li>
</ul>
<h2>Aanpak</h2>
<p>Aangezien ik thuis ben in PHP besloot ik er een webapplicatie van te maken.
Eerst de logica en daarna de formulier(en) voor in de browser. Met die eerste
stap was ik een behoorlijk tijdje zoet, en liep uiteindelijk tegen een beperking
aan: ik kon bij één van de strategiën de vertaalslag van een procedure in mijn
hoofd naar code niet maken. Ik zag niet hoe ik de handelingen die ik moeiteloos
op papier uitvoerde, moest omzetten naar instructies voor mijn programma.</p>
<p>Gelukkig kwam er hulp uit onverwachte hoek. Karin Schaap, een bevriende
programmeur, bood aan om mee te kijken en haar steen bij te dragen aan dit
stukje software. Ondanks het feit dat ze nog nooit met PHP had gewerkt, bracht
de samenwerking tussen ons een code-dialoog op gang. Afgewisseld door korte
trial-error-trial-succes momenten leidde dit tot een werkende oplosser!</p>
<h2>Uitbreiding</h2>
<p>Natuurlijk ging ik direct op zoek naar puzzels om mijn code te testen. Ik vond,
onder meer op <a href="https://www.binarypuzzle.com/">binarypuzzle.com</a>, een groot aantal puzzels die mijn code
glansvol en zonder problemen oplostte. Tot ik ook de 'zeer moeilijke' (very hard)
puzzels probeerde; mijn code was niet slim genoeg! Er moest nog minimaal één
strategie zijn die ik niet kende…</p>
<p>De <a href="https://www.binarypuzzle.com/tips.php">uitleg</a> die ik als basis had genomen voor mijn code reikte tot en met
vier strategiën. De vijfde werd kort aangehaald op een <a href="https://binarypuzzle.nl/strategy">andere pagina</a>, maar
<a href="https://en.wikipedia.org/wiki/Constraint_Handling_Rules">dat soort wiskunde</a> gaat ver boven mijn pet. Gelukkig was er ook
<a href="https://www.binarypuzzle.com/solving_binary_puzzles.php">een beschrijving</a> die ik wél snapte. Ik ging aan de slag en bouwde de
ontbrekende schakel in mijn ketting om de code te kraken.</p>
<h2>Code</h2>
<p>In deze webapplicatie wordt een HTML-<code>textarea</code> gebruikt om de puzzel in te
voeren. In de constructor van de klasse <code>Puzzle</code> wordt de rauwe tekst eerst
opgeschoond en gecontroleerd op juistheid. Tot slot wordt de puzzel opgesplitst
in losse cellen (<code>$this->_cells</code>).</p>
<pre><code class="php"><?php
class Puzzle
{
public function __construct(string $in)
{
// Clean up input
$in = preg_replace("/[^01.\n]/", '', $in);
$lines = explode("\n", trim($in));
$width = strlen($lines[0]);
$height = count($lines);
$oneline = str_replace("\n", '', $in);
// Puzzels bigger than 16x16 are too big to solve...
if (strlen($oneline) > 256) {
return false;
}
// Ensure that both width and height are even
if ($width % 2 != 0 || $height % 2 != 0) {
return false;
}
$this->_unsolved = $oneline;
$this->_width = $width;
$this->_height = $height;
$this->_amountMaxWide = intdiv($width, 2);
$this->_amountMaxHigh = intdiv($height, 2);
$this->_cells = str_split($oneline);
}
// ...
}
</code></pre>
<h3>Oplossen</h3>
<p>De hoofdfunctie van de oplosser is <code>solve()</code>. Ze bestaat uit een
<code>while(true)</code>-loop die met beleid wordt onderbroken of opnieuw gestart.
Eerst wordt stategie 1 losgelaten op de rijen, daarna op de kolommen van de
puzzel. Daarna volgen de andere strategiën. Als er na het uitvoeren van zo'n
stap iets is veranderd (een lege cel is ingevuld), dan wordt het hele proces
opnieuw gestart. De code werkt dus met zo eenvoudig mogelijke middelen, totdat
het niet anders kan.</p>
<pre><code class="php">private function _isSolved() : bool
{
return strpos(implode('', $this->_cells), '.') === false;
}
public const STRATEGY_1 = 1;
public const STRATEGY_2 = 2;
public const STRATEGY_3 = 4;
public const STRATEGY_4 = 8;
public const STRATEGY_5 = 16;
public function solve(int $strategies) : bool
{
$doStrategy1 = ($strategies & self::STRATEGY_1) > 0;
// same goes for strategy 2, 3, 4 and 5
while (true) {
if ($this->_isSolved()) {
return true;
}
$oldCells = implode('', $this->_cells);
for ($i = 1; $i <= 5; $i++) {
if (!${'doStrategy'.$i}) {
continue;
}
foreach ($this->_getRows() as $ix => $item) {
if ($this->_executeStrategy($item, $i, self::PUZLLE_ROW, $ix)) {
continue 3;
}
}
foreach ($this->_getColumns() as $ix => $item) {
if ($this->_executeStrategy($item, $i, self::PUZLLE_COL, $ix)) {
continue 3;
}
}
}
// If all this work didn't bring any change, break the loop.
if (strcmp($oldCells, implode('', $this->_cells)) === 0) {
return false;
}
}
return false;
}
</code></pre>
<h3>Strategiën</h3>
<p>In een binaire puzzel moet een rij of kolom aan een aantal voorwaarden voldoen.
De rij of kolom mag niet leeg zijn, niet groter of kleiner dan de rest van de
puzzel, mag geen drie nullen of enen naast elkaar hebben, of méér dan het
maximaal aantal toegestane nullen en enen.</p>
<pre><code class="php">private function _isValid(string $str, int $rowcol) : bool
{
if (strcmp($str, '') === 0) {
return false;
}
if (stripos($str, '000') !== false) {
return false;
}
if (stripos($str, '111') !== false) {
return false;
}
switch ($rowcol) {
case self::PUZLLE_ROW:
return $this->_isValidRow($str);
break;
case self::PUZLLE_COL:
return $this->_isValidColumn($str);
break;
}
return false;
}
private function _isValidRow(string $row) : bool
{
$amount0 = substr_count($row, '0');
$amount1 = substr_count($row, '1');
if ($amount0 > $this->_amountMaxWide) {
return false;
}
if ($amount1 > $this->_amountMaxWide) {
return false;
}
return true;
}
private function _isValidColumn(string $col) : bool
{
$amount0 = substr_count($col, '0');
$amount1 = substr_count($col, '1');
if ($amount0 > $this->_amountMaxHigh) {
return false;
}
if ($amount1 > $this->_amountMaxHigh) {
return false;
}
return true;
}
</code></pre>
<p>De functie die alle strategiën aanroept moet controleren of het resultaat van
zo'n aanroep geldig is. Zo ja, wordt de nieuwe rij of kolom teruggeplaatst in de
puzzel en kan het oplossen weer van voren af aan beginnen.</p>
<pre><code class="php">private function _validPuzzle() : bool
{
foreach ($this->_getRows() as $row) {
if (!$this->_isValid($row, self::PUZLLE_ROW)) {
return false;
}
}
foreach ($this->_getColumns() as $col) {
if (!$this->_isValid($col, self::PUZLLE_COL)) {
return false;
}
}
return true;
}
private function _executeStrategy(
string $oldItem, int $strategy, int $rowcol = -1, int $ix = -1
) : bool {
$newItem = $oldItem;
switch ($strategy) {
case 1: $newItem = $this->_strat1($oldItem, $rowcol);
break;
case 2: $newItem = $this->_strat2($oldItem, $rowcol);
break;
case 3: $newItem = $this->_strat3($oldItem, $rowcol);
break;
case 4: $newItem = $this->_strat4($oldItem, $rowcol);
break;
case 5: $newItem = $this->_strat5($oldItem, $rowcol);
break;
}
if (!$this->_isValid($newItem, $rowcol)) {
return false;
}
if (strcmp($newItem, $oldItem) !== 0) {
// See if new row causes the puzzle's columns to become invalid
// or if new column causes the puzzle's rows to become invalid
$dummyThis = clone $this;
switch ($rowcol) {
case self::PUZLLE_ROW:
$dummyThis->_setRow($newItem, $ix);
break;
case self::PUZLLE_COL:
$dummyThis->_setColumn($newItem, $ix);
break;
}
if (!$dummyThis->_validPuzzle()) {
return false;
}
$step = new PuzzleStep($strategy);
switch ($rowcol) {
case self::PUZLLE_ROW:
$step->setRowIndex($ix);
$this->_setRow($newItem, $ix);
break;
case self::PUZLLE_COL:
$step->setColIndex($ix);
$this->_setColumn($newItem, $ix);
break;
}
$step->setOldRowValue($oldItem);
$step->setNewRowValue($newItem);
$this->_steps[] = $step;
return true;
}
return false;
}
</code></pre>
<h4>1: Duo's en trio's</h4>
<p>Tussen twee nullen staat altijd een één: <code>0.0</code> wordt <code>010</code>. Tussen twee enen
staat altijd een nul: <code>1.1</code> wordt <code>101</code>. Naast twee nullen staat altijd een één:
<code>00.</code> wordt <code>001</code>, <code>.00</code> wordt <code>100</code>. Naast twee enen staat altijd een nul:
<code>11.</code> wordt <code>110</code>, <code>.11</code> wordt <code>011</code>.</p>
<pre><code class="php">private static function _strat1(string $str) : string
{
$replaceCount = 0;
$str = preg_replace('/00\./', '001', $str, 1, $replaceCount);
if ($replaceCount > 0) {
return $str;
}
$str = preg_replace('/\.00/', '100', $str, 1, $replaceCount);
if ($replaceCount > 0) {
return $str;
}
$str = preg_replace('/11\./', '110', $str, 1, $replaceCount);
if ($replaceCount > 0) {
return $str;
}
$str = preg_replace('/\.11/', '011', $str, 1, $replaceCount);
if ($replaceCount > 0) {
return $str;
}
$str = preg_replace('/0\.0/', '010', $str, 1, $replaceCount);
if ($replaceCount > 0) {
return $str;
}
$str = preg_replace('/1\.1/', '101', $str, 1, $replaceCount);
if ($replaceCount > 0) {
return $str;
}
return $str;
}
</code></pre>
<p>Een snellere en zeker efficiëntere methode van deze strategie zou gebruik maken
van <a href="https://php.net/manual/en/function.str-replace.php"><code>str_replace</code></a>, maar daarmee kun je niet elke vervanging (puzzelstap)
afzonderlijk vastleggen. En laat dát nou juist het doel zijn van deze oplosser :-)</p>
<h4>2: Maximaal aantal 0's en 1's</h4>
<p>Een rij (kolom) bevat altijd evenveel nullen als enen. Als de nullen 'op zijn',
moeten de resterende cellen met enen worden gevuld; en vice versa.</p>
<pre><code class="php">private function _strat2(string $str) : string
{
$amount0 = substr_count($str, '0');
$amount1 = substr_count($str, '1');
$amountXX = substr_count($str, 'xx');
$amountEmp = substr_count($str, '.');
$amount0 += $amountXX;
$amount1 += $amountXX;
if ($amountEmp == 1) {
$toInsert = $amount0 < $amount1 ? '0' : '1';
$str = str_replace('.', $toInsert, $str);
return $str;
}
if ($amount0 == $this->_amountMax) {
$str = str_replace('.', '1', $str);
return $str;
}
if ($amount1 == $this->_amountMax) {
$str = str_replace('.', '0', $str);
return $str;
}
if ($amount0 == $this->_amountMax - 1) {
$options = [];
for ($i = 0; $i < $this->_size; $i++) {
if (substr($str, $i, 1) != '.') {
continue;
}
$tmpOption = substr_replace($str, '0', $i, 1);
$tmpOption = str_replace('.', '1', $tmpOption);
if (self::_isValidRow($tmpOption)) {
$options[] = $tmpOption;
} else {
$tmpOption = substr_replace($str, '1', $i, 1);
return $tmpOption;
}
}
if (count($options) == 1) {
$str = reset($options);
return $str;
}
}
if ($amount1 == $this->_amountMax - 1) {
$options = [];
for ($i = 0; $i < $this->_size; $i++) {
if (substr($str, $i, 1) != '.') {
continue;
}
$tmpOption = substr_replace($str, '1', $i, 1);
$tmpOption = str_replace('.', '0', $tmpOption);
if (self::_isValidRow($tmpOption)) {
$options[] = $tmpOption;
} else {
$tmpOption = substr_replace($str, '0', $i, 1);
return $tmpOption;
}
}
if (count($options) == 1) {
$str = reset($options);
return $str;
}
}
return $str;
}
</code></pre>
<h4>3: 0..1 en 1..0 uitsluiten</h4>
<p>Tussen een nul en één met twee cellen tussenruimte, staan altijd een nul en een
één. Bijvoorbeeld: <code>0..1</code> of <code>1..0</code>. Dit gegeven kan worden gebruikt om met
strategie 2 deze twee lege cellen uit te sluiten.</p>
<pre><code class="php">private function _strat3(string $str) : string
{
$option = '';
if (strpos($str, '0..1') !== false) {
$option = preg_replace('/0\.\.1/', '0xx1', $str);
}
if (strpos($str, '1..0') !== false) {
$option = preg_replace('/1\.\.0/', '1xx0', $str);
}
if (!$option) {
return $str;
}
$newItem = self::_strat2($option);
$newItem = str_replace('xx', '..', $newItem);
return $newItem;
}
</code></pre>
<h4>4: Unieke rijen vergelijken</h4>
<p>Elke rij (kolom) is uniek. Als een rij (kolom) nog maar twee open cellen heeft,
en er is een volledige rij (kolom) die verder identiek is, moeten de lege cellen
'andersom' worden ingevuld.</p>
<pre><code class="php">private function _strat4(string $str, int $rowcol) : string
{
if (substr_count($str, '.') != 2) {
return $str;
}
$optionA = $str;
$optionA = preg_replace('/\./', '1', $optionA, 1);
$optionA = preg_replace('/\./', '0', $optionA, 1);
$optionB = $str;
$optionB = preg_replace('/\./', '0', $optionB, 1);
$optionB = preg_replace('/\./', '1', $optionB, 1);
switch ($rowcol) {
case self::PUZLLE_ROW: $items = $this->_getRows();
break;
case self::PUZLLE_COL: $items = $this->_getColumns();
break;
default: $items = array();
break;
}
foreach ($items as $ix2 => $item2) {
if ($item2 == $optionA) {
$newItem = $optionB;
} else if ($item2 == $optionB) {
$newItem = $optionA;
} else {
$newItem = '';
}
if ($this->_isValidRow($newItem)) {
return $newItem;
} else {
continue;
}
}
return $str;
}
</code></pre>
<h4>5: Alle mogelijkheden filteren</h4>
<p>Maak een lijst van alle mogelijkheden voor het invullen van een rij (kolom) met
lege cellen. Komen één of meer cellen daarvan overeen in alle mogelijkheden, dan
is de inhoud van die cel zeker.</p>
<pre><code class="php">private function _strat5(string $str, int $rowcol) : string
{
if (!$this->_allPossibles) {
// Create the maximum binary number based on the puzzle's size
$size = $this->_size;
$maxBinary = str_repeat('1', $size);
// Get all possible numbers represented by the puzzle's rows (columns)
$all = range(0, bindec($maxBinary));
$all = array_map(
function ($x) use ($size) {
// Pad them with zeroes to the correct length
return sprintf("%0${size}b", $x);
}, $all
);
// Reduce to only valid rows (columns)
$all = array_filter($all, array(__CLASS__, '_isValidRow'));
// Cache for later usage
$this->_allPossibles = $all;
unset($all);
}
$possibles = array_filter(
$this->_allPossibles,
function ($x) use ($str) {
$xChars = str_split($x);
$strChars = str_split($str);
$charsCount = count($xChars);
// Find possible candidates for row (column) $str
for ($i = 0; $i < $charsCount; $i++) {
if ($strChars[$i] == '.') {
continue; // ... by skipping empty cells...
}
if ($strChars[$i] != $xChars[$i]) {
return false; // ... not including non-matching...
}
}
return true; // ... and keeping all others
}
);
switch ($rowcol) {
case self::PUZLLE_ROW: $items = $this->_getRows();
break;
case self::PUZLLE_COL: $items = $this->_getColumns();
break;
default: $items = [];
break;
}
// Exclude existing rows (columns)
$possibles = array_filter(
$possibles,
function ($x) use ($items) {
return array_search($x, $items) === false;
}
);
// Loop through $str by character
for ($i = 0; $i < $this->_size; $i++) {
if (substr($str, $i, 1) != '.') {
continue; // Skip empty cells
}
// Create a concatenated string of all possible values for this cell
$valuesOnIndexI = array_reduce(
$possibles,
function ($a, $b) use ($i) {
return $a.substr($b, $i, 1);
}, ''
);
// If the options only contain zeroes, then this cell MUST be 0
if (strpos($valuesOnIndexI, '0') !== false
&& strpos($valuesOnIndexI, '1') === false
) {
$str = substr_replace($str, '0', $i, 1);
return $str;
}
// If the options only contain ones, then this cell MUST be 1
if (strpos($valuesOnIndexI, '1') !== false
&& strpos($valuesOnIndexI, '0') === false
) {
$str = substr_replace($str, '1', $i, 1);
return $str;
}
}
return $str;
}
</code></pre>
</div>2018-02-02T18:00:12+01:00https://www.fwiep.nl/blog/fwiep-nl-redress-2021FWieP.nl redress 20212021-03-25T16:42:09+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>In een <a href="https://www.fwiep.nl/blog/fwiep-nl-makeover-2020">eerder artikel</a> beschreef ik een radicale ombouw van mijn website.
Onder meer de overstap van Bootstrap 3 naar 4 passeerde de revue. Dit keer
is de verandering meer aan de oppervlakte; de website heeft onder meer een
nieuw kleurenthema gekregen: donker groen in plaats van fel rood.</p>
<h2>WAVE</h2>
<p>De directe aanleiding voor deze facelift was mijn ontdekking van <a href="https://wave.webaim.org/">WAVE</a>;
een hulpmiddel voor het evalueren van toegankelijkheid van websites en
-applicaties. Plotseling kon ik met één druk op de knop (<kbd>Ctrl</kbd> +
<kbd>Shift</kbd> + <kbd>U</kbd>) in Firefox zien dat mijn hele site te
kampen had met behoorlijke contrastfouten. En dat, terwijl toegankelijkheid
voor mij als <a href="https://www.fwiep.nl/slechtziendheid">slechtziende computergebruiker</a> heel belangrijk is!</p>
<h2>Regel voor regel</h2>
<p>Toen ik eenmaal de broncode van mijn site onderhanden had, besloot ik dan maar
ook letterlijke <em>elke</em> regel na te lopen. Als mij er ook maar het minste of
geringste aan opviel of stoorde, zou ik het verhelpen.</p>
<h3>RuntimeData</h3>
<p>Het komt vaak voor dat je als programmeur steeds weer dezelfde code, of dezelfde
data nodig hebt. Met object georiënteerd programmeren heb ik de code zo ver
mogelijk gecentraliseerd. Toch bleven er nog steeds in PHP globale variabelen
over die ik in de verschillende pagina's of zelfs klassen gebruikte. Dat
moest anders!</p>
<p>Met de klasse <code>RuntimeData</code>, zie hieronder, kan ik door de gehele site of
applicatie van dezelfde data gebruik maken. Deze wordt maar één keer
geinitialiseerd en is bovendien ook nog eens <em>typesafe</em>, het type van de
variabele staat bij het programmeren vast. Dit patroon heet <strong>singleton</strong>.</p>
<pre><code class="php">final class RuntimeData
{
public $fileBase;
// ...
private static $_instance;
public static function g() : self
{
if (is_null(self::$_instance)) {
self::$_instance = new self();
}
return self::$_instance;
}
private function __construct()
{
$this->fileBase = dirname($_SERVER['SCRIPT_FILENAME']);
// ...
}
}
// Somwhere in the application:
echo RuntimeData::g()->fileBase;
</code></pre>
<h3>Limburgs wereldwijd</h3>
<p>Onder toegankelijkheid valt ook het consequent opgeven van de taal van de
content. Mijn gehele site is gedeclareerd als Nederlands (<code>lang="nl"</code>).
De Engelstalige liedteksten zijn gekenmerkt met <code>lang="en"</code>, de Duitse met
<code>lang="de"</code>. Alleen de Limburgse teksten kon ik tot voor kort niet echt
specificeren. Als tijdelijke oplossing vond ik ergens het <code>lang="und"</code>-
attribuut: undefined oftewel onbekend. Wat blijkt? Limburgs is wereldwijd
bekend met een eigen taal-attribuut: <code>lang="li"</code>.</p>
<pre><code class="xml"><div lang="li">
Lot mich dich 'ns vertelle wie sjtolz ich bin dat 't sogar in XML ing
meugelikheed git um 't Limburgs es zoedanig aa te geave. Sjun, wa?
</div>
</code></pre>
<h3>Anti-cache</h3>
<p>Bij het bezoeken van een website of het gebruik van een webapplicatie
worden de bestanden daarvoor vaak één keer aangevraagd en daarna door de
browser opgeslagen voor later, oftewel <em>gecached</em>. Dat verkort de tijd
die de browser nodig heeft om de volgende pagina op te bouwen.</p>
<p>In sommige gevallen, maar zeker tijdens het ontwikkelen, is dit cachen
een doorn in het oog van zowel de ontwikkelaar als de gebruiker. De browser
moet er bij een update op de server namelijk handmatig van worden overtuigd
om de nieuw(st)e versie van het JavaScript, stylesheet of favicon te laden.
De betreffende toetscombinatie is vaak <kbd>Ctrl</kbd> + <kbd>F5</kbd>.</p>
<p>Om dit probleem te omzeilen maak ik sinds kort gebruik van een querystring
variabele. Deze wordt afgeleid van de inhoud van het bestand in kwestie.
Als het bestand wijzigt, wijzigt ook de variabele en zal de browser het
bestand opnieuw aanvragen.</p>
<pre><code class="xml"><link rel="stylesheet" href="https://www.fwiep.nl/css/app.min.css?v=b145e1ca..." />
</code></pre>
<h3>Integriteit</h3>
<p>Als je er voor kiest om scripts of stylesheets van een andere site in jouw
eigen pagina's te gebruiken, moet je er maar op vertrouwen dat de beheerder
van die site zijn/haar zaakjes op orde heeft en de bestanden niet opeens
aanpast. Wat als daar wordt ingebroken en de scripts door een crimineel
worden voorzien van een achterdeur die daarna jouw bezoekers bespioneert?</p>
<p>Om dit scenario te voorkomen, kun je met het <code>integrity</code>-attribuut een
cryptografische hash opgeven waarmee de browser het gedownloade bestand
controleert. Komt de hash niet overeen, wordt het bestand verworpen en
niet gebruikt of uitgevoerd.</p>
<p>Ondanks dat ik uit principe alle bestanden op <code>fwiep.nl</code> zelf host, vond
ik het toch een mooi idee om ook mijn scripts en stylesheets van deze
extra beveiliging te voorzien. Aldus geschiedde:</p>
<pre><code class="xml"><link rel="stylesheet" href="https://www.fwiep.nl/css/app.css?v=..." integrity="sha384-..." />
<script src="js/app.js?v=..." integrity="sha384-..."></script>
</code></pre>
<h3>Optimalisatie</h3>
<p>De betreffende hashes zou ik natuurlijk bij elk bezoek door PHP kunnen
laten berekenen, maar ik zag al meteen dat het efficiënter zou zijn om ze
als het ware offline te berekenen en daarna als constantes te gebruiken.
Aldus ging ik met <code>md5sum</code> en <code>openssl</code> handmatig aan de slag.</p>
<pre><code class="bash">md5sum css/app.css | cut -s -d ' ' -f 1;
openssl dgst -sha384 -binary css/app.css | openssl base64 -A;
</code></pre>
<p>Daarna kon ik de constantes als volgt toepassen:</p>
<pre><code class="php">// config.php
define('CSS_MD5', 'b145e1ca9107867e696de9b1b50b8938');
define('CSS_SHA', 'sha384-4lbQ/u6i562Ka/j+7UG5+q...');
// page.php
print '<link rel="stylesheet" href="https://www.fwiep.nl/css/app.css?v='.MD5_CSS.'" integrity="'.SHA_CSS.'" />';
</code></pre>
<h3>Automatisering</h3>
<p>Er ontbrak nog één schakel in de volledige automatisering van dit stuk
code. Als ik in VisualStudio Code, mijn ontwikkelomgeving, een aanpassing
maakte en de bestanden met een <a href="https://code.visualstudio.com/Docs/editor/tasks#_custom-tasks"><em>buildtask</em></a> opnieuw liet wegschrijven,
moesten de hashes nog opnieuw worden bepaald. Ik schreef het volgende
shell-script en nam het op in de bouwtaak:</p>
<pre><code class="bash"># Generate the MD5 filehash for the CSS file
MD5_CSS="$( md5sum css/app.css | cut -s -d ' ' -f 1 )";
# Replace the value of the defined constant with the new hash
sed -i -e "s!define('MD5_CSS', '[^']*');!define('MD5_CSS', '${MD5_CSS}');!" "config.php"
# Generate the cryptographic filehash for the CSS file
SHA_CSS="$( openssl dgst -sha384 -binary css/app.css | openssl base64 -A )";
# Replace the value of the defined constant with the new hash
sed -i -e "s!define('SHA_CSS', '[^']*');!define('SHA_CSS', 'sha384-${SHA_CSS}');!" "config.php"
</code></pre>
<h3>HighliteJS</h3>
<p>Al vanaf het begin van dit weblog maak ik gebruik van <a href="https://highlightjs.org/">Highlite.js</a>.
Met een extra stuk CSS en JavaScript worden lappen broncode op de pagina's
voorzien van de betreffende syntax kleuren. Dit bevordert de leesbaarheid
enorm. Tijdens de recente ombouw wilde ik dit project graag volledig
integreren in mijn bestaande code. Jammer genoeg vond ik de honderden
kilobytes extra JavaScript te veel van het goede voor mijn redelijk slanke
<code>app.min.js</code>.</p>
<p>Dus ging ik op zoek naar een mogelijkheid om het inkleuren al op de server
te doen. Ik vond <a href="https://github.com/scrivo/highlight.php">highlight.php</a> van Geert Bergman en was blij verrast.
Samen met de <a href="https://github.com/michelf/php-markdown">MarkdownExtra</a> parser van Michel Fortin kon ik zo elk
codeblok voorzien van de passende opmaak.</p>
<pre><code class="php">$hl = new Highlight\Highlighter();
$me = new Michelf\MarkdownExtra();
$me->code_block_content_func = function ($code, $language) use ($hl) {
try {
return $hl->highlight($language, $code)->value;
} catch (\DomainException $ex) {
return $hl->highlight('plaintext', $code)->value;
}
};
</code></pre>
<p>Ik hoefde alleen nog een passende stylesheet te kiezen en deze op te nemen
in mijn SCSS bronbestand:</p>
<pre><code class="css">/* main.scss */
@import "../vendor/scrivo/highlight.php/styles/a11y-light";
</code></pre>
<h3>Shariff en FontAwesome</h3>
<p>Bij de lancering van mijn <a href="https://www.fwiep.nl/muziek/noets-genog">nieuwste demo-EP</a> wilde ik graag elke bezoeker
de mogelijkheid geven om mijn muziek eenvoudig te delen. Met de hulp van
<a href="https://ct.de/-2467514">c't Shariff</a> was dit op een open source en privacyvriendelijke manier
mogelijk. Eén van de afhankelijkheden van dit project was het gebruik van
de <a href="https://fontawesome.com/">FontAwesome</a> lettertypen. Opeens had ik de beschikking over een keur
aan welbekende icoontjes, logo's en tekens.</p>
<p>Aldus maakte ik daar dankbaar gebruik van voor, onder andere, het aangeven
of een link naar een externe site verwijst. Ook kon ik links naar
<a href="https://www.fwiep.nl/download/1295939c-4a98-45c7-b5ab-f6cfbd3b5c6d/Weekkalender-2023.pdf">een PDF-download</a> als zodanig kenmerken. Handig!</p>
<pre><code class="js">$(document).ready(function()
{
// Make external links open in a new window, show "extern" indicator
$('a[rel=external], a[href^="https://"], a[href^="http://"]')
// Exclude alternate URLs
.not('a[rel="alternate"]')
// Set to open in a new window/tab
.attr('target', '_blank')
// and append a visual and text-only indicator
.append(
'<span class="visually-hidden"> (extern)</span>' +
'<span class="ms-1 fas fa-external-link-alt"></span>'
);
// Show a PDF-icon (and a screen reader note) to PDF-links
$('a[href$="\.pdf"]').each(function(ix,a) {
$(a).append(
'<span class="visually-hidden"> (PDF)</span>'+
'<span class="fas fa-file-pdf ms-1"></span>'
);
});
});
</code></pre>
</div>2021-03-25T16:42:09+01:00https://www.fwiep.nl/blog/nieuwe-uitdaging-fedora-met-selinuxNieuwe uitdaging: Fedora met SELinux2021-02-09T17:28:58+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Meer dan een jaar geleden schreef ik enthousiast over mijn <a href="https://www.fwiep.nl/blog/van-ubuntu-naar-debian-hoogste-tijd">overstap</a>
van Ubuntu naar Debian GNU/Linux als besturingssysteem voor mijn computers.
De conclusie was dat stabiliteit en vrijheid voorop staan – en dus
was de keuze voor Debian vanzelfsprekend.</p>
<p>In de afgelopen maanden kwam ik langzaam aan tot een ander, iets genuanceerder
inzicht. Voor een 'gewone' gebruiker die een computer enkel en alleen bedient, is
de stabiliteit een zege. Alles blijft consequent hetzelfde en werkend. De
veiligheid wordt achter de schermen gewaarborgd, maar méér verandert er ook niet.</p>
<p>Voor mij als software­ontwikkelaar betekent de oerdegelijke stabiliteit
daarentegen ook het gebruik van oude versies van programma's en ontwikkeltools.
Het duurt een gevoelde eeuwigheid voordat er bijvoorbeeld een update van
de grafische omgeving of de Linux-kernel beschikbaar is. Ik ging op zoek
en kwam terug bij <a href="https://getfedora.org/">Fedora</a>.</p>
<h2>Fedora</h2>
<p>Lang geleden had ik dit systeem al eens geprobeerd in een virtuele machine
en op een live-USB-stick. Toen kon ik er niet mee overweg, maar dat was
nu wel anders. De GNOME-shell als desktopomgeving was me inmiddels wel
bekend. Het pakket- en updatebeheer met <code>dnf</code> was even wennen, maar
verder geen probleem. De integratie van <a href="https://www.flatpak.org/">flatpak apps</a> was nieuw; ze
blijken niet allemaal even toegankelijk voor mijn schermlezer. Maar
gelukkig zijn er ook nog steeds de 'ouderwetse' <code>.rpm</code>-pakketten, al dan
niet via het systeemeigen pakketbeheer.</p>
<h2>SELinux</h2>
<p>Het grootste verschil met de mij eerder bekende Linux-distributies was de
extra laag aan beveiliging die met behulp van <em>SELinux</em> wordt gerealiseerd.
Het is een soort verkeersagent die bij elke actie van elk proces controleert
of dat wel is toegestaan, op basis van de geldende verkeersregels (zogenaamde
<em>policies</em>). Voor zo'n beetje alle gangbare applicaties worden er policies
meegeleverd. Als gewone gebruiker (zie boven) heb je er verder geen last van.
Al die tijd wordt jouw veiligheid gegarandeerd omdat er niets gebeurt wat
niet van tevoren is toegestaan.</p>
<h2>Webserver & PHP</h2>
<p>Als webontwikkelaar liep ik wél tegen de spreekwoordelijke lamp.</p>
<pre><code class="bash">sudo dnf install -y httpd php mariadb mariadb-server php-mysqlnd
</code></pre>
<p>Ik installeerde de Apache webserver, PHP en MySQL. Toen ik daarna mijn
bestaande hoofdmap had ingesteld in <code>httpd.conf</code>, verscheen bij het openen
van <code>http://localhost/</code> een dikke foutmelding. De webserver had geen
toegang tot de bestanden! Okee, dan controleren we de eigenaar, groep en
<a href="https://linuxhandbook.com/linux-file-permissions/">permissies</a> van de betreffende mappen en bestanden… Allemaal
in orde. Waar kwam deze foutmelding vandaan?</p>
<p>Het bleek dat SELinux controleerde of mijn PHP-bestanden wel door <code>httpd</code>
mochten worden gelezen. Nee dus! Een <a href="https://beginlinux.com/server_training/web-server/976-apache-and-selinux">stukje documentatie</a> later waren
alle contenttypes op <code>httpd_sys_content_t</code> ingesteld en had Apache toegang
tot de bestanden:</p>
<pre><code class="bash">sudo chcon -R -t httpd_sys_content_t /var/www/html
</code></pre>
<p>De tijdelijke mappen van onder andere <a href="https://mpdf.github.io/">mPDF</a> gaven daarna nog problemen
met SELinux. Apache moest hier niet alleen leestoegang, maar ook schrijfrechten
hebben:</p>
<pre><code class="bash">sudo chcon -R -t httpd_sys_rw_content_t /var/www/html/vendor/mpdf/mpdf/tmp
</code></pre>
<p>Vanaf mijn lokale computer draaide nu alles naar wens – tot en met
de databases en URL-rewriting. Bij het benaderen van mijn PC via het
netwerk kreeg ik daarentegen opnieuw een foutmelding; de webserver was
niet bereikbaar. Hier bleek dat de standaard ingeschakelde vuurmuur alle
verkeer blokkeerde. Dat was met één commando aangepast:</p>
<pre><code class="bash">sudo firewall-cmd --permanent --add-service=http
</code></pre>
<h2>Configuratie</h2>
<p>Bij het doortesten van al mijn (lokale) web-applicaties bleek dat er toch
nog steeds sommige dingen niet functioneerden. Zo kon ik geen e-mail
versturen vanuit de contactformulieren en werkte het via PHP versleutelen
met GnuPG ook niet meer. Ook nu hielp de juiste <a href="https://linux.die.net/man/8/httpd_selinux">documentatie</a> mij uit
de brand en leerde ik de <em>booleans</em> van SELinux kennen:</p>
<pre><code class="bash">sudo setsebool -P httpd_can_sendmail 1
sudo setsebool -P httpd_can_network_connect 1
sudo setsebool -P httpd_use_gpg 1
sudo setsebool -P httpd_setrlimit 1
</code></pre>
<p>Sinds de overstap bleek mijn brailledisplay alleen nog op de tekstconsole
te werken; niet meer onder de grafische desktop. Ik zocht contact met de
ontwikkelaars van BrlTTY en ziedaar: mijn gebruiker moest deel uitmaken
van de groep <code>brlapi</code>:</p>
<pre><code class="bash">sudo usermod -aG brlapi fwiep
</code></pre>
<p>Ook werkten de <kbd>DOT7</kbd> (backspace) en <kbd>DOT8</kbd> (enter)
toetsen niet meer, de andere zes wel. Dit bleek een
<a href="https://github.com/brltty/brltty/commit/67b9d27f7707a0f46d96de5d96c1b164754cb11f">bug in <code>brlapi_server</code></a>, een onderdeel van BrlTTY. Het voelt
<strong>zo goed</strong> om te hebben bijgedragen aan een programma dat door mijzelf,
maar ook door zoveel andere mensen wordt gebruikt. Het was een heel
leerzame ervaring voor wat betreft de kunst van het foutzoeken en het
belang van gedetailleerde logbestanden.</p>
<p>Apropos logbestanden; één commando heeft me enorm geholpen om de, tot nu
toe, laatste SELinux plooien glad te strijken:</p>
<pre><code class="bash">journalctl -t setroubleshoot
</code></pre>
<p>Hiermee krijg je, in gewone-mensen-taal, een opsomming van alles wat er
door SELinux wordt gecontroleerd en geblokkeerd. Nog veel belangrijker
zijn de suggesties om de betreffende situaties op te lossen. Jammer dat
ik dit commando niet metéén had ontdekt; dat had me een hoop tijd
gescheeld :-)</p>
<h2>Conclusie</h2>
<p>Ubuntu, Debian en Fedora; het zijn allemaal GNU/Linux distributies met een
specifieke doelgroep voor ogen. Het feit dat ik elke distro een tijd lang
met veel plezier heb gebruikt, toont maar weer dat er niets veranderlijker
is dan de mens.</p>
<p>Eerst was ik een onervaren Linux-beginner en werd ik aan de hand genomen
door de vele tools en keuzes die Ubuntu voor me maakte. Later knaagde mijn
geweten en koos ik bewust voor Debian met het oog op vrijheid, veiligheid
en stabiliteit. Tot slot haalde mijn eigen ontwikkeldrang me over om toch
voor Fedora te kiezen; een moderne, actuele distributie waar soms dingen
niet meteen werken. Maar samen maken we de wereld beter, voor ons allemaal.</p>
</div>2021-02-09T17:28:58+01:00https://www.fwiep.nl/blog/zeldzame-http-503-met-joomlaZeldzame HTTP-503 met Joomla!2020-11-27T12:20:23+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Al jaren ben ik vaste muzikant bij <a href="https://www.koorvoyage.nl/">Koor Voyage</a> uit Landgraaf. Sinds
een tijdje is het onderhoud van hun website aan mij toevertrouwd. We hebben
samen gekozen om gebruik te maken van het <a href="https://www.joomla.org/">Joomla!</a>-CMS. Dit is door
meerdere mensen tegelijk te beheren. Daarnaast is het voor zowel bezoeker
als beheerder volledig aanpasbaar en geschikt voor meerdere platformen.
De enige vereiste is een (redelijk) recente internetbrowser.</p>
<p>Ruim een jaar geleden maakte ik melding van een fout in het beheergedeelte
van de website. Als ik een pagina met behoorlijk wat hyperlinks wilde
bewerken, kreeg ik soms een onverklaar­bare foutmelding (HTTP-503) en moest
ik steeds weer opnieuw goochelen om één en ander (via de database) recht
te breien.</p>
<h2>Forum</h2>
<p>Na de nodige uren onderzoek gaf ik mijn eenmansstrijd op en besloot om
<a href="https://forum.joomla.org/viewtopic.php?t=973967">hulp te vragen</a> op het Joomla!-forum. Er kwam snel antwoord, maar de
suggesties leidden tot niets. Ik ging zelfs zo ver dat ik een apart artikel
aanmaakte voor dit topic om helpers zoveel mogelijk tegemoet te komen.</p>
<p>De fout bleef bestaan en frustreerde mij mateloos. Op mijn lokale PC trad
de fout niet op; en het bewerken van een artikel met toevoegen van hyperlinks
is een dermate basishandeling binnen elk CMS… dit moest toch zonder
fouten mogelijk zijn?</p>
<h2>Oorzaak en oplossing</h2>
<p>De afgelopen twee weken ben ik er opnieuw ingedoken en weer leidde al dit
zoeken tot niets. Tot vandaag – tot nu. Met een beetje inspiratie
kwam ik op <a href="https://www.strato.de/faq/hosting/fehler-503-error-service-unavailable-bei-der-verwendung-von-post-requests/">deze pagina in de helpdesk</a> van de hostingprovider (Strato)
terecht. Hier stond precies de omschrijving van het probleem – en
ook de bijbehorende oplossing! Een ouderwetse bescherming tegen
gastenboek-spam was standaard actief en zag mijn ingevoerde HTML
artikelinhoud met hyperlinks als ongewenst en potentieel gevaarlijk.</p>
<h2>Stil aan de overkant</h2>
<p>Nu zou ik graag de mensen op het Joomla!-forum op de hoogte brengen van
deze oplossing, daarmee mijn gestelde vraag beantwoorden en het onderwerp
sluiten. Maar, omdat de discussie al zo oud is, is dat niet meer mogelijk.
Het topic is gesloten, vergrendeld.</p>
<p>Oké, dan ga ik een beheerder benaderen om op z'n minst de discussie af te
laten sluiten en te verwijzen naar de oplossing - zodat een andere gebruiker
niet dezelfde misère hoeft door te maken als ik.</p>
<p>Wat blijkt? Ik heb niet genoeg ervaring/reputatie op dat forum om een beheerder
een bericht te mogen sturen. Mijn enige manier van reageren is het openen
van een nieuwe discussie. Ja-ja, zo kom ik ook aan een minimum aantal posts
op mijn forum… <a href="https://www.myinstants.com/instant/doh/">D'oh!</a></p>
</div>2020-10-23T11:56:23+02:00https://www.fwiep.nl/blog/pdf-maandkalender-in-phpPDF maandkalender in PHP2020-11-27T07:47:11+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Zo rond oktober/november gaan mijn vrouw en ik op zoek naar een aantal nieuwe
kalenders voor het volgende jaar. Vanaf 2019 maken we <a href="https://www.fwiep.nl/blog/persoonlijke-pdf-weekkalender-in-php">de grote weekkalender</a>
zelf. Dit jaar werd die uitgebreid met een kleiner broertje op A5-formaat.
Toch ontbrak er nog één onderdeel: een compacte maandkalender met weeknummers.
Ik ging opnieuw aan de slag en nam de code van de weekkalender als basis.
Het resultaat is te vinden <a href="https://github.com/fwiep/monthcalendar">op GitHub</a>.</p>
<h2>Eenvoudig</h2>
<p>De weekkalender heeft 52-53 pagina's met veel ruimte voor elke dag. Deze
maandkalender moest daarentegen compact worden en liefst het hele jaar in
één of twee oogopslagen samenvatten. Uiteindelijk koos ik voor een raster
van 3×2 maanden, 2 kantjes A5; zie <a href="https://www.fwiep.nl/download/1295939c-4a98-45c7-b5ab-f6cfbd3b5c6d/Maandkalender-2023.pdf">dit voorbeeld</a>.</p>
<p>Opnieuw maakte ik gebruik van <a href="https://mpdf.github.io/">mPDF</a> voor het omzetten van HTML naar
PDF. Zo kon ik met relatief eenvoudige opmaak (lees: tabellen) toch het
gewenste resultaat bereiken.</p>
<h2>Opmaak</h2>
<p>De eerste maand van 2023 ziet er als volgt uit:</p>
<pre><code class="plain"> januari 2023
----------------------
wk | 52 01 02 03 04 05
---+------------------
ma | 2 9 16 23 30
di | 3 10 17 24 31
wo | 4 11 18 25
do | 5 12 19 26
vr | 6 13 20 27
za | 7 14 21 28
zo | 1 8 15 22 29
</code></pre>
<p>Elke maand is één tabel van in totaal 7 kolommen en 9 rijen. Daarvan is
de eerste rij de titel (maandnaam + jaar). De volgende rij is gevuld met de
weeknummers. De eerste kolom bevat de weekdagnamen (ma-di-wo-do-vr-za-zo).
De rest van de tabel wordt gevuld met de dagnummers zelf. Voor het omliggende
raster van 3×2 nestte ik de maandtabellen in één omliggende tabel
genaamd <code>scaffold</code>.</p>
<details>
<summary>Code in/uitklappen</summary>
<pre><code class="css">table.scaffold{
margin: 0 auto;
}
table.month{
font-family: sans-serif;
}
table.month th,
table.month td {
border-right: solid 0.3mm black;
border-bottom: solid 0.3mm black;
padding: 1mm 2mm;
text-align: center;
}
table.month tr.title{
background-color: #999999;
}
table.month tr.week{
background-color: #cccccc;
}
</code></pre>
</details>
<h2>Code</h2>
<p>Met onderstaande code wordt op basis van een maandnummer (<code>$m</code>) en het
ingestelde jaar (<code>$this->_year</code>) een HTML-tabel gegenereerd. De functie
<code>_dtWrap()</code> is een hulpje om gemakkelijk met meerdere van elkaar afgeleide
datums te kunnen werken.</p>
<details>
<summary>Code in/uitklappen</summary>
<pre><code class="php">private function _generateMonthTable(int $m) : string
{
$firstThisMonth = new \DateTime($this->_year.'-'.$m.'-01');
$loopDate = clone $firstThisMonth;
$html = '<table class="month">';
$html .= sprintf(
'<tr class="title"><th colspan="7">%s %d</th></tr>',
strftime('%B', $firstThisMonth->getTimestamp()),
$this->_year
);
while ($loopDate->format('N') > 1) {
$loopDate->sub(new \DateInterval('P1D'));
}
// Array of 8 rows for weeknumbers (1x) + weekdays (7x)
$rows = [
-1 => [], 0 => [], 1 => [], 2 => [],
3 => [], 4 => [], 5 => [], 6 => []
];
// Construct first row (for weeknumbers)
for ($weekLoop = -1; $weekLoop < 6; $weekLoop++) {
if ($weekLoop == -1) {
$rows[-1][] = '<th>wk</th>';
continue;
}
$dt = self::_dtWrap($firstThisMonth, 7*$weekLoop);
$rows[-1][] = strftime('<td>%V</td>', $dt->getTimestamp());
}
// Construct second to eighth rows
foreach ($rows as $rowIx => &$row) {
if ($rowIx == -1) {
continue;
}
for ($colIx = -1; $colIx < 6; $colIx++) {
$dt = self::_dtWrap($loopDate, 7*$colIx + $rowIx);
if ($colIx == -1) {
// Print abbreviated weekday name
$row[] = strftime('<th>%a</th>', $dt->getTimestamp());
continue;
}
if ($dt->format('m') != $m) {
// Print empty cell
$row[] = '<td>&nbsp;</td>';
continue;
}
// Print day of month
$row[] = strftime('<td>%e</td>', $dt->getTimestamp());
}
}
foreach ($rows as $rowIx => &$row) {
if ($rowIx == -1) {
$html .= '<tr class="week">';
} else {
$html .= '<tr>';
}
$html .= implode('', $row);
$html .= '</tr>';
}
$html .= '</table>';
return $html;
}
private static function _dtWrap(\DateTime $dt, int $days = 0) : \DateTime
{
$outDt = clone $dt;
$di = new \DateInterval('P'.abs($days).'D');
if ($days < 0) {
$outDt->sub($di);
} else {
$outDt->add($di);
}
return $outDt;
}
</code></pre>
</details>
<h2>Finale</h2>
<p>Tot slot wordt in onderstaande functie het daadwerkelijke PDF-document
gegenereerd. Er wordt gekozen voor een A5-formaat in landschap-ligging.
Daarna wordt de stylesheet toegevoegd en – in 4 stappen van 3 –
de maanden per stuk als tabel in een cel aan het omliggende raster toegevoegd.</p>
<details>
<summary>Code in/uitklappen</summary>
<pre><code class="php">public function getPDF() : void
{
$pdfConfig = array(
'format' => 'A5',
'margin_left' => 5,
'margin_right' => 5,
'margin_top' => 5,
'margin_bottom' => 5,
'margin_header' => 0,
'margin_footer' => 0,
'orientation' => 'L',
);
$pdf = new \Mpdf\Mpdf($pdfConfig);
$pdf->SetTitle('Maandkalender '.$this->_year);
$pdf->SetAuthor('Frans-Willem Post (FWieP)');
$css = file_get_contents('style.css');
$pdf->WriteHTML($css, \Mpdf\HTMLParserMode::HEADER_CSS);
$pdf->AddPage();
$html = '<table class="scaffold"><tr>';
for ($m = 1; $m <= 3; $m++) {
// Add january - march to the first page's first row
$html .= '<td>'.$this->_generateMonthTable($m).'</td>';
}
$html .= '</tr><tr>';
for ($m = 4; $m <= 6; $m++) {
// Add april - june to the first page's second row
$html .= '<td>'.$this->_generateMonthTable($m).'</td>';
}
$html .= '</tr></table>';
$pdf->WriteHTML($html, \Mpdf\HTMLParserMode::HTML_BODY);
$pdf->AddPage();
$html = '<table class="scaffold"><tr>';
for ($m = 7; $m <= 9; $m++) {
// Add july - september to the second page's first row
$html .= '<td>'.$this->_generateMonthTable($m).'</td>';
}
$html .= '</tr><tr>';
for ($m = 10; $m <= 12; $m++) {
// Add october - december to the second page's second row
$html .= '<td>'.$this->_generateMonthTable($m).'</td>';
}
$html .= '</tr></table>';
$pdf->WriteHTML($html, \Mpdf\HTMLParserMode::HTML_BODY);
$pdf->Output('Maandkalender '.$this->_year.'.pdf', D::DOWNLOAD);
}
</code></pre>
</details>
<h2>Epiloog</h2>
<p>Nog een kleine tip achteraf: bij het afdrukken van deze maandkalender op
één vel van A4-formaat, ben je afhankelijk van de printerinstellingen. De
meeste programma's en besturingssystemen bieden een mogelijkheid om 2, 4,
8 of 16 pagina's op één vel te printen.</p>
<p>In mijn geval moest ik dan wel het schaal-percentage op 141% zetten. Laat
dat nou precies de verhouding tussen A5 en A4 zijn (<span
style="white-space: nowrap;">√<span style="text-decoration:overline;">2</span> × 100% = 141%</span>)!</p>
</div>2020-11-27T07:47:11+01:00https://www.fwiep.nl/blog/persoonlijke-pdf-weekkalender-in-phpPersoonlijke PDF weekkalender in PHP2020-11-27T07:40:08+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Zo rond de feestdagen zoeken mijn vrouw en ik steeds weer opnieuw naar
een passende weekkalender voor het daaropvolgende jaar. De ene is te groot
om op te hangen, de ander biedt niet genoeg ruimte om afspraken te noteren.
Een derde gaat dan weer qua budget ons boekje te buiten.</p>
<p>De afgelopen dagen bedacht ik, dat ik met een paar uurtjes programmeren
en wat handvaardigheid inzake papier en karton best zelf zo'n kalender
zou kunnen maken. Aldus geschiedde!</p>
<h2>Update</h2>
<p>Een jaar later ontstond de behoefte om de weekkalender niet alleen op
A4-formaat, maar ook op het half-zo-grote A5 af te drukken. Aldus heb ik
de broncode aangepast, een selectie­mogelijkheid toegevoegd en
<a href="https://github.com/fwiep/weekcalendar">bij GitHub</a> geüpdate.</p>
<h2>Basis</h2>
<p>De basis van elke weekkalender is een verzameling van 52 of 53 groepen
van 7 dagen. Onderstaand voorbeeld maakt zo'n verzameling op basis van
het opgegeven jaar. Eerst worden de nieuwjaarsdagen van het huidige en
opvolgende jaar bepaald. De startdatum van de eerste week kan vóór 1
januari liggen, dus telt de code terug totdat de voorliggende maandag
(<code>$startDate->format('N') == 1</code>) is bereikt.</p>
<p>Daarna begint het aanmaken van de weken zolang aan de volgende voorwaarden
wordt voldaan:</p>
<ul>
<li>de maandag-datum vóór 1 januari van het gevraagde jaar ligt,</li>
<li>het jaar van de getoonde week gelijk is aan het gevraagde jaar, of</li>
<li>de maandag-datum vóór 1 januari van het volgende jaar ligt</li>
</ul>
<p>Voor elke maandag worden daarna zeven dagen (<code>DateTime</code>-objecten)
toegevoegd, in een <code>array</code> gegroepeerd met jaar en weeknummer als sleutel.
Let daarbij op het verschil tussen <code>format('Y')</code> en <code>format('o')</code>. De
eerstgenoemde is het jaartal van de opgegeven datum, de tweede het jaartal
van de ISO-8601 week waartoe de datum behoort.</p>
<p>Een voorbeeld: maandag 30 december 2019 heeft als 'normaal' jaartal 2019.
Het is echter de eerste dag van week 1 van 2020. Zie voor meer informatie
de <a href="https://www.php.net/manual/en/function.date.php">officiële documentatie van <code>date()</code></a>.</p>
<pre><code class="php"><?php
$year = 2020;
$weeks = array();
$firstJan = new \DateTime($year.'-01-01');
$nextFirstJan = new \DateTime(($year+1).'-01-01');
$startDate = clone $firstJan;
while ($startDate->format('N') > 1) {
$startDate->sub(new \DateInterval('P1D'));
}
$loopDate = clone $startDate;
while ($loopDate <= $firstJan
or $loopDate->format('o') == $year
or $loopDate < $nextFirstJan
) {
$week = $loopDate->format('o-W');
for ($i = 0; $i < 7; $i++) {
$weeks[$week][] = clone $loopDate;
$loopDate->add(new \DateInterval('P1D'));
}
}
</code></pre>
<h2>Feestdagen</h2>
<p>Een kalenderjaar telt vele feestdagen. De feesten die altijd op dezelfde
datum vallen zijn eenvoudig aan onze kalender toe te voegen:</p>
<pre><code class="php">$holidays = [];
$holidays[($year-1).'-12-25'] = '1<sup>e</sup> Kerstdag';
$holidays[($year-1).'-12-26'] = '2<sup>e</sup> Kerstdag';
$holidays[$year.'-01-01'] = 'Nieuwjaarsdag';
$holidays[$year.'-02-14'] = 'Valentijnsdag';
$holidays[$year.'-04-27'] = 'Koningsdag';
$holidays[$year.'-05-04'] = 'Dodenherdenking';
$holidays[$year.'-05-05'] = 'Bevrijdingsdag';
$holidays[$year.'-12-25'] = '1<sup>e</sup> Kerstdag';
$holidays[$year.'-12-26'] = '2<sup>e</sup> Kerstdag';
$holidays[($year+1).'-01-01'] = 'Nieuwjaarsdag';
</code></pre>
<p>Andere feesten zoals Pinksteren, Hemelvaart en Carnaval zijn afgeleid van
het hoogfeest van Pasen. De datum van Paaszondag is op zijn beurt weer
afhankelijk van de maanstand en <a href="https://www.php.net/manual/en/function.easter-days.php">vele andere factoren</a>. Met onderstaande
functie bepaalt PHP de datum van Paaszondag in het opgegeven jaar:</p>
<pre><code class="php">function _getEasterDatetime(int $year) : \DateTime
{
$base = new \DateTime("$year-03-21");
$days = easter_days($year);
return $base->add(new \DateInterval("P{$days}D"));
}
</code></pre>
<p>Om de resterende feestdagen toe te voegen, moeten we ons baseren op de
datum van Pasen. Omdat de <code>add()</code> en <code>sub()</code> methoden van <code>DateTime</code>-objecten
echter van nature het moederobject veranderen, moeten we de basisdatum klonen.
Ik heb er voor gekozen om deze handeling en het optellen/aftrekken in één
functie samen te vatten:</p>
<pre><code class="php">function _dtWrap(\DateTime $dt, int $days) : \DateTime
{
$outDt = clone $dt;
$di = new \DateInterval('P'.abs($days).'D');
if ($days < 0) {
$outDt->sub($di);
} else {
$outDt->add($di);
}
return $outDt;
}
</code></pre>
<p>Het toevoegen van de variabele kerkelijke feesten kan er dan als volgt uitzien:</p>
<pre><code class="php">$dtEaster = _getEasterDatetime($year);
$holidays[_dtWrap($dtEaster, -49)->format('Y-m-d')] = 'Carnavalszondag';
$holidays[_dtWrap($dtEaster, -46)->format('Y-m-d')] = 'Aswoensdag';
$holidays[_dtWrap($dtEaster, -2)->format('Y-m-d')] = 'Goede vrijdag';
$holidays[$dtEaster->format('Y-m-d')] = '1<sup>e</sup> Paasdag';
$holidays[_dtWrap($dtEaster, 1)->format('Y-m-d')] = '2<sup>e</sup> Paasdag';
$holidays[_dtWrap($dtEaster, 39)->format('Y-m-d')] = 'Hemelvaartsdag';
$holidays[_dtWrap($dtEaster, 49)->format('Y-m-d')] = '1<sup>e</sup> Pinksterdag';
$holidays[_dtWrap($dtEaster, 50)->format('Y-m-d')] = '2<sup>e</sup> Pinksterdag';
</code></pre>
<h2>Vaders en moeders</h2>
<p>In Nederland worden zowel moeders als vaders geëerd met een eigen feestdag.
Deze valt altijd op respectivelijk de 2<sup>e</sup> zondag van mei en de
3<sup>e</sup> zondag van juni. Met onderstaande code worden deze twee
dagen aan onze kalender toegevoegd:</p>
<pre><code class="php">$dtMothersday = new \DateTime($year.'-05-01');
while ($dtMothersday->format('N') != 7) {
$dtMothersday->add(new \DateInterval('P1D'));
}
$dtMothersday->add(new \DateInterval('P7D'));
$holidays[$dtMothersday->format('Y-m-d')] = 'Moederdag';
$dtFathersday = new \DateTime($year.'-06-01');
while ($dtFathersday->format('N') != 7) {
$dtFathersday->add(new \DateInterval('P1D'));
}
$dtFathersday->add(new \DateInterval('P14D'));
$holidays[$dtFathersday->format('Y-m-d')] = 'Vaderdag';
</code></pre>
<h2>mPDF</h2>
<p>Met behulp van de <a href="https://github.com/mpdf/mpdf">mPDF-bibliotheek</a> kun je een HTML-document omzetten
naar het universele PDF-formaat. Het biedt ondersteuning voor eigen opmaak
door middel van <em>custom style sheets</em> (CSS), eigen lettertypes (fonts) en
nog véél meer. In dit geval heb ik gekozen voor één HTML-tabel van 8 rijen
per pagina en week. De eerste rij bevat het weeknummer, de maand en het
jaartal. De resterende rijen zijn de zeven dagen van de week met weekdag,
eventuele feestdag(en) en dagnummer.</p>
<pre><code class="php">$pdf = new \Mpdf\Mpdf();
foreach ($weeks as $days) {
$pdf->AddPage();
$monday = reset($days);
$sunday = end($days);
$mWeek = strftime('%B', $monday->format('U'));
$mMonday = strftime('%b', $monday->format('U'));
$mSunday = strftime('%b', $sunday->format('U'));
$yWeek = strftime('%Y', $monday->format('U'));
$yMonday = strftime('%Y', $monday->format('U'));
$ySunday = strftime('%Y', $sunday->format('U'));
$ySundayShort = strftime('%y', $sunday->format('U'));
$html = '<table>';
$html .= sprintf(
'<tr><th>Week %d - %s</th>',
$monday->format('W'),
($mMonday == $mSunday ? $mWeek : $mMonday.' / '.$mSunday)
);
$html .= sprintf(
'<th>%s</th></tr>',
($ySunday == $yMonday ? $yWeek : $yMonday.'/'.$ySundayShort)
);
foreach ($days as $ix => $day) {
$dayYMD = $day->format('Y-m-d');
$isHoliday = array_key_exists($dayYMD, $holidays);
$html .= '<tr>';
$html .= sprintf(
'<td>%s%s</td>',
strftime('%A', $day->format('U')),
($isHoliday ? '<br />'.join('<br />', $holidays[$dayYMD]) : '')
);
$html .= sprintf(
'<td>%d</td>',
$day->format('d')
);
$html .= '</tr>';
}
$html .= '</table>';
$pdf->WriteHTML($html);
}
$pdf->Output('Weekkalender-'.$year.'.pdf', D::DOWNLOAD);
</code></pre>
<h2>Resultaat</h2>
<p>De bovenstaande code is zover mogelijk vereenvoudigd om makkelijker te
kunnen begrijpen wat er gebeurt. De definitieve code van dit project heb
ik openbaar gemaakt op mijn pagina bij <a href="https://github.com/fwiep/weekcalendar">GitHub</a> en bevat onder andere
diverse opmaaktrucjes om één en ander passend te maken op A4 of A5.</p>
<p>Voor wie niet kan wachten, heb ik ook een <a href="https://www.fwiep.nl/download/1295939c-4a98-45c7-b5ab-f6cfbd3b5c6d/Weekkalender-2023.pdf">voorbeeld-PDF</a> toegevoegd
over het jaar 2023 (waar vaderdag samenvalt met de verjaardag van één van
de groten der aarde). Voortaan hoeven we niet meer op zoek naar een
passende weekkalender; we genereren een PDF-document en drukken dat af.
Een paar minuten knutselen en we kunnen er weer een jaar tegenaan!</p>
</div>2019-12-24T15:30:08+01:00https://www.fwiep.nl/blog/moto-g6-met-lineageos-of-nietMoto G6 met LineageOS; of niet?2020-11-26T21:03:43+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Sinds een aantal jaren ben ik een enthousiaste gebruiker van Android. Of
eigenlijk een enthousiaste bouwer van Android firmware, zogenaamde <em>Custom
ROMs</em>. Ik begon met mijn persoonlijke toestel van dat moment: een
<a href="https://www.fwiep.nl/blog/y550-met-zelfgebouwd-lineageos">Huawei Ascend Y550</a>. Op basis van het <a href="https://www.lineageos.org/">LineageOS</a>-project en een
passende Linux-kernel bouwde ik ongeveer elke maand een nieuw ROM voor het
inmiddels 8 jaar oude toestel. Zo lang de mensen van LineageOS versie
7.1.2 (Nougat) nog ondersteunen met de maandelijkse beveiligingsupdates,
zal ik dat ook <a href="https://forum.xda-developers.com/android/help/-t2923318">blijven doen</a>.</p>
<p>Toen de gelegenheid zich voordeed en het budget het toeliet kwam er een
nieuwe smartphone in huis: een Motorola G6 (codenaam <em>ali</em>). Bij die keuze
hield ik zoveel mogelijk rekening met bestaande (of misschien toekomstige)
ondersteuning door LineageOS.</p>
<h2>XDA-developers</h2>
<p>Motorola heeft de officiële ondersteuning voor dit toestel met de laatste
update beëindigd. Het zal dus altijd blijven steken op Android 9.0 met een
beveiligingsniveau van april 2020. Dat vind ik schandalig. Ik ging op zoek
naar een alternatief…</p>
<p>In eerste instantie vond ik een van LineageOS afgeleide ROM genaamd
<a href="https://forum.xda-developers.com/moto-g6/development/-t4097553">RevengeOS</a>. Meteen probeerde ik of ik de bouw zelf kon reproduceren.
Dat lukte niet – ik zocht verder. Toen kwam ik <a href="https://forum.xda-developers.com/moto-g6/development/-t4173293">een discussie</a>
op het spoor die precies bood wat ik zocht: een recent custom ROM voor
de G6 met LineageOS op basis van Android 10.0 en beveiligingsupdates tot
en met oktober 2020. Ik zocht en vond <a href="https://forum.xda-developers.com/moto-g6/how-to/-t3791238">de instructies</a> voor de
installatie – klaar!</p>
<h2>Hoop en tegenslag</h2>
<p>Ook dit ROM probeerde ik natuurlijk zelf te bouwen; zeker omdat het
gebruikelijk is om bij het publiceren van een ROM ook altijd te verwijzen
naar de bronnen voor kernel en besturings­systeem. Ik ging met beide aan
de slag en… het bouwen mislukte. Niet één keer, niet twee keer,
maar vaak. Heel vaak. Telkens opnieuw worstelde ik me door de foutmeldingen
op het scherm.</p>
<p>Toen de bouw uiteindelijk slaagde kon ik mijn geluk niet op! Ik flashte
het versgebouwde ROM en wachtte geduldig af totdat de boot-animatie van
LineageOS zou plaatsmaken voor het daaropvolgende startscherm. Ik bleef
wachten… en wachten… tot vijf uur lang. Uiteindelijk gaf
ik op en <a href="https://forum.xda-developers.com/showpost.php?p=83773689&postcount=13">vroeg de originele bouwer om hulp</a>.</p>
<h2>Terug naar af</h2>
<p>Omdat een niet actuele, maar werkende smartphone beter is dan een niet
opstartende smartphone, zocht en vond ik dan toch de instructies om terug
te keren naar de originele firmware van Motorola. Gelukkig is deze zonder
problemen te <a href="https://mirrors.lolinet.com/firmware/moto/ali/official/RETEU/">downloaden bij lolinet.com</a>.</p>
<p>Het toestel moet eerst in bootloader modus worden opgestart. Schakel het
daartoe eerst uit. Daarna houd je <kbd>Vol-</kbd> vast terwijl je de
<kbd>Power</kbd>-toets indrukt. Tot slot sluit je het toestel via USB aan
op een computer en voer je onderstaande commando's uit:</p>
<details>
<summary>Code in-/uitklappen</summary>
<pre><code class="bash">fastboot devices
fastboot oem fb_mode_set
fastboot flash partition gpt.bin
fastboot flash bootloader bootloader.img
fastboot flash modem NON-HLOS.bin
fastboot flash fsg fsg.mbn
fastboot erase modemst1
fastboot erase modemst2
fastboot flash dsp adspso.bin
fastboot flash logo logo.bin
fastboot flash boot boot.img
fastboot flash recovery recovery.img
fastboot flash system system.img_sparsechunk.0
fastboot flash system system.img_sparsechunk.1
fastboot flash system system.img_sparsechunk.2
fastboot flash system system.img_sparsechunk.3
fastboot flash system system.img_sparsechunk.4
fastboot flash system system.img_sparsechunk.5
fastboot flash system system.img_sparsechunk.6
fastboot flash system system.img_sparsechunk.7
fastboot flash system system.img_sparsechunk.8
fastboot flash system system.img_sparsechunk.9
fastboot flash vendor vendor.img_sparsechunk.0
fastboot flash vendor vendor.img_sparsechunk.1
fastboot flash oem oem.img
fastboot erase cache
fastboot erase userdata
fastboot erase DDR
fastboot oem fb_mode_clear
fastboot oem lock
fastboot oem lock
</code></pre>
</details>
<p>Hiermee wordt het complete toestel weer 'als nieuw' met een
<a href="https://forum.xda-developers.com/showpost.php?p=82721513&postcount=4">vergrendelde bootloader</a> en originele firmware van de fabrikant.</p>
<h2>Alternatief</h2>
<p>Koppig als ik ben, bleef ik zoeken naar een alternatieve, veiligere
firmware voor mijn toestel. Ik vond uiteindelijk een LineageOS-afgeleide
genaamd <a href="https://forum.xda-developers.com/moto-g6/development/-t4170941">ResurrectionRemixOS</a>. Hierbij gaf de ontwikkelaar aan dat hij
dit toestel volledige en officiële ondersteuning wil geven binnen het
RROS-project. Daar wordt een mens blij van!</p>
<p>Dit keer lukte het mij wél om, met de geleerde lessen van de andere ROMs
in mijn achterhoofd, het actuele ROM (versie 8.6.4) zelf te bouwen. Dit
was op dat moment zelfs actueler dan het officiële ROM (versie 8.6.3).</p>
<h2>Conclusie</h2>
<p>Normaal gesproken ga ik altijd voor de oorspronkelijke, schone versie van
software, hardware en firmware. <a href="https://resurrectionremix.com/">ResurrectionRemixOS</a> is een afgeleide
van LineageOS, maar het is wel bijna volledig functioneel en door mijzelf
tot een ROM te bouwen. De uitbreidingen en aanpassingen ten opzichte van
LineageOS zijn weloverwogen en best mooi om te zien. Op deze manier kan
mijn toestel er weer jaren tegenaan - zo lang de hardware het volhoudt.
:)</p>
</div>2020-11-26T21:03:43+01:00https://www.fwiep.nl/blog/android-tekstinvoer-via-usbAndroid tekstinvoer via USB2020-07-26T18:19:39+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Bij het inrichten of aanpassen van een Android smartphone komt het met enige
regelmaat voor, dat je lange en vaak complexe tekenreeksen moet invoeren op het
armzalige schermtoetsenbord van de telefoon in kwestie. Denk daarbij bijvoorbeeld
aan wachtwoorden van e-mailaccounts of een WLAN-wachtwoord. Zou het niet prachtig
zijn als je dit met behulp van een computertoetsenbord zou kunnen doen? Eventueel
nog verder vereenvoudigd door gebruik making van een klembord (kopiëren en
plakken)? Gelukkig behoort dit alles tot de mogelijkheden, als je de telefoon
via USB met de computer verbindt!</p>
<h2>ADB</h2>
<p>De afkorting <code>ADB</code> staat voor <em>Android Debug Bridge</em>, een stukje software dat
een verbinding (brug) legt tussen een Android-systeem enerzijds en een computer
anderzijds. Het maakt van begin af aan deel uit van Google's Android-ontwikkeltools
en wordt vaak gebruikt tijdens de ontwikkeling van apps en aangepaste firmware,
zogenaamde <em>Custom ROMs</em>.</p>
<p><code>ADB</code> wordt uitgevoerd op de computer en legt (meestal via USB) verbinding met
het Android-systeem. Laatstgenoemde moet die verbinding wel eerst toestaan. Als
eerste moeten de ontwikkelaarsopties worden ingeschakeld via Instellingen / Over
de telefoon / 7x tikken op Build-nummer. Hierna verschijnt er in Instellingen
een extra optie (bijna onderaan): Opties voor ontwikkelaars. Kies hier voor
<code>Android-foutopsporing</code> en schakel deze functie in.</p>
<h2>Input</h2>
<p>In dit artikel zal ik slechts één mogelijke toepassing van deze duizendpoot op
de commandline toelichten: het commando <code>adb shell input text</code>. Hiermee kun je
tekstvelden waar de Android-focus staat vanaf het toetsenbord van de computer
vullen. Ga daartoe als volgt te werk:</p>
<ol>
<li>Installeer <code>ADB</code> op de computer met bijvoorbeeld <code>$ sudo apt install adb</code>.</li>
<li>Schakel de Android-foutopsporing in, zoals hierboven beschreven.</li>
<li>Sluit de telefoon via USB aan op de computer.</li>
<li>Open een dialoog waarin wordt gevraagd voor het WLAN-wachtwoord dat lang,
complex en moeilijk te typen is. Plaats de cursor in het betreffende tekstveld.</li>
<li>Vul het wachtwoord in met <code>$ adb shell input text 'WatHierStaatIsGeheim'</code>.</li>
</ol>
<h2>Te complex</h2>
<p>Natuurlijk is niets zo eenvoudig als het in eerste instantie lijkt - ook dit
stappenplan is verre van perfect. Met name de lees- en bijzondere tekens in een
aantal wachtwoorden lieten zich niet zo 1-2-3 via <code>bash</code> en <code>adb</code> overpompen. Ik
moest op zoek naar de <a href="https://wiki.bash-hackers.org/syntax/quoting">juiste aanhalingstekens</a> om de tekens heelhuids aan de
overkant te krijgen.</p>
<p>Uiteindelijk bleek (ten minste bij gebruik van <code>adb</code>) de volgende methode
doeltreffend: verpak de tekenreeks eerst in dubbele, daarna in enkele
aanhalingstekens.</p>
<pre><code class="bash">adb shell input text '"WatHierSta@t1s&ehe!m"'
</code></pre>
<p>Toch was er nog één veldslag voordat ik mijn wachtwoord zonder problemen op het
Android-display zag verschijnen. Het dollarteken (<code>$</code>) en de dubbele
aanhalingstekens (<code>"</code>) werden nog steeds door <code>bash</code> geïnterpreteerd. Een
backslash (<code>\</code>) was voldoende om ook deze tekens ongehavend te kunnen invoeren.</p>
<pre><code class="bash">adb shell input text '"Wat\"Hier\$ta@t1s&ehe!m"'
</code></pre>
<h2>Update</h2>
<p>Ook bovengenoemde methode bleek niet volledig effectief toen ik recent een wel
heel complex wachtwoord via deze methode wilde invoeren. Het bevatte niet alleen
een dollar-teken, maar ook een enkel aanhalingsteken, uitroepteken, hoedje,
rechte haak en een accolade. De uiteindelijke oplossing was, om het wachtwoord
als tekst in een bestand te zetten (<code>ww.txt</code>), dat door <code>printf</code> te laten
<em>escapen</em> en dat op zijn beurt aan <code>adb</code> te voeren:</p>
<pre><code class="bash">adb shell input text $( printf "%q" $( cat ww.txt ) )
</code></pre>
<h2>Update (2)</h2>
<p>Bovenstaand commando heeft nog één tekortkoming, zoals ik afgelopen tijd
ontdekte. Als het wachtwoord de combinatie <code>%s</code> bevat, dan wordt dit door
<code>adb</code> vertaald naar een spatie.</p>
<p>De enige manier om zo'n wachtwoord (of welke tekenreeks dan ook) via <code>adb</code>
in te voeren, is door de actie op te splitsen in twee of meer losse
aanroepen - aldus <a href="https://exceptionshub.com/android-adb-shell-input-events.html">deze zeer informatieve pagina</a>. Een voorbeeld:</p>
<pre><code class="bash">$ cat ww.txt
tPaQ%s{brnHKX5Pt[b$Z
$ adb shell input text $( printf "%q" $( cat ww.txt ) )
# The '%s' is output as a space ' ' character
# tPaQ {brnHKX5Pt[b$Z
$ cat ww1.txt
tPaQ%
$ cat ww2.txt
s{brnHKX5Pt[b$Z
$ adb shell input text $( printf "%q" $( cat ww1.txt ) )
# The '%' is output as is, it has no special meaning
$ adb shell input text $( printf "%q" $( cat ww2.txt ) )
# The 's' is output as is, now it has no special meaning
</code></pre>
</div>2017-12-28T22:28:14+01:00https://www.fwiep.nl/blog/eindelijk-vpn-wireguardEindelijk VPN: WireGuard2020-05-11T11:31:39+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Vier jaar geleden revalideerde ik als slechtziende elf maanden lang bij
<a href="https://www.fwiep.nl/slechtziendheid">Koninklijke Visio</a> in Apeldoorn. Elke week een paar dagen ver van huis
met alleen de (mobiele) telefoon als direct contact met het thuisfront.
Met mijn laptop als bagage wilde ik ook graag een veilige digitale
verbinding, maar ik had geen idee hoe ik één en ander moest aanpakken.</p>
<p>Het principe van een <em>virtual private network</em> (VPN) was me wel duidelijk,
maar het lukte me niet om dit met de bestaande technieken in te richten op
<a href="https://www.fwiep.nl/blog/nbg6716-router-met-openwrt">onze OpenWrt-router</a>. Dit lag waarschijnlijk niet aan de software die
ik op dat moment wilde gebruiken, maar aan mijn beperkte begrip van de
toch wel complexe materie.</p>
<h2>WireGuard</h2>
<p>Hier komt het nieuwe VPN protocol <a href="https://www.wireguard.com/"><em>WireGuard</em></a> om de hoek kijken. Het
maakt het (in vergelijking met OpenVPN of IPsec) bijna kinderlijk eenvoudig
om twee systemen via een tunnel beveiligd met elkaar te laten communiceren.</p>
<p>De basis is heel eenvoudig: over één UDP-poort wordt versleutelde data
overgedragen van peer naar peer en andersom. Beide partners zijn gelijkwaardig.
In de configuratie worden IP-adressen en DNS-server aangegeven. Tot slot heeft
elk systeem een sleutelpaar (publiek en privé), waarmee alle data wordt
versleuteld. Het systeem kent zelf zijn eigen privésleutel en de publieke
sleutel van de tegenpartij.</p>
<h2>Server (OpenWrt)</h2>
<p>Hieronder volgende stappen die ik heb doorlopen om op een router met
OpenWrt (achter een provider-modem met NAT) het lokale netwerk (LAN) via
WireGuard van buitenaf bereikbaar te maken.</p>
<pre><code class="sh">opkg update && opkg install luci-app-wireguard qrencode;
wg genkey | tee privatekey.txt | wg pubkey > publickey.txt;
</code></pre>
<p>Eerst worden de pakketlijsten bijgewerkt en de benodigde software
geïnstalleerd. Daarna genereert het commando <code>wg genkey</code> een privésleutel.
Deze wordt (tijdelijk) opgeslagen en meteen doorgegeven aan <code>wg pubkey</code>
dat hiermee de publieke sleutel genereert. Ook deze wordt tijdelijk
opgeslagen.</p>
<p><strong>Let op: het nieuwe protocol wordt pas geladen na een herstart van de router.</strong></p>
<h3>Interface</h3>
<p>Maak via <code>Network</code> → <code>Interfaces</code> → <code>Add new interface</code> een nieuwe
interface aan. Geef een naam op, bijvoorbeeld <code>wg0</code>. Kies bij protocol voor
<code>WireGuard VPN</code>. Bevestig met <code>Create interface</code>.</p>
<p>Log via SSH of sFTP in en open het bestand met de privésleutel. Kopieer en
plak de sleutel in het veld <code>Private Key</code>. Kies een poortnummer ver boven
de 1024 en vul dat in bij <code>Listen Port</code>, bijvoorbeeld <code>51920</code>. Kies een
IP-reeks voor het nieuwe VPN, en geef de router bijvoorbeeld
<code>192.168.42.1/24</code>.</p>
<p>Koppel op het tabblad <code>Firewall Settings</code> de nieuwe interface aan de zone
<code>LAN</code>. Later komen we nog terug voor het vullen van het tabblad <code>Peers</code>.</p>
<h3>Firewall</h3>
<p>Log via SSH in en voeg met de volgende commando's een nieuwe regel aan
de vuurmuur toe, die het verkeer van buitenaf op de gekozen UDP-poort
toestaat:</p>
<pre><code class="sh">uci add firewall rule
uci set firewall.@rule[-1].name="Allow-Wireguard-Inbound"
uci set firewall.@rule[-1].src="wan"
uci set firewall.@rule[-1].target="ACCEPT"
uci set firewall.@rule[-1].proto="udp"
uci set firewall.@rule[-1].dest_port="51920"
uci commit firewall
/etc/init.d/firewall restart
</code></pre>
<h3>Peers</h3>
<p>Om gesprekspartners (peers) toe te voegen, zal voor ieder systeem een eigen
instellingen­bestand worden gemaakt, met eigen IP-adres en eigen sleutelpaar.
Zo'n configuratie ziet er bijvoorbeeld als volgt uit:</p>
<pre><code class="ini">[Interface]
Address = 192.168.42.2/32
DNS = 192.168.1.1
ListenPort = 51920
PrivateKey = <CLIENT-PRIVATE-KEY>
[Peer]
AllowedIPs = 192.168.42.0/24, 192.168.1.0/24
Endpoint = <PUBLIC-IP-ADDRESS-OR-DYNDNS-HOSTNAME>:51920
PublicKey = <SERVER-PUBLIC-KEY>
</code></pre>
<p>Het stukje <code>[Interface]</code> beschrijft de client zelf: zijn IP-adres in het VPN,
zijn ingestelde DNS-server, de gekozen UDP-poort en zijn privésleutel als
base64-gecodeerde tekenreeks. Deze kun je laten genereren door <code>wg genkey</code>.</p>
<p>Het gedeelte <code>[Peer]</code> beschrijft in ons geval de VPN-server met OpenWrt. De
toegestane IP-reeksen zijn respectivelijk die van het VPN en het LAN.
<code>Endpoint</code> is het IP-adres of de (dynamische) domeinnaam met poortnummer
waarop de router van buitenaf bereikbaar is. Tot slot volgt nog de publieke
sleutel van de router.</p>
<p>Ga in OpenWrt terug naar het tabblad <code>Peers</code> van de WireGuard-interface.
Geef eventueel een beschrijving (<code>Description</code>) van de client. Plak de
publieke sleutel van de client in het veld <code>Public Key</code>. Vul bij
<code>Allowed IPs</code> het IP-adres van de client in, bijvoorbeeld <code>192.168.42.2/32</code>.
Zet een vinkje bij <code>Route Allowed IPs</code> en vul bij <code>Persistent Keep Alive</code>
een waarde van <code>25</code> in. Klik op <code>Save</code> en de eerste peer is een feit.</p>
<h2>Client (Android)</h2>
<p>De officiële app van de WireGuard-ontwikkelaars is in de <a href="https://play.google.com/store/apps/details?id=com.wireguard.android">Play Store</a>
te downloaden. Hiermee kun je vanuit het niets een verbinding opzetten,
maar ook een bestaande configuratie inlezen uit een bestand. Plaats het
bestand <code>wg0.conf</code> bijvoorbeeld via USB in de Download-map van Android.
Daarna volgt een klik op de grote Plus-knop. Kies voor <code>Importeren uit
bestand</code>. Daarna kan met één vingertik de VPN-verbinding worden in- en
uitgeschakeld.</p>
<h2>Client (Debian)</h2>
<p>Op mijn Debian systeem was het inrichten iets ingewikkelder, omdat het nieuwe
WireGuard-protocol nog niet in de <code>stable</code>-variant van het besturingssysteem
is toegelaten. Met onderstaande commando's wordt het <code>unstable</code>-repository
toegevoegd en de software daaruit geïnstalleerd.</p>
<pre><code class="sh">$ echo "deb http://deb.debian.org/debian/ unstable main" | \
sudo tee /etc/apt/sources.list.d/unstable-wireguard.list;
$ printf 'Package: *\nPin: release a=unstable\nPin-Priority: 150\n' | \
sudo tee /etc/apt/preferences.d/limit-unstable;
$ sudo apt update && sudo apt install wireguard resolveconf;
</code></pre>
<p>Maak het configuratiebestand <code>wg0.conf</code> zoals hierboven beschreven en
plaats dat bij voorkeur in de map <code>/etc/wireguard</code>. Daarna kun je met
deze commando's de tunnel graven en na gebruik weer dichtgooien:</p>
<pre><code class="sh">sudo wg-quick up wg0;
sudo wg-quick down wg0;
</code></pre>
<h2>Conclusie</h2>
<p>Dankzij de grote inspanningen van de WireGuard-ontwikkelaars en de mensen
die daarover schrijven in de media (<a href="https://www.linode.com/docs/networking/vpn/set-up-wireguard-vpn-on-debian/">link 1</a>, <a href="https://chrisbuchan.co.uk/uncategorized/wireguard-setup-openwrt/">link 2</a>)
is het mij eindelijk gelukt om een veilig en gebruiks­vriendelijk VPN
in te richten. Het heeft even geduurd, maar het was zeker de moeite waard!</p>
</div>2020-05-11T11:31:39+02:00https://www.fwiep.nl/blog/slimme-meter-monitor-met-p1monSlimme meter monitor met P1MON2020-05-04T20:23:13+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Sinds geruime tijd hebben we een zogenaamde <a href="https://www.netbeheernederland.nl/consumenteninformatie/slimmemeter"><em>slimme meter</em></a> in huis; een
elektriciteitsmeter die de meterstanden digitaal bijhoudt en op aanvraag
doorstuurt naar de netbeheerder. Wat ik niet wist, was dat dit apparaat
ook een voorziening heeft voor de eindgebruiker om zelf die gegevens op
te vragen.</p>
<h2>Seriële verbinding</h2>
<p>Met de P1-poort kun je zowel het gas- en elektriciteitsverbruik als ook
de eventueel geleverde elektrische energie uitlezen. Maar welk apparaat
kon ik nou aansluiten op een zespolige RJ-12 connector in de meterkast?
De connector bevat de volgende signalen (zie <a href="https://www.netbeheernederland.nl/_upload/Files/Slimme_meter_15_a727fce1f1.pdf">officiële specificatie</a>):</p>
<ol>
<li>+5V voeding</li>
<li>data request</li>
<li>massa (data)</li>
<li>(vrij)</li>
<li>data</li>
<li>massa (voeding)</li>
</ol>
<p>De daadwerkelijke data is een geïnverteerd serieel Rx-signaal. Een korte
zoektocht op internet bracht mij bij een zogenaamde <a href="https://www.sossolutions.nl/slimme-meter-kabel">slimme-meter-kabel</a>.
Deze heeft een RJ-12 ansluiting aan de ene, en een USB-aansluiting aan de
andere kant. Daar tussenin zit een klein stukje elektronica dat de 'naakte'
data van de P1-poort omzet naar een gangbaar serieel signaal.</p>
<h2>P1 monitor</h2>
<p>Wat nu nog restte, was het daadwerkelijke uitlezen, opslaan en verwerken
van de gegevens uit de slimme meter. Gelukkig zijn daarvoor een aantal
kant-en-klare mogelijkheden; koppelingen met verschillende domotica systemen,
maar ook de <a href="https://www.ztatz.nl/">P1 monitor</a>-software van ZTATZ. Dit is een voor de Raspberry
Pi 3 samengesteld Nederlandstalig pakket, dat rechtstreeks op een SD-kaart
kan worden gezet. De slimme-meter-kabel wordt dan met één van de USB-poorten
van de Pi verbonden – klaar.</p>
<h2>Instabiel</h2>
<p>Toen ik deze combinatie van hard- en software de afgelopen weken inrichtte,
werkte het van begin af aan zonder problemen. Althans – dat dacht ik.
Na de eerste nacht stelde ik vast dat de P1-monitor was uitgevallen en
sindsdien geen data meer ontving van de slimme meter. De statuspagina van
de monitor gaf verder geen problemen aan; de Pi had 't niet te warm of koud,
was niet te zwaar belast. Na een herstart werkte alles weer. Maar ook de
volgende nacht was het weer raak. Waar kon dit aan liggen?</p>
<p>Ik nam contact op met de netbeheerder en vroeg of zij de voorgaande nacht
iets bijzonders hadden gemerkt. Konden zij onze meter wél uitlezen? Ja dus.
Aldus groeide het vermoeden dat de slimme-meter-kabel defect zou zijn. En
dat terwijl het een kabel met A-merk chip (FTDI FT232R) betrof…</p>
<h2>Onderzoek</h2>
<p>Ik stond op het punt om een nieuwe kabel te bestellen, totdat ik bedacht
eens de logbestanden van de Raspberry Pi te bekijken. Daar sprong één melding
in grote veelvoud steeds opnieuw in het oog: <code>Over-current change detected...</code>.
Er was duidelijk iets mis met de voedingsspanning of -stroom. Ik had –
uit zuinigheid – de minst krachtige USB-voeding met micro-USB aansluiting
gebruikt die ik kon vinden (1,25 Ampère). Ik redeneerde dat de Pi in z'n
eentje zonder randapparatuur toch geen 2 of 3 Ampère nodig zou hebben?</p>
<p>Blijkbaar wel, zie <a href="https://www.raspberrypi.org/documentation/computers/raspberry-pi.html">documentatie</a>. Toen ik dan toch maar de officiële
3A-voeding inprikte en de Pi opnieuw startte, was de foutmelding verdwenen
en draaide de monitor zonder problemen; uren, dagen, weken lang. We hebben
sindsdien een steeds beter inzicht in ons eigen elektriciteits- en gasverbruik.
Bovendien was de P1-monitor één van de redenen om met <a href="https://www.fwiep.nl/blog/eindelijk-vpn-wireguard">WireGuard VPN</a>
aan de slag te gaan… :-)</p>
</div>2020-05-04T20:23:13+02:00https://www.fwiep.nl/blog/reanimatie-van-een-gemberbroodReanimatie van een Gemberbrood2020-05-04T11:43:38+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Zoals menigeen bewaar ook ik mijn oude telefoons. Zij het de bedrade toestellen
voor op het bureau, of de vorige smartphone die elke dag nog kleiner lijkt dan
de dag daarvoor. Zo vond ik deze week mijn eerste Android-toestel terug:
een Samsung Galaxy mini, typenummer GT-S5570 uit januari 2011.</p>
<p>Ik kon me herinneren dat ik er ooit mee had gestoeid. Ik wilde er toen een
nieuwe(re) firmware op zetten. Blijkbaar was dat niet gelukt, want het
toestel startte niet meer normaal op. Zou ik op de een of andere manier de
originele firmware terug kunnen halen?</p>
<h2>Forum</h2>
<p>Gelukkig weet ik in zo'n geval het XDA-developers forum te vinden en was
er ook nu een ervaren iemand zo vriendelijk om <a href="https://forum.xda-developers.com/t/tut-official-gingerbread-2-3-6-05-05-2012-galaxymini-s5570-odin-updated.1255305/">zijn instructies</a> achter
te laten. De download-links waren niet allemaal meer geldig; maar ja, wat
kun je verwachten van een toestel en forumpost die al bijna 10 jaar oud zijn?</p>
<p>Van mijn eerdere poging had ik de daadwerkelijke firmware nog bewaard; een
ZIP-bestand van 134 MB, uitgepakt 238 MB met de bijzondere extensie <code>.tar.md5</code>.
In bovengenoemde post volgde ik de instructies van Methode 2 (alles-in-één).</p>
<h2>Odin en Windows</h2>
<p>Voor het buiten de gebaande paden flashen of updaten van firmware op toestellen
van Samsung, kun je niet anders dan gebruik maken van <a href="https://odindownload.com/"><em>Odin</em></a>, een
Windows-applicatie met een iets minder-dan-gemiddeld legale bijsmaak. Het
programma kent alle ins en outs van zo'n beetje de gehele Samsung
product­catalogus, maar is niet echt intuïtief te bedienen. Gelukkig
was de genoemde forumpost heel duidelijk inzake welk vinkje je wél, en welk
vinkje je ABSOLUUT NIET moest aanvinken voor een succesvolle flash.</p>
<p>Nadeel voor mij was wel dat dit programma enkel en alleen onder Windows
draait. Mijn ervaring met het cross-platform alternatief <a href="https://www.glassechidna.com.au/heimdall/">Heimdall</a> is
minder goed – waarschijnlijk door gebrek aan inzicht van mijn kant.
Aldus ging ik op zoek naar een mogelijkheid om in <a href="https://www.fwiep.nl/blog/windows-vm-voor-noodgevallen">een virtuele machine</a>
de oude smartphone met Android 2.3 te reanimeren.</p>
<h2>Mottenballen</h2>
<p>Met een behoorlijke dosis trial-and-error kwam ik tot de conclusie dat mijn
virtuele Windows niet geschikt was om een in download-modus staande
Android-dinosaurus via USB een nieuw leven te geven. Het statusdisplay van
Odin gaf wel '<code>Connecting...</code>' aan, maar na een time-out eindigde de
zoveelste poging altijd met '<code>FAILED</code>'. Ik moest en zou ergens een fysieke
Windows-machine vandaan halen!</p>
<p>"Wie wat bewaart, die heeft wat," moet ik hebben gedacht toen ik in een
ver verleden een oude Dell Optiplex 760 aan de kant zette als mogelijke
reserve-PC. Origineel werd dit systeem met <em>Windows Vista</em> uitgeleverd.
De geliefde opvolger daarvan (Windows 7) is sinds januari 2020
<a href="https://www.microsoft.com/nl-nl/windows/windows-7-end-of-life-support-information">vogelvrij verklaard</a>. Dus dit systeem was (qua beveiliging) helemáál
ten dode opgeschreven.</p>
<h2>Reanimatie</h2>
<p>Ondanks mijn bedenkingen besloot ik toch met de geplande reanimatie door
te gaan. Ik installeerde het besturingssysteem (Vista) opnieuw met behulp
van een met <a href="https://github.com/WoeUSB">WoeUSB-cli</a> gebakken USB-stick; op zich al een novum. Uit
veiligheidsoverweging liet ik de (inter-)netwerkverbinding onaangesloten.
Ik volgde de instructies nauwlettend en kopieerde via een USB-stick alle
bestanden die ik op de Vista-PC nodig had daar naartoe.</p>
<p>Onder andere moesten de juiste <a href="https://developer.samsung.com/mobile/android-usb-driver.html">USB-stuurprogramma's</a> van Samsung worden
geïnstalleerd. Toen het flashen met Odin maar niet wilde lukken, zocht ik
mijn toevlucht tot het vervloekte Samsung Kies – een applicatie die
onder Windows 10 zelfs standaard verboden is en geblokkeerd wordt. Het
installatiebestand van Kies vereiste een internetverbinding en ik moest
dan toch maar overstag…</p>
<h2>Finale</h2>
<p>Uiteindelijk is het me gelukt om met Windows Vista, de 64-bit stuurprogramma's
van Dell, Kies en Odin Multi-downloader de smartphone terug tot leven te wekken.
Ik koppelde hem via wifi aan ons gastennetwerk en richtte een Google-account
in. De mailbox was bereikbaar en ik was blij! Tot zover de succesvolle kant
van dit verhaal.</p>
<p>Tot mijn spijt stelde ik vast dat de Android browser - zelfs met juist
ingestelde datum en tijd - geen HTTPS-verbindingen aankon. Aangezien het
overgrote merendeel van het internet tegenwoordig (gelukkig!) met SSL
versleuteld communiceert, kon ik ook deze toepassing afschrijven.</p>
<h2>Jammer, maar helaas</h2>
<p>Dat brengt me, na een heel lang verhaal, bij de kern van dit artikel. Ik had
er bijna een <code>TL;DW</code> (too long, didn't write) van gemaakt. Wat denk je te
bereiken als je een smartphone van bijna 10 jaar oud nog nieuw leven in wil
blazen? Het is nogal ontnuchterend om vast te stellen dat zo'n ding écht
niets meer kan betekenen. Ik vind het zonde van de grondstoffen; de accu
is nog goed, GPS en 2G met een passende SIM-kaart zullen beslist nog werken.</p>
<p>Ik denk dat ik het toestel weer onderin de bureaula stop. Tot er op een dag
iemand met een briljant idee op de proppen komt om zo'n oertijd-gemberbrood
een nieuwe bestemming te geven…</p>
</div>2020-05-04T11:43:38+02:00https://www.fwiep.nl/blog/windows-vm-voor-noodgevallenWindows VM voor noodgevallen2020-03-10T22:21:33+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>De meeste elektronische apparaten waarop software draait die vervangen of
bijgewerkt kan worden, ondersteunen slechts een klein deel van de
besturingssystemen. Natuurlijk zijn er uitzonderingen, zoals een
navigatie-apparaat met eigen WiFi-verbinding. Soms wordt ook Apple macOS
ondersteund, maar meestal is men <a href="https://support.mio.com/">aangewezen</a> op een computer met
Microsoft Windows.</p>
<p>Sinds mijn volledige overstap naar GNU/Linux, meer dan tien jaar geleden,
liep ik regelmatig tegen bovengenoemde beperking aan. Ik gebruikte aldus
een virtuele machine van <a href="https://www.virtualbox.org/">Oracle VirtualBox</a>, met daarin een reguliere
Windows-installatie.</p>
<h2>modern.ie</h2>
<p>Microsoft maakt het tegenwoordig voor (web-) ontwikkelaars gemakkelijk om
hun applicaties en sites te testen met Edge en/of Internet Explorer.
Ze bieden daartoe via <a href="https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/">https://modern.ie</a> kant-en-klare virtuele
machines aan in verschillende formaten - daaronder ook dat van Oracle
VirtualBox.</p>
<p>Je hebt zo voor 90 dagen de beschikking over een volledig functionerende
en up-to-date Windows 10. Hierna kun je de machine opnieuw importeren en
starten. De teller start dan weer bij nul.</p>
<h2>KVM & friends</h2>
<p>Recent ontdekte ik de in GNU/Linux ingebouwde mogelijkheden om andere
besturingssystemen te virtualiseren. In de kernel zijn met <a href="https://www.linux-kvm.org/">KVM</a> de
nodige voorbereidingen getroffen; met <em>Qemu</em> en <em>virt-manager</em> kun je
er als gewone gebruiker mee aan de slag. Wie minder eisen stelt, en
meer gebruikersgemak wil, kan op <a href="https://wiki.gnome.org/Apps/Boxes">GNOME Boxes</a> terugvallen.</p>
<h2>Conversie</h2>
<p>Nu was het alleen nog een uitdaging om één van de door Microsoft aangeboden
virtuele machines draaiend te krijgen onder <code>KVM</code>. Ik onderzocht met <code>file</code>
het al eerder gedownloade en uitgepakte bestand:</p>
<pre><code class="bash">$ file "MSEdge - Win10.ova";
MSEdge - Win10.ova: POSIX tar archive
</code></pre>
<p>Een POSIX tar archief? Okee, dan wagen we een kijkje met <code>tar</code>..:</p>
<pre><code class="bash">$ tar tvf "MSEdge - Win10.ova"
-rw-r----- vboxovf10/vbox_v6.0.4r128413 6365 2019-03-19 12:41 MSEdge - Win10.ovf
-rw-rw---- vboxovf10/vbox_v6.0.4r128413 7255868928 2019-03-19 12:41 MSEdge - Win10-disk001.vmdk
</code></pre>
<p>Het <code>.ovf</code>-bestand bleek een <code>XML</code>-document met de eigenschappen en
systeemeisen van de virtuele machine. Het <code>.vmdk</code>-bestand is een <code>VMware4
disk image</code>. Zou ik dit kunnen omvormen tot iets bruikbaars voor KVM?</p>
<p>Gelukkig zijn er meer mensen met deze gedachte. Er is zelfs iemand die
<a href="https://github.com/lentinj/ie-vm">een waar scripttrio</a> schreef om het gehele proces van downloaden,
uitpakken, converteren en installeren te automatiseren. Ik bekeek die
broncode en heb (ten minste de eerste stappen) voor mezelf als volgt
samengevat:</p>
<pre><code class="bash">tar xvf "MSEdge - Win10.ova";
qemu-img convert -O qcow2 "MSEdge - Win10-disk001.vmdk" "MSEdge - Win10-disk001.qcow2";
</code></pre>
<h2>Virt-manager</h2>
<p><code>virt-manager</code> is een grafisch programma om virtuele machines en machines
op afstand te beheren. Hoewel ik normaalgesproken een commandline aanpak
altijd de voorkeur geef, moet ik eerlijk bekennen met het onderliggende
<code>virsh</code> nog niet genoeg ervaring te hebben. Wie weet voel ik me in de toekomst
vrij genoeg en update ik dit artikel met de passende instructies.</p>
<p>In het menu "File" koos ik voor "New virtual machine". Dan "Import
existing disk image". Met "Browse" en "Local browse" zocht ik het zojuist
gemaakte <code>.qcow2</code>-bestand op. De verdere instellingen spreken min of meer
voor zich. Wel voegde ik nog een SATA-CD-ROM station toe en stelde bij
de NIC (netwerkcontroller) het "Device model" in op "virtio".</p>
<h2>VirtIO</h2>
<p>Om Windows 10 met optimale prestaties in een virtuele machine met Qemu
en KVM te gebruiken, zullen er een aantal stuurprogramma's (drivers)
moeten worden geïnstalleerd. Gelukkig zijn deze bij het Fedora-project
als <a href="https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/latest-virtio/virtio-win.iso">directe download</a> beschikbaar. Dit <code>.iso</code>-bestand moet bij de
eerste start van de machine worden toegevoegd aan het virtuele
CD-ROM-station. Hierop staan dan de drivers voor onder andere de virtuele
grafische kaart.</p>
<h2>Conclusie</h2>
<p>Met de hierboven beschreven combinatie van programma's heb ik onder mijn
<a href="https://www.fwiep.nl/blog/van-ubuntu-naar-debian-hoogste-tijd">huidige Debian</a> succesvol het navigatieapparaat geüpdate. Als dat over
een half jaar opnieuw moet gebeuren, voer ik de stappen vanaf de conversie
opnieuw uit en kan ik met een schone lei beginnen; geheel legaal en
functioneel.</p>
</div>2020-03-10T22:21:33+01:00https://www.fwiep.nl/blog/audio-cd-met-cd-text-via-commandlineAudio-cd met cd-text via commandline2020-03-07T18:11:43+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Vroeger was <a href="https://userbase.kde.org/K3b">k3b</a> mijn favoriete programma om cd's en dvd's te branden. Het
is flexibel en biedt (nog steeds) heel veel mogelijkheden. Het grootste nadeel
vind ik het hele KDE-framework waar het gebruik van maakt. In mijn
<a href="https://www.gnome.org/">GNOME-desktop</a> zou ik al die 'ballast' meeïnstalleren voor één enkel
programma. Dat vond ik niet de bedoeling en dus ging ik op zoek naar een
alternatief.</p>
<p>Al snel vond ik <a href="https://wiki.gnome.org/Apps/Brasero">Brasero</a>, een onderdeel van het GNOME-project. Dit programma
maakt deel uit van de desktop omgeving en is heel eenvoudig te bedienen. Met de
mogelijkheden en flexibiliteit van k3b in mijn achterhoofd, voelde dit echter
als een stap terug - niet vooruit of een andere kant op.</p>
<h2>Commandline</h2>
<p>Vanwege <a href="https://www.fwiep.nl/slechtziendheid">mijn slechtziendheid</a> werk ik graag zonder grafische vensters en een
muiscursor die je overal zoekt, behalve waar hij is. Het toetsenbord heeft mijn
voorkeur voor invoer, de commandoprompt oftewel <em>commandline interface</em> (CLI)
voor uitvoer. Er bleek minimaal één programma geschikt om cd's te branden via
de terminal: <a href="http://cdrdao.sourceforge.net/"><code>cdrdao</code></a>.</p>
<p>Deze applicatie schrijft, op basis van een simpel tekstbestand (Table Of
Contents, <em>TOC</em>), de gegevens of audio naar een lege cd-r. Ze biedt ondersteuning
voor simulatie van het branden, instellen van brandsnelheid, keuze van het te
gebruiken stuurprogramma en nog veel meer. Hiermee ging het branden zeker lukken!</p>
<h2>CD-TEXT</h2>
<p>Met name cd-spelers in autoradio's geven vaak de titel en artiest van afgespeelde
muziek op hun display weer. Deze gegevens worden, samen met de muziek, op de cd-r
opgeslagen. Dit heet <em>CD-TEXT</em>. Met dank aan <a href="http://apocalyptech.com/linux/cdtext/">een leerzame blogpost</a> vond ik
de mogelijkheid om mijn zelfgebrande audio-cd's zoals <a href="https://www.fwiep.nl/muziek/achter-de-naas-aa">"Achter de naas aa"</a>
van deze extra informatie te voorzien.</p>
<p>Het artikeltje beschrijft het formaat van bovengenoemd TOC-bestand, met de
velden voor metadata zoals artiest en titel. De auteur schrijft dat het een
nogal spraakzaam, uitgeschreven syntax is, maar ook:</p>
<blockquote>
<p>... a few seconds in any scripting language would be enough to create a
quick template.</p>
</blockquote>
<h2>Dynamisch TOC-bestand</h2>
<p>Ik wilde graag een reeks <a href="https://xiph.org/flac/">FLAC</a>-bestanden min of meer direct op cd kunnen
branden, met overname van de metadata in de vorm van CD-TEXT. Het was dus zaak
om als eerste die gegevens uit te lezen; ik koos voor <code>metaflac</code>. Hieronder een
voorbeeld van wat zo'n aanroep uitgeeft:</p>
<pre><code class="bash">$ metaflac --list --block-type=VORBIS_COMMENT 01.flac
METADATA block #1
type: 4 (VORBIS_COMMENT)
is last: false
length: 253
vendor string: Lavf56.40.101
comments: 10
comment[0]: ALBUM=Achter de naas aa
comment[1]: ALBUMARTIST=Frans-Willem Post
comment[2]: ARTIST=Frans-Willem Post
comment[3]: CONTACT=https://www.fwiep.nl/
comment[4]: DATE=2016
comment[5]: DISCNUMBER=1
comment[6]: ENCODED-BY=FWieP
comment[7]: TITLE=Recursive Love Affair
comment[8]: TRACKNUMBER=01
comment[9]: TRACKTOTAL=10
</code></pre>
<h3>MetaFLAC</h3>
<p>Omdat ik het beste thuis ben in de PHP-scripttaal, besloot ik dit stuk code in
een shell-script te stoppen, dat ik op de commandoprompt kan aanroepen. De
belangrijkste functie in dat script is deze:</p>
<pre><code class="php">function getInfoFromFlac($flacFile)
{
$cmd = 'metaflac --list --block-type=VORBIS_COMMENT '
.escapeshellarg($flacFile);
$flacInfo = array();
$dummy = exec($cmd, $flacInfo);
$out = array(
'CD_TITLE' => null,
'CD_ARTIST' => null,
'TRACK_TITLE' => null,
'TRACK_ARTIST' => null
);
foreach ($flacInfo as $fi) {
$m = array();
if (preg_match('/(?<=ALBUM=).*$/', $fi, $m)) {
$out['CD_TITLE'] = array_shift($m);
continue;
}
if (preg_match('/(?<=ALBUMARTIST=).*$/', $fi, $m)) {
$out['CD_ARTIST'] = array_shift($m);
continue;
}
if (preg_match('/(?<=TITLE=).*$/', $fi, $m)) {
$out['TRACK_TITLE'] = array_shift($m);
continue;
}
if (preg_match('/(?<=ARTIST=).*$/', $fi, $m)) {
$out['TRACK_ARTIST'] = array_shift($m);
continue;
}
}
return $out;
}
</code></pre>
<p>Op basis van een bestandsnaam (<code>$flacFile</code>) wordt <code>metaflac</code> aangeroepen en
diens uitvoer gefilterd met <code>preg_match()</code>. Uiteindelijk geeft deze functie een
<code>array</code> met vier sleutels: <code>CD_TITLE</code>, <code>CD_ARTIST</code>, <code>TRACK_TITLE</code> en <code>TRACK_ARTIST</code>.</p>
<h3>PHP-script</h3>
<pre><code class="php">$arguments = $argv;
$TEMPLATE_MAIN = <<<SHBSHDGGDSGDGYDGSGDGYSDGSD
CD_DA
CD_TEXT {
LANGUAGE_MAP {
0 : 29 // Dutch
}
LANGUAGE 0 {
TITLE "%1\$s"
PERFORMER "%2\$s"
}
}
SHBSHDGGDSGDGYDGSGDGYSDGSD;
$TEMPLATE_TRACK = <<<SHBSHDGGDSGDGYDGSGDGYSDGSD
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
CD_TEXT {
LANGUAGE 0 {
TITLE "%1\$s"
PERFORMER "%2\$s"
}
}
FILE "%3\$s" 0
SHBSHDGGDSGDGYDGSGDGYSDGSD;
$scriptName = array_shift($arguments);
$USAGE = sprintf(
'Usage: %1$s file-1.flac [file-2.flac [file-X...]]',
basename($scriptName)
);
if (count($arguments) == 0) {
print $USAGE.PHP_EOL;
exit(1);
}
$cdTitle = 'Onbekende CD';
$cdArtist = 'Onbekende Artiest';
$flacInfo = array();
$flacFile = array_shift($arguments);
if (!is_null($flacFile)
&& file_exists($flacFile)
&& in_array(mime_content_type($flacFile), array('audio/flac', 'audio/x-flac'))
) {
$flacInfo = getInfoFromFlac($flacFile);
$cdTitle = $flacInfo['CD_TITLE'];
$cdArtist = $flacInfo['CD_ARTIST'];
}
// Output
printf(
$TEMPLATE_MAIN,
prepareStringForCdText($cdTitle),
prepareStringForCdText($cdArtist)
);
while (
!is_null($flacFile)
&& file_exists($flacFile)
&& in_array(mime_content_type($flacFile), array('audio/flac', 'audio/x-flac'))
) {
$flacInfo = getInfoFromFlac($flacFile);
// Output
printf(
$TEMPLATE_TRACK,
prepareStringForCdText($flacInfo['TRACK_TITLE']),
prepareStringForCdText($flacInfo['TRACK_ARTIST']),
str_replace('.flac', '.wav', $flacFile)
);
$flacFile = array_shift($arguments);
}
exit(0);
</code></pre>
<p>De PHP-variabele <code>$argv</code> bevat altijd de argumenten van het aangeroepen script.
In dit geval dus de bestandsnamen van de FLAC-bestanden die ik wil uitlezen. Met
behulp van <code>array_shift()</code> wordt er stuk voor stuk doorheen gelopen, waarbij de
eerste tevens wordt gebruikt om de CD-gegevens uit te lezen. Let op de controle
op MIME-type. Met andere bestanden kan <code>metaflac</code> niets beginnen.</p>
<p>De uitvoer van het script bestaat uit twee delen: de kop die slechts één keer
wordt uitgegeven (<code>$TEMPLATE_MAIN</code>) en het terugkerende deel per audiobestand
(<code>$TEMPLATE_TRACK</code>). In eerstgenoemde worden de cd-artiest en -titel ingevuld,
in laatstgenoemde de artiest en titel van de betreffende track, alsook de
bestandsnaam die <code>cdrdao</code> moet gebruiken om vanuit te branden.</p>
<p>Een oplettende lezer zal opmerken, dat hier met <code>.wav</code>-bestanden wordt gewerkt.
Zie daartoe de beschrijving van het volledige brand-script, hieronder.</p>
<h3>To ASCII or not to ASCII?</h3>
<p>Tijdens het testen van dit script stelde ik vast, dat CD-TEXT geen ondersteuning
biedt voor volledige UTF-8. Tekens zoals é, ë, ü of Ø werden totaal verbouwd, in
elk geval waren ze niet leesbaar op de genoemde autoradio… Wat blijkt? CD-TEXT
komt uit een tijd waarin UTF-8 nog niet eens bestond; officieel is de
<a href="https://en.wikipedia.org/wiki/CD-Text#Format">te gebruiken codering niet vastgelegd</a>. Het veiligste wat je kan doen, is
alle tekst in simpele <code>ASCII</code>-codering opslaan. Aldus geschiedde, met de hulp
van <a href="http://php.net/manual/en/function.iconv.php"><code>iconv()</code></a>. "André Rieu" wordt "Andre Rieu", "Bløf" wordt "Blof".</p>
<pre><code class="php">function prepareStringForCdText($s)
{
$s = is_string($s) ? $s : (string)$s;
$s = trim($s);
$s = @iconv('UTF-8', 'ASCII//TRANSLIT', $s);
$s = str_replace('"', '', $s);
return $s;
}
</code></pre>
<h3>Alternatief: ISO-8859-1</h3>
<p>Ik wist dat ik ooit, tijdens het luisteren van een audio-cd, letters met
accenten had gezien op het display van de voornoemde autoradio. Aldus zocht ik
zo'n cd en las het TOC-bestand daarvan in:</p>
<pre><code class="plain">TITLE "Dreamer"
PERFORMER "Ren\351 Dehue & Frans-Willem Post"
</code></pre>
<p>De e-acute wordt blijkbaar omgezet naar een bepaalde code, die de cd-speler
daarna vertaalt naar een specifiek niet-ASCII teken. Het blijkt het nummer van
het <code>é</code>-karakter binnen de <a href="https://en.wikipedia.org/wiki/ISO/IEC_8859-1#Codepage_layout">ISO-8859-1 codering</a> te zijn, maar dan in octale
notatie. Uiteindelijk heb ik de hulpfunctie als volgt aangepast:</p>
<pre><code class="php">function prepareStringForCdText($s)
{
$o = '';
$s = is_string($s) ? $s : (string)$s;
$s = trim($s);
$chars = preg_split('//u', $s, -1, PREG_SPLIT_NO_EMPTY);
foreach ($chars as $c) {
$ord = ord($c);
if ($ord > 127 || $ord < 20) {
$o .= sprintf(
'\\%03o',
ord(iconv('utf-8', 'ISO-8859-1//TRANSLIT', $c))
);
} else {
$o .= $c;
}
}
$o = str_replace('"', '\\034', $o);
return $o;
}
</code></pre>
<h3>Update</h3>
<p>Het moest er gewoon van komen: de afgelopen week (voorjaar 2020) wilde
ik opnieuw een audio-cd branden met cd-text. Ik voerde mijn scripts uit
en zie daar:</p>
<pre><code class="plain">Writing to media...
ERROR: toc.txt:20: Illegal token: \47
ERROR: toc.txt:20: syntax error at "EOF" missing EndString
ERROR: toc.txt:20: syntax error at "EOF" missing \}
</code></pre>
<p>Ik zocht en vond de boosdoener in het titelveld van één van de FLAC-bestanden.
Daar stond "<code>A swingin‘ safari</code>". Mijn script vertaalde het typografische
enkele aanhalingsteken naar een apostrophe, maar dan in octale notatie:
"<code>A swingin\47 safari</code>". Blijkbaar kan <code>cdrdao</code> daar niet goed mee overweg.
Of toch wel?</p>
<p>Ik probeerde of ik met een octale notatie van drie cijfers (met voorloopnul)
wél het gewenste teken kon invoeren - eureka! In het script hierboven is
deze fout verholpen met <code>'\\%03o'</code> als formaat voor <code>sprintf()</code>.</p>
<h2>Brand-script</h2>
<p>Het shell-script dat het volledige brandproces automatiseert, maakt gebruik van
<code>avconv</code> (het vroegere <code>ffmpeg</code>), <code>cdrdao</code> en het hierboven beschreven
<code>metaflac</code>-script. De eerste paar blokken code zijn controles of de betreffende
programma's voorhanden zijn. Daarna volgt een loop door alle opgegeven
FLAC-bestanden. Waar nodig worden ze geconverteerd naar WAVE-bestand. Daarna
wordt het TOC-bestand aangemaakt en het brandproces gestart.</p>
<pre><code class="bash">#!/bin/bash
# Process a series of flac-audio files into an audio-cd
# using avconv, metaflac and cdrdao
FLACS=("${@}");
USAGE="Usage: $( basename ${0} ) file1.flac [file2.flac [fileX.flac]]";
if [ ${#} = 0 ]; then
echo "${USAGE}";
exit 1;
fi;
if ! hash "avconv" 2>/dev/null; then
echo "avconv utility not found in PATH. Exiting.";
exit 2;
fi;
if ! hash "metaflac" 2>/dev/null; then
echo "metaflac utility not found in PATH. Exiting.";
exit 3;
fi;
if ! hash "cdtext.php" 2>/dev/null; then
echo "cdtext.php script not found in PATH. Exiting.";
exit 4;
fi;
if ! hash "cdrdao" 2>/dev/null; then
echo "cdrdao utility not found in PATH. Exiting.";
exit 5;
fi;
for i in "${!FLACS[@]}"; do
F="${FLACS[${i}]}";
MIME="$( file --brief --mime-type "${F}")";
if [ "${MIME}" = "audio/x-flac" -o "${MIME}" = "audio/flac" ]; then
if [ -f "${F%%.flac}.wav" ]; then
echo "WAV-file exists, skipping conversion...";
continue;
fi
echo "Converting "${F}"...";
avconv -v -8 -i "${F}" -ac 2 -ar 44100 "${F%%.flac}.wav";
fi;
done;
echo "Extracting metadata, writing TOC-file...";
cdtext.php "${@}" > toc.txt;
echo "Writing to media...";
# NOTE:
# the ":0x10" suffix to the driver option is mandatory for FWiePs
# HL-DT-ST DVDRAM GH24NSB0 (LN00) dvd-burner, when writing CD-TEXT
cdrdao write --speed 8 --device /dev/sr0 --driver generic-mmc:0x10 -v 1 -n --eject toc.txt
echo "All done! :-)";
exit 0;
</code></pre>
</div>2017-09-27T23:50:03+02:00https://www.fwiep.nl/blog/fwiep-nl-makeover-2020FWieP.nl makeover 20202020-03-07T17:13:01+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Voor de incidentele bezoeker valt het misschien niet meteen op, maar deze
website is afgelopen week volledig vernieuwd. De inhoud en functionaliteit
zijn gelijk gebleven, maar achter de schermen is er behoorlijk veel
veranderd. In dit artikel geef ik een kijkje onder de motorkap.</p>
<h2>Basis</h2>
<p>Aan het basisprincipe van een website is niets veranderd. Nog steeds
genereert een PHP-interpreter een lap HTML-code die met behulp van CSS
en JavaScript een functioneel en aanschouwelijk digitaal thuis oplevert.
Wel heb ik een aantal zaken toegepast die de dataoverdracht versnellen,
mijn site beter vindbaar en bepaalde onderdelen beter toegankelijk
maken.</p>
<h2>CSS</h2>
<p>Als hulpmiddel bij het opbouwen van een website voor kleine en grote
schermen maak ik sinds een aantal jaren gebruik van <a href="https://getbootstrap.com/">Bootstrap</a>. Dit
framework neemt mij een hoop werk uit handen:</p>
<ul>
<li>consequente opmaak van bijna alle mogelijke onderdelen</li>
<li>een grafisch raster dat flexibel schaalt met de breedte van het scherm</li>
<li>volledige aanpasbaarheid qua kleuren en afmetingen</li>
</ul>
<p>Laatstgenoemde eigenschap benutte ik voorheen slechts ten dele. Ik
kopieerde complete blokken van de uiteindelijke CSS-code en paste deze
aan. Groot nadeel: als ik een kleur wilde veranderen, moest ik handmatig
op zoek naar alle plekken waar deze voorkwam; geen pretje bij het
ontwikkelen of ombouwen van een website.</p>
<h3>SASS</h3>
<p>Toen ik de afgelopen weken besloot de overstap van Bootstrap versie 3
naar versie 4 te maken, wilde ik ook van bovengenoemd nadeel af. Ik had
wel eens eerder gehoord over <a href="https://sass-lang.com/guide">SASS</a>, maar hoe moest dat dan precies?
Gelukkig is Bootstrap er op voorbereid om <a href="https://bootstrap.themes.guide/how-to-customize-bootstrap.html">te worden aangepast</a>.</p>
<p>Bijkomend voordeel van SASS: mijn bronbestand voor de opmaak (<code>.scss</code>)
is veel kleiner dan het oude <code>.css</code>-bestand. Ook is het beter leesbaar
en makkelijker te onderhouden. Als laatste voordeel kan de compiler het
uiteindelijke bestand extreem compact maken (minificeren). Zo worden
meerdere bestanden samengevoegd en op één regel geperst. Voor de browser
van de bezoeker maakt dit niets uit, behalve dat de dataoverdracht
sneller gaat:</p>
<pre><code class="html"><link rel="stylesheet" href="https://www.fwiep.nl/css/app.min.css">
</code></pre>
<h2>JavaScript</h2>
<p>Ook bij het gebruikte JavaScript kon ik met een compiler de leesbaarheid
tijdens het ontwikkelen verbeteren en de prestaties voor de bezoeker
verhogen. De bronbestanden van <a href="https://jquery.com/">jQuery</a>, Bootstrap en mijn eigen
aanpassingen worden door <code>UglifyJS</code> samengevoegd en geminificeerd.
Uiteindelijk voeg ik slechts één tag in mijn HTML toe om alle scripts
mee te nemen:</p>
<pre><code class="html"><script src="js/app.min.js"></script>
</code></pre>
<p>Daarnaast worden op weblog-pagina's zoals deze nog CSS- en
JavaScript-bestanden toegevoegd van <a href="https://highlightjs.org/">HighlightJS</a>, maar dat mag de
pret niet drukken.</p>
<h2>Plyr</h2>
<p>Als muzikant wil ik graag <a href="https://www.fwiep.nl/muziek">mijn muziek</a> met andere mensen delen. Tot
voor kort gebruikte ik daarvoor de standaard HTML5-<code>audio</code>-tag. Ik was er
van overtuigd dat de audioplayers zo het best toegankelijk waren. De
browsers zouden er toch zeker voor zorgen dat bijvoorbeeld een
<a href="https://www.fwiep.nl/slechtziendheid">slechtziende gebruiker</a> met het toetsenbord de muziek kon
beluisteren?</p>
<p>Nee dus. Ik kreeg van verschillende kanten de feedback dat de players in
elke browser anders uitzagen en niet altijd (volledig) met het toetsenbord
te bedienen waren. Ik ging op zoek en vond een universeel en open-source
alternatief: de <a href="https://plyr.io/">Plyr-mediaplayer</a>. Door de duidelijke documentatie
was hij snel geïntegreerd.</p>
<h2>Sitemap</h2>
<p>Een internet zoekmachine kan natuurlijk elke website vanaf de home-pagina
doorspitten op zoek naar inhoud en verdere links. Veel envoudiger is
daarentegen het gebruik van een zogenaamde <em>sitemap</em>, een XML-bestand
met een machineleesbare opsomming van alle pagina's. Dat scheelt de
crawler een heel stuk werk en geeft de beheerder een beetje meer controle
over zijn vindbaarheid.</p>
<p>In het verleden maakte ik gebruik van één statische sitemap; een simpele
opsomming van alle bestaande pagina's - klaar. Toen lanceerde ik dit
weblog en wilde ik de artikelen ook toevoegen. Natuurlijk moest dat wel
geautomatiseerd gebeuren, dus combineerde ik beide smaken in, in totaal,
drie XML-bestanden:</p>
<ul>
<li><code>sitemap.xml</code>: een opsomming van <code><sitemap></code>s in één <code><sitemapindex></code></li>
<li><code>sitemap-static.xml</code>: alle statische pagina's</li>
<li><code>sitemap</code>: een verwijzing naar een PHP-script dat alle weblog-artikelen
toevoegde aan een aparte sitemap</li>
</ul>
<p>Bij de ombouw van de afgelopen weken heb ik dit vereenvoudigd door met
Apache's URL-rewrite regels een verzoek voor <code>/sitemap.xml</code> om te buigen
naar een PHP-script dat alles afhandelt. De lijst met pagina's wordt nu
gegenereerd vanuit het (deels dynamische) navigatiemenu. Zo weet ik
zeker dat alles wat in het menu verschijnt, ook wordt aangeboden aan de
zoekrobots.</p>
<h2>Conclusie</h2>
<p>Door de ombouw naar Bootstrap 4 heb ik weer een hoop geleerd over dit
framework en kan ik het veel effectiever inzetten. Het neemt me nu nóg
meer werk uit handen en bespaart mij zo tijd. Tijd die ik dan weer kan
besteden aan nieuwe muziek- en/of programmeerprojecten.</p>
</div>2020-03-07T17:13:01+01:00https://www.fwiep.nl/blog/digitale-studio-met-ardourDigitale studio met Ardour & friends2020-03-05T18:16:03+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Ongeveer vanaf de eeuwwisseling maak ik voor mezelf en met vrienden <a href="https://www.fwiep.nl/muziek">af en toe
muziekopnames</a>. Door de jaren heen zijn qua apparatuur en software een aantal
generaties gepasseerd. Op dit moment maak ik uitsluitend gebruik van
<a href="https://www.gnu.org/philosophy/open-source-misses-the-point.html">Vrije Software</a>. Hieronder zal ik een poging doen mijn set-up te
beschrijven.</p>
<h2>Ardour</h2>
<p><a href="https://ardour.org/">Ardour</a> is een Digital Audio Workstation (DAW); een programma om digitale
muziekopnames te maken.</p>
<h3>Downloaden</h3>
<p>Hoewel GNU/Linux-distributies als <a href="http://ubuntustudio.org/">Ubuntu Studio</a> Ardour integreren, heb ik
er voor gekozen om de nieuwste versie direct van de ontwikkelaars te gebruiken.
Dit heeft als voordeel dat eventuele fouten sneller zijn verholpen, en dat je
gebruik kan maken van de nieuwste functies en mogelijkheden. Als nadeel moet je
op de koop toe nemen dat sommige dingen niet, niet meer, nog niet, of niet
helemaal naar behoren werken.</p>
<p>De pret begint met het <a href="https://community.ardour.org/srctar">downloaden van de broncode</a>.</p>
<h3>Compileren</h3>
<p>Daarna mag je je systeem voorbereiden op het compileren van de broncode. In mijn
geval lukte dat met de installatie van onderstaande pakketten:</p>
<pre><code class="bash">sudo apt install \
chrpath doxygen g++ graphviz libarchive-dev libasound2-dev libaubio-dev \
libboost-dev libcurl4-gnutls-dev libfftw3-dev libglib2.0-dev \
libglibmm-2.4-dev libgtkmm-2.4-dev libjack-jackd2-dev liblilv-dev liblo-dev \
liblrdf0-dev librubberband-dev libsamplerate0-dev libserd-dev \
libsndfile1-dev libsord-dev libsratom-dev libsuil-dev libtaglib-ocaml-dev \
libxml2-dev lv2-dev vamp-plugin-sdk;
</code></pre>
<p>Het systeem is hiermee een behoorlijke tijd zoet. Misschien wel net zo lang als
met de volgende stap; het configureren van de bouw. Pak het gedownloade bestand
uit en navigeer naar de nieuwe map.</p>
<pre><code class="bash">./waf configure --optimize
</code></pre>
<p>Vraag me niet waarom het commando klinkt als een blaffende hond; <a href="https://waf.io/faq.html">volgens de
makers</a> was het een makkelijk typebare naam. Tot slot volgt de kers op de
ijsberg, het neusje van de walvis: het compileren zelf:</p>
<pre><code class="bash">./waf
</code></pre>
<p>Dit duurt een aantal minuten, afhankelijk van de eigenschappen van je systeem.</p>
<h3>Starten</h3>
<p>Eenmaal gecompileerd, bevindt zich het uitvoerbaar programma <code>ardev</code> in de map
<code>gtk2_ardour</code>. Je kunt het programma direct starten (zie onderstaand commando)
of de map in je <code>$PATH</code>-variabele opnemen. Je zou ook nog een
<a href="https://developer.gnome.org/integration-guide/stable/desktop-files.html.en"><code>.desktop</code></a>-bestand kunnen maken en die in <code>~/.local/share/applications</code>
neerzetten.</p>
<pre><code class="bash">cd gtk2_ardour/
./ardev
</code></pre>
<h2>Hydrogen</h2>
<p><a href="http://hydrogen-music.org/">Hydrogen</a> is een drumsequencer voor GNU/Linux. Je kunt met behulp van
blokjes-patronen en verschillende drumkits complete slagwerk arrangementen
samenstellen.</p>
<h2>Samenspel: JACK</h2>
<p>Een centrale rol in de samenwerking van alle audio-programma's is weggelegd voor
<a href="http://www.jackaudio.org/">JACK</a>, de Jack Audio Connection Kit. Dit programma draait op de achtergrond
en verbindt alle andere componenten met elkaar. Met het grafische tool
<a href="https://qjackctl.sourceforge.io/"><code>qjackctl</code></a> kun je de instellingen aanpassen en de server 'in de gaten
houden'.</p>
<h2>Samenspel (2): Klick</h2>
<p>Hoewel Ardour de hoofdrol kan spelen in het (via JACK) gelijktijdig afspelen en
stoppen van alle pro­gram­ma's, heb ik ervoor gekozen om met behulp van
<a href="http://das.nasophon.de/klick/">Klick</a> een centrale metronoom de baas te laten zijn over maatsoort en tempo.</p>
<p>Met behulp van een simpel tekstbestand, een zogenaamde tempomap, kun je de
structuur van een opname vastleggen. Labels (die als markers worden
geïmporteerd), maatsoort en tempo (-veranderingen) worden per regel genoteerd.
Bijvoorbeeld zo:</p>
<pre><code class="plain">intro: 17 2/4 83
couplet: 19 2/4 83
refrein1: 17 2/4 83
bridge: 11 2/4 83
refrein2: 17 2/4 83
outtro: 3 2/4 83-50
fine: 20 2/4 50
</code></pre>
<p>De metronoom laat zich als volgt starten:</p>
<pre><code class="bash">klick -T -P -s 3 -f /pad/naar/tempo-map
</code></pre>
<h2>Werkwijze</h2>
<p>Met de overstap naar versie 5 heeft Ardour geleerd om het tempo van de opname
gelijdelijk te verhogen of te verlagen (accelerando of ritardando); de
zogenaamde 'tempo ramps'. Jammer genoeg werken deze hellingen alleen binnen
Ardour, maar niet via JACK voor alle programma's tegelijk.</p>
<p>Na <a href="https://community.ardour.org/node/14220">ruggespraak met de ontwikkelaar</a> besloot ik om dan maar zelf een
oplossing te zoeken om <em>toch</em> met tempomaps te kunnen werken, markers te kunnen
importeren en ritardando's te kunnen gebruiken. Het werd uiteindelijk het
volgende meerstappenplan:</p>
<h2>Update</h2>
<p>Op dit moment (voorjaar 2020, Ardour versie 5.12.0) kunnen de markers worden
'vastgeplakt' aan maatnummers, oftewel hun muzikale positie. Ze schuiven dan
mee met eventuele maatsoort en tempo-wisselingen. Zie <a href="https://manual.ardour.org/working-with-markers/marker-context-menu/">de Ardour handleiding</a>
voor meer informatie over deze functie. Ideaal!</p>
<ol>
<li>Maak een tempomap met de structuur van het nummer en één statisch tempo.</li>
<li>Maak in Ardour een nieuwe sessie aan en sluit het programma.</li>
<li>Voer het <a href="http://das.nasophon.de/klick/"><code>klick2ardour.py</code></a>-script uit en importeer zo de markers,
maatsoorten en tempo.</li>
<li>Start Ardour en open de nieuwe sessie.</li>
<li>Zet alle markers vast met de functie "<em>Glue to Bars and Beats</em>" in het
contextmenu.</li>
<li>Voeg in Ardour waar nodig de tempo ramps toe.</li>
<li>Voeg de tempo wisselingen toe in het tempomap-bestand.</li>
<li>Start de Klick metronoom met <code>-f /pad/naar/tempo-map</code>.</li>
<li>Start Hydrogen</li>
<li>Laat je muzikale creativiteit de vrije loop!</li>
</ol>
</div>2017-01-19T17:43:54+01:00https://www.fwiep.nl/blog/songbook-pro-backup-ontleedSongbook Pro backup ontleed2020-01-25T10:59:05+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Tot voor kort maakte ik gebruik van een zelfgebouwde webapplicatie om
ChordPro muziekbestanden weer te geven op iPad en tablet-PC. Tijdens
band­repetities en optredens is dat veel praktischer dan het
meeslepen van dikke mappen met pakken papier die nooit op volgorde
liggen. Toch had ook deze aanpak minimaal één nadeel…</p>
<h2>Offline</h2>
<p>Als mijn iPad of tablet geen internetverbinding had, kon ik de webapplicatie
niet gebruiken. Ik was aangewezen op een WLAN ter plaatse of het gebruik
maken van een mobiele hotspot. Toen we echter overstapten van analoge naar
digitale geluidstechniek, moest mijn apparaat met het WLAN van de mixer
worden verbonden – en dus niet meer met internet. Wat nu?</p>
<h2>Songbook Pro</h2>
<p>Er zijn een groot aantal apps die het <a href="https://www.chordpro.org/">ChordPro</a>-formaat ondersteunen
en offline functioneren. Ik koos uiteindelijk voor <a href="https://songbook-pro.com/">Songbook Pro</a>,
beschikbaar voor iOS, Android en Windows. Na het uitproberen van de
testversie en de aankoop kon het grote importeren beginnen. Als eerste
voegde ik mijn <a href="https://www.fwiep.nl/muziek/#h2disco">volledige discografie</a> toe.</p>
<p>Liedjes (<em>songs</em>) worden in mappen (<em>folders</em>) ondergebracht en in <em>sets</em>
gegroepeerd. Zo'n set heeft een naam en een datum. Jammer genoeg kon de
kalender in de app alléén per maand bladeren - vanaf het huidige jaar
gerekend. Mijn oudste opname dateert uit 2001, dus ik zou heel wat tijd
kwijt zijn geweest aan het opzoeken van al die data…</p>
<h2>Backup van binnen</h2>
<p>Zoals het een degelijke applicatie betaamt, kunnen alle gegevens binnen
Songbook Pro worden gebackupt naar een externe locatie. Zo kun je op z'n
minst de app met alle data opnieuw inrichten, mocht dat nodig zijn. Of
de app tussen meerdere apparaten synchroniseren. Ik was nieuwsgierig en
onderzocht zowel het backup- (<code>SongbookPro Backup.sbpbackup</code>) als het
synchronisatiebestand (<code>sbp.sync</code>).</p>
<p>Het zijn gecomprimeerde (ZIP-) bestanden, die onder elk gangbaar
besturingssysteem kunnen worden geopend. Als er nog geen PDF-documenten
in de app zijn geïmporteerd, zijn er slechts twee bestanden in het archief:</p>
<pre><code class="bash">$ unzip -l sbp.sync;
Archive: sbp.sync
Length Date Time Name
--------- ---------- ----- ----
512518 2020-01-19 18:26 dataFile.txt
32 2020-01-19 18:26 dataFile.hash
--------- -------
512550 2 files
</code></pre>
<p>Aan de grootte van de beide bestanden is te zien, dat het <code>.txt</code>-bestand
waarschijnlijke de data bevat en het <code>.hash</code>-bestand niet méér dan –
waarempel! – een 32 byte (128 bit) hash is. Het eerste waar ik aan
dacht was het MD5-algoritme; raak!</p>
<pre><code class="bash">$ unzip sbp.sync;
Archive: sbp.sync
inflating: dataFile.txt
inflating: dataFile.hash
$ cat dataFile.hash;
0ab9130e3e306b5f83c39a9671fc9a12
$ md5sum dataFile.txt;
0ab9130e3e306b5f83c39a9671fc9a12 dataFile.txt
</code></pre>
<h2>dataFile.txt</h2>
<p>Het databestand begint met (waarschijnlijk) een versienummer en een <code>CRLF</code>
regeleinde. Daarop volgt, in één regel zonder regeleinde, een verzameling
<code>JSON</code>-data:</p>
<pre><code class="plain">1.0<CR><LF>
{"songs":[ /* whole bunch of JSON data... */ ]}
</code></pre>
<p>Hoe kon ik hierin nou mijn set-datums eenvoudig aanpassen?</p>
<h2>Script</h2>
<p>Ik zocht een manier om het geheel in leesbare vorm in mijn favoriete
editor te kunnen openen en schreef onderstaand script. Het maakt gebruik
van <code>md5sum</code>, <code>unzip</code>, <code>zip</code> en <a href="https://stedolan.github.io/jq/"><code>jq</code></a>.</p>
<pre><code class="bash">#!/usr/bin/env bash
#
# Take apart a SongbookPro backup- or sync-file, open it for editing
# in your favorite texteditor, then put it back together again so the
# app accepts it and can restore it properly.
#
# Saves to a date-stamped filename in the source directory.
EDITOR="$(which geany)";
MD5SUM="$(which md5sum)";
UNZIP="$(which unzip)";
ZIP="$(which zip)";
JQ="$(which jq)";
USAGE="Usage: ${0} (.sync|.sbpbackup)-file";
if [ ${#} -ne 1 ]; then
echo "${USAGE}";
exit 1;
fi
OLDPWD="$(pwd)";
INFILE="${1}";
WORKDIR="$(mktemp --directory)";
# Switch to working dir
cd "${WORKDIR}";
# Unzip
"${UNZIP}" "${INFILE}" 2>&1 >/dev/null;
# Capture original first line, remove trailing CR
FIRSTLINE="$(head -n 1 "dataFile.txt" | tr -d '\r')";
# Save second line to separate file
tail -n +2 "dataFile.txt" > "dataFile.txt.json";
# JSON-prettify
"${JQ}" -M '.' "dataFile.txt.json" > "dataFile.txt.tmp.json";
# Open up for editing
"${EDITOR}" "dataFile.txt.tmp.json";
# Truncate original file, add original first and new second line
echo -en "${FIRSTLINE}\r\n" > "dataFile.txt";
SECONDLINE="$( "${JQ}" -c '.' "dataFile.txt.tmp.json" )";
echo -n "${SECONDLINE}" >> "dataFile.txt";
# Calculate file hash
echo -en "$( "${MD5SUM}" "dataFile.txt" | cut -d' ' -f1 )" > "dataFile.hash";
# Rezip to unique filename
OUTFILE="$(basename "${INFILE}" )";
OUTEXT="${OUTFILE#*.}";
OUTFILE="${OUTFILE%.*}-$( date '+%F-%H-%M-%S' ).${OUTEXT}";
"${ZIP}" -r "$(dirname "${INFILE}" )"/"${OUTFILE}" . -x "dataFile.txt.json" "dataFile.txt.tmp.json" 2>&1 >/dev/null;
# Clean up
rm -r "${WORKDIR}";
# Back to old working dir, exit normally
cd "${OLDPWD}";
exit 0;
</code></pre>
<h2>Poging in PHP</h2>
<p>Na verloop van tijd kwam het idee om de collecties van Songbook Pro en
mijn eigen app te synchroniseren. In elk geval wilde ik graag mijn app
als een soort van backup achter de hand houden – voor het geval dat.
Daarvoor moest ik dan wel de backup- of synchronisatiebestanden van
Songbook Pro kunnen inlezen in PHP. Ik schreef het volgende script:</p>
<pre><code class="php">function extractData(string $filename)
{
$zip = new \ZipArchive();
if ($zip->open('sbp.sync') !== true) {
return false;
}
for ($i = 0; $i < $zip->numFiles; $i++) {
if ($zip->getNameIndex($i) == 'dataFile.txt') {
// Get a resource pointer to the compressed datafile
$ptr = $zip->getStream($zip->getNameIndex($i));
// Read the uncompressed file and split it by CRLF
$json = explode("\r\n", stream_get_contents($ptr));
// Disregard first line, keep the second line
$json = array_pop($json);
// Decode the JSON into PHP objects
$json = json_decode($json);
}
}
$zip->close();
return $json;
}
</code></pre>
<h2>Jammer maar helaas</h2>
<p>Toen ik eenmaal alle gegevens kon uitlezen, kwam ik een fundamenteel
verschil tussen beide apps op het spoor. Mijn app heeft de volgende
(boom-)structuur:</p>
<pre><code class="plain">Book > Set > Song2Set > Song
</code></pre>
<p>In één <code>Book</code> zitten één of meerdere <code>Set</code>s. Meerdere <code>Song</code>s zijn via
een koppeltabel <code>Song2Set</code> met <code>Set</code> verbonden. Songs kunnen dus aan
meerdere sets worden toegevoegd.</p>
<p>Songbook Pro gebruikt daarentegen de volgende structuur:</p>
<pre><code class="plain">Folder > Song
Set > Song
</code></pre>
<p><code>Song</code>s worden rechtstreeks aan een <code>Folder</code> gekoppeld. <code>Set</code>s bestaan
uit één of meerdere gekoppelde Songs. De Set- en Folder-koppeling staat
los van elkaar.</p>
<p>Dit verschil in opbouw is niet zomaar te overbruggen. Ik zou ofwel mijn
eigen app compleet moeten ombouwen, of de situatie accepteren en verder
gaan met andere projecten. Ik koos voor dat laatste :-). Mijn inspanningen
zijn zeker niet voor niets geweest; ik heb er een hoop van geleerd!</p>
</div>2020-01-25T10:59:05+01:00https://www.fwiep.nl/blog/van-ubuntu-naar-debian-hoogste-tijdVan Ubuntu naar Debian - hoogste tijd2019-11-08T17:30:46+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Wat maakt een computersysteem persoonlijk? Natuurlijk zijn er de bestanden
die er op staan, de foto's en documenten, de muziek en downloads. Maar ook
de geïnstalleerde programma's zeggen iets over wie het apparaat gebruikt.
In deze opsomming wordt het onderliggende besturings­systeem nog wel
eens vergeten. De keuze voor deze software tussen hardware en gebruiker
is net zo bepalend voor het gebruikersgemak, de productiviteit en het
plezier in het gebruik.</p>
<p>In 2007 maakte ik voor het eerst kennis met GNU/Linux en stapte ik, beetje
bij beetje, over van propriëtaire naar vrije software. Mozilla Firefox als
internetbrowser, Open- en later LibreOffice voor tekstverwerking en
spreadsheets, Thunderbird als e-mail client. Als besturingssysteem koos
ik voor Ubuntu. Het was mijn eerste ontmoeting met een OS dat niet uit
Redmond kwam. Uiteindelijk verdween ook de dual-boot installatie en
bevolkte enkel nog <a href="https://nl.wikipedia.org/wiki/Tux">de pinguïn</a> mijn computer.</p>
<h2>Ontwikkeling</h2>
<p>In twaalf jaar tijd maakte ik een groot aantal updates mee van alle
programma's en ook Ubuntu werd bijna elke zes maanden bijgewerkt. In al
die tijd leerde ik het systeem steeds beter kennen en werd het mijn
digitale thuis.</p>
<p>De afgelopen twee, drie jaar gaat Canonical, het commerciële bedrijf
achter Ubuntu, echter een kant op die mij steeds minder bevalt. Was het
eerst een grafische desktopomgeving (Unity) en display-server (Mir) die
door geen enkele andere distributie werden opgepakt, kwam daarna een
pakketformaat voor standalone Linux-applicaties (Snap) waar het bedrijf
met een ware app-store de volledige controle over wil houden.</p>
<p>Dit alles druist voor mijn gevoel volledig in tegen de vrijheid waar een
open GNU/Linux-systeem voor staat. Het commerciële belang is voor Canonical
duidelijk belangrijker dan de ondersteuning door de GNU/Linux-gemeenschap.
Men richt zich liever op cloud-computing en het internet-of-things; de
ouderwetse GNU/Linux-desktop lijkt geen prioriteit meer te hebben.</p>
<h2>Alternatieven</h2>
<p>Aldus kwam de gedachte om over te stappen naar een andere distributie;
maar welke? <a href="https://getfedora.org/">Fedora</a> heeft de reputatie helemaal voorop te lopen in
de ontwikkelingen met ondersteuning voor de nieuwste hard- en software.
Wel verschilt het pakket- en systeembeheer behoorlijk van datgene wat ik
gewend was. Verder dan een proefinstallatie in een virtuele machine kwam
ik niet…</p>
<p>Toen bedacht ik dat Ubuntu gebaseerd is op <a href="https://www.debian.org/">Debian</a>, één van de oudste
distributies überhaupt. Zou ik me daarin thuisvoelen? Ik voorzag een
virtuele machine van een verse installatie en werd welkom geheten door
GNOME-shell, een desktopomgeving waar ik me op dat moment totaal niet in
kon vinden. Ik zat nog vastgebakken in mijn jarenlange Ubuntu-ervaringen
met Unity en zijn vriendjes…</p>
<h2>Komt tijd, komt raad</h2>
<p>Toen Canonical in 2017-2018 dan toch hun eigen desktopomgeving opgaf en
GNOME-shell opnieuw invoerde, moest ik wel mee. Het was een hele zoektocht
om mijn "nieuwe" computer te verkennen. Gelukkig is <a href="https://www.gnome.org/">GNOME</a> per definitie
goed gedocumenteerd en kon het ook in mijn behoeften qua toegankelijkheid
(<a href="https://wiki.gnome.org/Projects/Orca">Orca schermlezer</a>, Braille-display) voorzien.</p>
<p>Afgelopen week (najaar 2019) probeerde ik opnieuw hoe ik me in een virtuele
Debian thuis zou voelen. Het bleek een stuk minder verwarrend en frustrerend
dan de keer daarvoor. Zou ik de overstap wagen? De sprong in het diepe,
na twaalf jaar lang niets anders dan Ubuntu?</p>
<h2>Warm bad</h2>
<p>Niet alleen de GNOME-desktopomgeving is goed gedocumenteerd; ook Debian
blinkt uit in de hoeveelheid en kwaliteit van handleidingen, instructies
en achtergrondinformatie. Met name de vragen "<a href="https://wiki.debian.org/WhyDebian">Waarom Debian?</a>" en
"<a href="https://wiki.debian.org/DontBreakDebian">Hoe sloop ik mijn Debian niet onbewust?</a>" openden mijn ogen en
bevestigden mijn keuze. Stabiliteit staat voorop. Daar kan ik mij
volledig in vinden.</p>
<p>Klein praktijkvoorbeeld dat mij iedere keer weer doet glimlachen: onder
Ubuntu werkte het schakelen tussen meerdere taalprofielen met mijn
schermlezer niet naar behoren. Soms moest ik programma's sluiten en
opnieuw openen, soms van venster wisselen… Bovendien werd de
uitspraak dan een ratjetoe van Nederlands, Engels en/of Duits; een
puinhoop waarvan ik dacht dat hij onvermijdelijk was. "Het zal wel aan
mij liggen…"</p>
<p>Toen stelde ik onder Debian mijn taalprofielen van Orca opnieuw in en
zie daar! Niet alleen staan mijn persoonlijke instellingen (waaronder de
genoemde profielen) nu in een lees- en back-upbaar <code>JSON</code>-bestand, maar
de uitspraak is nu eindelijk consequent en volledig correct!</p>
<p>Ja, de meegeleverde versie 3.30 van Orca bevat minimaal
<a href="https://github.com/GNOME/orca/blob/8f05816f953d3337dff61d9c0b710f9b4ad0e3ea/po/nl.po#L10991">één schoonheidsfoutje</a>, maar daar kan ik mee leven tot aan de volgende
stabiele release van Debian — wanneer die dan ook mag komen.</p>
</div>2019-11-08T17:30:46+01:00https://www.fwiep.nl/blog/alternatief-voor-attachmentextractorAlternatief voor AttachmentExtractor2019-11-04T11:40:29+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Al vele jaren maak ik dankbaar gebruik van <a href="https://www.mozilla.org/nl/thunderbird/">Mozilla Thunderbird</a>, een
programma om e-mails mee op te halen, te lezen, versturen en beheren.
Functionaliteit die niet standaard aan boord is, kan met behulp van zogenaamde
<em>add-ons</em> worden toegevoegd. Zo gebruikte ik <a href="https://addons.mozilla.org/nl/thunderbird/addon/attachmentextractor/">AttachmentExtractor</a> voor het
eenvoudig tegelijk opslaan van bijlagen uit meerdere berichten. Dat was heel
flexibel, snel en comfortabel…</p>
<p>Totdat ik laatst mijn computer opnieuw installeerde, en ook Thunderbird vanuit
het niets naar wens mocht configureren. De genoemde add-on bleek niet (meer)
compatibel met nieuwere versies van Thunderbird! Ik ging op zoek naar een
werkbaar alternatief maar vond, binnen de gebaande paden, geen oplossing.</p>
<h2>Update</h2>
<p>Na de overstap van Ubuntu naar Debian, waar ik op korte termijn een eigen
artikel aan zal wijden, bleken de hieronder staande instructies niet meer
te werken. Het pakket <code>cpanminus</code> was niet beschikbaar in de repositories.
Aldus ging ik op zoek naar een alternatief voor het alternatief en vond de
<code>maildir-utils</code> met het programma <code>mu</code> en het <a href="https://www.djcbsoftware.nl/code/mu/cheatsheet.html#retrieving-attachments-from-messages">commando <code>extract</code></a>.</p>
<p>Samen met <a href="https://gnupg.org/"><code>gpg</code></a> voor het eventuele ontsleutelen van de berichten en
bijlagen ontstond uiteindelijk onderstaand shell-script. Dit gaat op zoek
naar <code>.gz</code>-bijlagen en pakt deze uit. Tot slot wordt het <code>.eml</code>-bestand
verwijderd.</p>
<pre><code class="bash">#!/usr/bin/env bash
# Loop through *.eml files
find "." -maxdepth 1 -name "*.eml" 2>/dev/null -print | while read f
do
echo "Processing '${f}'...";
echo "Extracting...";
( mu extract "${f}" 'encrypted\.asc' 2>/dev/null && \
gpg --decrypt-files 'encrypted.asc' 2>/dev/null && \
mu extract 'encrypted' '.*\.gz' 2>/dev/null && \
rm 'encrypted' 'encrypted.asc';
) || ( \
mu extract "${f}" '.*\.gz' 2>/dev/null \
);
echo "Removing...";
rm "${f}";
done
</code></pre>
<h2>Met Perl in de aanval</h2>
<p>Met een zoekmachine in de hand, kom je tegenwoordig een heel eind. Ik kwam
terecht bij een forum over UNIX en aanverwante artikelen. Een onbekende auteur
had <a href="https://www.tek-tips.com/faqs.cfm?fid=4138">een Perl-script</a> geschreven om e-mail bijlagen automatisch op te slaan.
Zo onervaren als ik ben in Perl, kon ik toch zien, dat dit script veel méér deed
dan alleen het opslaan van de bijlagen. Ook moest ik eerst nog een aantal
benodigdheden installeren:</p>
<pre><code class="bash">sudo apt install cpanminus
sudo cpanm MIME::Parser
</code></pre>
<h2>Stripshow</h2>
<p>Het script verwacht een <code>.eml</code>-bestand op <code>STDIN</code>, dus selecteerde ik in
Thunderbird een aantal berichten met bijlagen, drukte <kbd>Ctrl</kbd> +
<kbd>S</kbd> en riep het script op de commandline aan:</p>
<pre><code class="bash">extract-attachment.pl < mijn-email.eml
</code></pre>
<p>Ja, de bijlagen werden opgeslagen, maar ook de tekstuele inhoud van de berichten
werd als HTML-bestand opgeslagen. Ik ging aan de slag om het script zo slank en
schoon mogelijk te maken. Uiteindelijk bleef het volgende stukje code over,
waarbij ik op eigen initiatief de hard gecodeerde <code>$TMPDIR</code> heb vervangen door
de huidige map (<code>cwd</code>):</p>
<pre><code class="perl">#!/usr/bin/perl
use MIME::Parser;
use Cwd qw(cwd);
my $dir = cwd;
sub main {
my $parser = new MIME::Parser;
$parser->output_dir($dir);
$entity = $parser->read(\*STDIN);
unlink<$dir/msg-*.txt>;
unlink<$dir/msg-*.html>;
1;
}
exit(&main ? 0 : -1);
</code></pre>
<h2>Conclusie</h2>
<p>Als bestaande programma's of uitbreidingen niet meer worden onderhouden en
daardoor niet meer werken, gaan creatieve geesten altijd op zoek naar een
werkbaar alternatief. Voor mij is dit slanke script, een eenvoudige wrapper om
Perls <code>MIME::Parser</code>, ruim voldoende. Ik kan nu weer mijn dagelijkse mails met
bijlagen geautomatiseerd verwerken.</p>
</div>2018-01-03T15:17:31+01:00https://www.fwiep.nl/blog/strings-vergelijken-in-mariadbStrings vergelijken in MariaDB2019-08-28T15:32:48+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>De afgelopen dagen liep ik in één van mijn webapplicaties tegen een probleem
aan: het updaten van een tekstveld wilde maar niet lukken - tenzij ik extra
tekens toevoegde, de aanpassing deed en na opslaan de extra tekens weer
verwijderde. Concreet wilde ik de bedrijfsnaam <code>TUV Nederland</code> corrigeren
naar <code>TÜV Nederland</code>. Maar na elke klik op <code>Opslaan</code> was mijn correctie
verdwenen. Wat was hier aan de hand?</p>
<h2>SQL</h2>
<p>Dus ging ik op zoek in de betreffende SQL-query; de stored procedure genaamd
<code>SP_Bedrijf</code>. Daar stond het <code>UPDATE</code>-statement dat alleen werd uitgevoerd
als één van de velden daadwerkelijk is veranderd.</p>
<pre><code class="sql">UPDATE `Bedrijf` SET
`bedrijf_naam` = BedrijfNaam,
`bedrijf_adres_straat` = BedrijfAdresStraat,
`bedrijf_adres_huisnummer` = BedrijfAdresHuisnummer,
...
WHERE
(`Bedrijf_id` = ID)
AND
(
StrEq(`bedrijf_naam`, BedrijfNaam) = 0 OR
StrEq(`bedrijf_adres_straat`, BedrijfAdresStraat) = 0 OR
StrEq(`bedrijf_adres_huisnummer`, BedrijfAdresHuisnummer) = 0 OR
...
);
</code></pre>
<p>Voor deze controle maakte ik gebruik van <a href="https://mariadb.com/kb/en/library/strcmp/"><code>STRCMP()</code></a>. Ik wilde graag dat
<code>NULL</code>-waarden gelijk gesteld zouden worden aan <code>''</code>. Dus schreef ik er deze
wrapper omheen:</p>
<pre><code class="sql">DROP FUNCTION IF EXISTS `StrEq`;
CREATE FUNCTION `StrEq`(
`a` LONGTEXT,
`b` LONGTEXT
) RETURNS BOOL
BEGIN
DECLARE `O` BOOL DEFAULT 0;
IF (NULLIF(a, '') IS NULL) AND (NULLIF(b, '') IS NULL) THEN
SET `O` = 1;
ELSEIF (STRCMP(a, b) = 0) THEN
SET `O` = 1;
END IF;
RETURN `O`;
END;
</code></pre>
<h2>Diagnose</h2>
<p>In een <code>MySQL</code>-console probeerde ik de procedure uit te voeren, maar mijn
functie zei bij alle velden dat er niets was veranderd; en dus werd het
<code>UPDATE</code>-statement niet uitgevoerd. Dan maar de functie rechtstreeks aanspreken:</p>
<pre><code class="plain">mysql> SELECT StrEq('TÜV Nederland', 'TUV Nederland') as `Equal`;
+-------+
| Equal |
+-------+
| 1 |
+-------+
1 row in set (0.000 sec)
</code></pre>
<p>Blijkbaar was <code>STRCMP()</code> er van overtuigd dat een <code>Ü</code> hetzelfde is als een <code>U</code>.</p>
<h2>Oplossing</h2>
<p>Met behulp van <a href="https://mariadb.com/kb/en/library/setting-character-sets-and-collations/">een hoop documentatie</a> kwam ik de uiteindelijke oplossing
op het spoor. Elke tekenreeks (<em>string</em>) heeft een karakterset (<em>characterset</em>),
de tabel waarmee de tekens naar binaire data worden vertaald. Dit is
tegenwoordig (gelukkig!) bijna altijd <a href="https://www.utf-8.com/">UTF-8</a>. Daarbij komt ook nog de
<em>collation</em>, een eigenschap waar ik zo 1-2-3 geen betere vertaling dan
'verzameling' of 'rangorde' voor kon vinden. Deze speelt een grote rol bij
sorteren en – jawel! – vergelijken.</p>
<p>De aanroep van <code>STRCMP()</code> kon ik op de volgende manier uitbreiden:</p>
<pre><code class="sql">...
ELSEIF (STRCMP(
CONVERT(a USING utf8) COLLATE utf8_bin,
CONVERT(b USING utf8) COLLATE utf8_bin
) = 0) THEN
SET `O` = 1;
END IF;
...
</code></pre>
<p>Op deze manier worden beide parameters gedwongen zich te gedragen als keurige
<code>UTF-8</code>-burgers in een <code>bin</code>aire rangorde die zich perfect laat sorteren en
vergelijken. Eindelijk!</p>
<pre><code class="plain">mysql> SELECT StrEq('TÜV Nederland', 'TUV Nederland') as `Equal`;
+-------+
| Equal |
+-------+
| 0 |
+-------+
1 row in set (0.000 sec)
</code></pre>
</div>2019-08-28T15:32:48+02:00https://www.fwiep.nl/blog/download-npo-streams-zonder-browserDownload NPO-streams zonder browser2019-07-12T18:15:45+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Een tijd geleden schreef ik over een <a href="https://www.fwiep.nl/blog/streamen-van-npo-nieuws-via-vlc">download-script</a> voor de live-stream
van NPO-nieuws, het kanaal met 24/7 nieuws van de nederlandse publieke omroep.
Sindsdien is er een hoop veranderd in het aanbod van de NPO, waardoor mijn
script niet meer naar behoren werkte.</p>
<p>In eerste instantie probeerde ik op eigen houtje de streams op het spoor te
komen door de broncode en het netwerkverkeer door te lichten. Ik kwam niet
verder dan een onbetrouwbaar downloadscript dat soms wel, maar meestal niet
functioneerde. Dit moest anders!</p>
<h2>Update (2)</h2>
<p>Vanaf begin 2019 is alle content van de NPO beveiligd met DRM-technologie.
Hierdoor is het wel in een browser of via hun eigen app te bekijken, maar
niet te voor offline gebruik te downloaden. Jammer, maar helaas.</p>
<h2>YouTube-DL</h2>
<p>Toen kwam ik het <a href="https://ytdl-org.github.io/youtube-dl/">youtube-dl</a>-project op het spoor. Deze mensen hadden al
het 'vuile werk' inzake streams al opgeknapt en boden legio mogelijkheden om
er voor en na het downloaden mee te stoeien. Ze bleken zelfs al een module aan
boord te hebben voor de NPO…</p>
<p>Er waren wel nog een aantal handelingen nodig om de stream in VLC te kunnen
bekijken:</p>
<ul>
<li>download-URL bepalen met behulp van <code>youtube-dl</code></li>
<li>met <code>avconv</code> (<code>ffmpeg</code>) het bestand downloaden als <code>.ts</code>-transportstream</li>
<li>met VLC deze transportstream openen en weergeven</li>
<li>na afloop, <code>avconv</code> afschieten en het tijdelijke bestand verwijderen</li>
</ul>
<h2>Update</h2>
<p>Met dank aan <a href="https://blog.yaats-advies.nl/">Eldert Francke</a> is er voor het direct bekijken van de live-streams
een kortere oplossing, die ook nog eens de beste beeldkwaliteit levert (hier als
voorbeeld de stream van NPO2):</p>
<pre><code class="bash">youtube-dl -o - "https://www.npo.nl/live/npo-2" | vlc -
</code></pre>
<p><a href="https://www.vandenoever.info/">Jos van den Oever</a> wees mij in een reactie op een nog korter en mogelijk
eleganter commando om de live streams van NPO te bekijken. Hij maakt gebruik van
<a href="https://mpv.io/">mpv</a>.</p>
<pre><code class="bash">mpv "https://www.npo.nl/live/npo-1"
</code></pre>
<h2>Wrapper-script</h2>
<p>Aldus besloot ik in Bash een wrapper-script te schrijven, dat al deze stappen
comfortabel samenvat. Hiermee kan ik nu de reguliere (1, 2, 3) en thema-kanalen
van de NPO (Nieuws, Cultura) live bekijken zonder browser, dan wel downloaden.</p>
<pre><code class="bash">#!/bin/bash
#
# Script for viewing the NPO-livestreams using a temporary file as buffer,
# utilizing youtube-dl (https://rg3.github.io/youtube-dl/)
#
USAGE="Usage: $( basename ${0} ) [-h] [-k] -s npo-1|npo-2|npo-3|npo-nieuws|npo-cultura
-h Show this usage message
-k Keep the downloaded transport stream
-s Stream to download and view
";
KEEPFILE=0;
STREAMTOVIEW='';
# Process commandline arguments
while getopts "h?ks:" opt; do
case "$opt" in
k) KEEPFILE=1;
;;
s) STREAMTOVIEW=$OPTARG
;;
h|\?|*)
echo "${USAGE}";
exit 0;
;;
esac
done
if [ "${STREAMTOVIEW}x" == "x" ]; then
echo "Please choose one of the streams to view.
";
echo "${USAGE}";
exit 1;
fi
URL="$( youtube-dl --get-url "https://www.npo.nl/live/${STREAMTOVIEW}" 2>/dev/null )";
# Could the URL be determined?
if [ ${?} -ne 0 ]; then
echo "Error downloading stream.";
exit 1;
fi
# Make a temporary transportstream buffer
TEMPFILE="$( mktemp /tmp/tmpXXXXXXXX.ts )";
# Start the download and conversion
avconv -y -i "${URL}" "${TEMPFILE}" 2>/dev/null &
# Capture the process ID
AVCONVPID=$!;
# Give it time to buffer some data
sleep 2;
# Start the media viewer
vlc "${TEMPFILE}" 2>/dev/null;
# Kill the download process
kill -2 ${AVCONVPID};
# Clean-up
if [ ${KEEPFILE} -eq 1 ]; then
echo "Keeping temporary file: ${TEMPFILE}";
else
rm "${TEMPFILE}";
fi
exit 0;
</code></pre>
</div>2017-05-01T23:33:13+02:00https://www.fwiep.nl/blog/handmatige-pgp-wkd-met-pythonHandmatige PGP-WKD met Python2019-07-05T08:42:33+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Door <a href="https://www.heise.de/security/meldung/Angriff-auf-PGP-Keyserver-demonstriert-hoffnugslose-Situation-4458354.html">een nieuwsbericht</a> op één van mijn favoriete tech-sites werd ik er op
gewezen dat de sleutelpraktijken van PGP communicatie niet echt meer werkbaar
zijn. Grof gezegd ligt het systeem op z'n gat door een recente aanval. De
communicatie op zich is hartstikke veilig, maar het overdragen van de sleutels
is een puinhoop en niet bepaald gebruiksvriendelijk.</p>
<p>Gelukkig is er met <a href="https://keys.openpgp.org/">keys.openpgp.org</a> een alternatief, maar hier gaat een
deel van het oude PGP, het zogenaamde <em>Web of Trust</em> verloren. Mijn
<a href="https://www.fwiep.nl/download/0xC0932DFE211E9A9D.asc">persoonlijke PGP-sleutel</a> heb ik desondanks aangemeld en geverifiëerd.</p>
<h2>Web Key Directory</h2>
<p>In één van de commentaren op het nieuwsbericht werd gesproken over een mogelijke
aanvulling op de ouderwetse PGP-sleuteloverdracht: <a href="https://wiki.gnupg.org/EasyGpg2016/PubkeyDistributionConcept#Ask_the_mail_service_provider_.28MSP.29">WKD</a> oftewel <em>Web Key
Directory</em>. Dat is een methode om via HTTPS een sleutel automatisch op te halen
op basis van een e-mailadres (zie ook <a href="https://keyserver.mattrude.com/guides/web-key-directory/">link 1</a>, <a href="https://metacode.biz/openpgp/web-key-directory">link 2</a> en <a href="https://tools.ietf.org/id/draft-koch-openpgp-webkey-service-05.html#key-discovery">link 3</a>).</p>
<p>Het adres wordt gesplitst in een lokaal deel (vóór het <code>@</code>-teken) en de hostnaam.
Daarna wordt het lokale deel in drie stappen omgetoverd tot een goed lees- en
uitspreekbare tekenreeks. Ten eerste worden alle hoofdletters vervangen door
kleine letters. Dan wordt de tekst met <code>SHA1</code> gehasht. Tot slot wordt dit met
<code>zbase32</code> gecodeerd tot een reeks van 32 letters en cijfers.</p>
<p>E-mailprogramma's zoals <a href="https://www.thunderbird.net/">Thunderbird</a> met <a href="https://www.enigmail.net/index.php">Enigmail</a> kunnen daarna vanaf
het eerste bericht versleuteld mailen met de ontvanger. Ze halen op de
achtergrond de sleutel op, als die kan worden gevonden op de webserver
achter het <code>@</code>-teken. Voor <code>info@fwiep.nl</code> luidt de betreffende URL bijvoorbeeld:</p>
<pre><code class="plain">https://www.fwiep.nl/.well-known/openpgpkey/hu/mg6owx9w8c3ejg3tu31f4tha5n17d4rj
</code></pre>
<h2>Z-base32</h2>
<p>Eerder werkte ik al met de base64-codering, waarin met 64 leesbare tekens om
het even welke binaire data kan worden overgedragen. Dat er ook een 32-tekens
variant van zou bestaan, verbaasde me niet. Maar van <a href="https://philzimmermann.com/docs/human-oriented-base-32-encoding.txt">z-base32</a> had ik nog
nooit gehoord. Het is een codering die geoptimaliseerd is om door mensen te
worden gelezen, bewerkt en doorgegeven.</p>
<p>In eerste instantie vond ik <a href="https://github.com/tv42/zbase32">op GitHub</a> een project dat precies bood wat
ik zocht, maar ik snapte niets van de Go-programmeertaal en hoe ik de code
moest aanspreken of compileren tot een functionerend iets. Er was zelfs een
pakket in de Ubuntu repositories met deze inhoud - maar ook hier zonder
documentatie of hint van zijn toepassing.</p>
<p>Toen vond ik in de <code>README</code> van bovengenoemd project de stelling dat het
volledig compatibel wilde zijn met de Python-module <a href="https://pypi.org/project/zbase32/">zbase32</a>. Ook
hier ontbrak, voor zover ik kon zien, documentatie of voorbeeldcode om met
het project aan de slag te gaan.</p>
<h2>Eerste keer Python</h2>
<p>Ik besloot om dit als positieve ontwikkeling te zien en vervolgens met Python
te gaan stoeien - een programmeertaal waar ik veel over las, maar bijna geen
praktijkervaring mee had. Een eerste <code>import zbase32</code> was snel geregeld. Met
<code>import inspect</code> en de <a href="https://docs.python.org/3/library/inspect.html#inspect.getmembers">bijbehorende documentatie</a> kwam ik de functies
van <code>zbase32</code> langzaam op het spoor:</p>
<pre><code class="python">>>> import inspect, zbase32
>>> inspect.getmembers(zbase32, inspect.isfunction)
</code></pre>
<p>Ik vond de <code>zbase32.b2a()</code> functie om binaire data naar een tekenreeks te
coderen. Samen met <code>hashlib.sha1()</code> en wat huishoudelijke code daar omheen
kon ik uiteindelijk onderstaand script schrijven:</p>
<pre><code class="python">#!/usr/bin/env python
#
# Transforms an emailaddress (first commandline argument) to a valid
# PGP-WKD URL. See https://keyserver.mattrude.com/guides/web-key-directory/
# for details.
#
import sys
import hashlib
import zbase32
def main(argv):
if len (argv) != 2:
print "Error: this script expects one single parameter."
sys.exit (1)
parts = argv[1].lower().split("@")
if len(parts) != 2:
print "Provided string is not a valid email address!"
sys.exit(2)
scheme = "https://"
path = "/.well-known/openpgpkey/hu/"
print scheme + parts[1] + path + zbase32.b2a(hashlib.sha1(parts[0]).digest())
if __name__ == "__main__":
main(sys.argv)
</code></pre>
<h2>Epiloog</h2>
<p>Uiteindelijk kon ik dus van mijn eigen e-mailadressen de bijpassende URL
genereren om te gebruiken met WKD via HTTPS. Pas later ontdekte ik op de
verschillende sites een verwijzing naar de <code>gpg</code>-optie <code>--with-wkd-hash</code>.
Hiermee krijg je de <code>zbase32</code>-hash vanzelf getoond bij het betreffende
e-mailadres. D'oh!</p>
</div>2019-07-05T08:42:33+02:00https://www.fwiep.nl/blog/sorteren-met-join-tijdens-updateSorteren met JOIN tijdens UPDATE2019-06-03T11:39:28+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Een paar weken geleden besloot ik mijn productie web- en databaseserver
opnieuw in te richten. Ik vond dat een goede gelegenheid om dan ook maar
meteen over te stappen van het door Oracle commerciëel ontwikkelde MySQL
naar het volledig open source en vrije MariaDB. In een eerder artikel
schreef ik over <a href="https://www.fwiep.nl/blog/debuggen-van-een-zwijgzame-mariadb">de installatie en het debuggen</a> daarvan. Deze week
stelde ik vast dat een bepaalde functie in één van mijn web-apps niet
meer naar behoren functioneerde. Wat was hier aan de hand?</p>
<h2>Sorteren</h2>
<p>De <a href="https://www.fwiep.nl/blog/chordpro-naar-html-converter">web-applicatie</a> bevat muzikale uitwerkingen met tekst en akkoorden
in ChordPro-formaat (<em>Song</em>s). Deze worden gegroepeerd in setlijsten
(<em>Set</em>s). Achter de schermen gebeurt dit met een koppeltabel (<em>Song2Set</em>).
Om een set alfabetisch te kunnen sorteren, moest ik de <code>song2set_sortorder</code>
kunnen bepalen op basis van de <code>song_title</code> en <code>song_artist</code> uit de
gekoppelde <code>Song</code>-tabel.</p>
<p>Omdat het <code>UPDATE</code>-statement van MySQL geen <code>ORDER BY</code> ondersteunt van een
andere tabel dan diegene die geüpdate wordt, moest ik op zoek naar een
oplossing. Met behulp van <a href="https://stackoverflow.com/a/16444752/6219514">StackOverflow</a> kwam daar het volgende uit:</p>
<pre><code class="sql">DROP PROCEDURE IF EXISTS `set_alfasort`;
CREATE PROCEDURE `set_alfasort`(
IN `SETID` BIGINT
)
BEGIN
SET @DUMMY = -1;
UPDATE `Song2Set` AS S2S
JOIN
(
SELECT `song_id`
FROM `Song` AS S
ORDER BY
S.`song_title` ASC,
S.`song_artist` ASC
) AS SO
ON S2S.`song_id` = SO.`song_id`
SET
`song2set_sortorder` = @DUMMY := @DUMMY + 1
WHERE
`set_id` = SETID;
END;
</code></pre>
<p>Hiermee wordt in één <code>UPDATE</code>-statement bij elke Song in dat Set de
sorteervolgorde ingesteld op basis van een alfabetische sortering van de
titel en artiest. Dit werkte lange tijd naar wens.</p>
<p>Tot ik merkte dat er iets niet klopte in de sortering; ik maakte de
volgende procedure, die eveneens een tijd lang naar behoren functioneerde:</p>
<pre><code class="sql">DROP PROCEDURE IF EXISTS `set_alfasort`;
CREATE PROCEDURE `set_alfasort`(
IN `SETID` BIGINT
)
BEGIN
SET @rownr = -1;
UPDATE
`Song2Set` AS S2S
SET
`song2set_sortorder` = (
SELECT DUMMY.`rowNumber` FROM (
SELECT
@rownr:=@rownr+1 as rowNumber,
S.`song_id`
FROM (
SELECT
S2.`song_id`,
S2.`song_title`
FROM
Song as S2
RIGHT JOIN `Song2Set` AS S2S
ON S2.`song_id` = S2S.`song_id`
WHERE
S2S.`set_id` = SETID
ORDER BY
S2.`song_title` ASC,
S2.`song_artist` ASC
) AS S
) as DUMMY
WHERE
DUMMY.`song_id` = S2S.`song_id`
)
WHERE
S2S.`set_id` = SETID;
END;
</code></pre>
<p>Tot ik afgelopen week merkte dat de setlijsten niet volledig alfabetisch
werden gesorteerd; alsof er een soort <code>GROUP BY</code> actief was, een extra
niveau tijdens het sorteren. Ik bedacht dat misschien de overstap van MySQL
naar MariaDB de oorzaak kon zijn. Om dat uit te sluiten richtte ik met
<a href="https://howchoo.com/g/nmrlzmq1ymn/how-to-install-docker-on-your-raspberry-pi">Docker</a> een <a href="https://hub.docker.com/r/hypriot/rpi-mysql">MySQL-server</a> in op een Raspberry Pi. Ik vulde de
database en voerde de procedure uit…</p>
<p>Jammer, maar helaas. Ook nu werden de songs niet allemaal goed gesorteerd.
Zowel MySQL als MariaDB vertoonden hetzelfde gedrag met zowel de oude als
de nieuwe procedure. Ik moest wederom op zoek naar een alternatief.</p>
<h2>Alternarief</h2>
<p>Ik ben geen held in SQL; hoe eenvoudiger, hoe liever. Ik probeerde tot nu
toe het <code>UPDATE</code>-statement in één query samen te vatten. Wat zou er gebeuren
als ik het sorteren in meerdere stappen zou doen? De procedure zou
misschien langer, maar beslist ook begrijpelijker worden. Toen bedacht
ik het volgende:</p>
<pre><code class="sql">DROP PROCEDURE IF EXISTS `set_alfasort`;
CREATE PROCEDURE `set_alfasort`(
IN `SETID` BIGINT
)
BEGIN
CREATE TEMPORARY TABLE tmptbl (
`id` BIGINT AUTO_INCREMENT,
`song_id` BIGINT,
PRIMARY KEY (`id`)
) AS (
SELECT
S.`song_id`
FROM
`Song` AS S JOIN
`Song2Set` AS S2S ON
(S2S.`song_id` = S.`song_id` AND S2S.`set_id` = SETID)
ORDER BY
S.`song_title` ASC,
S.`song_artist` ASC
);
UPDATE
`Song2Set` AS S2S
INNER JOIN tmptbl AS DUMMY ON DUMMY.`song_id` = S2S.`song_id`
SET
S2S.`song2set_sortorder` = DUMMY.`id`
WHERE
S2S.`set_id` = SETID;
DROP TABLE tmptbl;
END;
</code></pre>
<p>Dit stuk code doet precies wat ik wil en is zelfs door mij te begrijpen
:-). Op naar de volgende bug!</p>
</div>2019-06-03T11:39:28+02:00https://www.fwiep.nl/blog/web-statistieken-met-goaccessWeb-statistieken met GoAccess2019-04-25T20:33:34+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Nog vóór de invoering van de <a href="https://www.autoriteitpersoonsgegevens.nl/nl/over-privacy/wetten/algemene-verordening-gegevensbescherming-avg">Algemene Verordening Gegevensbescherming</a> (AVG)
ben ik gestopt met het gebruik van Google Analytics. Hiermee kun je volledig
automatisch en zeer comfortabel statistieken verzamelen en inzien van elk bezoek
dat aan jouw website wordt gebracht. Grote kanttekening is daarbij echter wel,
dat Google elk stukje (eventueel privacy-gevoelige) informatie meeleest en voor
zijn eigen doelen gebruikt. Dat wilde ik niet op mijn geweten hebben.</p>
<p>Toch is het soms wel degelijk interessant om te zien of een website of een
specifieke pagina überhaupt wordt bezocht. Komen er zoekmachines (crawlers,
bots) langs? Van waaruit wordt naar de eigen site doorverwezen, of komen de
bezoekers rechtstreeks?</p>
<h2>Webserver logs</h2>
<p>Een gemiddelde webserver zoals <a href="https://httpd.apache.org/">Apache</a> houdt een uitgebreid logboek bij
voor elke aanvraag die hij afhandelt. Natuurlijk worden ook alle fouten
bijgehouden, maar dan wel in een eigen bestand. Je kunt er voor kiezen om
elke domeinnaam op een (virtuele) server <a href="https://httpd.apache.org/docs/2.4/logs.html">zijn eigen logbestanden</a> te geven:</p>
<pre><code class="plain"><VirtualHost *:80>
ServerName fwiep.nl
ServerAlias www.fwiep.nl
#...
ErrorLog ${APACHE_LOG_DIR}/fwiep.nl_error.log
CustomLog ${APACHE_LOG_DIR}/fwiep.nl_access.log combined
#...
</VirtualHost>
</code></pre>
<h2>Analyse</h2>
<p>Aldus ging ik op zoek naar een mogelijkheid om de logbestanden te analyseren
zonder dat iemand anders of zelfs een bedrijf misbruik zou kunnen maken van
die gegevens. Ik probeerde <a href="https://awstats.sourceforge.io/">AWStats</a>, maar werd overweldigd door de enorme
hoeveelheid mogelijkheden die dat programma biedt. Bovendien kon ik het niet
eenvoudig inrichten... Sorry, ik ben misschien een beetje verwend :)</p>
<p>Toen vond ik met <code>apt search apache log viewer</code> in mijn terminal een pareltje
waar ik nog nooit van had gehoord: <a href="https://goaccess.io/">GoAccess</a>. Het is een commandline tool
dat ofwel interactief kan werken of naar <code>HTML</code>, <code>JSON</code> of <code>CSV</code> exporteert.</p>
<h2>Archief</h2>
<p>Normaal gesproken worden alle logbestanden <em>geroteerd</em>. Dat betekent dat er
na verloop van tijd een cijfer aan de bestandsnaam wordt toegevoegd, waarna
een nieuw (leeg) bestand de oude naam krijgt. Hierna worden oude bestanden
automatisch gecomprimeerd met <code>gzip</code>. Zo gaat geen enkele melding verloren,
maar nemen de bestanden zo min mogelijk ruimte in beslag. Bijvoorbeeld:</p>
<pre><code class="plain">ls -1 /var/log/apache2/*access.log*
fwiep.nl_access.log
fwiep.nl_access.log.1
fwiep.nl_access.log.2.gz
fwiep.nl_access.log.3.gz
</code></pre>
<h2>Overzicht</h2>
<p>Gelukkig zijn er een aantal standaard tools zoals <code>zcat</code>, <code>zgrep</code> en <code>zless</code>
beschikbaar die direct met gecomprimeerde bestanden kunnen werken. Gekoppeld
aan GoAccess zoals in onderstaand commando, worden zowel de actuele meldingen
als ook die in gecomprimeerde archiefbestanden samen verwerkt tot een zeer
overzichtelijk <code>HTML</code>-rapport. Bravo!</p>
<pre><code class="bash">zcat -f fwiep.nl_access.log.* | goaccess --log-format=COMBINED -o fwiep.html;
</code></pre>
</div>2019-04-25T20:33:34+02:00https://www.fwiep.nl/blog/huawei-y550-firmware-vervangenHuawei Y550 firmware vervangen2019-04-24T17:02:12+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>De firmware van smartphones en andere 'slimme' aparraten die door fabrikanten
wordt meegeleverd, is vaak verouderd, biedt niet de nieuwste functionaliteit
en zit bovendien vaak vol beveiligingslekken. Zij is dan kwetsbaar voor een
veelvoud van aanvallen van buitenaf.</p>
<p>Om deze problemen op te lossen kan de firmware van zo'n apparaat soms worden
vervangen. Dit document beschrijft de voorbereiding en de procedure van het
updaten van de Huawei Ascend Y550 smartphone naar de nieuwste firmware die op
dat moment beschikbaar is. Over de afgelopen jaren heb ik mijn eigen toestellen
op deze manier met succes geüpdatet.</p>
<h2>Update</h2>
<p>In september 2017 heb ik <a href="https://www.fwiep.nl/blog/huawei-y550-firmware-update">een vervolgpost</a> geschreven, met actuele links
naar downloads en documentatie. De instructies en download links in onderstaande
post zijn op moment van schrijven eveneens actueel. In augustus 2018 heb ik
<a href="https://www.fwiep.nl/blog/y550-met-zelfgebouwd-lineageos">een artikel</a> geschreven met de instructies om de firmware zelf samen te
stellen. In april 2019 heb ik dat artikel in het Engels
<a href="https://www.fwiep.nl/y550/how-to-build-7.1.2.html">opnieuw gepubliceerd</a> als losstaande handleiding.</p>
<h2>Opbouw firmware</h2>
<p>Het geheugen van een tegenwoordige Android-smartphone is opgebouwd uit drie
delen of partities:</p>
<ul>
<li>Boot: bevat de minimale instructies die de telefoon laten opstarten</li>
<li>Recovery: bevat een mini-besturingssysteem dat gebruikt wordt om de rest van
het systeem in te richten, te updaten, schoon te vegen of te bewerken</li>
<li>Systeem: bevat het daadwerkelijke Android-besturingssysteem, de kernel, apps
en instellingen</li>
</ul>
<p>Deze onderdelen kunnen worden vervangen door uitgebreidere en/of veiligere
alternatieven.</p>
<h2>Voorbereidingen</h2>
<p>Voordat met het daadwerkelijke vervangen van de firmware kan worden begonnen,
moeten een aantal handelingen worden uitgevoerd.</p>
<h3>Backups maken</h3>
<p>Voordat je begint met het vervangen van de firmware, is het zeer verstandig om
backups te maken van de bestanden en gegevens die je lief zijn, bijvoorbeeld:</p>
<ul>
<li>WhatsApp berichten en media</li>
<li>SMS- en belgeschiedenis</li>
<li>inhoud van SD kaart</li>
</ul>
<h3>Ontgrendelcode</h3>
<p>Om een alternatieve firmware te kunnen installeren, moet de telefoon worden
'ontgrendeld'. Het toestel moet als het ware ‘vrij’ worden gemaakt van de
alleenheerschappij van de fabrikant. Het opvragen van de ontgrendelcode kon
vroeger op twee manieren: per online formulier en via e-mail naar Huawei.
Op dit moment (april 2019) lijkt de betaalde software <a href="https://forum.xda-developers.com/honor-7x/how-to/how-to-unlock-huawei-bootloader-removal-t3780903">DC-Unlocker</a>
nog de enige mogelijkheid om de bootloader te ontgrendelen. Zonder deze code
kan er geen CustomROM of Custom Recovery worden geïnstalleerd.</p>
<h3>Installeren software (PC)</h3>
<p>Onder GNU/Linux (Ubuntu) is de installatie van de programma’s <code>fastboot</code> en
<code>adb</code> eenvoudig:</p>
<pre><code class="bash">sudo apt update;
sudo apt install -y android-tools-fastboot android-tools-adb;
</code></pre>
<h3>Downloaden nieuwe firmware</h3>
<ul>
<li><a href="https://dl.twrp.me/y550/">TeamWin Recovery Project</a> (TWRP) - kies bij voorkeur versie <code>3.2.3-0</code>;<br />
met versie <code>3.3.0-0</code> kwam mijn telefoon niet verder dan het boot-logo.</li>
<li><a href="https://www.fwiep.nl/y550/4xqwt_lp_modem-signed.zip">Modem stuurprogramma</a> (<a href="https://www.fwiep.nl/y550/4xqwt_lp_modem-signed.zip.md5">MD5-bestand</a>)</li>
<li><a href="https://forum.xda-developers.com/showpost.php?p=69543714&postcount=74">CustomROM</a> - kies bij voorkeur het meest recente bestand</li>
<li><a href="http://opengapps.org/">Open GApps</a> - kies processor architectuur (ARM), Android versie (7.1)</li>
</ul>
<h2>Procedure</h2>
<p>Het ontgrendelen van de bootloader en installeren van een CustomROM is niet
moeilijk, maar heeft één belangrijke bijwerking: het toestel wordt teruggezet
naar fabrieksinstellingen, oftewel volledig gewist.</p>
<h3>Bootloader ontgrendelen</h3>
<ol>
<li>Zet de telefoon uit.</li>
<li>Druk <kbd>Power</kbd> + <kbd>Volume-down</kbd> totdat het toestel start<br />
(de melding ‘Device in fastboot mode’ verschijnt)</li>
<li>Sluit de telefoon aan via USB.</li>
<li>Voer het volgende commando uit op de computer (in plaats van 123… vul
je de unlock code in):</li>
</ol>
<pre><code class="sh">fastboot oem unlock 1234567812345678
</code></pre>
<ol start="5">
<li>Het toestel wordt teruggezet naar fabrieksinstellingen.</li>
</ol>
<h3>Nieuwe Recovery</h3>
<ol>
<li>Schakel het toestel uit.</li>
<li>Druk <kbd>Power</kbd> + <kbd>Volume-down</kbd> totdat het toestel start<br />
(de melding ‘Device in fastboot mode’ verschijnt).</li>
<li>Sluit de telefoon aan via USB.</li>
<li>Voer het volgende commando uit op de computer:</li>
</ol>
<pre><code class="bash">fastboot flash recovery twrp-3.2.3-0-y550.img
</code></pre>
<ol start="5">
<li>Schakel het toestel uit.</li>
</ol>
<h3>Nieuwe firmware flashen</h3>
<ol>
<li>Schakel het toestel uit.</li>
<li>Druk <kbd>Power</kbd> + <kbd>Volume-up</kbd> totdat het toestel start -
het bedieiningspaneel van TWRP verschijnt.</li>
<li>Sluit de telefoon aan via USB. Op de computer zijn nu het interne geheugen
en eventuele SD-kaart via MTP te benaderen.</li>
<li>Kies in TWRP voor <code>Wipe</code>, dan <code>Advanced wipe</code> (alles behalve External SD).</li>
<li>Kopieer vanaf de computer de volgende bestanden naar het interne geheugen:
<ul>
<li><code>4xqwt_lp_modem-signed.zip</code> met bijbehorend <code>.md5</code>-bestand</li>
<li><code>lineage-14.1-20190410-UNOFFICIAL-y550.zip</code> met bijbehorend <code>.md5</code>-bestand</li>
<li><code>open_gapps-arm-7.1-micro-20190423.zip</code> met bijbehorend <code>.md5</code>-bestand</li>
</ul></li>
<li>Kies in TWRP voor <code>Install</code>, selecteer de drie zip bestanden in de
bovenstaande volgorde.</li>
<li>Start het flashen.</li>
<li>Kies er voor om zowel het cache- als ook het Dalvik-cache geheugen te wissen.</li>
<li>Herstart het toestel. Dit kan de eerste keer behoorlijk lang duren.</li>
</ol>
<h2>Problemen oplossen</h2>
<p>Als het toestel niet meer normaal opstart, of je ondanks alles de originele
firmware terug wil installeren, kan dat met behulp van de volgende procedure.</p>
<h3>Downloaden</h3>
<ol>
<li>Download de <a href="https://files.fm/u/gdmxigp#/view/Y550_L01_V100R001C00B239SP01CUSTC432D001_Firmware_Nonspecific_West_European__Channel_Others_Android_4.4.4_EMUI_2.3_05012PEK.zip">originele firmware van Huawei</a>. Dit zip-bestand bevat een
bestand genaamd UPDATE.APP (ongeveer 1,5 Gbyte groot).</li>
<li>Download de <a href="https://files.fm/u/gdmxigp#/view/HuaweiUpdateExtractor_0.9.8.0.zip">Huawei Firmware Extractor</a> en installeer dit programma op
een Windows-PC.</li>
<li>Open hiermee het UPDATE.APP bestand en exporteer de bestanden BOOT.img,
RECOVERY.img en SYSTEM.img.</li>
</ol>
<h3>Firmware flashen</h3>
<ol>
<li>Schakel het toestel uit.</li>
<li>Druk <kbd>Power</kbd> + <kbd>Volume-down</kbd> totdat het toestel start.
De melding ‘Device in fastboot mode’ verschijnt.</li>
<li>Sluit de telefoon aan via USB.</li>
<li>Navigeer op de computer naar de map met uitgepakte <code>.img</code> bestanden en
voer de volgende commando's uit:</li>
</ol>
<pre><code class="bash">fastboot flash boot BOOT.img;
fastboot flash recovery RECOVERY.img;
fastboot flash system SYSTEM.img;
fastboot reboot;
</code></pre>
<p>Het toestel start opnieuw op en is weer als nieuw (behalve dat de bootloader
ontgrendeld is).</p>
</div>2016-11-24T09:34:12+01:00https://www.fwiep.nl/blog/huawei-y550-firmware-updateHuawei Y550 firmware update2019-04-24T17:02:12+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>In een <a href="https://www.fwiep.nl/blog/huawei-y550-firmware-vervangen">eerdere post</a> beschreef ik de noodzaak van en de stappen om een Huawei
Y550 smartphone van nieuwe firmware te voorzien. Sindsdien is er een hoop gebeurd
op het vlak van die alternatieve firmwares, zogenaamde <em>Custom ROMs</em>. De
procedure is in grote lijnen gelijk gebleven, maar de te installeren bestanden
niet.</p>
<h2>Downloaden</h2>
<ul>
<li><a href="https://dl.twrp.me/y550/">TeamWin Recovery Project</a> (TWRP) - kies bij voorkeur versie <code>3.2.3-0</code>;<br />
met versie <code>3.3.0-0</code> kwam mijn telefoon niet verder dan het boot-logo.</li>
<li><a href="https://www.fwiep.nl/y550/4xqwt_lp_modem-signed.zip">Modem stuurprogramma</a> (<a href="https://www.fwiep.nl/y550/4xqwt_lp_modem-signed.zip.md5">MD5-bestand</a>)</li>
<li><a href="https://forum.xda-developers.com/showpost.php?p=69543714&postcount=74">CustomROM</a> - kies bij voorkeur het meest recente bestand</li>
<li><a href="http://opengapps.org/">Open Gapps</a> - kies processor architectuur (ARM), Android versie (7.1)</li>
</ul>
<h2>Flashen</h2>
<p>Om een nieuwe firmware te kunnen installeren (flashen), moet de telefoon
ontgrendeld worden. Dit kon vroeger via een online formulier of via e-mail
naar Huawei. Tegenwoordig is <a href="https://forum.xda-developers.com/honor-7x/how-to/how-to-unlock-huawei-bootloader-removal-t3780903">DC-unlocker</a> blijkbaar de enige werkende
optie. Daarna volgt het installeren van een zogenaamde <em>Custom Recovery</em>, een
kleine buitenboord-Linux die backups kan maken, het geheugen kan indelen
(partitioneren) en firmware kan vervangen.</p>
<ol>
<li>Installeer, zoals in de <a href="https://www.fwiep.nl/blog/huawei-y550-firmware-vervangen">eerdere post</a> beschreven, de TWRP custom recovery.</li>
<li>Start de telefoon in de nieuwe recovery-modus (<kbd>Power</kbd> + <kbd>Volume-up</kbd>).</li>
<li>Voer eventueel een fabrieksreset (factory wipe) uit.</li>
<li>Installeer modemstuurprogramma, CustomROM en de OpenGApps in één flash.</li>
<li>Voer een cache-wipe uit (dit wordt automatisch aanbevolen na het flashen).</li>
</ol>
<p>De eerstvolgende keer opstarten duurt lang. Dit is normaal.</p>
<h2>Nabewerken</h2>
<p>Als je, zoals ik, geen waarde hecht aan ongewenste advertenties in apps die je
gebruikt of op websites die je bezoekt, kun je er voor kiezen een <em>adblocker</em> te
installeren. De app <a href="https://blokada.org/">Blokada</a> filtert niet alleen bekende advertenties weg,
maar blokkeert ook de communicatie met bekende tracking-servers en -netwerken.</p>
</div>2017-09-20T16:00:30+02:00https://www.fwiep.nl/blog/y550-met-zelfgebouwd-lineageosY550 met zelfgebouwd LineageOS2019-04-24T17:02:12+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>De veiligheid van de ingebakken software (firmware) van mijn Huawei Y550
smartphone houdt me al vele jaren bezig. In een eerder artikel beschreef
ik de procedure om van de originele firmware <a href="https://www.fwiep.nl/blog/huawei-y550-firmware-vervangen">over te stappen</a> naar
zogenaamde <em>custom ROMs</em>. Een opvolgend artikel beschreef <a href="https://www.fwiep.nl/blog/huawei-y550-firmware-update">het updaten</a>
van zo'n bestaand CustomROM. In dit artikel beschrijf ik het proces van
het zelf bouwen van een CustomROM.</p>
<p>Let op: ik bedoel hiermee het fabriceren van het installatie-image zelf. Andere
ontwikkelaars zorgen voor de spreekwoordelijke stenen, specie, steigers en soep
tijdens de pauze. Een open source project als dit kan niet zonder het werk van
anderen. Je staat als het ware op de <a href="https://en.wikipedia.org/wiki/Standing_on_the_shoulders_of_giants">schouders van reuzen</a>. Onderstaande
post is ook als losstaande <a href="https://www.fwiep.nl/y550/how-to-build-7.1.2.html">Engelstalige handleiding</a> beschikbaar.</p>
<h2>LineageOS</h2>
<p>Het meest bekende alternatieve besturingssysteem voor smartphones en tablets is
<a href="https://www.lineageos.org/">LineageOS</a>, een wereldwijd open source project, gedragen door duizenden
vrijwilligers. Op basis van de open broncode van Android die Google regelmatig
vrijgeeft, bouwen deze enthousiastelingen actuele firmware voor honderden
verschillende toesteltypen, met nieuwe functies maar ook beveiligingsupdates.</p>
<h2>Basis</h2>
<p>Het model Ascend Y550 van Huawei wordt niet officieel ondersteund door de
vrijwilligers van LineageOS. De hardware lijkt echter veel op die van een ander
model dat <em>wel</em> wordt ondersteund: Huawei Honor 4/4X, codenaam <code>cherry</code>. Het
bouwen van de firmware voor dit toestel wordt <a href="https://wiki.lineageos.org/devices/cherry/build">uitvoerig beschreven</a>.</p>
<p>Gelukkig zijn er ook ontwikkelaars, die weliswaar geen officiële ondersteuning
kunnen bieden, maar wel een toestel up to date houden, op basis van hoe het hen
uitkomt. Zo is er een groep ontwikkelaars die, speciaal voor de Y550 en andere
soortgelijke maar niet officiëel ondersteunde typen, een <a href="https://github.com/desalesouche/android_kernel_huawei_msm8916">Linux-kernel</a>
onderhoudt op GitHub. Eén van hen is <a href="https://forum.xda-developers.com/member.php?u=4479868">desalesouche</a>; degene wiens adviezen
en informatie hebben geleid tot het schrijven van dit artikel.</p>
<h2>Vereisten</h2>
<p>De benodigdheden om een CustomROM te kunnen bouwen zijn:</p>
<ul>
<li>een 64-bit computer met GNU/Linux, bijvoorbeeld <a href="https://www.ubuntu.com/download/desktop">Ubuntu 18.04 LTS</a></li>
<li>100 tot 200 GByte vrije schijfruimte voor broncode en cache</li>
<li>minimaal 8 GByte RAM</li>
<li>een degelijke internetverbinding</li>
</ul>
<h2>Voorbereiding</h2>
<p>Voor zover nog niet gebeurd, installeer Ubuntu. Kies voor de minimale
installatie; méér dan de basis systeemprogramma's zullen we niet nodig
hebben. Werk je in een virtuele machine, vergeet dan niet ook <code>openssh-server</code>
te installeren. Je kunt het systeem dan via SSH vanaf je lokale terminal
bereiken.</p>
<pre><code class="bash">sudo apt update;
sudo apt install -y openssh-server;
</code></pre>
<h3>Installatie software</h3>
<p>Er is behoorlijk wat software nodig om de broncode van LineageOS binnen te halen,
te beheren en te compileren tot een kant-en-klaar image. Hieronder een samengevat
commando om alles in één keer te installeren:</p>
<pre><code class="bash">sudo apt update && sudo apt upgrade -y;
sudo apt install -y bc bison build-essential ccache curl flex \
g++-multilib gcc-multilib git gnupg gperf imagemagick \
lib32ncurses5-dev lib32readline-dev lib32z1-dev liblz4-tool \
libncurses5-dev libsdl1.2-dev libssl-dev libwxgtk3.0-dev libxml2 \
libxml2-utils lzop pngcrush rsync schedtool squashfs-tools libxslt1-dev \
openjdk-8-jdk android-tools-adb android-tools-fastboot python;
</code></pre>
<h3>Mappen en <code>repo</code></h3>
<p>De volgende twee mappen moeten worden aangemaakt:</p>
<pre><code class="bash">mkdir -p ~/bin;
mkdir -p ~/android/lineage;
</code></pre>
<p>In de eerste bevindt zich straks het programma <code>repo</code>, een belangrijk commando
om alle onderliggende projecten van ons CustomROM te beheren en te updaten. De
tweede map is de hoofdmap waarin alle broncode zal worden opgeslagen.</p>
<p>Het volgende commando downloadt het <code>repo</code>-script en maakt het uitvoerbaar:</p>
<pre><code class="bash">curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo;
chmod a+x ~/bin/repo;
</code></pre>
<p>Om er zeker van te zijn dat de map <code>~/bin</code> in de <code>${PATH}</code> omgevingsvariabele
is opgenomen, open <code>~/.profile</code> en voeg eventueel onderstaande code toe, als
die er nog niet staat:</p>
<pre><code class="bash"># set PATH so it includes user's private bin if it exists
if [ -d "$HOME/bin" ] ; then
PATH="$HOME/bin:$PATH"
fi
</code></pre>
<p>Voer voor de zekerheid het commando <code>source ~/.profile</code> uit om de wijzigingen
van kracht te laten zijn.</p>
<h3>Initialiseren</h3>
<p><code>repo</code> maakt onder de motorkap veelvuldig gebruik van <a href="https://git-scm.com/">git</a>, een systeem om
wijzigingen aan met name broncode van software comfortabel en inzichtelijk te
beheren. Het gebruik van git vereist dat je jouw naam en e-mailadres instelt,
zodat eventuele wijzigingen naar jou kunnen worden teruggevolgd. Stel de
gegevens als volgt in:</p>
<pre><code class="bash">git config --global user.name "Voornaam Achternaam";
git config --global user.email voorachternaam@mijne-mailadres.com;
</code></pre>
<p>Daarna volgt de daadwerkelijke initialisatie:</p>
<pre><code class="bash">cd ~/android/lineage
repo init -u https://github.com/LineageOS/android.git -b cm-14.1
</code></pre>
<p>Als je houdt van kleurtjes op je scherm, antwoord dan <kbd>Y</kbd> op de
betreffende vraag.</p>
<p>In de verborgen <code>.repo</code>-map staat welke projecten op wat voor manier moeten worden
samengevoegd om de totale broncode bij elkaar te krijgen. Dit staat opgesomd
in zogenaamde <a href="https://forum.xda-developers.com/showthread.php?t=2329228"><em>manifest</em>s</a>. Dit zijn in <code>XML</code> opgemaakte bestanden, die
met elke normale teksteditor te bewerken zijn.</p>
<h3>Synchroniseren</h3>
<p>Dan volgt de eerste synchronisatie, waarin van alle betrokken projecten de
nieuwste wijzigingen worden binnengehaald. De eerste keer kan dit behoorlijk
lang duren.</p>
<pre><code class="bash">repo sync --force-sync
</code></pre>
<p>Maak daarna de map <code>local_manifests</code> in de <code>.repo</code> map aan. Maak daarin een
nieuw <code>XML</code>-bestand met de volgende inhoud en een willekeurige naam. Het mijne
heet <code>fwiep.xml</code>:</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<manifest>
<remote name="githubfwiep" fetch="https://github.com/fwiep" />
<project
path="device/cyanogen/msm8916-common"
name="LineageOS/android_device_cyanogen_msm8916-common"
remote="github" />
<project
path="device/qcom/common"
name="LineageOS/android_device_qcom_common"
remote="github" />
<project
path="device/huawei/msm8916-common"
name="android_device_huawei_msm8916-common"
remote="githubfwiep" revision="cm-14.1" />
<project
path="device/huawei/y550"
name="android_device_huawei_y550"
remote="githubfwiep" revision="cm-14.1" />
<project
path="vendor/huawei"
name="proprietary_vendor_huawei"
remote="githubfwiep" revision="cm-14.1" />
<project
path="kernel/huawei/y550-vid"
name="HUAWEI-Y550-Y635/android_kernel_huawei_msm8916"
remote="github" revision="los-14.1" />
<project
path="bootable/recovery-twrp"
name="omnirom/android_bootable_recovery"
remote="github" revision="android-7.1" />
</manifest>
</code></pre>
<p>Dan volgt opnieuw een synchronisatie:</p>
<pre><code class="bash">repo sync --force-sync;
</code></pre>
<h3>Y550 aanpassing</h3>
<p>In bovenstaand manifest staan een aantal projecten met het attribuut
<code>remote="githubfwiep"</code>. Hiermee wordt verwezen naar mijn speciaal voor de
Y550 ingerichte kopieën van de officiële <code>cherry</code> repos.</p>
<p>Voor de specifieke aanpasssingen <a href="https://forum.xda-developers.com/android/help/to-root-huawei-y500-t2923318/post76164949">vroeg ik advies</a> aan de ervaren vrouwen en
mannen op het XDA-forum. Let op: deze edits zijn dus al uitgevoerd als je de
bovenstaande projecten met <code>repo</code> hebt binnengehaald.</p>
<p>In <code>./device/huawei/msm8916-common/system.prop</code></p>
<pre><code class="plain">ro.sf.lcd_density=240
</code></pre>
<p>In <code>./device/huawei/cherry/BoardConfig.mk</code></p>
<pre><code class="makefile">TARGET_OTA_ASSERT_DEVICE := Y550-L01,Y550-L02,Y550-L03
</code></pre>
<p>In <code>./device/huawei/msm8916-common/BoardConfigCommon.mk</code></p>
<pre><code class="makefile">DEVICE_RESOLUTION := 480x854
</code></pre>
<p>In <code>./device/huawei/msm8916-common/BoardConfigCommon.mk</code></p>
<pre><code class="makefile">TARGET_KERNEL_SOURCE := kernel/huawei/y550-vid
TARGET_KERNEL_CONFIG := cm_hwY550_defconfig
</code></pre>
<p>In <code>./device/huawei/msm8916-common/msm8916.mk</code></p>
<pre><code class="makefile">PRODUCT_AAPT_PREF_CONFIG := hdpi
TARGET_SCREEN_HEIGHT := 854
TARGET_SCREEN_WIDTH := 480
</code></pre>
<h3><code>/vendor</code></h3>
<p>Jammer genoeg is niet alle broncode van een Android-smartphone open source.
De meeste fabrikanten houden een deel van de firmware geheim. Hiervan kan dus
niemand (behalve de fabrikant) onderzoeken hoe ze werkt, of eventuele fouten en
beveiligingslekken corrigeren.</p>
<p>CustomROMs zijn afhankelijk van zulke stukken firmware, omdat ze onmisbaar zijn
voor de functionaliteit van de hardware. LineageOS voorziet hierin door middel
van de <code>/vendor</code>-map. Hierin worden stukken originele firmware als binaire data
opgeslagen en overgenomen in het uiteindelijke image.</p>
<p>Hoewel het misschien niet helemaal correct of legaal is, heb ik er voor gekozen
om die bestanden eenmalig aan mijn repositories op GitHub toe te voegen. Zo kan
eenieder die de instructies volgt direct aan de slag. De liefhebbers en
die-hards verwijs ik graag naar de officiële instructies (<a href="https://wiki.lineageos.org/devices/cherry/build#extract-proprietary-blobs">link 1</a>,
<a href="https://wiki.lineageos.org/extracting_blobs_from_zips.html#extracting-proprietary-blobs-from-block-based-otas">link 2</a>) van LineageOS.</p>
<h3>CCache</h3>
<p>Om het bouwen van images te versnellen, maakt LineageOS gebruik van een zogenaamd
<em>compiler-cache</em>, oftewel <code>ccache</code>. Hierin worden reeds gecompileerde bestanden
opgeslagen, die bij een volgende bouw kunnen worden gekopiëerd, als de broncode
sindsdien niet is gewijzigd. Dit kan behoorlijk veel tijd schelen. Door éénmalig
het volgende commando uit te voeren stel je de gewenste cache-grootte in;
bijvoorbeeld 50 GByte:</p>
<pre><code class="bash">ccache -M 50G;
</code></pre>
<p>Daarnaast moet nog een omgevingsvariabele worden ingesteld. Voeg dit commando
ook toe aan je <code>~/.bashrc</code>, zodat het bij elke shell sessie wordt uitgevoerd:</p>
<pre><code class="bash">export USE_CCACHE=1;
</code></pre>
<h3>jack</h3>
<p>Tot slot van de algemene voorbereidingen volgt nog het inrichten van een centrale
hulp voor het compilatieproces: <a href="https://source.android.com/setup/build/jack">jack</a>, niet te verwarren met de
<a href="http://www.jackaudio.org/">Jack Audio Connection Kit</a> (JACK). Het programma is nogal kieskeurig wat
geheugen betreft, beklaagt zich regelmatig over een tekort en laat de bouw dan
spontaan vastlopen. Hieronder volgt mijn persoonlijke werkende combinatie van
instellingen.</p>
<p>Vul het bestand <code>~/.jack-settings</code> met de volgende inhoud:</p>
<pre><code class="plain"># Server settings
SERVER_HOST=127.0.0.1
SERVER_PORT_SERVICE=8076
SERVER_PORT_ADMIN=8077
# Internal, do not touch
SETTING_VERSION=4
JACK_SERVER_VM_ARGUMENTS="-Dfile.encoding=UTF-8 -XX:+TieredCompilation -Xmx4096m"
</code></pre>
<p>Dan nog voor de zekerheid een extra omgevingsvariabele (ook in <code>~/.bashrc</code>):</p>
<pre><code class="bash">export ANDROID_JACK_VM_ARGS="-Dfile.encoding=UTF-8 -XX:+TieredCompilation -Xmx4096m";
</code></pre>
<h2>Bouw</h2>
<p>Eindelijk kan de bouw van het daadwerkelijke image beginnen!</p>
<pre><code class="bash">cd ~/android/lineage;
source build/envsetup.sh;
LANG=C brunch y550 2>&1 | tee $( date +'%Y-%m-%d_%H-%M-%S' )-build.log;
</code></pre>
<p>Als alles meezit begint de computer nu behoorlijk hard te werken. Processors
lopen warm en geheugen wordt tot de nok toe gevuld. Na afloop verschijnt een
melding dat het bouwen succesvol was en hoe lang dit heeft geduurd.</p>
<h2>Tips</h2>
<p>Als de bouw niet direct slaagt, is het best een uitdaging om in de overvloed
van console meldingen een hint van de oorzaak te vinden. Wees gerust, dit hoort
bij het leerproces van het zelf bouwen van software. Als je tot hier bent gekomen,
ben je zelfredzaam genoeg om ook de laatste obstakels vakkundig uit de weg te
ruimen.</p>
<h3>VM specificaties</h3>
<p>Als je besluit in een virtuele machine te bouwen: geef de gast minimaal 2
processorkernen en zo veel mogelijk werkgeheugen. Meer is beter, veel meer is
veel beter.</p>
<h3>Jack</h3>
<p>Beklaagt <code>jack</code> zich over te weinig geheugen en heb je daar iets aan gedaan,
vergeet dan niet om de jack-server opnieuw te starten:</p>
<pre><code class="bash">croot;
./prebuilts/sdk/tools/jack-admin kill-server;
./prebuilts/sdk/tools/jack-admin start-server;
</code></pre>
<h2>Succes</h2>
<p>In de map <code>/out/target/product/y550</code> staan de zojuist gebouwde images te
wachten om te worden geïnstalleerd. Voor het eenvoudig flashen van het CustomROM,
zoals beschreven in mijn <a href="https://www.fwiep.nl/blog/huawei-y550-firmware-update">eerdere posts</a>, is het
<code>lineage-14.1-XXXXXXXX-UNOFFICIAL-y550.zip</code>-archief met bijbehorend
<code>.md5</code>-bestand voldoende. Deze kopieer je naar jouw Y550 en flasht het met
behulp van <a href="https://dl.twrp.me/y550/">TWRP</a>. Klaar!</p>
</div>2018-04-17T11:05:12+02:00https://www.fwiep.nl/blog/backups-met-xsplit-maar-dan-veiligerBackups met xsplit, maar dan veiliger2019-02-12T22:10:13+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>In de tijd van schijnbaar eindeloze online opslagruimte en steeds grotere
capaciteiten van harde schijven en SSD's, zou je de oude vertrouwde optische
media zoals CD en DVD bijna vergeten. In dit geval wed ik echter liever
op méér dan één paard bij het back-uppen van mijn dierbare data.</p>
<p>Natuurlijk hebben de plastic schijfjes een aantal nadelen ten opzichte van
magnetische of flash-opslag. Ze verouderen sneller, hebben bepaalde eisen aan
temperatuur en luchtvochtigheid - en kunnen lang niet zoveel data bevatten als
bijvoorbeeld een USB-stick van 16 GByte. Bovendien duurt het beschrijven en
uitlezen een veelvoud langer dan met andere media.</p>
<h2>Ruimtegebrek</h2>
<p>Lang geleden brandde ik al menige CD, later DVD vol met van alles en nogwat.
Vaak liep ik dan tegen het probleem aan, dat de volledige data niet op één
schijf paste. Dus ging ik handmatig aan de slag om, met kopieën van het volledige
project, steeds weer een ander gedeelte te verwijderen in de compilatie. Totdat
alle data in zo'n compilatie stond opgeslagen. Daarna kon het branden beginnen.</p>
<p>Toen ik GNU/Linux ging gebruiken en met de commandline vertrouwd raakte, ontdekte
ik het Python-script <em>xsplit</em> van <a href="http://www.rikishi42.net/SkunkWorks/xsplit_dvd">Rikishi 42</a>. Het verdeelt alle bestanden
in de bronmap zo efficiënt mogelijk, in porties van ongeveer 4,5 GByte groot;
precies genoeg om één DVD te vullen.</p>
<h2>Heel fanatiek</h2>
<p>Omdat ik mijn verzameling data niet naar een aparte <code>/in</code>-map wilde kopiëren,
legde ik een aantal <a href="https://www.nixtutor.com/freebsd/understanding-symbolic-links/">symbolische links</a> naar de mappen met mijn data aan.
Daarna voerde ik het <code>xsplit</code>-script uit en waarempel: naast de <code>/in</code>-map
verscheen een <code>/out</code>-map met daarin de mappen <code>DVD_001</code>, <code>DVD_002</code> etc. Ik
inspecteerde de inhoud en stelde vast dat de mappen inderdaad niet groter waren
dan 4,5 GByte. Jammer genoeg was het indelen van <a href="https://www.fwiep.nl/blog/van-samba-naar-nfs-onder-openwrt">mijn NFS-mappen</a> niet gelukt
en moest ik opnieuw beginnen.</p>
<p>Ik verwijderde de <code>/out</code>-map en controleerde in de <code>/in</code>-map of mijn originele
bestanden nog gelinkt stonden… Nee dus. Stonden ze op de originele
locatie, dan? Ook weg! Het script had alle bestanden verschoven en ik had ze
zonder na te denken weggegooid.</p>
<h2>Aanpassing</h2>
<p>Opgelucht haalde ik een tweede backup tevoorschijn en plaatste de bestanden
terug op mijn harde schijf. Zou er geen mogelijkheid zijn om Python symbolische
links te laten gebruiken, in plaats van de originele bestanden te verplaatsen?
De <a href="https://docs.python.org/3/library/os.html#os.symlink">Python-documentatie</a> schoot mij daarop zeer deskundig te hulp.</p>
<p>In regel 159 van het <code>xsplit</code>-script vond ik het volgende commando:</p>
<pre><code class="python">os.rename(FullName, DestName)
</code></pre>
<p>Met het commando <code>symlink()</code> was de aanpassing snel gemaakt:</p>
<pre><code class="python">os.symlink(FullName, DestName)
</code></pre>
<p>Maar wat was dat? De hele mappenstructuur werd netjes overgenomen in de
<code>/out</code>-map, maar de daadwerkelijke bestanden toonden een <em>broken link</em>: een link
waarvan het doelbestand niet kan worden gevonden. Een korte zoektocht in de
documentatie bracht me bij de uiteindelijke oplossing:</p>
<pre><code class="python">os.symlink(os.path.realpath(FullName), DestName)
</code></pre>
<h2>Conclusie</h2>
<p>Hoewel het niet meer vaak voorkomt dat ik een verzameling bestanden over
meerdere media moet verdelen, is dit script toch zeker de moeite waard. Ik moet
nog steeds zelf in een brandprogramma als <em>Brasero</em> of <em>K3B</em> de daadwerkelijke
compilaties maken; maar het puzzelen om de schijfjes zo goed mogelijk te vullen
wordt mij dankbaar uit handen genomen. En met de <code>symlink()</code>-aanpassing kan het
ook nog allemaal zonder dat het een megabyte extra aan opslagruimte kost.</p>
</div>2019-02-12T22:10:13+01:00https://www.fwiep.nl/blog/van-samba-naar-nfs-onder-openwrtVan Samba naar NFS onder OpenWrt2019-02-07T21:41:11+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>In <a href="https://www.fwiep.nl/blog/nbg6716-router-met-openwrt">een eerder artikel</a> beschreef ik het inrichten van een OpenWrt-router die
tevens via Samba bestanden kon delen met andere netwerkgebruikers. Op zich werkte
deze setup naar behoren, maar het was een hels karwei om in te richten. Bovendien
stoorden mij een aantal kleinigheden:</p>
<ul>
<li>Samba biedt geen ondersteuning voor twee of meer bestanden in dezelfde map die
enkel verschillen in hoofd- en kleine letters van hun bestandsnaam. Dat betekent
onder meer dat je een bestand <code>Document.txt</code> niet kan hernoemen naar
<code>dOCUMENT.txt</code> of vice versa.</li>
<li>Alle bestanden hebben dezelfde lees-, schrijf- en uitvoerrechten. Met name dit
laatste was altijd te zien op de commandline.</li>
</ul>
<p>Zou er geen andere mogelijkheid bestaan om bestanden te delen in een lokaal
netwerk? Liefst met ondersteuning voor hoofd- en kleine letters en bestandsrechten
voor lezen, schrijven en uitvoeren.</p>
<h2>NFS</h2>
<p>Toen herinnerde ik me <code>NFS</code>, het <em>Network-FileSystem</em> dat al sinds mensenheugenis
deel uitmaakt van Unix en Linux. Ik zocht en vond <a href="https://openwrt.org/docs/guide-user/services/nas/nfs_configuration">de documentatie</a> om één en
ander op mijn OpenWrt-router in te richten en binnen no-time was de overstap een
feit.</p>
<p>In principe bestaat de installatie op de router (server) uit twee delen. Ten
eerste wordt het pakket <code>nfs-kernel-server</code> geïnstalleerd. Daarna worden in
<code>/etc/exports</code> de te delen mappen met hun eigenschappen ingesteld.</p>
<h2>Exports</h2>
<p>Als eerste wordt het pad genoteerd van de map die moet worden gedeeld. Daarna
volgt ofwel een asterisk (<code>*</code>) voor <em>ALLE</em> netwerkgebruikers, of een reeks
IP-adressen in <a href="https://doc.m0n0.ch/quickstartpc/intro-CIDR.html">CIDR-notatie</a>, of één specifiek IP-adres.</p>
<pre><code class="plain">/pad/naar/map1 *(rw)
/pad/naar/map2 192.168.1.0/24(rw)
/pad/naar/map3 192.168.1.12(rw,all_squash,anonuid=1000,anongid=1000)
</code></pre>
<p>De opties tussen ronde haken bepalen hoe de share omgaat met lees- en
schrijfverzoeken (<code>ro</code> = read-only, alleen-lezen, <code>rw</code> = read-write,
lezen-schrijven). Zie ook bijvoorbeeld de <a href="https://linux.die.net/man/5/exports">manpage van <code>exports</code></a>.</p>
<p>Met <code>all_squash</code>, <code>anonuid</code> en <code>anongid</code> wordt alle lees- en schrijfverzoeken
als één gebruiker/groep uitgevoerd. Dit is voor de meeste netwerken voldoende -
zo ook het mijne.</p>
<h2>Ubuntu en Raspbian</h2>
<p>Op mijn client-pc's met Ubuntu was het voldoende om het pakket <code>nfs-common</code> te
installeren en de Samba-regel in <code>/etc/fstab</code> te vervangen door een behoorlijk
ingekorte regel met NFS.</p>
<pre><code class="plain"># Oude Samba-mount
#//192.168.1.1/map /mnt/map cifs credentials=/home/ikke/.smbcreds,uid=1000,gid=1000,iocharset=utf8,nobrl,vers=2.0 0 0
# Nieuwe NFS-mount
192.168.1.1:/mnt/map /mnt/map nfs auto,_netdev 0 0
</code></pre>
<p>Op Raspbian, het besturingssysteem van de Raspberry Pi, moest daarnaast ook nog
een extra commando worden uitgevoerd voordat de NFS-share toegankelijk is:</p>
<pre><code class="plain">systemctl start rpcbind.service;
</code></pre>
</div>2019-02-07T21:41:11+01:00https://www.fwiep.nl/blog/random-startrek-episode-selectorRandom StarTrek episode selector2018-10-14T17:12:17+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Heb je dat ook wel eens? De volledige collectie van alle StarTrek
televisieseries staat in je kast of op je harde schijf, en je hebt geen idee
welke aflevering je vandaag wil bekijken. Dit <code>PHP</code>-script neemt je die keuze
uit handen en selecteert een willekeurige aflevering uit een willekeurig seizoen
van een willekeurige serie.</p>
<p>Dit was mijn eerste project waarin ik zelf een <code>XML</code>-schema schreef en daarna
het bijbehorende <code>XML</code>-bestand vulde met behulp van <a href="http://en.memory-alpha.org/wiki/Portal:Main">MemoryAlpha</a>. De uitvoer
van het script ziet er als volgt uit:</p>
<pre><code class="plain">Today's random StarTrek episode:
TOS 2x04 Who Mourns for Adonais?
</code></pre>
<h2>Update</h2>
<p>Na twaalf jaar radiostilte is er een nieuwe serie aan de StarTrek canon
toegevoegd: <em>Discovery</em>. De tot nu toe bekendegemaakte afleveringen van het
eerste seizoen heb ik aan dit script en bijbehorende bestanden toegevoegd. De
broncode van dit project is sinds oktober 2018 <a href="https://github.com/fwiep/trekisode">op GitHub beschikbaar</a>.</p>
<h2>Script</h2>
<pre><code class="php"><?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
date_default_timezone_set(@date_default_timezone_get());
$x = new DOMDocument();
$o = array();
$p = dirname(__FILE__);
if(@$x->load($p.'/data.xml') && @$x->schemaValidate($p.'/schema.xsd')) {
foreach ($x->getElementsByTagName('Series') as $series) {
$sr = new Series(
$series->getAttribute('code'),
$series->getAttribute('name')
);
foreach ($series->getElementsByTagName('Season') as $season) {
$se = new Season(
(int)$season->getAttribute('number')
);
foreach ($season->getElementsByTagName('Episode') as $episode) {
$ep = new Episode(
(int)$episode->getAttribute('number'),
(string)$episode->getAttribute('name'),
(int)strtotime($episode->getAttribute('airdate'))
);
$se->episodes[] = $ep;
}
$sr->seasons[] = $se;
}
$o[] = $sr;
}
$randSeries = $o[array_rand($o)];
$randSeason = $randSeries->seasons[array_rand($randSeries->seasons)];
$randEpisode = $randSeason->episodes[array_rand($randSeason->episodes)];
$outputFormat =
"<pre>Today's random StarTrek episode:<br />".
"<a href=\"http://en.memory-alpha.org/wiki/%1\$s\">%1\$s</a> ".
"<a href=\"http://en.memory-alpha.org/wiki/%1\$s_Season_%2\$d\">%2\$dx%3\$02d</a> ".
"<a href=\"http://en.memory-alpha.org/wiki/%5\$s_(episode)\">%4\$s</a>".
"</pre>";
if(isset($argv) && $argc > 0) {
$outputFormat =
"Today's random StarTrek episode:\n".
"%1\$s %2\$dx%3\$02d %4\$s\n";
}
printf($outputFormat,
$randSeries->code,
$randSeason->number,
$randEpisode->number,
$randEpisode->name,
str_replace(' ', '_', $randEpisode->name)
);
}
class Series
{
public $code;
public $name;
public $seasons;
public function __construct($code, $name) {
$this->code = $code;
$this->name = $name;
}
}
class Season
{
public $number;
public $episodes;
public function __construct($number) {
$this->number = $number;
}
}
class Episode
{
public $number;
public $name;
public $airdate;
public function __construct($number, $name, $airdate) {
$this->number = $number;
$this->name = $name;
$this->airdate = $airdate;
}
}
</code></pre>
<h2>XML-data</h2>
<p>Hieronder een verkorte weergave van het <a href="https://www.fwiep.nl/download/8c8b28a3-f780-4c00-bf01-53272bb0441c/data.xml">volledige XML-bestand</a>.</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<StarTrek xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="schema.xsd" xmlns="">
<Series code="TOS" name="The Original Series">
<Season number="1">
<Episode number="1" airdate="1988-10-04" name="The Cage"/>
<Episode number="1" airdate="1966-09-22" name="Where No Man Has Gone Before"/>
<Episode number="2" airdate="1966-11-10" name="The Corbomite Maneuver"/>
<!-- ... -->
</Season>
<!-- ... -->
</Series>
<Series code="TAS" name="The Animated Series">
<Season number="1">
<Episode number="1" airdate="1973-09-08" name="Beyond the Farthest Star"/>
<!-- ... -->
</Season>
<Season number="2">
<!-- ... -->
</Season>
</Series>
<Series code="TNG" name="The Next Generation">
<Season number="1">
<Episode number="1" airdate="1987-09-28" name="Encounter at Farpoint, Part I"/>
<!-- ... -->
</Season>
<!-- ... -->
</Series>
<Series code="DS9" name="Deep Space 9">
<Season number="1">
<Episode number="1" airdate="1993-01-03" name="Emissary"/>
<!-- ... -->
</Season>
<!-- ... -->
</Series>
<Series code="VOY" name="Voyager">
<Season number="1">
<Episode number="1" airdate="1995-01-16" name="Caretaker"/>
<!-- ... -->
</Season>
<!-- ... -->
</Series>
<Series code="ENT" name="Enterprise">
<Season number="1">
<Episode number="1" airdate="2001-09-26" name="Broken Bow"/>
<!-- ... -->
</Season>
<!-- ... -->
</Series>
<Series code="DIS" name="Discovery">
<Season number="1">
<Episode number="1" airdate="2017-09-24" name="The Vulcan Hello" />
<!-- ... -->
</Season>
<!-- ... -->
</Series>
</StarTrek>
</code></pre>
<h2>XSD-schema</h2>
<p>En tot slot, het <a href="https://www.fwiep.nl/download/8c8b28a3-f780-4c00-bf01-53272bb0441c/schema.xsd">XML-schema</a> om de data te valideren:</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="StarTrek" type="TypeStarTrek">
<xs:unique name="uniqueSeries">
<xs:selector xpath="./Series" />
<xs:field xpath="@code" />
</xs:unique>
</xs:element>
<xs:complexType name="TypeStarTrek">
<xs:sequence>
<xs:element maxOccurs="7" minOccurs="7" name="Series"
type="TypeSeries">
<xs:unique name="uniqueSeasonNUmber">
<xs:selector xpath="./Season" />
<xs:field xpath="@number" />
</xs:unique>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="TypeSeries">
<xs:sequence>
<xs:element maxOccurs="7" minOccurs="1" name="Season"
type="TypeSeason">
<xs:unique name="uniqueEpisodeNumber">
<xs:selector xpath="./Episode" />
<xs:field xpath="@number" />
</xs:unique>
</xs:element>
</xs:sequence>
<xs:attribute name="code" type="SeriesCode" use="required" />
<xs:attribute name="name" type="StringNonEmpty" use="required" />
</xs:complexType>
<xs:complexType name="TypeSeason">
<xs:sequence>
<xs:element maxOccurs="30" minOccurs="0" name="Episode"
type="TypeEpisode">
</xs:element>
</xs:sequence>
<xs:attribute name="number" type="SeasonNumber" use="required" />
</xs:complexType>
<xs:complexType name="TypeEpisode">
<xs:attribute name="number" type="EpisodeNumber" use="required" />
<xs:attribute name="airdate" type="DateEmpty" use="required" />
<xs:attribute name="name" type="StringNonEmpty" use="required" />
</xs:complexType>
<xs:simpleType name="StringNonEmpty">
<xs:restriction base="xs:string">
<xs:minLength value="1" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="DateEmpty">
<xs:union memberTypes="Empty xs:date" />
</xs:simpleType>
<xs:simpleType name="EpisodeNumber">
<xs:restriction base="xs:integer">
<xs:minInclusive value="0" />
<xs:maxInclusive value="30" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="SeasonNumber">
<xs:restriction base="xs:integer">
<xs:minInclusive value="1" />
<xs:maxInclusive value="7" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="SeriesCode">
<xs:restriction base="xs:string">
<xs:enumeration value="TOS" />
<xs:enumeration value="TAS" />
<xs:enumeration value="TNG" />
<xs:enumeration value="DS9" />
<xs:enumeration value="VOY" />
<xs:enumeration value="ENT" />
<xs:enumeration value="DIS" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Empty">
<xs:restriction base="xs:string">
<xs:enumeration value="" />
</xs:restriction>
</xs:simpleType>
</xs:schema>
</code></pre>
</div>2013-04-12T05:25:26+01:00https://www.fwiep.nl/blog/lg-dvd-station-met-bluebirds-rootkitLG DVD-station met Bluebirds rootkit2018-10-01T20:37:15+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Een tijdje geleden kreeg ik een desktop computer onder behandeling waar bij
nader inzien iets bijzonders mee aan de hand was; iets dat ik nog nooit eerder
had meegemaakt. Toen ik de computer van een blanco Linux-installatie had
voorzien, begon ik de verschillende randapparatuur te testen. Ook het DVD-station
(LG GH22NS50) kwam aan de beurt.</p>
<h2>DVD's afspelen</h2>
<p>Het afspelen van commerciële, door middel van CSS (<em>Content Scrambling System</em>)
beveiligde DVD's is voor GNU/Linux-gebruikers een juridisch pijnpunt. Gelukkig
is het heel eenvoudig om het alsnog legaal voor elkaar te krijgen. Installeer en
configureer het volgende pakket:</p>
<pre><code class="bash">sudo apt install libdvd-pkg;
sudo dpkg-reconfigure libdvd-pkg;
</code></pre>
<h2>Lege lade… of toch niet?</h2>
<p>Toen ik met de VLC mediaspeler een film had afgespeeld, de DVD verwijderde en de
lade sloot, verscheen er plotseling een verwisselbaar apparaat. Een virtuele
cd-rom met een Windows-programma genaamd <code>Bluebirds.exe</code>. Als ik de lege lade
opende verdween de schijf, als ik haar opnieuw sloot verscheen ze weer als
vanzelf. Wat was hier aan de hand?</p>
<p>LG had blijkbaar op een blauwe maandag het idee om de computers van hun klanten
te voorzien van een hulpje in de taakbalk genaamd <em>Bluebirds</em>. Ik heb niet kunnen
achterhalennn wat het doel zou moeten zijn van deze applicatie, maar wel dat
zo'n beetje alle gebruikers er een hekel aan hebben.</p>
<h2>Verwarring alom</h2>
<p>Ik ging op zoek naar gelijkgezinden en vond een <a href="https://msfn.org/board/topic/135300-lg-gh22ns50-bluebirds-removal-tool/">informatieve forumpost</a>.
Deze verwees onder meer naar de <a href="https://www.lg.com/us/support-product/lg-GH22NS50">officiële downloadpagina</a> waar maar liefst
4 × <a href="https://gscs-b2c.lge.com/downloadFile?fileId=KRSWD000004172-b1-a1.zip">dezelfde hyperlink</a> staat opgenoemd naar de meest recente update
<code>TL03</code>. Om de verwarring compleet te maken, staan de firmware updates voor twee
verschillende typen DVD-stations (<code>GH22LS50</code> en <code>GH22NS50</code>) gebroederlijk naast
elkaar.</p>
<p>Omdat ik GNU/Linux gebruik, moest ik een mogelijkheid zoeken om de update te
installeren. Met behulp van <a href="https://www.winehq.org/">Wine</a> startte het updateprogramma, maar het
DVD-station werd niet herkend! Ik probeerde een ander updatebestand, een
andere Wine-versie… het mocht niet baten. Uiteindelijk besloot ik het
DVD-station uit te bouwen en met een externe SATA-adapter aan een Windows-PC
te hangen. Daar verliep de update zonder problemen.</p>
<h2>Epiloog</h2>
<p>Ik wilde heel graag weten waarom de installatie met Wine niet functioneerde;
zeker omdat deze compatibiliteitslaag sinds mijn laatste ervaringen behoorlijk
gegroeid is in functionaliteit en stabiliteit. Wat blijkt? Ik had in mijn ijver
de firmware voor het verkeerde type gedownload. <a href="https://www.myinstants.com/instant/doh/">D'oh!</a></p>
</div>2018-10-01T20:37:15+02:00https://www.fwiep.nl/blog/tectonic-suguru-puzzels-in-phpTectonic/Suguru puzzels in PHP2018-07-13T22:44:48+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Puzzelen kan bij iemands leven horen. Met een pen en tijdschrift, TV-gids of
puzzelboekje op schoot vliegt de tijd voorbij. Maar niet alleen als
puzzel-consument is het goed toeven met breinbrekers op papier; als
software-ontwikkelaar gaan ook de hersens aan de slag: hoe zou ik dit proces
kunnen automatiseren? Hoe giet ik deze werkwijze in code?</p>
<p>Als eerste waagde ik me aan een Sudoku-oplosser, maar deze heeft het nooit veel
verder geschopt dan een kanttekening in mijn lokale Git-historie. Daarna volgde
begin dit jaar een <a href="https://www.fwiep.nl/binair/">Binaire oplosser</a> met bijbehorend
<a href="https://www.fwiep.nl/blog/binaire-puzzels-oplossen-in-php">artikel in dit weblog</a>. Op zoek naar afwisseling vonden mijn vrouw en ik een
boekje van Denksport genaamd <em>Tectonic</em>: een soort Sudoku, maar dan anders. De
oplosser die ik in dit artikel beschrijf is <a href="https://www.fwiep.nl/tectonic/">online beschikbaar</a>.</p>
<h2>Spelregels</h2>
<p>Een <a href="https://nl.wikipedia.org/wiki/Tectonic">Tectonic-puzzel</a>, ook wel Suguru genoemd, bestaat uit een rechthoekig
vlak met cellen. Deze zijn gegroepeerd in puzzelstukken van 1 tot maximaal 5
cellen. In elk puzzelstuk mag een cijfer slechts één keer voorkomen. Dezelfde
cijfers mogen elkaar niet raken; ook niet diagonaal.</p>
<h2>Opgave</h2>
<p>Op papier is een Tectonic opgebouwd uit een legpuzzel van cellen met een dunne
rand, die elk deel uitmaken van een (eventueel onregelmatig) puzzelstuk met een
dikke rand. Hierin zijn dan één of meer cellen vooringevuld. De eerste uitdaging
was een formaat te bedenken waarin ik deze informatie kon opslaan en verwerken.</p>
<p>Met het oog op de toekomst en eventuele andere applicaties die met dezelfde data
aan de slag zouden kunnen, viel mijn keuze al snel op <code>XML</code>. Met een bijbehorend
<code>XSD</code>-schema kon ik vooruit vastleggen hoe de structuur moest zijn en er in mijn
code op vertrouwen dat nooit van deze opbouw zou worden afgeweken.</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<Puzzles xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="TectonicPuzzles.xsd">
<Puzzle height="5" width="3">
<Cell PieceNumber="1" Value="" />
<Cell PieceNumber="1" Value="" />
<Cell PieceNumber="2" Value="" />
<Cell PieceNumber="3" Value="" />
<Cell PieceNumber="1" Value="3" />
<Cell PieceNumber="2" Value="" />
<Cell PieceNumber="3" Value="" />
<Cell PieceNumber="1" Value="" />
<Cell PieceNumber="1" Value="2" />
<Cell PieceNumber="3" Value="" />
<Cell PieceNumber="3" Value="" />
<Cell PieceNumber="4" Value="" />
<Cell PieceNumber="5" Value="" />
<Cell PieceNumber="4" Value="" />
<Cell PieceNumber="4" Value="1" />
</Puzzle>
</Puzzles>
</code></pre>
<p>Het bijpassende schema ziet er relatief eenvoudig uit:</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="Puzzles" type="PuzzlesType" />
<xs:complexType name="PuzzlesType">
<xs:sequence minOccurs="1" maxOccurs="1">
<xs:element name="Puzzle" type="PuzzleType" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="PuzzleType">
<xs:sequence minOccurs="1" maxOccurs="unbounded">
<xs:element name="Cell" type="CellType" />
</xs:sequence>
<xs:attribute name="width" type="xs:nonNegativeInteger" use="required" />
<xs:attribute name="height" type="xs:nonNegativeInteger" use="required" />
</xs:complexType>
<xs:complexType name="CellType">
<xs:attribute name="PieceNumber" type="xs:nonNegativeInteger" />
<xs:attribute name="Value" type="EmptyValue" />
</xs:complexType>
<xs:simpleType name="EmptyValue">
<xs:restriction base="xs:string">
<xs:pattern value="[1-5]?" />
</xs:restriction>
</xs:simpleType>
</xs:schema>
</code></pre>
<h2>Opbouw</h2>
<p>Het <code>Puzzle</code>-object heeft een verzameling <code>PuzzleCell</code>-objecten
(<code>$puzzle->_cells</code>). De index van zo'n cel wordt van linksboven naar rechts, dan
naar beneden bepaald. Elke cel is met referenties gekoppeld aan één
<code>PuzzlePiece</code>-object (<code>$cell->piece</code>). Ook heeft elke cel maximaal acht
referenties naar de naburige cellen (<code>$cell->neighbourCells</code>).</p>
<h2>Aanpak</h2>
<p>Net zoals op papier, baseert mijn aanpak op het achtereenvolgens wegstrepen van
mogelijkheden in een cel; totdat er nog maar één mogelijkheid overblijft. Dus
begint elke cel in eerste instantie met net zoveel mogelijkheden als er cellen
zijn in dat betreffende puzzelstuk. Vooringevulde cellen worden daarbij
overgeslagen.</p>
<pre><code class="php">foreach ($this->_cells as $ix => $c) {
if (strlen($c->value) == 1) {
continue;
}
$c->value = implode('', range(1, count($c->piece->cells)));
}
</code></pre>
<p>Daarna worden de naburige cellen van elke cel verzameld.</p>
<details>
<summary>Code in/uitklappen</summary>
<pre><code class="php">private function _getRowIndex(int $cellIndex) : int
{
if (array_key_exists($cellIndex, $this->_cells)) {
return intdiv($cellIndex, $this->_width);
}
return -1;
}
/**
* Hook up all cell's neighbours as an array of referenced cells
* @return void
*/
private function _hookupNeighbours() : void
{
foreach ($this->_cells as $ix => $c) {
// Loop through all cells to find neighbour cells
$rowIndex = $this->_getRowIndex($ix);
// top left
$neighbourIx = $ix - 1 - $this->_width;
$neighbourRowIndex = $this->_getRowIndex($neighbourIx);
if ($neighbourRowIndex != -1 && $neighbourRowIndex == $rowIndex - 1) {
$c->neighbourCells[0] = $this->_cells[$neighbourIx];
}
// top
$neighbourIx = $ix - $this->_width;
$neighbourRowIndex = $this->_getRowIndex($neighbourIx);
if ($neighbourRowIndex != -1 && $neighbourRowIndex == $rowIndex - 1) {
$c->neighbourCells[1] = $this->_cells[$neighbourIx];
}
// top right
$neighbourIx = $ix + 1 - $this->_width;
$neighbourRowIndex = $this->_getRowIndex($neighbourIx);
if ($neighbourRowIndex != -1 && $neighbourRowIndex == $rowIndex - 1) {
$c->neighbourCells[2] = $this->_cells[$neighbourIx];
}
// right
$neighbourIx = $ix + 1;
$neighbourRowIndex = $this->_getRowIndex($neighbourIx);
if ($neighbourRowIndex != -1 && $neighbourRowIndex == $rowIndex) {
$c->neighbourCells[5] = $this->_cells[$neighbourIx];
}
// bottom right
$neighbourIx = $ix + 1 + $this->_width;
$neighbourRowIndex = $this->_getRowIndex($neighbourIx);
if ($neighbourRowIndex != -1 && $neighbourRowIndex == $rowIndex + 1) {
$c->neighbourCells[8] = $this->_cells[$neighbourIx];
}
// bottom
$neighbourIx = $ix + $this->_width;
$neighbourRowIndex = $this->_getRowIndex($neighbourIx);
if ($neighbourRowIndex != -1 && $neighbourRowIndex == $rowIndex + 1) {
$c->neighbourCells[7] = $this->_cells[$neighbourIx];
}
// bottom left
$neighbourIx = $ix - 1 + $this->_width;
$neighbourRowIndex = $this->_getRowIndex($neighbourIx);
if ($neighbourRowIndex != -1 && $neighbourRowIndex == $rowIndex + 1) {
$c->neighbourCells[6] = $this->_cells[$neighbourIx];
}
// left
$neighbourIx = $ix - 1;
$neighbourRowIndex = $this->_getRowIndex($neighbourIx);
if ($neighbourRowIndex != -1 && $neighbourRowIndex == $rowIndex) {
$c->neighbourCells[3] = $this->_cells[$neighbourIx];
}
}
}
</code></pre>
</details>
<h2>Oplossen: stap voor stap</h2>
<p>De kern van de oplosser is de methode <code>solve()</code>. Met een <code>do</code>-<code>while</code>-loop wordt
net zolang geprobeerd mogelijkheden weg te strepen, totdat de status van de
cellen niet meer verandert (strategieën 1 t/m 8). Dan wordt bij één van de
cellen, met slechts twee mogelijkheden, geprobeerd of de eerste optie tot een
geldige oplossing leidt (strategie 9). Zo nee, wordt de tweede optie ingevuld.</p>
<details>
<summary>Code in/uitklappen</summary>
<pre><code class="php">/**
* Tries to solve the puzzle
* @param int $depth the current recursion depth
* @return bool TRUE on success, FALSE on failure
*/
public function solve(int $depth = 0) : bool
{
if ($depth > 9) {
// Emergency break for stopping recursion
return false;
}
do {
$previousHTML = $this->toHTML();
$this->_solveStrategy1();
$this->_solveStrategy2();
$this->_solveStrategy3();
$this->_solveStrategy4();
$this->_solveStrategy5();
$this->_solveStrategy6();
$this->_solveStrategy7();
$this->_solveStrategy8();
} while ($this->_isValid() && $this->toHTML() != $previousHTML);
if ($this->_isComplete() && $this->_isValid()) {
return true;
}
return $this->_solveStrategy9($depth);
}
/**
* Whether the puzzle contains no unsure values
* @return bool
*/
private function _isComplete() : bool
{
$cellsWithSingleValue = array_filter(
$this->_cells,
function ($c) {
return strlen($c->value) == 1;
}
);
return count($cellsWithSingleValue) == count($this->_cells);
}
/**
* Whether this puzzle's solution is valid
* @return bool
*/
private function _isValid() : bool
{
foreach ($this->_cells as $c) {
if (empty($c->value)) {
return false;
}
if (strlen($c->value) > 1) {
continue;
}
$neighboursWithSameValue = array_filter(
$c->neighbourCells,
function ($c2) use ($c) {
return $c2->value == $c->value;
}
);
if ($neighboursWithSameValue) {
return false;
}
}
foreach ($this->_pieces as $p) {
for ($i = 1; $i <= count($p->cells); $i++) {
$cellsWithThisOption = array_filter(
$p->cells,
function ($c) use ($i) {
return $c->value == (string)$i;
}
);
if (count($cellsWithThisOption) > 1) {
return false;
}
}
}
return true;
}
</code></pre>
</details>
<h3>Wegstrepen</h3>
<pre><code class="php">/**
* Removes a specific option from the cell's value
* @param int|string $v the single value to remove
* @return bool TRUE when the value was present before being removed
*/
public function removeValue($v) : bool
{
$v = (string)$v;
if ($vIsPresent = (strpos($this->value, $v) !== false)) {
$this->value = str_replace($v, '', $this->value);
}
return $vIsPresent;
}
</code></pre>
<h3>Strategie 1</h3>
<pre><code class="php">/**
* Remove solved cells' values from current cells' options
* @return void
*/
private function _solveStrategy1() : void
{
foreach ($this->_pieces as $p) {
foreach ($p->cells as $c) {
if (strlen($c->value) > 1) {
continue;
}
foreach ($p->cells as $c2) {
if ($c2 === $c) {
continue;
}
if (strlen($c2->value) == 1) {
continue;
}
$c2->removeValue($c->value);
}
}
}
return;
}
</code></pre>
<h3>Strategie 2</h3>
<pre><code class="php">/**
* Remove solved cells' values from neighbour cells
* @return void
*/
private function _solveStrategy2() : void
{
foreach ($this->_cells as $ix => $c) {
if (strlen($c->value) == 1) {
foreach ($c->neighbourCells as $c2) {
$c2->removeValue($c->value);
}
}
}
return;
}
</code></pre>
<h3>Strategie 3</h3>
<pre><code class="php">/**
* Set cell's value, if no other cell in that piece has that value
* @return void
*/
private function _solveStrategy3() : void
{
foreach ($this->_pieces as $p) {
$cellCountThisPiece = count($p->cells);
for ($i = 1; $i <= $cellCountThisPiece; $i++) {
$cellsWithThisOption = array_filter(
$p->cells,
function ($c) use ($i) {
return strpos($c->value, (string)$i) !== false;
}
);
if (count($cellsWithThisOption) == 1) {
$c = array_shift($cellsWithThisOption);
if (strlen($c->value) > 1) {
$c->setValue($i);
}
}
}
}
return;
}
</code></pre>
<h2>Buren</h2>
<p>Elke cel heeft drie tot maximaal acht naburige cellen. In een aantal strategiën
worden deze buren gebruikt om mogelijkheden weg te strepen. Zie de methode
<code>_hookupNeighbours()</code> en <code>getNeighbour()</code>:</p>
<pre><code class="php">/**
* Gets the cell's neighbour at the specified index
* Neighbours are indexed in the following order:
* 0 1 2
* 3 X 5
* 6 7 8
* where X is the current cell
* @param int $ix the neighbour's 0-based index
* @return PuzzleCell|NULL
*/
public function getNeighbour(int $ix) : ?PuzzleCell
{
if (array_key_exists($ix, $this->neighbourCells)) {
return $this->neighbourCells[$ix];
}
return null;
}
</code></pre>
<h3>Strategie 4</h3>
<pre><code class="php">/**
* Removes possibility from cell if certain neighbours have
* two same possibilities
* @return void
*/
private function _solveStrategy4() : void
{
foreach ($this->_cells as $ix => $c) {
if (strlen($c->value) != 2) {
continue;
}
// ... Current cell X is north-west partner
// .XA or south-east partner (being Y)
// .BY
$neighY = $c->getNeighbour(8);
$neighA = $c->getNeighbour(5);
$neighB = $c->getNeighbour(7);
$this->_removeFromTwo($c, $neighY, $neighA, $neighB);
// ... Current cell X is north-east partner
// AX. or south-west partner (being Y)
// YB.
$neighY = $c->getNeighbour(6);
$neighA = $c->getNeighbour(3);
$neighB = $c->getNeighbour(7);
$this->_removeFromTwo($c, $neighY, $neighA, $neighB);
}
return;
}
</code></pre>
<h3>Strategie 5</h3>
<pre><code class="php">/**
* Removes possibility from cell if certain neighbours have
* two same possibilities
* @return void
*/
private function _solveStrategy5() : void
{
foreach ($this->_cells as $ix => $c) {
// .AB Current cell X is horizontal partner
// .XY
// .CD
$cY = $c->getNeighbour(5);
$cA = $c->getNeighbour(1);
$cB = $c->getNeighbour(2);
$cC = $c->getNeighbour(7);
$cD = $c->getNeighbour(8);
$this->_removeFromFour($c, $cY, $cA, $cB, $cC, $cD);
// ... Current cell X is vertical partner
// AXC
// BYD
$cY = $c->getNeighbour(7);
$cA = $c->getNeighbour(3);
$cB = $c->getNeighbour(6);
$cC = $c->getNeighbour(5);
$cD = $c->getNeighbour(8);
$this->_removeFromFour($c, $cY, $cA, $cB, $cC, $cD);
}
return;
}
</code></pre>
<h3>Strategie 6</h3>
<pre><code class="php">/**
* Removes paired possibilities from piece's cells if two other cells match
* @return void
*/
private function _solveStrategy6() : void
{
foreach ($this->_pieces as $nr => $p) {
foreach ($p->cells as $c) {
if (strlen($c->value) != 2) {
continue;
}
$partner = array_filter(
$p->cells,
function ($c2) use ($c) {
return ($c2 !== $c && $c2->value === $c->value);
}
);
if (!$partner) {
continue;
}
$partner = array_shift($partner);
foreach ($p->cells as $c2) {
if ($c2 === $c) {
continue;
}
if ($c2 === $partner) {
continue;
}
foreach (str_split($c->value) as $v) {
$c2->removeValue($v);
}
}
}
}
return;
}
</code></pre>
<h3>Strategie 7</h3>
<pre><code class="php">/**
* Removes possibility from cell if certain neighbours have
* three same possibilities
* @return void
*/
private function _solveStrategy7() : void
{
foreach ($this->_cells as $ix => $c) {
// ... Current cell is top left
// .XA
// .BC
$neighA = $c->getNeighbour(5);
$neighB = $c->getNeighbour(7);
$neighC = $c->getNeighbour(8);
$this->_removeFromThree($c, $neighA, $neighB, $neighC);
// ... Current cell is top right
// AX.
// BC.
$neighA = $c->getNeighbour(3);
$neighB = $c->getNeighbour(6);
$neighC = $c->getNeighbour(7);
$this->_removeFromThree($c, $neighA, $neighB, $neighC);
// .AB Current cell is bottom left
// .XC
// ...
$neighA = $c->getNeighbour(1);
$neighB = $c->getNeighbour(2);
$neighC = $c->getNeighbour(5);
$this->_removeFromThree($c, $neighA, $neighB, $neighC);
// AB. Current cell is bottom right
// CX.
// ...
$neighA = $c->getNeighbour(0);
$neighB = $c->getNeighbour(1);
$neighC = $c->getNeighbour(3);
$this->_removeFromThree($c, $neighA, $neighB, $neighC);
}
return;
}
</code></pre>
<h3>Strategie 8</h3>
<pre><code class="php">/**
* Removes possibility from cell if certain neighbours have
* three same possibilities
* @return void
*/
private function _solveStrategy8() : void
{
foreach ($this->_cells as $ix => $c) {
// ..A Current cell is left of stack of three
// .XB
// ..C
$neighA = $c->getNeighbour(2);
$neighB = $c->getNeighbour(5);
$neighC = $c->getNeighbour(8);
$this->_removeFromThree($c, $neighA, $neighB, $neighC);
// A.. Current cell is right of stack of three
// BX.
// C..
$neighA = $c->getNeighbour(0);
$neighB = $c->getNeighbour(3);
$neighC = $c->getNeighbour(6);
$this->_removeFromThree($c, $neighA, $neighB, $neighC);
// ... Current cell is on top of row of three
// .X.
// ABC
$neighA = $c->getNeighbour(6);
$neighB = $c->getNeighbour(7);
$neighC = $c->getNeighbour(8);
$this->_removeFromThree($c, $neighA, $neighB, $neighC);
// ABC Current cell is under row of three
// .X.
// ...
$neighA = $c->getNeighbour(0);
$neighB = $c->getNeighbour(1);
$neighC = $c->getNeighbour(2);
$this->_removeFromThree($c, $neighA, $neighB, $neighC);
}
return;
}
</code></pre>
<h3>Hulpjes</h3>
<p>Onderstaande functies worden vanuit stategiën 4, 5, 7 en 8 aangeroepen:</p>
<details>
<summary>Code in/uitklappen</summary>
<pre><code class="php">/**
* Removes values from cell enclosed by or adjacent to two same-value cells
* @param PuzzleCell $x current cell
* @param PuzzleCell $y current cell's partner
* @param PuzzleCell $a first cell to be stripped of current cell's values
* @param PuzzleCell $b second cell to be stripped of current cell's values
* @return void
*/
private function _removeFromTwo(
?PuzzleCell $x, ?PuzzleCell $y, ?PuzzleCell $a, ?PuzzleCell $b
) : void {
if ($x && $y && $a && $b && $y->value == $x->value) {
foreach (str_split($x->value) as $v) {
$a->removeValue($v);
$b->removeValue($v);
}
}
return;
}
/**
* Removes values from cell enclosed by a three-cell piece or adjacent to
* three same-value cells
* @param PuzzleCell $x the current cell to remove values from
* @param PuzzleCell $a the first neighbouring cell
* @param PuzzleCell $b the second neighbouring cell
* @param PuzzleCell $c the third neighbouring cell
* @return void
*/
private function _removeFromThree(
PuzzleCell $x, ?PuzzleCell $a, ?PuzzleCell $b, ?PuzzleCell $c
) : void {
if ($a && $b && $c
&& $a->piece === $b->piece
&& $b->piece === $c->piece
&& $x->piece !== $a->piece
) {
for ($i = 1; $i <= self::MAXVALUE; $i++) {
if (strpos($a->value, (string)$i) === false
|| strpos($b->value, (string)$i) === false
|| strpos($c->value, (string)$i) === false
|| strpos($x->value, (string)$i) === false
) {
continue;
}
$otherCellsWithThisOption = array_filter(
$a->piece->cells,
function ($c2) use ($a, $b, $c, $i) {
return
$c2 !== $a
&& $c2 !== $b
&& $c2 !== $c
&& strpos($c2->value, (string)$i) !== false;
}
);
if (!$otherCellsWithThisOption) {
$x->removeValue($i);
}
}
}
return;
}
/**
* Removes values from cell adjacent to two same-value cells
* @param PuzzleCell $x current cell
* @param PuzzleCell $y current cell's partner
* @param PuzzleCell $a first cell to be stripped of current cell's values
* @param PuzzleCell $b second cell to be stripped of current cell's values
* @param PuzzleCell $c third cell to be stripped of current cell's values
* @param PuzzleCell $d fourth cell to be stripped of current cell's values
* @return void
*/
private function _removeFromFour(
?PuzzleCell $x, ?PuzzleCell $y,
?PuzzleCell $a, ?PuzzleCell $b, ?PuzzleCell $c, ?PuzzleCell $d
) : void {
if (!$x || !$y) {
return;
}
$this->_removeFromFourTwo($x, $y, $a, $b);
$this->_removeFromFourTwo($x, $y, $c, $d);
}
/**
* Removes values from cell adjacent to two same-value cells
* @param PuzzleCell $x current cell
* @param PuzzleCell $y current cell's partner
* @param PuzzleCell $m first cell to be stripped of current cell's values
* @param PuzzleCell $n second cell to be stripped of current cell's values
* @return void
*/
private function _removeFromFourTwo(
PuzzleCell $x, PuzzleCell $y, ?PuzzleCell $m, ?PuzzleCell $n
) : void {
if (!$m || !$n) {
return;
}
if ($x->piece === $y->piece
&& ($m->piece !== $x->piece || $n->piece !== $x->piece)
) {
for ($i = 1; $i <= self::MAXVALUE; $i++) {
if (strpos($x->value, (string)$i) !== false
&& strpos($y->value, (string)$i) !== false
) {
$otherCellsWithThisValue = array_filter(
$x->piece->cells,
function ($c) use ($x, $y, $i) {
return
$c !== $x
&& $c !== $y
&& strpos($c->value, (string)$i) !== false;
}
);
if (!$otherCellsWithThisValue) {
if ($m->piece !== $x->piece) {
$m->removeValue($i);
}
if ($n->piece !== $x->piece) {
$n->removeValue($i);
}
}
}
}
}
if ($y->value == $x->value && strlen($x->value) == 2) {
foreach (str_split($x->value) as $v) {
$m->removeValue($v);
$n->removeValue($v);
}
}
}
</code></pre>
</details>
<h2>Recursief proberen</h2>
<p>Als het wegstrepen met de acht voorgaande strategiën de oplossing niet dichterbij
brengt, komt er met strategie 9 een stukje <em>recursiviteit</em> om de hoek kijken.
Hierin roept een stuk code zichzelf tijdens het uitvoeren opnieuw aan.</p>
<p>Er wordt bij de eerste cel met slechts twee opties een tijdelijke kopie van de
huidige puzzel gemaakt. Dan wordt de eerste optie gekozen. Kan deze puzzel zo
volledig worden ingevuld en opgelost, was de keuze juist. Zo niet, wordt de
tweede optie gekozen en daarmee verder gepuzzeld.</p>
<pre><code class="php">/**
* On cells with two options, try each value in turn
* @param int $depth the current recursion depth
* @return bool
*/
private function _solveStrategy9(int &$depth) : bool
{
foreach ($this->_cells as $ix => $c) {
if (strlen($c->value) != 2) {
continue;
}
$valuesToTry = str_split($c->value);
foreach ($valuesToTry as $v) {
$thisXML = $this->toXML();
$tryPuzzle = new Puzzle($thisXML, true);
$tryPuzzle->_cells[$ix]->value = $v;
if ($tryPuzzle->solve(++$depth)) {
$this->_pieces = $tryPuzzle->_pieces;
$this->_cells = $tryPuzzle->_cells;
return true;
}
}
}
return false;
}
</code></pre>
<h2>Nawoord</h2>
<p>Vanwege de leesbaarheid heb ik een aantal details van de hier beschreven code
gewijzigd ten opzichte van de originele bron. Ook heb ik de logica van de
stappen die leiden naar de uiteindelijke oplossing weggelaten. Deze is niet
noodzakelijk voor het begrip van de werking van de oplosser.</p>
<p>Op dit moment is het invoeren van nieuwe puzzels nog een behoorlijk intensief
maar vooral visueel stuk handwerk. Voor de toekomst zou ik graag een
mogelijkheid zien, om als gebruiker eigen puzzels te kunnen toevoegen /
uploaden. Misschien geautomatiseerde beeldherkenning van een gescande puzzel?
Of analyse van een screenshot of een live webcam… <a href="https://www.fwiep.nl/contact">Ik sta open</a> voor
suggesties!</p>
</div>2018-07-13T22:44:48+02:00https://www.fwiep.nl/blog/wifi-hotspot-onder-ubuntu-18-04-ltsWiFi-hotspot onder Ubuntu 18.04 LTS2018-06-06T16:01:23+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Soms is het noodzakelijk om een WiFi-netwerk op te zetten met een bepaalde naam
en bijpassend wachtwoord. Zo ook in dit geval, waar ik een iPad van al zijn
bekende netwerkinstellingen moest ontdoen. Pas na deze reset maakte het apparaat
opnieuw verbinding met mijn gastennetwerk; maar was dus ook de gegevens van zijn
eigen 'thuisnetwerk' vergeten…</p>
<p>Ik bedacht dat ik mijn <a href="https://www.fwiep.nl/blog/y550-met-zelfgebouwd-lineageos">mobiele telefoon met LineageOS</a> kon gebruiken als
mobiele hotspot. Hier kon ik zonder problemen het <code>SSID</code> en het bijbehorende
wachtwoord instellen. De iPad maakte daarna zonder morren een draadloze
verbinding. Toch was ik nog niet gerustgesteld. Zou het apparaat bij thuiskomst
zich daadwerkelijk volledig automatisch met het thuisnetwerk verbinden?</p>
<h2>Poging 2</h2>
<p>Aldus ging ik in mijn hardwarevoorraad op zoek naar een oude draadloze router
die ik tijdelijk kon gebruiken als proefkonijn. Onderweg kwam ik een
USB-WiFi-adapter tegen en dacht: dat kan natuurlijk ook! Ik plugde het
apparaatje in mijn desktop-PC en stelde vergenoegd vast, dat Ubuntu 18.04 LTS's
netwerkmanager mij <a href="https://askubuntu.com/questions/180733/how-to-setup-an-access-point-mode-wi-fi-hotspot/439530#439530">een heel comfortabele manier</a> bood om een hotspot in te
richten.</p>
<p>Ik paste met behulp van <code>nm-connection-editor</code> zowel <code>SSID</code> als wachtwoord aan
en… Er gebeurde helemaal niets. Steeds opnieuw werd het <code>SSID</code> terug
ingesteld op de hostnaam van mijn desktop-PC. Herstarten van NetworkManager of
het gehele systeem mochten niet baten. Telkens weer verdween de door mij
ingestelde netwerknaam.</p>
<h2>Lang leve CLI</h2>
<p>Ik zocht en vond <a href="https://askubuntu.com/questions/971925/wifi-hotspot-in-17-10/971942#971942">een oplossing</a> in de vorm van <code>nmcli</code>. De NetworkManager
blijkt namelijk ook via de commandline te kunnen worden bestuurd en bestierd.
Met de volgende twee commando's was het pseudo-thuisnetwerk voor de genoemde
iPad een feit:</p>
<pre><code class="bash">nmcli connection modify Hotspot ssid MIJN-THUIS-SSID
nmcli connection up Hotspot
</code></pre>
</div>2018-06-06T16:01:23+02:00https://www.fwiep.nl/blog/debuggen-van-een-zwijgzame-mariadbDebuggen van een zwijgzame MariaDB2018-05-20T17:36:43+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Dezer dagen vraagt een gemiddelde Ubuntu installatie met regelmaat voor een
upgrade naar versie 18.04 LTS (Long Term Support), een uitgave met verlengde
ondersteuning (tot vijf jaar) voor een deel van de geleverde software. Aangezien
ik tot nu toe vrij tevreden was over Ubuntu, besloot ik er dan maar meteen een
frisse herinstallatie van te maken.</p>
<p>In de afgelopen jaren had ik een tekstbestand gevuld met commando's en
geheugensteuntjes die ik als leidraad kon gebruiken bij de installatie. Zo zou
ik zeker geen belangrijke zaken vergeten te installeren of te configureren.
Daaronder ook de MySQL-databaseserver en de koppeling naar PHP.</p>
<h2>Gelijkvloerse drempel</h2>
<p>Bij het inrichten van mijn webapplicaties op <code>http://localhost/</code> bleek dat ze de
verbinding met de database niet konden maken. In eerdere versies van MySQL en
Ubuntu verscheen er tijdens de installatie een venster met de vraag om een
wachtwoord. Nu was die vraag niet gesteld. Wat bleek? Er was een blanco
wachtwoord voor de beheerdersaccount (<code>root</code>) ingesteld.</p>
<p>Aldus dacht ik slim te zijn en met een <a href="https://www.fwiep.nl/blog/reset-mysql-root-wachtwoord">eerder geschreven artikel</a> het
wachtwoord opnieuw in te stellen. Jammer genoeg werkten deze instructies niet
meer. Wat nu?</p>
<h2>Root-toegang</h2>
<p>Met behulp van onderstaande commando's kon ik de <code>root</code>-account toch weer met
een wachtwoord laten verbinden. In de shell opende ik een MySQL-client met
beheerdersrechten (<code>sudo</code>):</p>
<pre><code class="bash">sudo mysql -u root
</code></pre>
<p>Bij de standaard installatie wordt de authenticatieplugin <code>auth_socket</code> gebruikt.
Hierbij speelt het wachtwoord totaal geen rol. In plaats daarvan wordt de
gebruikersnaam uit de shell vergeleken met de gebruikersnaam in de database. Met
de volgende queries wordt deze manier van authenticatie uitgeschakeld:</p>
<pre><code class="sql">use mysql;
update user set plugin='' where User='root';
flush privileges;
\q;
</code></pre>
<p>Tot slot volgt het opnieuw instellen van het <code>root</code>-wachtwoord, met nog een
aantal verdere acties ter betere beveiliging van de databaseserver. Dit commando
wordt ten strengste aanbevolen, zeker als een server 'live', in productie is.</p>
<pre><code class="bash">sudo mysql_secure_installation
</code></pre>
<h2>Overstap</h2>
<p>Al geruime tijd zijn de merknaam en ontwikkeling van <code>MySQL</code> in handen van
Oracle, een groot commercieel softwarebedrijf uit de Verenigde Staten. Omdat een
deel van de ontwikkelaars zich niet kon vinden in de koers van dat bedrijf,
hebben ze de <a href="https://nl.wikipedia.org/wiki/Fork_(ontwikkeling)">broncode geforkt</a>. Vanaf dat moment was er een alternatieve
databaseserver genaamd <a href="https://mariadb.com/"><code>MariaDB</code></a>. De installatie verloopt zonder problemen,
maar let wel op bovengenoemd akkefietje met het <code>root</code>-wachtwoord:</p>
<pre><code class="bash">sudo apt install mariadb-server
</code></pre>
<h2>Debugging</h2>
<p>Na de overstap voerde ik de scripts uit, om alle bestaande databases terug in te
lezen. Bij één van mijn applicaties ging dit niet naar behoren. Ik zag geen
enkele foutmelding tijdens het importeren, maar er werd ook geen data getoond in
de applicatie. Wat was hier aan de hand?</p>
<p>Het enige directe verschil tussen deze en mijn voorgaande applicaties was het
gebruik van <a href="https://php.net/manual/en/intro.pdo.php">PHP Data Objects</a>, oftewel PDO. Bij het inlezen van de SQL-scripts
maakte ik gebruik van PDO's <code>exec()</code>-commando; niet wetende dat deze functie
eigenlijk bedoeld is om slechts één statement uit te voeren.</p>
<p>Dat bleek toen ik met de oude vertrouwde <code>var_dump(); die();</code> al debuggend op
zoek ging naar waarom bepaalde stored procedures niet werden geïmporteerd. Ook
ontbrak er een complete tabel; terwijl anderen zonder problemen, inclusief data
voorhanden waren.</p>
<h3>Unieke tekenreeks</h3>
<p>Door het stap voor stap importeren vond ik de boosdoener. MariaDB beklaagde zich
over een <code>UNIQUE</code>-sleutel die ik op een <code>VARCHAR(400)</code>-veld had gelegd met de
volgende foutmelding:</p>
<pre><code class="plain">1071 - Specified key was too long; max key length is 767 bytes
</code></pre>
<p>De oplossing was even eenvoudig als elegant. Ik maakte er een <code>VARCHAR(255)</code> van
en paste tenslotte alle procedures die met de betreffende tabel in aanraking
kwamen aan. Gelukkig was er op dat moment geen enkele rij met méér dan 255
tekens in het betreffende veld.</p>
<h2>Debug-hulp</h2>
<p>Uit deze zoektocht blijkt maar weer eens, hoe belangrijk een effectieve debugger
kan zijn voor iedere software-ontwikkelaar. Ik zocht en vond <a href="https://stackoverflow.com/a/23258691">een mogelijkheid</a>
om bij de eerst optredende fout direct de uitvoering te stoppen, en de betreffende
melding te tonen. Ik verwerkte dit stuk code tot een eigen implementatie van
<code>exec()</code> genaamd <code>execMulti()</code>:</p>
<pre><code class="php">private static $_instance = null;
public static function getInstance()
{
if (is_null(self::$_instance)) {
$dsn = 'mysql:host='.DBHOSTNAME.';charset=utf8';
self::$_instance = new \PDO($dsn, DBUSERNAME, DBPASSWORD);
$createSQL = sprintf(
'CREATE DATABASE IF NOT EXISTS `%1$s` DEFAULT CHARACTER SET '.
'utf8 DEFAULT COLLATE utf8_general_ci; USE `%1$s`;',
DBNAME
);
self::$_instance->execMulti($createSQL);
}
return self::$_instance;
}
public static function execMulti(string $queries, string &$errorMessage = null)
{
$db = self::getInstance();
$db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_SILENT);
$db->setAttribute(\PDO::ATTR_EMULATE_PREPARES, 1);
$stmt = $db->prepare($queries);
$stmt->execute();
$i = 1;
do {
$i++;
} while ($stmt->nextRowset());
$error = $stmt->errorInfo();
if ($error[0] != "00000") {
$errorMessage = "Query $i failed: ".$error[2].PHP_EOL;
return false;
}
return true;
}
</code></pre>
</div>2018-05-20T17:36:43+02:00https://www.fwiep.nl/blog/nummermelding-in-nederland-let-opNummermelding in Nederland: let op!2018-03-19T10:26:38+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>TL;DR</h2>
<p>Het onderstaande artikel is een verslag van onze telefonie-perikelen met een
ADSL- en glasvezelprovider, een leverancier van telefoonapparatuur en twee
compleet verschillende protocollen voor <a href="https://nl.wikipedia.org/wiki/Nummermelder">nummermelding</a>.</p>
<p>Na een haastige overstap van VoIP-telefonie achter een ADSL-modem naar analoge
telefonie met centrale achter een glasvezelmodem, bleek dat de geadviseerde
centrale niet compatibel was met de nummer­melding van onze provider.
Uiteindelijk moest er <a href="https://www.telec.nl/webshop/fsk-dtmf-omzetter/category_pathway-195">een converter</a> aan te pas komen om de nummermelding te
laten werken.</p>
<p>De les die ik geleerd heb: Vraag je advies aan een vreemde, vertrouw dit dan
niet blind. Neem de tijd om het te onderzoeken en op waarde te schatten, hoe
dringend de situatie ook lijkt.</p>
<h2>Inleiding</h2>
<p>Een paar jaar geleden hadden we hier in huis een internetverbinding via ADSL en
maakten we gebruik van VoIP-telefoontoestellen. Zo konden we, naast het
vanzelfsprekende bellen en gebeld worden, ook intern bellen en een extern gesprek
doorverbinden. Het had heel wat voeten in de aarde voordat ik de SIP-gegevens
bij onze provider (Telfort) had losgepeuterd, maar het was uiteindelijk toch
gelukt!</p>
<p>Met het oog op de toekomst maakten we gebruik van het aanbod om bij dezelfde
provider over te stappen naar een verbinding via glasvezel. De monteur kwam,
plaatste een modem, voerde een functietest uit en verdween. Alles werkte naar
behoren; we hadden eindelijk glasvezel-internet, met snelheden waarvan ik
voorheen alleen maar kon dromen!</p>
<h2>Analoge telefonie</h2>
<p>Bij de eerste poging een telefoongesprek te voeren, bleek dat de VoIP-toestellen
niet meer functioneerden met de bekende SIP-gegevens (servernaam, gebruikersnaam,
wachtwoord). Aldus probeerde ik opnieuw bij Telfort om deze gegevens te
bemachtigen. Immers: we betalen voor het gebruik van hun diensten, dan mogen we
toch zeker ook de noodzakelijke gegevens om dat te kunnen doen met onze eigen
apparatuur?</p>
<p>Telfort houdt deze informatie echter tot op de dag van vandaag geheim. Er was
voor mij geen enkele mogelijkheid om het afgesloten telefoonabonnement te
gebruiken met de bestaande digitale VoIP-toestellen. Volgens de klantenservice
kon ik wel op de analoge aansluiting van het modem een analoog toestel aansluiten.
Deze combinatie van apparatuur werd volledig ondersteund.</p>
<h2>Centrale</h2>
<p>Als ik méér wilde dan een enkel toestel, dan moest er een (analoge)
telefooncentrale komen, gevolgd door minimaal drie nieuwe (analoge) toestellen.
Gelukkig adviseerde ons het bedrijf dat de VoIP-toestellen had geleverd in de
aankoop van de nieuwe apparatuur. Niet lang daarna leverden zij onze bestelling
aan huis en kon de grote verbouwing beginnen.</p>
<p>Het was mijn eerste kennismaking met een telefooncentrale, analoog of digitaal.
Aldus werkte ik het (Engelstalige) handboek door en richtte de apparatuur zo ver
mogelijk in. Eindelijk: we konden bellen, gebeld worden, intern vergaderen en
externe gesprekken doorverbinden! Na afloop vond ik ook de Nederlandse
handleiding op de website van de fabrikant; altijd handig.</p>
<h2>Nummermelding</h2>
<p>In het dagelijks gebruik bleek nog één functie te ontbreken: <a href="https://nl.wikipedia.org/wiki/Nummermelder">nummermelding</a>
bij inkomende gesprekken. Dit is een functie waarmee je kan zien welk nummer van
buiten belt. Als je toestellen dat nummer aan een naam kunnen koppelen kun je zo
letterlijk zien wie er belt.</p>
<p>Vóór de overstap naar glasvezel en analoge telefonie had dit wel altijd gewerkt.
Nader onderzoek bracht aan het licht dat er wereldwijd twee protocollen zijn
voor het meesturen van de nummermelding: <a href="https://nl.wikipedia.org/wiki/DTMF">DTMF</a> en <a href="https://nl.wikipedia.org/wiki/Frequentieverschuivingsmodulatie">FSK</a>. Het Nederlandse
KPN (en haar dochterbedrijven zoals Telfort) gebruiken DTMF, de rest van
Nederland en de wereld FSK.</p>
<p>Wat bleek? De nieuwe analoge toestellen konden beide protocollen aan. Maar de
splinternieuwe telefooncentrale was van het type FSK. De op dat moment
betaalbaarste oplossing was <a href="https://www.telec.nl/webshop/fsk-dtmf-omzetter/category_pathway-195">een DTMF-FSK-converter</a>. Dit apparaatje bevindt
zich tussen modem en telefooncentrale en zet het DTMF-signaal om in een
FSK-signaal. Nu hadden we eindelijk ook nummermelding!</p>
<h2>Wijsheid achteraf</h2>
<p>Dus dáárom zat er een Engelstalige (UK) handleiding bij de centrale en kennen de
toestellen slechts een 12- in plaats van 24-uurs klok. We hadden het advies van
de leverancier blindelings opgevolgd en zo een verzameling Britse telefoons en
bijpassende centrale in huis gehaald. In de Nederlandse webshop van de
leverancier staat tegenwoordig alleen nog de DTMF-variant van de centrale. Wel
zo verstandig!</p>
</div>2018-03-19T10:26:38+01:00https://www.fwiep.nl/blog/geluiden-op-commando-in-gnomeGeluiden op commando in GNOME2018-02-21T11:43:13+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Een behoorlijke tijd geleden werkte ik samen met een aantal andere programmeurs.
Eén van hen had een klein programmaatje gemaakt dat met een druk op de knop een
geluidje afspeelde. Het was in <a href="https://www.autoitscript.com/site/">AutoIt</a> geschreven, heette <code>WavRunner.exe</code>
en zorgde voor de nodige hilariteit tijdens onze werkdag. Met name
<a href="https://www.myinstants.com/instant/doh/">Homer Simpson</a> was akoestisch goed vertegenwoordigd.</p>
<h2>GNOME</h2>
<p>Omdat ik GNU/Linux als besturingssysteem gebruik, kan ik onder Ubuntu (zonder al
te grote omwegen) geen <code>.exe</code> bestanden uitvoeren. Ik ging op zoek naar een
vergelijkbaar alternatief voor gebruik met de GNOME desktop.</p>
<p>Ik zag het niet zitten om een programma te schrijven dat continu op de
achtergrond zou luisteren naar bepaalde toetscombinaties. In plaats daarvan
bedacht ik, dat ik voor elke toets (en bijbehorend <code>wav</code>-bestand) een sneltoets
zou kunnen toevoegen. Ik vond het commandline programma <code>play</code> dat een
<code>wav</code>-geluidsbestand kan afspelen. Nu nog de sneltoets toevoegen…</p>
<h2>dconf en dan?</h2>
<p>Was het vroeger nog redelijk eenvoudig om via bepaalde <code>XML</code>-bestanden
instellingen van GNOME te wijzigen, moet dit tegenwoordig via een binair
opgeslagen database genaamd <a href="https://wiki.gnome.org/Projects/dconf">dconf</a>. Gelukkig zijn er mensen die de moeite
nemen om hier tegenaan te programmeren en ziedaar: <a href="https://askubuntu.com/questions/26056/where-are-gnome-keyboard-shortcuts-stored/217310#217310">een <code>Perl</code>-script</a> dat
alle bestaande sneltoetsen kan exporteren en importeren.</p>
<h2>Script</h2>
<p>Met bovengenoemd script als werkpaard heb ik onderstaande wrapper geschreven
die voor alle <code>wav</code>-bestanden in een opgegeven map een sneltoets toevoegt. Als
eerste worden alle bestaande sneltoetsen geëxporteerd. Al eerder door het script
toegevoegde shortcuts worden weggefilterd. Dan volgt het toevoegen per
<code>wav</code>-bestand. Tot slot wordt de volledige lijst opniieuw geïmporteerd.</p>
<p>Ik heb gekozen voor de toetscombinatie <kbd>Ctrl</kbd> + <kbd>Shift</kbd> +
<kbd>Alt</kbd> + letter of cijfer. Je zou ook de <kbd>Super</kbd>-toets kunnen
gebruiken, maar dat zorgde voor onvoorspelbaar gedrag in combinatie met het
openen van programma's en vensters (<kbd>Super</kbd> + <kbd>1</kbd>,
<kbd>Super</kbd> + <kbd>2</kbd> etc.).</p>
<pre><code class="bash">#!/bin/bash
GNOMESCRIPT='/path/to/keybindings.pl';
WAVFOLDER='/path/to/folder/containing/wav-files';
if [ ! -f "${GNOMESCRIPT}" ]; then
echo "The GNOME-keys script could not be found.";
exit 1;
fi
if [ ! -x "${GNOMESCRIPT}" ]; then
echo "The GNOME-keys script is not executable.";
exit 2;
fi
# Create a temporary file for exporting
EXPORTFILE="$( mktemp --suffix=.csv )";
# Export existing keyboard shortcuts
"${GNOMESCRIPT}" -e "${EXPORTFILE}";
# Create a temporary file for importing
IMPORTFILE="$( mktemp --suffix=.csv )";
# Filter out existing wavplay-shortcuts
grep -v 'wavplay' "${EXPORTFILE}" > "${IMPORTFILE}";
# Loop through folder
find "${WAVFOLDER%/}/" -maxdepth 1 -iname "?.wav" -print | while read i
do
F="$( basename ${i} )";
# Add a shortcut for each wav-file
echo -e \
"custom\t'wavplay-${F%%.wav}'\t'play ${i}'\t'<Primary><Shift><Alt>${F%%.wav}'" \
>> "${IMPORTFILE}";
done
# Import existing and new keyboard shortcuts
"${GNOMESCRIPT}" -i "${IMPORTFILE}";
# Clean up
rm "${EXPORTFILE}" "${IMPORTFILE}"
# Exit normally
exit 0;
</code></pre>
<h2>Let op</h2>
<p>De namen van de geluidsbestanden mogen uit slechts één letter of cijfer bestaan.
Deze aanpak werkt dus alleen voor de toetsen A-Z en 0-9, maar dat mag de pret
niet drukken. Met 36 verschillende geluiden kom je een heel eind :-)</p>
</div>2018-02-21T11:43:13+01:00https://www.fwiep.nl/blog/parameters-van-mysql-procedures-let-opParameters van MySQL procedures; let op!2017-10-31T21:59:33+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Tijdens de bouw van mijn tot nu toe laatste webapplicatie liep ik tegen een
eigenaardig probleem aan. De <code>sp_vw_</code>-stored procedures van mijn MySQL-database
leken de gevraagde records niet meer te filteren als ik daar wel om vroeg. Het
was alsof de volledige <code>WHERE</code>-clausule genegeerd werd. Na een aantal dagen
zoeken, broeden en nog steeds geen oplossing te hebben gevonden, besloot ik een
vraag te gaan stellen op <a href="https://stackoverflow.com/">StackOverflow</a>.</p>
<h2>Beslagen ten ijs</h2>
<p>Daartoe wilde ik beslagen ten ijs komen, om de lezer een minimalistisch maar
functionerend voorbeeld te geven van het wangedrag van mijn database. Ik
kopieerde de SQL-code van mijn project en stripte bijna alles, tot het nu
volgende script overbleef:</p>
<pre><code class="sql">DROP TABLE IF EXISTS `Book`;
CREATE TABLE `Book` (
`book_id` bigint(20) NOT NULL AUTO_INCREMENT,
`book_name` varchar(50) NOT NULL,
PRIMARY KEY (`book_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT `Book` (`book_name`) VALUES
('Book One'),
('Book Two'),
('Book Three');
DROP PROCEDURE IF EXISTS `sp_vw_book`;
CREATE PROCEDURE `sp_vw_book`(
IN `BOOK_ID` BIGINT(20)
)
SELECT * FROM `Book` WHERE
(BOOK_ID IS NULL OR `book_id` = BOOK_ID);
</code></pre>
<p>Als ik deze procedure aanriep, maakte het geen verschil of ik nou op <code>book_id</code>
wilde filteren of niet:</p>
<pre><code class="sql">mysql> CALL sp_vw_book(null);
+---------+------------+
| book_id | book_name |
+---------+------------+
| 1 | Book One |
| 2 | Book Two |
| 3 | Book Three |
+---------+------------+
3 rows in set (0.00 sec)
mysql> CALL sp_vw_book(2);
+---------+------------+
| book_id | book_name |
+---------+------------+
| 1 | Book One |
| 2 | Book Two |
| 3 | Book Three |
+---------+------------+
3 rows in set (0.00 sec)
</code></pre>
<h2>De oorzaak</h2>
<p>Wat blijkt? Als een parameter van een stored procedure dezelfde naam heeft als
een kolom van de tabel waarop gefilterd wordt (ongeacht hoofd- of kleine letters),
dan vindt er geen enkele filtering meer plaats. De filterclausule wordt dan
gelijk aan <code>WHERE (... OR BOOK_ID = BOOK_ID)</code> oftewel <code>WHERE 1 = 1</code>.</p>
<h2>De oplossing</h2>
<p>De uiteindelijke oplossing is zowel eenvoudig als elegant: hernoem de parameter
zodat deze verschilt van de te filteren kolomnaam. Je moet er maar op komen!</p>
<pre><code class="sql">DROP PROCEDURE IF EXISTS `sp_vw_book`;
CREATE PROCEDURE `sp_vw_book`(
IN `BOOKID` BIGINT(20)
)
SELECT * FROM `Book` WHERE
(BOOKID IS NULL OR `book_id` = BOOKID);
</code></pre>
<pre><code class="sql">mysql> CALL sp_vw_book(null);
+---------+------------+
| book_id | book_name |
+---------+------------+
| 1 | Book One |
| 2 | Book Two |
| 3 | Book Three |
+---------+------------+
3 rows in set (0.00 sec)
mysql> CALL sp_vw_book(2);
+---------+-----------+
| book_id | book_name |
+---------+-----------+
| 2 | Book Two |
+---------+-----------+
1 row in set (0.01 sec)
</code></pre>
</div>2017-10-31T21:59:33+01:00https://www.fwiep.nl/blog/chordpro-naar-html-converterChordPro naar HTML converter2017-10-23T10:22:53+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Als <a href="https://www.fwiep.nl/slechtziendheid">slechtziende</a> muzikant ervoer ik met regelmaat dat papieren bladmuziek
ook nadelen heeft. Als de volgorde van repeteren verschilde van de volgorde
tijdens het optreden, moest ik continu zoeken in de stapel met uitwerkingen, die
alfabetisch gesorteerd waren. Dit moest toch overzichtelijker kunnen?</p>
<p>Een deel van mijn mede-muzikanten maakte gebruik van <a href="https://onsongapp.com/">OnSong</a>, een iOS-app
waarmee je een verzameling digitale bladmuziek comfortabel kunt beheren en
gebruiken. Na bestudering van het <a href="https://onsongapp.com/docs/features/formats/onsong/">bestandsformaat</a> besloot ik om al mijn
muziek om te zetten en de app te kopen. Tot slot nog een derdehands iPad op de
kop tikken en de digitale revolutie op de bühne kon beginnen!</p>
<h2>Tegenslag</h2>
<p>Bij het experimenteren ontdekte ik een bug in OnSong, die ik meldde via hun
suppert-afdeling. Bij de volgende bug dacht ik slim te zijn en keek op de lijst
met aankomende updates; misschien was de bug al bekend en verholpen in de
volgende versie van OnSong. Wat bleek? Wordt iOS 5.0 (dat op mijn iPad 1 draait)
vanaf de komende update niet meer ondersteund. Effectief betekende dit, dat mijn
iPad het voor zijn hele verdere digitale leven moest doen met versie 1.99995 van
OnSong.</p>
<h2>Monsterklus</h2>
<p>Als <a href="https://www.fwiep.nl/programmeren">webontwikkelaar</a> zoek ik in zo'n geval altijd naar andere mogelijkheden
om, bijvoorbeeld met een webapplicatie in een browser, mijn doel alsnog te
bereiken. Hoewel OnSong en vergelijkbare apps ook offline (zonder
internetverbinding) te gebruiken zijn, kies ik voor een online variant. Hierdoor
kan ik met mijn huidige kennis en vaardigheden een heel eind vooruit. Zou ik er
in de toekomst voor kiezen om de applicatie ook offline te willen gebruiken,
kan dat bijvoorbeeld met behulp van <a href="https://www.w3schools.com/html/html5_webstorage.asp">HTML5 WebStorage</a>.</p>
<h2>ChordPro-formaat</h2>
<p>De basis van het OnSong-bestandsformaat is het <em>ChordPro</em>-formaat, een losse
specificatie die door de meest uiteenlopende applicaties en programma's wordt
gebruikt. Omdat het geen officiële standaard is, hangt het van de implementatie
af, hoe het gegenereerde bestand uitziet. De basis is echter altijd gelijk,
hieronder een voorbeeld:</p>
<pre><code class="plain">{title: Will they remember?}
{artist: Frans-Willem Post}
{time: 4/4}
{key: C}
{tempo: 66}
Verse 1:
[C] spent the day [Caug]talking [C6] i'm glad that it's [Caug]over
[C] 'm allmost re[Caug]gretting [C6] i invited them [C7]over
[F] but it's [Fm]been worth my [C]while [C7]
[F] see them a[Fm]gain in a little [C]while [C7]
[F] but, will they re[Ab7]mem[G7]ber?
</code></pre>
<p>Dit wordt uiteindelijk zoiets als dit:</p>
<pre><code class="plain"> Will the remember?
Frans-Willem Post
4/4, C, 66 BPM
Verse 1:
C Caug C6 Caug
spent the day talking I'm glad that it's over
C Caug C6 C7
'm allmost regretting I invited them over
F Fm C C7
but it's been worth my while
F Fm C C7
see them again in a little while
F Ab7 G7
but, will they remember?
</code></pre>
<p>Het bestand bevat als eerste een aantal gegevens in accolades, zoals titel,
artiest, tempo, maat- en toonsoort. Daarna volgen één of meerdere gedeelten
die van een label voorzien kunnen zijn. De akkoorden staan middenin de tekst
tussen rechte haken.</p>
<h2>Aanpak</h2>
<p>Hoe kon ik met eenvoudige HTML één tekstregel met akkoorden transformeren naar
twee regels waarin de akkoorden precies op de juiste plaats staan; boven de
betreffende lettergreep? Dit was een doordenkertje... maar uiteindelijk bleken
tabellen de juiste aanpak.</p>
<p>Ik kon voor elke regel in het bronbestand een HTML-tabel met twee rijen genereren,
waarbij in de bovenste rij de akkoorden kwamen te staan, in de onderste de tekst
in kleine stukjes. Het automatisch meegroeien van de <code>td</code>-cel zou er voor
zorgen dat het akkoord boven de juiste lettergreep zou blijven staan. Een
aantal uitdagingen lagen wel nog op de loer: een reeks van meerdere akkoorden
zonder tekst, akkoorden aan het begin en einde van een regel…</p>
<h2>Script</h2>
<p>In wezen bestaat de converter uit twee gedeelten: de parser, een script dat het
bronbestand analyseert en 'leegplukt', en de uiteindelijke HTML-output. Hieronder
volgt een vereenvoudigde weergave van de daadwerkelijke code:</p>
<h3>Parser</h3>
<pre><code class="php">class SongParser
{
private static $_remarkRegex = '!^\s*#(?P<REMARK>.*)$!';
private static $_kvRegex = '!^\{(?P<KEY>[^:]+):\s*(?P<VALUE>[^}]+)\}$!';
private static $_lblRegex = '!^(?P<LABEL>[^:]+):$!';
public static function parseFile(string $filename)
{
$lines = file($filename, FILE_IGNORE_NEW_LINES);
if (false === $lines) {
return false;
}
$s = new Song();
$section = new SongSection();
foreach ($lines as $l) {
$m = array();
if (preg_match(static::$_remarkRegex, $l, $m) === 1) {
// The line is a remark, discard and continue
continue;
} else if (preg_match(static::$_kvRegex, $l, $m) === 1) {
// The line contains metadata, fill the Song's properties
switch ($m['KEY']) {
case 'title':
$s->setTitle($m['VALUE']);
break;
case 'artist':
$artists = preg_split(
'!;!u', $m['VALUE'], -1, PREG_SPLIT_NO_EMPTY
);
foreach ($artists as $a) {
$s->addArtist($a);
}
break;
case 'time':
$s->setTimeSignature($m['VALUE']);
break;
case 'tempo':
$s->setTempo(intval($m['VALUE']));
break;
case 'key':
$k = new Key($m['VALUE']);
$s->setKey($k);
break;
case 'author':
$s->setAuthor($m['VALUE']);
break;
case 'comment':
case 'c':
case 'comment_bold':
case 'cb':
case 'comment_italic':
case 'ci':
// Pass comments to the line handling code
$section->addLine($l);
break;
}
} else if (preg_match(static::$_lblRegex, $l, $m) === 1) {
// The line contains a section label
$section->setLabel($m['LABEL']);
} else if (F\Utils::isEmpty(trim($l))) {
// The line is empty, start a new section
$section = new SongSection();
$s->addSection($section);
} else {
// The line has no special meaning, add it to the current section
$section->addLine($l);
}
}
if (!$s->getSections()) {
$s->addSection($section);
}
return $s;
}
}
</code></pre>
<h3>HTML-weergave</h3>
<pre><code class="php">function h(string $s) {
return htmlentities($s, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
$oArtists = h(implode(' & ', $song->getArtists()));
$oTimeSignature = h($song->getTimeSignature());
$oKey = h(implode(' / ', $song->getKey()->getKeys()));
$oTempo = sprintf('%0d', $song->getTempo());
$oHTML = '';
foreach ($song->getSections() as $sect) {
if (!empty($sect->getLabel())) {
$oHTML .= sprintf(
'<h3>%1$s:</h3>'.PHP_EOL,
h($sect->getLabel())
);
}
foreach ($sect->getLines() as $l) {
if (isEmpty(trim($l))) {
continue;
}
$commentMatches = '';
$reComment = '!^\s*\{
(?:
c(?P<STYLESHORT>[bi])? # short style directive
|
comment(?P<STYLELONG>_bold|_italic)? # long style directive
)
:(?P<VALUE>[^}]+) # actual comment
\}\s*$!x';
if (preg_match($reComment, $l, $commentMatches) === 1) {
ob_start();
include 'html/tpl_p_comment.php';
$oHTML .= ob_get_clean();
continue;
}
$isChordsOnly = (preg_match('!^\s*(\[[^\]]+\]\s*)+$!', $l) === 1);
$parts = preg_split(
'!(\[[^\]]+\])!', $l, -1,
PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE
);
if ($isChordsOnly) {
ob_start();
include 'html/tpl_table_chords.php';
$oHTML .= ob_get_clean();
} else {
ob_start();
include 'html/tpl_table_chords-lyrics.php';
$oHTML .= ob_get_clean();
}
}
$oHTML .= '<hr />';
}
</code></pre>
<p>De HTML-uitvoer ziet er dan ongeveer zo uit:</p>
<pre><code class="html"><h1><span class="title"><?php print $oTitle ?></span><br />
<span class="artist"><?php print $oArtists ?></span></h1>
<div class="right">
<span class="time"><?php print $oTimeSignature ?></span>,
<span class="key"><?php print $oKey ?></span>,
<span class="tempo"><?php print $oTempo ?> BPM</span>
</div>
<?php print $oHTML ?>
</code></pre>
<p>De verschillende stukjes HTML-code worden apart ge<code>include</code>-ed.</p>
<h3>Akkoorden</h3>
<pre><code class="php">$oTDs = '';
foreach ($parts as $p) {
if (empty($p[0])) {
continue;
}
$oTDs .= sprintf(
'<td>%1$s</td>'.PHP_EOL,
h(trim($p[0], '[]'))
);
}
</code></pre>
<pre><code class="html"><table>
<tr class="tr-chords"><?php print $oTDs ?></tr>
</table>
</code></pre>
<h3>Akkoorden met tekst</h3>
<pre><code class="php">$oTDsChords = '';
$oTDsLyrics = '';
foreach ($parts as $p) {
if (empty($p[0])) {
continue;
}
$isChord = (preg_match('!^\[[^\]]+\]$!', $p[0]) === 1);
if ($isChord) {
$lastOnLine = (strlen($l) == ($p[1] + strlen($p[1])));
$followedBySpace = (substr($l, $p[1] + strlen($p[0]), 1) === ' ');
$restIsChordsOnly
= (preg_match('!^\s*(\[[^\]]+\]\s*)+$!', substr($l, $p[1])) === 1);
if ($lastOnLine || $followedBySpace || $restIsChordsOnly) {
$oTDsLyrics .= sprintf('<td>&nbsp;</td>'.PHP_EOL);
}
$oTDsChords .= sprintf(
'<td>%1$s</td>'.PHP_EOL,
h(trim($p[0], '[]'))
);
} else {
$onStartOfLine = ($p[1] == 0);
$startsWithSpace = (substr($p[0], 0, 1) === ' ');
$notPrecededByChord = (substr($l, $p[1]-1, 1) !== ']');
if ($onStartOfLine || $startsWithSpace || $notPrecededByChord) {
$oTDsChords .= sprintf('<td>&nbsp;</td>'.PHP_EOL);
}
$oTDsLyrics .= sprintf(
'<td>%1$s</td>'.PHP_EOL,
h($p[0])
);
}
}
</code></pre>
<pre><code class="html"><table>
<tr class="tr-chords"><?php print $oTDsChords ?></tr>
<tr class="tr-lyrics"><?php print $oTDsLyrics ?></tr>
</table>
</code></pre>
<h3>Commentaar</h3>
<pre><code class="php">$extraClasses = '';
$matchedStyles = array(
$commentMatches['STYLESHORT'], $commentMatches['STYLELONG']
);
foreach ($matchedStyles as $style) {
switch ($style) {
case 'b':
case '_bold':
$extraClasses .= ' bold';
break;
case 'i':
case '_italic':
$extraClasses .= ' italic';
break;
}
}
</code></pre>
<pre><code class="html"><p class="comment<?php print $extraClasses ?>">
<?php print h($commentMatches['VALUE']) ?>
</p>
</code></pre>
<h2>Wordt vervolgd</h2>
<p>De bovenstaande code is slechts het begin van het ChordProApp-project, maar wel
het belangrijkste deel. Het was een uitdaging om in mijn straatje van PHP, HTML
en CSS een nieuwe uitdaging aan te gaan. Zo gauw ik méér te melden heb,
verschijnt er weer een nieuw artikeltje. Tot dan!</p>
</div>2017-10-23T10:22:53+02:00https://www.fwiep.nl/blog/nbg6716-router-met-openwrtNBG6716 router met OpenWrt2017-10-17T10:31:13+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>De Zyxel NBG6716 router is een multiband router voor thuisgebruik. Hij biedt van
huis uit een aantal functies en is, binnen het gestelde budget, voldoende klaar
voor de toekomst. Toen ik echter voor de veiligheid probeerde een <a href="https://www.fwiep.nl/blog/bestanden-delen-met-sftp">poort
forwarding</a> met verschillende <code>src</code>- en <code>dst</code> nummers in te stellen, liep ik
tegen de grenzen van de originele firmware aan.</p>
<p>Zyxel bleek zijn grafische webinterface bovenop een (verouderde) versie van
<a href="https://openwrt.org/">OpenWrt</a> te hebben gebouwd. Ik zou dus ook via SSH in de configuratie kunnen
gaan stoeien, maar ik wilde meer controle, meer zekerheid en veiligheid. Aldus
ging ik op zoek naar een alternatieve firmware.</p>
<h2>OpenWrt</h2>
<p>Omdat de originele firmware op OpenWrt baseerde, leek mij dit open source
project de meest voor de hand liggende plaats om met de zoektocht te beginnen.
Er bleek inderdaad al <a href="https://wiki.openwrt.org/toh/zyxel/zyxel_nbg6716">een pagina</a> gewijd te zijn aan dit model; vanaf dat
moment wist ik dat het goed zat!</p>
<h2>Update</h2>
<p>In oktober 2017 werd bekend dat er een <a href="https://www.krackattacks.com/">ontwerpfout in het WPA2-protocol</a> zit,
waardoor onversleutelde WLAN-communicatie af te luisteren is. Toen ik op zoek
ging naar een update voor OpenWrt, ontdekte ik dat de firmware onder een nieuwe
naam wordt doorontwikkeld: LEDE-project. Genoeg stof voor <a href="https://www.fwiep.nl/blog/wpa2-krack-van-openwrt-naar-lede">een nieuw artikel</a>
in mijn weblog :-)</p>
<h2>Installatie</h2>
<h3>Van Zyxel naar OpenWRT</h3>
<p>Op de genoemde pagina stond dat de installatie van de meest recente versie
OpenWrt niet meer dan drie commando's lang zou zijn. Eenmaal als <code>root</code> via SSH
aangemeld, voerde ik de volgende statements uit:</p>
<pre><code class="sh">cd /tmp;
wget "http://downloads.openwrt.org/chaos_calmer/15.05.1/ar71xx/nand/openwrt-15.05.1-ar71xx-nand-nbg6716-squashfs-factory.bin";
mtd -r write "openwrt-15.05.1-ar71xx-nand-nbg6716-squashfs-factory.bin" /dev/mtd7;
</code></pre>
<p>De router startte opnieuw op en daarna was de webinterface op poort 80 actief.
Als eerste moest een nieuw wachtwoord voor het <code>root</code>-account worden ingesteld.
Daarna kon de configuratie beginnen.</p>
<h3>Van OpenWRT naar OpenWRT</h3>
<p>Tijdens het stoeien met de nieuwe firmware kan het nodig zijn om met een schone
lei te beginnen. Download daartoe het <a href="http://downloads.openwrt.org/chaos_calmer/15.05.1/ar71xx/nand/openwrt-15.05.1-ar71xx-nand-nbg6716-squashfs-sysupgrade.tar">SysUpgrade</a>-bestand en upload dit op
de pagina <code>System</code> → <code>Backup / Flash Firmware</code>. Je kunt eventueel bestaande
instellingen behouden (<code>Keep settings</code>), maar dan is de lei minder schoon.</p>
<h2>Basisinstellingen</h2>
<h3>Systeem</h3>
<ul>
<li>SSH server activeren (<code>System</code> → <code>Administration</code>)</li>
<li>Tijdzone instellen (<code>System</code> → <code>System</code>)</li>
</ul>
<h3>Lokaal netwerk</h3>
<ul>
<li>IP-adres instellen (<code>Network</code> → <code>Interfaces</code> → <code>LAN</code>)</li>
<li>DHCP reeks aanpassen (<code>Network</code> → <code>Interfaces</code> → <code>LAN</code>)</li>
<li>DHCP static leases toevoegen (<code>Network</code> → <code>DHCP and DNS</code>)</li>
</ul>
<h3>Draadloos</h3>
<ul>
<li>2,4 GHz netwerk toevoegen (<code>Network</code> → <code>Wifi</code>)</li>
<li>5 GHz netwerk toevoegen (<code>Network</code> → <code>Wifi</code>)</li>
</ul>
<h2>Modules</h2>
<p>OpenWrt brengt in de basisvariant niet alle functionaliteit, toeters en bellen
mee die voor een platform of apparaat beschikbaar zijn. In plaats daarvan kun je
als beheerder specifieke onderdelen, zogenaamde <em>modules</em>, toevoegen. In dit
geval wilde ik de router als mini-NAS gaan inzetten. Dat wil zeggen dat hij de
opslagruimte van een aangesloten USB-stick via het netwerk beschikbaar maakt.</p>
<p>Om wat voor module dan ook te kunnen toevoegen, moet allereerst de lijst met
beschikbare modules worden geüpdate. Dit kan via <code>System</code> → <code>Software</code> ->
<code>Update lists</code>. Mocht dat niet voldoende zijn, kan men nog altijd via SSH het
commando <code>opkg update</code> uitvoeren.</p>
<p>Met dank aan de <a href="https://wiki.openwrt.org/doc/recipes/usb-storage-samba-webinterface">uitvoerige documentatie</a> was de installatie van de gedeelde
map niet al te complex. Ik voegde een aantal modules aan het systeem toe:</p>
<ul>
<li><code>kmod-usb-core</code>: basisondersteuning voor USB apparaten</li>
<li><code>kmod-usb-storage</code>: ondersteuning voor verwisselbare media</li>
<li><code>kmod-usb2</code>: ondersteuning voor USB2.0</li>
<li><code>block-mount</code>: scripts voor het koppelen van apparaten en partities</li>
<li><code>samba36-server</code>: server voor het delen van mappen en printers</li>
<li><code>luci-app-samba</code>: uitbreiding van webinterface voor het beheren van Samba</li>
</ul>
<p>Tot zover verliep alles volgens plan. Toen de aangesloten USB-stick niet werd
herkend moest ik op zoek naar de oorzaak...</p>
<h2>Bestandssysteem</h2>
<p>Iets waar een gemiddelde computergebruiker niet bij stilstaat is het
bestandssysteem waarmee zijn bestanden worden opgeslagen. In de Windows-wereld
is dit meestal NTFS, bij Apple meestal HFS+, onder GNU/Linux vaak Ext4. Op
USB-sticks die aan meerdere systemen moeten worden gekoppeld kiest men voor FAT,
FAT32 of exFAT.</p>
<p>Bij het gebruik van de originele firmware was mijn USB-stick geformatteerd met
FAT32. Bij gebruik vanaf een GNU/Linux-systeem waren daardoor alle bestanden
als uitvoerbaar gemarkeerd (<code>chmod 755</code>). Dit was voor mij de reden om de stick
nu opnieuw te formatteren; maar dan met Ext4. OpenWrt (een GNU/Linux variant)
kan daar zonder problemen mee overweg, als je de module <code>kmod-fs-ext4</code>
installeert.</p>
<h2>(Gasten-) netwerk</h2>
<p>Een thuisrouter is het centrale punt van het netwerk. Hij zorgt dat alle
apparaten een IP-adres krijgen, stelt opslagruimte ter beschikking en deelt de
internetverbinding. In dit geval wilde ik niet alleen een bedraad en draadloos
eigen netwerk, maar ook een draadloos gastennetwerk.</p>
<p>Wederom dankzij de <a href="https://wiki.openwrt.org/doc/recipes/guest-wlan-webinterface">uitvoerige documentatie</a> van OpenWrt was dit een relatief
fluitje van een cent. Als eerste voeg je een nieuw draadloos netwerk (SSID) toe.
Daarna pas je de instellingen zo aan, dat dit nieuwe (netwerk-) interface zich
in een eigen firewall zone bevindt. De configuratie van die firewall luistert
heel nauw; volg de instructies of zorg dat je weet wat je doet. Zo niet, dan kun
je nog <a href="https://forum.openwrt.org/viewtopic.php?id=69841">altijd vragen</a> :-)</p>
<h2>Probleem achteraf</h2>
<p>Met de standaardinstellingen is een via Samba gedeelde map alleen door de
eigenaar beschrijfbaar (rechten: <code>0644</code>). In mijn geval wilde ik graag met
meerdere gebruikers (die zich in dezelfde groep bevinden) de bestanden kunnen
lezen en schrijven. Maar op dat moment kon de ene gebruiker wel een map aanmaken
en daar bestanden in bewerken. De ander kon echter die bestanden wel openen,
maar geen nieuwe bestanden toevoegen of de map verwijderen.</p>
<p>Ik moest op zoek naar een mogelijkheid om de Samba-cliënt gebruiker meer rechten
te geven. Ik stoeide met bestandsrechten op server en cliënt, <code>umask</code>,
<code>ACL</code>'s… tot file-attributes aan toe. Uiteindelijk bleek het een
Samba-optie van de gedeelde map:</p>
<pre><code class="plain">option create_mask '0774'
option dir_mask '0775'
</code></pre>
<p>Op zich is het best verwarrend, dat onder GNU/Linux de term <code>mask</code> wordt
gebruikt als masker van bits dat met 777 wordt ge-<code>AND</code>-t. Denk aan het commando
<code>umask</code>, daar wordt met <code>002</code> en <code>777</code> uiteindelijke <code>775</code> gemaakt. Samba,
daarentegen, verwacht het uiteindelijke getal in één keer. <em>Weird…</em></p>
<h2>Conclusie</h2>
<p>Het is verstandig om na het inrichten van de router, een backup te maken van
alle instellingen. Dit kan het eenvoudigst via de pagina <code>System</code> → <code>Backup /
Flash Firmware</code>, <code>Generate archive</code>. Dit bestand kun je later op dezelfde pagina
opnieuw uploaden om de configuratie te herstellen.</p>
<p>Na een aantal uren stoeien met alle mogelijke schermen van de LuCI webinterface
en af en toe een paar commando's via SSH, is mijn router nu weer net als nieuw.
Nee, hij is beter dan dat. De <a href="https://www.fwiep.nl/blog/backup-met-raspberry-pi-en-usb-hdd">dagelijkse backup</a> verloopt sneller en ik heb
de volledige controle over mijn netwerken. Het was de moeite meer dan waard!</p>
</div>2017-02-16T20:13:34+01:00https://www.fwiep.nl/blog/wpa2-krack-van-openwrt-naar-ledeWPA2-KRACK - van OpenWrt naar LEDE2017-10-17T10:28:58+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>In een <a href="https://www.fwiep.nl/blog/nbg6716-router-met-openwrt">eerder artikel</a> schreef ik over mijn thuisrouter die ik van een nieuwe,
uitgebreidere en veiligere firmware heb voorzien. Op 16 oktober 2017 werd bekend
dat er <a href="https://www.krackattacks.com/">een ontwerpfout</a> zit in het WPA2-protocol, dat gebruikt wordt voor het
versleutelen van zo'n beetje alle WLAN-dataverkeer (WiFi). Natuurlijk ging ik
direct op zoek naar een mogelijkheid om mijn apparatuur te beveiligen.</p>
<p>Op het forum van OpenWrt vond ik een actieve discussie over het beveiligingslek,
en melding van een beschikbare patch in het <em>LEDE-project</em>. Was hier sprake van
nóg een alternatieve firmware?</p>
<h2>LEDE-project</h2>
<p>Het LEDE-project is een herstart van OpenWrt. De actuele ontwikkeling vindt
in het nieuwe project plaats, binnen afzienbare tijd zal de oude naam verdwijnen.
De interne structuur en de onderlinge communicatie tussen ontwikkelaars en
community zijn veranderd, lees verbeterd.</p>
<h2>Zyxel NBG6716</h2>
<p>Omdat de nieuwe firmware voortbouwt op het oude project, was een upgrade niet al
te moeilijk. Via <code>System</code> → <code>Backup/Flash firmware</code> upload je de
<a href="https://openwrt.org/toh/hwdata/zyxel/zyxel_nbg6716_a01">nieuwe firmware</a> en behoudt de huidige instellingen (Keep settings).
Eventuele waarschuwingen over bestaande Samba-template bestanden kun je zonder
problemen negeren.</p>
<p>Daarna moeten een aantal pakketten opnieuw worden geïnstalleerd (<code>System</code> →
<code>Software</code>). Denk daarbij om als eerste de pakketlijsten te verversen. Dit kan
via een knop, of via SSH: <code>opkg update</code>.</p>
<ul>
<li><code>kmod-usb-core</code>: basisondersteuning voor USB apparaten</li>
<li><code>kmod-usb-storage</code>: ondersteuning voor verwisselbare media</li>
<li><code>kmod-usb2</code>: ondersteuning voor USB2.0</li>
<li><code>block-mount</code>: scripts voor het koppelen van apparaten en partities</li>
<li><code>samba36-server</code>: server voor het delen van mappen en printers</li>
<li><code>luci-app-samba</code>: uitbreiding van webinterface voor het beheren van Samba</li>
<li><code>kmod-fs-ext4</code>: ondersteuning voor het EXT4-bestandssysteem</li>
<li><code>wget</code>: universeel download-tool, zie <a href="https://oldwiki.archive.openwrt.org/doc/howto/wget-ssl-certs">deze wiki-pagina</a> voor meer informatie</li>
<li><code>ca-certificates</code>: basisverzameling SSL-certificaten, zie <code>wget</code> hierboven</li>
</ul>
<p>Tot slot moet ook nog de CRON-service worden gestart en geactiveerd:</p>
<pre><code class="bash">/etc/init.d/cron start
/etc/init.d/cron enable
</code></pre>
<h2>KRACK-fix</h2>
<p>Nog geen 24 uur na het bekend worden van de WPA2-kwetsbaarheid, hebben de
ontwikkelaars al een patch aan de ontwikkel-versie toegevoegd. Een paar uur
later verscheen <a href="https://forum.openwrt.org/t/critical-wifi-vulnerability-found-krack/7450/23">een forumpost</a> met instructies om de patches te installeren.</p>
<p>In het kort komt het neer op het opnieuw installeren van twee pakketten: <code>wpad</code>
(of <code>wpad-mini</code>) en <code>hostapd-common</code>. Dit gaat, net zoals hierboven beschreven
via <code>System</code> → <code>Software</code> voorafgegaan door een actualisatie van de pakketlijsten.</p>
<h2>Naspel</h2>
<p>Mijn 'nieuwe' router had al zijn oude instellingen meegenomen. Het bedrade,
draadloze en gasten-netwerk, geplande taken, de USB-netwerkshare via Samba. Maar
de verbinding met de netwerkmap wilde niet meer tot stand komen. In het <code>syslog</code>
zag ik de volgende foutmelding:</p>
<pre><code class="plain">VFS: cifs_mount failed w/return code = -13
</code></pre>
<p>Na lang zoeken en diep nadenken schoot mij de uiteindelijke oplossing te binnen.
Als de Samba-server opnieuw was geïnstalleerd en zijn configuratie werd terug
geplaatst vanuit een backup (Keep settings), zouden dan ook de wachtwoorden
voor de Samba-gebruikers worden teruggezet?</p>
<p>Aldus voerde ik voor elke gebruiker een <code>smbpasswd -a</code> uit en voilà! Mijn
CIFS-mount slaagde zonder problemen en ik kon eindelijk weer aan mijn bestanden.
Missie geslaagd!</p>
</div>2017-10-17T10:28:58+02:00https://www.fwiep.nl/blog/wifi-wachtwoord-dynamisch-of-nietWiFi-wachtwoord: Dynamisch of niet?2017-10-02T08:41:12+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Het aanbieden van internettoegang aan bezoekers is tegenwoordig heel normaal.
Meestal gebeurt dit in de vorm van een extra draadloos netwerk, een zogenaamd
<em>gastennetwerk</em>. De meeste routers en accesspoints ondersteunen het verspreiden
van meerdere netwerken (SSIDs), zo ook mijn eigen <a href="https://www.fwiep.nl/blog/nbg6716-router-met-openwrt">Zyxel NBG6716 met OpenWrt</a>.
Toen ik in mijn favoriete computertijdschrift een artikel las over het, vanwege
de veiligheid, met regelmaat aanpassen van het wachtwoord, dacht ik: dat is een
goed idee voor mijn gastennetwerk!</p>
<h2>Benodigdheden</h2>
<p>Aldus ging ik op zoek naar informatie over de verschillende onderdelen van dit
project:</p>
<ul>
<li>het genereren van een veilig wachtwoord</li>
<li>het regelmatig vervangen van de WPA-sleutel van het gastennetwerk</li>
<li>een simpel stappenplan voor een gast die van het netwerk gebruik wil maken</li>
</ul>
<h3>Velig wachtwoord</h3>
<p>De <a href="https://wiki.openwrt.org/doc/uci/wireless/encryption">documentatie van OpenWrt</a> verwees me door naar een programma genaamd
<code>pwgen</code>, dat voor de meeste GNU/Linux-distributies en ook OpenWrt beschikbaar
is. Met het volgende commando verschijnt er een volledig willekeurige tekenreeks
van precies 63 alfanumerieke tekens:</p>
<pre><code class="bash">pwgen --secure 63 1
</code></pre>
<h3>Wachtwoord vervangen</h3>
<p>Op de genoemde pagina staan de commando's om het wachtwoord van een met WPA2
beveiligd netwerk in te stellen. De router met OpenWrt is van huis uit via SSH
te beheren, dus is er ook een mogelijkheid om de <code>uci</code>-commando's vanaf een
andere computer uit te voeren.</p>
<p>Tot zover was mijn plan te realiseren; alleen nog uitzoeken hoe ik welke eindjes
aan elkaar moest knopen, dat in één of twee shell-scripts gieten en met een
<code>cron</code>-job automatiseren. Simpel, toch?</p>
<h2>Probleem: gebruiker</h2>
<p>Men zegt wel eens: gemak dient de mens. In het geval van een menselijke
computergebruiker is dat vaak méér dan waar. Het invoeren van een wachtwoord dat
aan (mijn) strenge veiligheidseisen voldoet, is niet bepaald comfortabel. Daarom
wilde ik die gebruiker zo veel mogelijk tegemoet komen bij het verbinden met
mijn gastennetwerk.</p>
<p>Met behulp van een QR-code kun je niet alleen URL's of visitekaartjes delen,
maar ook complexe wachtwoorden van draadloze netwerken. Onder Android kun je er
zelfs een <a href="https://github.com/zxing/zxing/wiki/Barcode-Contents#wifi-network-config-android">volledig verbindingsprofiel</a> in kwijt.</p>
<pre><code class="plain">WIFI:T:WPA;S:"mijn-gastennetwerk";P:"uYvQ9X6nUAFCUQbmbVmaLZkU8";H:false;
</code></pre>
<p>Voorwaarde is dan natuurlijk wel, dat de gebruiker een mogelijkheid heeft om de
QR-code uit te lezen. Dit is lang niet altijd het geval; ten minste niet bij de
mensen die van <em>mijn</em> gastennetwerk gebruik willen maken.</p>
<h2>WPS als redder in nood</h2>
<p>Was er niet ooit een mogelijkheid om een apparaat met één druk op de knop met
een netwerk te verbinden? Jawel, dit proces heet <em>Wifi Protected Setup</em> (WPS).
Android en Windows bieden ondersteuning voor WPS, GNU/Linux, iOS en macOS niet.
De variant met een pincode van 8 cijfers is <a href="https://tools.kali.org/wireless-attacks/bully">al lang te kraken</a> en dus geen
optie, maar de Push-Button-Connection (PBC) achtte ik voldoende veilig. Ik ging
op zoek naar de <a href="https://wiki.openwrt.org/doc/uci/wireless#wps_options">mogelijkheden</a> binnen OpenWrt en <a href="https://forum.archive.openwrt.org/viewtopic.php?id=72196">liep vervolgens muurvast</a>.</p>
<h2>Epiloog</h2>
<p>Hoe uitdagend dit project ook zou zijn; de gegarandeerde irritatie bij mijn
gasten vanwege het steeds weer opnieuw in moeten voeren van een lang en complex
wachtwoord, weegt niet op tegen de veiligheidswinst van een regelmatig wisselende
WPA-sleutel op mijn gastennetwerk.</p>
<p>Meestal leidt denkwerk vooraf tot een (veel) beter product. In dit geval
betekende het een hoop onderzoek, maar geen programma, script of applicatie. Is
dit project daarom mislukt? Nee, ik vind van niet. Door overleg en planning
vooraf kunnen werk en energie dáár worden ingezet, waar ze het hardste nodig
zijn. Daarnaast schaadt onderzoek nooit; kennis is altijd waardevol.</p>
</div>2017-10-02T08:41:12+02:00https://www.fwiep.nl/blog/nieuwste-orca-schermlezer-en-terugNieuwste Orca schermlezer - en weer terug2017-08-09T20:08:14+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Al geruime tijd volg ik de ontwikkeling van de <a href="https://wiki.gnome.org/Projects/Orca">Orca schermlezer</a> op de voet.
Gemiddeld druppelen twee tot drie e-mails per dag binnen met vragen, suggesties
en opmerkingen van over de hele wereld. Afgelopen week vroeg er iemand of het
mogelijk was om de allernieuwste Orca te installeren onder Ubuntu 16.04.</p>
<p>Prompt kwam er <a href="https://mail.gnome.org/archives/orca-list/2017-August/msg00030.html">een deskundig antwoord</a> met relatief eenvoudige instructies:</p>
<ol>
<li>sudo apt install git</li>
<li>sudo apt-get build-dep gnome-orca</li>
<li>git clone git://git.gnome.org/orca</li>
<li>cd gnome-orca</li>
<li>./autogen.sh && make && sudo make install</li>
</ol>
<h2>Git</h2>
<p>Het GNOME-project, waar Orca deel van uitmaakt, gebruikt <em>Git</em>-versiebeheer. Dit
betekent dat elke wijziging aan de broncode kan worden teruggezocht en eventueel
ongedaan gemaakt. De broncode wordt beheerd in een soort boomstructuur
(<em>repository</em>). De stam van de boom heet <em>master</em>, de zijtakken <em>branches</em>.</p>
<p>Voor een nieuwe versie van het project wordt een aparte branch aangemaakt, die
daarna wordt klaargestoomd voor de uitlevering (release). Al die tijd kan het
werk in master gewoon doorgaan. De twee versies van het project bestaan dan
vreedzaam naast elkaar.</p>
<h2>Scherpe snede</h2>
<p>De meest actuele ontwikkeling vindt plaats in master. Daar kunnen dus ook fouten
of ontbrekende functionaliteit optreden. Men wordt in het algemeen niet voor
niets gewaarschuwd vanwege de "bleeding edge of development".</p>
<p>Ik wilde desondanks toch eens proberen of ik de allernieuwste Orca kon gebruiken
onder mijn huidige besturingssysteem (Ubuntu GNOME 16.04). Ik voerde de
instructies uit en warempel: de schermlezer sprak:</p>
<pre><code class="plain">Screenreader on.
</code></pre>
<p>Het was gelukt! Maar het tempo van de spraak was niet zoals ik gewend was. En de
meertalige profielen die ik voor Nederlands, Duits en Engels had ingesteld waren
ook verdwenen. Al mijn persoonlijke instellingen leken buitenspel te staan?</p>
<h2>Op zoek</h2>
<p>Mijn eerste gedachte: Dan gaan we terug naar de oude Orca. Nieuwe ge-<code>kill</code>d,
oude gestart met <code>/usr/bin/orca --replace</code>. Ook nu klonk de bekende
welkomsboodschap, maar mijn instellingen waren nog steeds niet van de partij.
Bovendien bleek de versie van Orca nog steeds hoger dan de met Ubuntu
meegeleverde 3.18.2.</p>
<p>Een controle met <code>dconf</code> bevestigde dat mijn instellingen nog steeds waren
opgeslagen:</p>
<pre><code class="bash">dconf dump /org/gnome/orca/
</code></pre>
<p>Aldus ging ik op zoek in de branches van Orca's repository. Daar vond ik een tak
met de naam <code>origin/gnome-3-18</code>; precies passend bij mijn huidige
desktopomgeving. Ook dit maal voerde ik de commando's uit om mijn eigen
exemplaar van Orca te bouwen en te installeren…</p>
<p>Maar nog steeds gedroeg de schermlezer zich alsof hij niets wist van mijn
persoonlijke voorkeuren. Hoe kon ik nu terug naar een werkende en vertrouwde
omgeving?</p>
<h2>Vraag en antwoord</h2>
<p>Omdat de mensen bij Orca's mailinglijst nooit te beroerd zijn om te helpen,
stelde ik de vraag aan de specialisten. Een goede nachtrust later probeerde ik
opnieuw om de bestaande Orca's te verwijderen en met een schone lei te beginnen.
Ik zocht met het volgende commando naar alles wat met Orca te maken had:</p>
<pre><code class="bash">find / -iname "*orca*" 2>/dev/null;
</code></pre>
<p>Opvallend was de map <code>~/.config/orca</code>. Daarin staan instellingen die bij het
volledig verwijderen (<code>sudo apt purge gnome-orca</code>) toch nog behouden blijven.
Toen ik deze map verwijderde en Orca opnieuw installeerde met
<code>sudo apt install gnome-orca</code>, hoefde ik nog enkel mijn instellingen terug in te
laden:</p>
<pre><code class="bash">dconf load /org/gnome/orca/ < mijn-backup.ini;
</code></pre>
<h2>Finale</h2>
<p>Een druk op <kbd>Super</kbd> + <kbd>Alt</kbd> + <kbd>S</kbd> later begroette
Orca mij met het gewende spreektempo. De sneltoetsen en de profielen waren er
weer. Ik had eindelijk 'mijn' schermlezer terug!</p>
<p>Natuurlijk kon ik het niet laten, en moest de mensen van de mailinglijst
<a href="https://mail.gnome.org/archives/orca-list/2017-August/msg00050.html">op de hoogte brengen</a>. Ik zal niet de eerste zijn die dit overkomt, maar
misschien wel de laatste.</p>
</div>2017-08-09T20:08:14+02:00https://www.fwiep.nl/blog/liturgische-lezingen-in-phpLiturgische lezingen in PHP2017-08-08T15:35:26+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Dit is deel 2 van een tweedelig artikel over een online feestdagkalender met
archief van bijbehorende bijbellezingen. Lees ook <a href="https://www.fwiep.nl/blog/kerkelijk-jaar-in-php">het eerste deel</a>.</p>
<h2>Liturgisch jaar</h2>
<p>De katholieke kerk maakt gebruik van een driejarige cyclus van lezingen. Hierin
wordt elke drie jaar dezelfde lezing voor een zelfde zondag gelezen. Men spreekt
van het <em>Liturgisch jaar A, B of C</em>. Om te weten welke lezing voor een bepaalde
dag geldt, moet eerst worden bepaald in welk Liturgisch jaar de datum valt.</p>
<p>De functie <code>_getLiturgicYear()</code> verwacht als argument een <code>DateTime</code>-object; een
datum waarvoor wordt bepaald in welk liturgisch jaar hij ligt. Op basis van de
datum van Kerstmis (25-12) in dat jaar, wordt met <code>_getFirstAdvent()</code> de datum
van de 1e Advent bepaald. Daarna wordt het jaartal eerst door 100, dan door 3
gedeeld. De rest van deze berekeningen bepaalt in relatie tot de 1e Advent in
welk jaar de datum valt.</p>
<pre><code class="php">class LiturgischJaar
{
const JAAR_A = 'A';
const JAAR_B = 'B';
const JAAR_C = 'C';
}
private static function _getFirstAdvent(\DateTime $christmas)
{
$dayChristmas = (int)$christmas->format('w');
if ($dayChristmas == 0) {
$dayChristmas = 28;
} else {
$dayChristmas = 21 + $dayChristmas;
}
$diAdvent1 = new \DateInterval('P'.$dayChristmas.'D');
return self::wrap($christmas)->sub($diAdvent1);
}
private static function _getLiturgicYear(\DateTime $dt)
{
$christmas = new \DateTime($dt->format('Y').'-12-25');
$firstAdvent = self::_getFirstAdvent($christmas);
$year = (int)$dt->format('Y');
$year = ($year % 100);
$x = ($year % 3);
switch($x){
case 0:
return ($dt >= $firstAdvent) ? self::JAAR_C : self::JAAR_B;
case 1:
return ($dt >= $firstAdvent) ? self::JAAR_A : self::JAAR_C;
case 2:
return ($dt >= $firstAdvent) ? self::JAAR_B : self::JAAR_A;
}
}
</code></pre>
<h2>Bron</h2>
<p>In het verleden bood de <a href="https://rkbijbel.nl/kbs/">Katholieke Bijbelstichting</a> een online archief aan
met liturgische lezingen van alle zon- en feestdagen. Er was echter geen hapklare
manier om de bezoekers van 'mijn' site van <a href="https://www.rk-ubachoverworms.nl/lezingen/">Parochiecluster U.o.W.</a> hier naar
door te verwijzen. Dus heb ik het archief geautomatiseerd toegankelijk gemaakt.
Onderstaande code leest en verwerkt de (gecachte) originele XML-bestanden uit
die tijd.</p>
<h2>Code</h2>
<p>De code werkt in grote lijnen als volgt:</p>
<ul>
<li>Als eerste wordt bepaald in welk liturgisch jaar de datum valt, en welke
(feest-) dag het is.</li>
<li>Bestaat er al een XML-bestand van deze specifieke datum? Dan is dat
onze oplossing.</li>
<li>Is de ddatum geen bekende feestdag (<code>DAG_ONBEKEND</code>)? Dan hoeven we ook niet te
zoeken in de cache-bestanden.</li>
<li>Is de datum 24 of 25 december? Kies dan voor de vaststaande XML-bestanden van
het hoogfeest van Kerstmis.</li>
<li>Verzamel alle cachebestanden en bepaal voor elk het liturgisch jaar en de
feestdag. Komen beide overeen met de gezochte datum? Dan is dat onze oplossing.</li>
</ul>
<pre><code class="php"><?php
$l = new LiturgischJaar($date);
$d = (int)$date->format('d');
$m = (int)$date->format('m');
$xmlfile = sprintf('cache/bijbelnet_%1$s.xml', $date->format('Y-m-d'));
if (file_exists($xmlfile)) {
$this->sourceURL = $xmlfile;
return;
}
if ($this->dag == LiturgischJaar::DAG_ONBEKEND) {
$this->sourceURL = null;
return;
}
if ($m == 12 && $d == 24) {
$this->sourceURL = 'cache/bijbelnet_0000-12-24.xml';
return;
}
if ($m == 12 && $d == 25) {
$this->sourceURL = 'cache/bijbelnet_0000-12-25.xml';
return;
}
$cachefiles = array();
if ($dir = opendir('cache')) {
while (false !== ($f = readdir($dir))) {
if (!is_dir($f)
&& preg_match(
'/^bijbelnet_[12][0-9]{3}(?:-[0-9]{2}){2}.xml$/i',
$f
) > 0
) {
$cachefiles[] = $f;
}
}
closedir($dir);
asort($cachefiles);
}
foreach ($cachefiles as $cf) {
$matches = array();
preg_match(
'/^bijbelnet_([12][0-9]{3}(?:-[0-9]{2}){2}).xml$/i',
$cf,
$matches
);
$dt = new \DateTime($matches[1]);
$lj = new LiturgischJaar($dt);
if ($lj->jaarABC == $this->jaarABC && $lj->welkeDag == $this->dag) {
$this->sourceURL = 'cache/'.$cf;
return;
}
}
</code></pre>
<h2>Caching</h2>
<p>Hoewel ik het nooit objectief heb gemeten, was ik er van overtuigd dat het
telkens weer opnieuw analyseren van <em>alle</em> cachebestanden enorm veel tijd zou
kosten. Daarom bedacht ik een caching-oplossing waarbij ik in één
geautomatiseerde ronde voor alle cachebestanden het liturgisch jaar en de
feestdag bepaalde, en dat resultaat als herbruikbaar bestand bewaarde.</p>
<pre><code class="php"><?php
private static $_datematchesfile = 'cache/datematches.cache';
private $_datematches = array();
public function __construct(\DateTime $date)
{
// Bestaat het cache-bestand? Lees het dan in.
if (file_exists(self::$_datematchesfile)) {
$this->_datematches = unserialize(
base64_decode(file_get_contents(self::$_datematchesfile))
);
} else {
// Zo niet, maak het dan aan.
$this->_writeCache();
}
// Komt de gezochte datum al voor in het cache-geheugen?
if (array_key_exists($date->format('Y-m-d'), $this->_datematches)) {
$this->sourceURL
= 'cache/bijbelnet_'.
$this->_datematches[$date->format('Y-m-d')].
'.xml';
return;
}
// ...
if ($lj->jaarABC == $this->jaarABC && $lj->welkeDag == $this->dag)
{
// Voeg, tijdens het doorlopen van alle XML-bestanden, alle
// bepaalde feestdagen toe aan het cache-geheugen.
$this->_datematches[$date->format('Y-m-d')] = $matches[1];
$this->_writeCache();
$this->sourceURL = 'cache/'.$cf;
return;
}
}
private function _writeCache()
{
file_put_contents(
self::$_datematchesfile,
base64_encode(serialize($this->_datematches))
);
}
</code></pre>
<h2>Conclusie</h2>
<p>Met de code uit deze tweedelige artikelreeks heeft de parochiesite een
bijna eeuwigdurende kalender voor de zondagse bijbellezingen. Het was voor mij
een uitdagend project waarin ik, samen met pastoor Ed Smeets, de al dan niet
geschreven regels van het kerkelijk en liturgisch jaar in code heb gegoten.</p>
<p>Toen we er twee weken geleden achter kwamen, dat één bepaalde feestdag ontbrak,
moest ik op zoek naar hoe het ook weer allemaal werkte. Dan loont het om een of
twee artikeltjes te schrijven. Al is het maar als eigen documentatie voor later!</p>
<p>N.B. Als er behoefte is aan een beschrijving van het low-level dirty-work met de
genoemde XML-bestanden, kan ik daar een nieuw artikel aan wijden.
Laat het me weten, bijvoorbeeld via <a href="https://www.fwiep.nl/contact">Contact</a>!</p>
</div>2017-08-08T15:35:26+02:00https://www.fwiep.nl/blog/kerkelijk-jaar-in-phpKerkelijk jaar in PHP2017-08-07T09:31:23+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Dit is deel 1 van een tweedelig artikel over een online feestdagkalender met
archief van bijbehorende bijbellezingen. Lees hier <a href="https://www.fwiep.nl/blog/liturgische-lezingen-in-php">het tweede deel</a>.</p>
<p>Een van mijn langstlopende programmeerprojecten is de website van het
<a href="https://www.rk-ubachoverworms.nl/">Parochiecluster U.o.W.</a>. Het is niet alleen een homepage, maar ook een
online archief van parochieblaadjes, misroosters en lezingen uit het verleden.
Met name de laatstgenoemden zijn een eigen artikeltje in dit weblog waard.</p>
<h2>Kerkelijk jaar</h2>
<p>Een kalenderjaar begint op 1 januari en eindigt op 31 december. Het kerkelijk
jaar begint bij de zondag van de 1e Advent en eindigt op de zondag van Christus
Koning. Deze twee zondagen zijn afgeleid van de dag van Kerstmis (25 december)
en Paaszondag, waarbij laatstgenoemde weer afhangt van de maanstand en het begin
van de lente. Een kerkelijk jaar is als volgt opgebouwd:</p>
<ul>
<li>Advent</li>
<li>Kerst</li>
<li>Tijd door het jaar</li>
<li>Veertigdagentijd</li>
<li>Pasen</li>
<li>Tijd door het jaar</li>
</ul>
<p>Hier in hebben een aantal feesten een vaste datum (Kerstmis, Nieuwjaar,
Allerheiligen, Allerzielen). Anderen zijn al dan niet afgeleid van deze data en
vallen altijd op een zondag (Adventzondagen, Pasen, Pinksteren).</p>
<h2>Pasen als basis</h2>
<p>De datum van Pasen wordt in het kerkelijk jaar als basis gebruikt voor de andere
feestdagen en zondagen. PHP heeft twee functies aan boord om de paasdatum voor
een opgegeven jaar te berekenen. <a href="https://php.net/manual/en/function.easter-date.php"><code>easter_date()</code></a> werkt enkel binnen het
bereik van de Unix-timestamp (1970-2038). <a href="https://php.net/manual/en/function.easter-days.php"><code>easter_days()</code></a> beslaat
daarentegen een groter gebied (1538-4099). Om met <code>DateTime</code>s te kunnen werken
in plaats van Unix-timestamps, heb ik de volgende functie geschreven:</p>
<pre><code class="php">private static function _getEasterSunday($year)
{
if (!is_int($year) || $year < 1583 || $year > 4099) {
throw new \InvalidArgumentException();
}
$dtEaster = new \DateTime($year.'-03-21');
$dI = new \DateInterval('P'.easter_days($year, CAL_EASTER_ROMAN).'D');
$dtEaster->add($dI);
return $dtEaster;
}
</code></pre>
<p>Ook de datum van de 1e Adventszondag wordt als referentie gebruikt. Deze wordt
op basis van de dag van Kerstmis bepaald.</p>
<pre><code class="php">private static function _getFirstAdvent(\DateTime $christmas)
{
$dayChristmas = (int)$christmas->format('w');
if ($dayChristmas == 0) {
$dayChristmas = 28;
} else {
$dayChristmas = 21 + $dayChristmas;
}
$diAdvent1 = new \DateInterval('P'.$dayChristmas.'D');
return self::wrap($christmas)->sub($diAdvent1);
}
</code></pre>
<h2>Hulpfuncties</h2>
<p>De methodes <code>sub()</code> en <code>add()</code> van <code>DateTime</code>-objecten voeren een bewerking uit
op het gegeven object. Hiermee veranderen ze de waarde van het object zelf. Dit
is voordeel en nadeel tegelijk; je kunt bewerkingen direct achter elkaar noteren:</p>
<pre><code class="php">$dt = new \DateTime();
// $dt heeft de waarde van NU
$dt->add(new \DateInterval('P1D'))->sub(new \DateInterval('P2D'));
// $dt heeft de waarde van GISTEREN
</code></pre>
<p>Als je, zoals in dit script, telkens weer van één datum moet afleiden, is dit
directe bewerken niet gewenst. Het volgende zou immers kunnen gebeuren:</p>
<pre><code class="php">$dtPasen = new \DateTime('2017-04-16');
$dtGoedeVrijdag = $dtPasen->sub(new \DateInterval('P2D'));
// $dtPasen heeft nu dezelfde datum als Goede Vrijdag!
$dtTweedePaasdag = $dtPasen->add(new \DateInterval('P1D'));
// $tweedePaasdag valt op deze manier op zaterdag!
</code></pre>
<p>Om dit gedrag te omzeilen heb ik onderstaande functie geschreven; een wrapper
die voor elk <code>DateTime</code>-object een nieuw object teruggeeft met dezelfde waarde:</p>
<pre><code class="php">/**
* Creates a new DateTime instance from the given instance.
*
* @param DateTime $datetime the DateTime to wrap
*
* @return DateTime
*/
public static function wrap(\DateTime $datetime)
{
return new \DateTime($datetime->format(\DateTime::ISO8601));
}
</code></pre>
<p>Tot slot nog een klein hulpje, om gemakkelijk op basis van een getal, een
<code>DateInterval</code>-object aan te maken, dat met <code>DateTime::sub()</code> en <code>DateTime::add()</code>
kan worden gebruikt:</p>
<pre><code class="php">/**
* Creates a DateInterval object of the given amount of days
*
* @param int $dagen the amount of days
*
* @return \DateInterval
*/
private static function _di($dagen)
{
if (!is_int($dagen) || $dagen < 0) {
return new \DateInterval('P0D');
} else {
return new \DateInterval('P'.$dagen.'D');
}
}
</code></pre>
<p>Soms moet je uit meerdere <code>DateTime</code>s kiezen welke het dichtst bij een bepaalde
referentiedatum ligt. Dat kan met onderstaande functie:</p>
<pre><code class="php">/**
* Finds the chronologically closest DateTime
*
* @param DateTime[] $a the array to search
* @param DateTime $refdate the reference date
* @param bool $includePast include dates in the past
* @param bool $includeFuture include dates in the future
*
* @return DateTime
*/
private static function _getClosest(
$a, $refdate, $includePast = true, $includeFuture = true
) {
$smallestDiff = 0;
$smallestKey = null;
foreach ($a as $k => $v) {
$diff = $refdate->diff($v, false);
$diff = (int)$diff->format('%r%a');
if ((($includePast && $diff <= 0) || ($includeFuture && $diff >= 0))
&& (($includePast && (is_null($smallestKey)
|| $diff > $smallestDiff))
|| ($includeFuture && (is_null($smallestKey)
|| $diff < $smallestDiff)))
) {
$smallestDiff = $diff;
$smallestKey = $k;
}
}
return is_null($smallestKey) ? false : $a[$smallestKey];
}
</code></pre>
<h2>Welke feestdag?</h2>
<p>Dan volgt nu de kern van het artikel: de bepaling op welke feestdag een
opgegeven datum valt. Dit proces bestaat uit de volgende stappen:</p>
<ul>
<li>data van Pasen, Kerstmis en 1e Advent bepalen<br />
(voor zowel het vorige, huidige als volgende jaar)</li>
<li>referentiedatum bepalen</li>
<li>afgeleide feestdagen bepalen</li>
<li>controleren of de gezochte datum één van de gevonden data is<br />
(zo ja, hebben we ons antwoord!)</li>
<li>feestdagen bepalen die voorrang hebben op de reguliere zondag</li>
<li>controleren of de gezochte datum één van de gevonden data is<br />
(zo ja, hebben we ons antwoord!)</li>
<li>lopen door de reguliere zondagen, van 2 tot Aswoensdag, van 33 tot H.Hart</li>
<li>controleren of de gezochte datum één van de gevonden data is<br />
(zo ja, hebben we ons antwoord!)</li>
<li>feestdagen bepalen die geen voorrang hebben op de reguliere zondag</li>
<li>controleren of de gezochte datum één van de gevonden data is<br />
(zo ja, hebben we ons antwoord!)</li>
<li>de feestdag is onbekend</li>
</ul>
<pre><code class="php">private function _determineHoliday()
{
$y = (int)$this->_dt->format('Y');
$yPrev = $y - 1;
$yNext = $y + 1;
$diJaar = new \DateInterval('P1Y');
/* Datum van Pasen bepalen */
$pasenPrev = self::_getEasterSunday($yPrev);
$pasenNow = self::_getEasterSunday($y);
$pasenNext = self::_getEasterSunday($yNext);
/* Datum van Kerst bepalen */
$kerstPrev = new \DateTime($yPrev.'-12-25');
$kerstNow = new \DateTime($y.'-12-25');
$kerstNext = new \DateTime($yNext.'-12-25');
/* Datum van de 1e Advent bepalen */
$adventPrev = self::_getFirstAdvent($kerstPrev);
$adventNow = self::_getFirstAdvent($kerstNow);
$adventNext = self::_getFirstAdvent($kerstNext);
/* Juiste referentie-data bepalen */
$eersteAdvent = self::_getClosest(
array($adventPrev, $adventNow, $adventNext),
$this->_dt,
true,
false
);
$kerst = new \DateTime($eersteAdvent->format('Y').'-12-25');
$pasen = self::_getClosest(
array($pasenPrev, $pasenNow, $pasenNext),
$eersteAdvent,
false,
true
);
$volgendJaarAdvent = self::_getFirstAdvent(
self::wrap($kerst)->add($diJaar)
);
$yAdvent = (int)$eersteAdvent->format('Y');
$yPasen = (int)$pasen->format('Y');
/* Alle bekende feestdagen berekenen op basis van Pasen en Kerstmis */
$tweedeAdvent = self::wrap($eersteAdvent)->add(self::_di(7));
$derdeAdvent = self::wrap($tweedeAdvent)->add(self::_di(7));
$vierdeAdvent = self::wrap($derdeAdvent)->add(self::_di(7));
$vooravondKerst = self::wrap($kerst)->sub(self::_di(1));
$tweedeKerst = self::wrap($kerst)->add(self::_di(1));
if ($kerst->format('w') == 0) {
$heiligefamilie = new \DateTime($yAdvent.'-12-30');
} else {
$heiligefamilie = self::wrap($kerst)
->add(self::_di(7-(int)$kerst->format('w')));
}
$nieuwjaar = self::wrap($kerst)->add(self::_di(7));
$dagNieuwjaar = (7 - (int)$nieuwjaar->format('w'));
$diOpenbaring = new \DateInterval('P'.$dagNieuwjaar.'D');
$openbaring = self::wrap($nieuwjaar)->add($diOpenbaring);
$doopRefDate = new \DateTime($yPasen.'-01-07');
while ($doopRefDate->format('w') != 0) {
$doopRefDate->add(self::_di(1));
}
if ($doopRefDate == $openbaring) {
$doopvanheer = self::wrap($openbaring)->add(self::_di(1));
} else {
$doopvanheer = $doopRefDate;
}
$aswoensdag = self::wrap($pasen)->sub(self::_di(46));
$vasten1 = self::wrap($pasen)->sub(self::_di(42));
$vasten2 = self::wrap($pasen)->sub(self::_di(35));
$vasten3 = self::wrap($pasen)->sub(self::_di(28));
$vasten4 = self::wrap($pasen)->sub(self::_di(21));
$vasten5 = self::wrap($pasen)->sub(self::_di(14));
$palmzondag = self::wrap($pasen)->sub(self::_di(7));
$wittedonderdag = self::wrap($pasen)->sub(self::_di(3));
$goedevrijdag = self::wrap($pasen)->sub(self::_di(2));
$tweedepaas = self::wrap($pasen)->add(self::_di(1));
$paaszondag2 = self::wrap($pasen)->add(self::_di(7));
$paaszondag3 = self::wrap($pasen)->add(self::_di(14));
$paaszondag4 = self::wrap($pasen)->add(self::_di(21));
$paaszondag5 = self::wrap($pasen)->add(self::_di(28));
$paaszondag6 = self::wrap($pasen)->add(self::_di(35));
$hemelvaart = self::wrap($pasen)->add(self::_di(39));
$paaszondag7 = self::wrap($pasen)->add(self::_di(42));
$pinksteren = self::wrap($pasen)->add(self::_di(49));
$tweedePinkst = self::wrap($pasen)->add(self::_di(50));
$drieeenheid = self::wrap($pasen)->add(self::_di(56));
$sacramentsdag = self::wrap($pasen)->add(self::_di(63));
$hhart = self::wrap($pasen)->add(self::_di(68));
$christuskoning = self::wrap($volgendJaarAdvent)->sub(self::_di(7));
$arr = array(
self::DAG_ADVENT1 => $eersteAdvent,
self::DAG_ADVENT2 => $tweedeAdvent,
self::DAG_ADVENT3 => $derdeAdvent,
self::DAG_ADVENT4 => $vierdeAdvent,
self::DAG_VOORAVOND_KERST => $vooravondKerst,
self::DAG_KERSTMIS => $kerst,
self::DAG_HEILIGE_FAMILIE => $heiligefamilie,
self::DAG_TWEEDEKERSTDAG => $tweedeKerst,
self::DAG_NIEUWJAAR => $nieuwjaar,
self::DAG_OPENBARING => $openbaring,
self::DAG_DOOPVANDEHEER => $doopvanheer,
self::DAG_ASWOENSDAG => $aswoensdag,
self::DAG_VASTEN1 => $vasten1,
self::DAG_VASTEN2 => $vasten2,
self::DAG_VASTEN3 => $vasten3,
self::DAG_VASTEN4 => $vasten4,
self::DAG_VASTEN5 => $vasten5,
self::DAG_PALMZONDAG => $palmzondag,
self::DAG_WITTEDONDERDAG => $wittedonderdag,
self::DAG_GOEDEVRIJDAG => $goedevrijdag,
self::DAG_PASEN => $pasen,
self::DAG_TWEEDEPAASDAG => $tweedepaas,
self::DAG_PASEN2 => $paaszondag2,
self::DAG_PASEN3 => $paaszondag3,
self::DAG_PASEN4 => $paaszondag4,
self::DAG_PASEN5 => $paaszondag5,
self::DAG_PASEN6 => $paaszondag6,
self::DAG_HEMELVAART => $hemelvaart,
self::DAG_PASEN7 => $paaszondag7,
self::DAG_PINKSTEREN => $pinksteren,
self::DAG_TWEEDEPINKSTERDAG => $tweedePinkst,
self::DAG_HDRIEEENHEID => $drieeenheid,
self::DAG_SACRAMENTSDAG => $sacramentsdag,
self::DAG_HHART => $hhart,
self::DAG_CHRISTUS_KONING => $christuskoning
);
foreach ($arr as $k => $v) {
if ($this->_dt == $v) {
$this->welkeDag = $k;
return true;
}
}
/* Feestdagen die voorrang hebben op de zondagen */
$opdrachtheer = new \DateTime($yPasen.'-02-02');
$geboortejan = new \DateTime($yPasen.'-06-24');
$petruspaulus = new \DateTime($yPasen.'-06-29');
$gedaanteverandering = new \DateTime($yPasen.'-08-06');
$mariatenhemel = new \DateTime($yPasen.'-08-15');
$kruisverheffing = new \DateTime($yPasen.'-09-14');
$allerheiligen = new \DateTime($yPasen.'-11-01');
$allerzielen = new \DateTime($yPasen.'-11-02');
$willibrord = new \DateTime($yPasen.'-11-07');
$stjanlateranen = new \DateTime($yPasen.'-11-09');
$arr = array(
self::DAG_OPDRACHTHEER => $opdrachtheer,
self::DAG_GEBOORTEJAN => $geboortejan,
self::DAG_PETRUSPAULUS => $petruspaulus,
self::DAG_GEDAANTEVERANDERING => $gedaanteverandering,
self::DAG_MARIATENHEMEL => $mariatenhemel,
self::DAG_KRUISVERHEFFING => $kruisverheffing,
self::DAG_ALLERHEILIGEN => $allerheiligen,
self::DAG_ALLERZIELEN => $allerzielen,
self::DAG_HWILLIBRORD => $willibrord,
self::DAG_STJANLATERANEN => $stjanlateranen
);
foreach ($arr as $k => $v) {
if ($this->_dt == $v) {
$this->welkeDag = $k;
return true;
}
}
/* Zondagen door het jaar */
for ($i = 2; $i <= 33; $i++) {
$loopZondag = self::wrap($doopvanheer)->add(self::_di(($i-1)*7));
$loopZondag->sub(self::_di((int)$doopvanheer->format('w')));
if ($loopZondag > $aswoensdag) {
break;
}
$zondagConst = constant(sprintf('self::DAG_ZONDAG_DHJ_%02d', $i));
$this->_s($zondagConst, $loopZondag);
if ($this->_dt == $loopZondag) {
$this->welkeDag = $zondagConst;
return true;
}
}
for ($i = 33; $i >= 2; $i--) {
$loopZondag = self::wrap($christuskoning)
->sub(self::_di((34-$i)*7));
if ($loopZondag < $hhart) {
break;
}
$zondagConst = constant(sprintf('self::DAG_ZONDAG_DHJ_%02d', $i));
$this->_s($zondagConst, $loopZondag);
if ($this->_dt == $loopZondag) {
$this->welkeDag = $zondagConst;
return true;
}
}
/* Feestdagen die GEEN voorrang hebben op de zondagen */
$hjozef = new \DateTime($yPasen.'-03-19');
$aankondiging = new \DateTime($yPasen.'-03-25');
if ($aankondiging >= $palmzondag && $aankondiging <= $goedevrijdag) {
$aankondiging = new \DateTime($yPasen.'-04-08');
}
$onbevlekt = new \DateTime($yPasen.'-12-08');
$arr = array(
self::DAG_HJOZEF => $hjozef,
self::DAG_AANKONDIGING => $aankondiging,
self::DAG_ONBEVLEKTONTVANG => $onbevlekt
);
foreach ($arr as $k => $v) {
if ($this->_dt == $v) {
$this->welkeDag = $k;
return true;
}
}
/* Weekdag of onbekend */
$this->welkeDag = self::DAG_ONBEKEND;
return false;
}
</code></pre>
<h2>Wordt vervolgd</h2>
<p>Dit is deel 1 van een tweedelig artikel over een online feestdagkalender met
archief van bijbehorende bijbellezingen. Lees hier <a href="https://www.fwiep.nl/blog/liturgische-lezingen-in-php">het tweede deel</a>.</p>
</div>2017-08-07T09:31:23+02:00https://www.fwiep.nl/blog/nostalgie-in-php-en-mysqlNostalgie in PHP en MySQL2017-06-17T16:40:46+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Laatst kwam ik tussen oude back-ups een SQL-bestand tegen van een zelfgebouwd
forum uit 2010. Ook de code was bewaard gebleven en stond zelfs in één van mijn
lokale Git-repositories. Hoe leuk zou het zijn om al die oude berichten nog eens
te kunnen lezen? Om van de hilarische code-kwaliteit nog maar te zwijgen…</p>
<h2>Setup</h2>
<p>Het inlezen van de database was redelijk eenvoudig: een <code>source setup.sql</code> in de
MySQL-console en het was geregeld. Het op gang helpen van de PHP-code zou
behoorlijk wat meer tijd in beslag nemen. In eerste instantie verscheen er een
blanco pagina bij het oproepen van <code>index.php</code> - een slecht teken.</p>
<h2>Diagnose</h2>
<p>Aldus ging ik met <code>echo</code> en <code>var_dump</code> op zoek naar de plek waar PHP de bocht
uit vloog. Ik kwam terecht bij een aanroep van <code>mysql_query()</code>, een veelgebruikte
functie om een aanvraag op de database uit te voeren. Maar, ik heb toch een
MySQL-server draaien, en de juiste gegevens in de configuratie ingesteld?</p>
<p>Jawel, maar sinds PHP 5.5 zijn de oude <code>mysql_</code>-functies afgeschreven en
vervangen door de modernere, snellere en veiligere <code>mysqli_</code>-varianten. Als ik
dit forum op mijn huidige (PHP7) machine wilde draaien, moest ik een alternatief
zoeken.</p>
<h2>Aanpak</h2>
<p>Door de gehele broncode lagen honderden aanroepen verspreid - ik had geen zin om
alle code door te spitten en overal aanpassingen te maken. Als vanzelf kwam de
gedachte om wrapper-functies te schrijven. Als PHP de functies niet meer aan
boord heeft, dan kan ik ze toch zelf opnieuw definiëren? En dan achter de
schermen de nieuwe functies aanroepen.</p>
<p>Maar hoe vind ik alle verschillende functies die ik moet 'vervangen'? <code>grep</code> to
the rescue! Met onderstaande one-liner vond ik alle aan MySQL geliëerde commando's
in de broncode:</p>
<pre><code class="bash">$ grep -rihIPo 'mysql_[^(]+' * | sort | uniq
mysql_affected_rows
mysql_errno
mysql_fetch_array
mysql_num_rows
mysql_pconnect
mysql_query
mysql_real_escape_string
mysql_result
mysql_select_db
mysql_set_charset
</code></pre>
<p><code>grep</code> zoekt in tekstbestanden naar tekenreeksen of patronen. In dit geval
recursief in alle mappen (<code>-r</code>) en bestanden (<code>*</code>) met Perl-compatibele regular
expressies (<code>-P</code>). Verder wordt geen verschil gemaakt tussen hoofd- en kleine
letters (<code>-i</code>), worden binaire bestanden genegeerd (<code>-I</code>) en geen bestandsnamen
in de output opgenomen (<code>-h</code>). Met <code>-o</code> geeft <code>grep</code> alléén het gematchte stukje
terug.</p>
<p>Dan volgt de magie van een eenvoudige reguliere expressie. Hier wordt gezocht
naar de tekst <code>mysql_</code>, gevolgd door alles wat geen openende ronde haak is
(<code>[^(]+</code>). De uitvoer wordt gesorteerd en gefilterd op duplicaten. Uiteindelijk
bleven de getoonde funcies over, waarvoor ik een wrapper moest schrijven.</p>
<h2>Wrappers</h2>
<p>Gelukkig gebruikte ik in die tijd wel al het systeem van één centrale <code>include</code>.
Daarin werd de databaseverbinding gelegd en alle globale functies gedefiniëerd.
Ik maakte de volgende aanpassing:</p>
<pre><code class="php"><?php
$db = mysqli_connect(
$config['host'], $config['user'], $config['password'], $config['database']
);
mysqli_set_charset($db, 'utf8');
function mysql_affected_rows(){
global $db;
return mysqli_affected_rows($db);
}
function mysql_errno(){
global $db;
return mysqli_errno($db);
}
function mysql_fetch_array($q){
return mysqli_fetch_array($q);
}
function mysql_num_rows($q){
return mysqli_num_rows($q);
}
function mysql_query($q){
global $db;
return mysqli_query($db, $q);
}
function mysql_real_escape_string($q){
global $db;
return mysqli_real_escape_string($db, $q);
}
function mysql_result($q, $o){
mysqli_data_seek($q, $o);
$row = $q->fetch_row();
return $row[0];
}
</code></pre>
</div>2017-06-17T16:40:46+02:00https://www.fwiep.nl/blog/wachtwoordexport-met-kpcli-en-expectWachtwoordexport met kpcli en Expect2017-04-28T14:52:31+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Zoals ik in een <a href="https://www.fwiep.nl/blog/wachtwoordbeheer-met-kpcli-keepass">eerdere blogpost</a> beschreef, maak ik gebruik van <a href="http://kpcli.sourceforge.net/"><code>kpcli</code></a>,
een commandline applicatie die databases met KeePass-wachtwoorden kan beheren.
De tot nu toe ontbrekende functie binnen <code>kpcli</code> was een mogelijkheid om een
lijst van wachtwoorden te exporteren. Hiermee zou je dan bijvoorbeeld een
papieren back-up kunnen maken.</p>
<p>Ja, er bestaat minimaal één KeePass2-compatibel programma voor GNU/Linux dat
deze mogelijkheden wel biedt, maar het is jammer genoeg (nog)
<a href="http://www.mono-project.com/archived/accessibility/">niet toegankelijk</a> voor <a href="https://www.fwiep.nl/blog/werken-met-orca">mijn schermlezer</a>. Ik zocht verder en vond geen
alternatief dat zowel toegankelijk was, als kon exporteren onder GNU/Linux.</p>
<h2>Automatisering</h2>
<p>Toen bedacht ik, dat <code>kpcli</code> op zich alle informatie op het scherm toont, als je
het juiste commando geeft:</p>
<pre><code class="plain">kpcli:/> show /Root/Path/To/Entry
Path: /Root/Path/To
Title: Entry
Uname: myuser
Pass: My$upeR$EcReTp@s$W0Rd
URL: https://example.com/login.php
Notes:
kpcli:/>
</code></pre>
<p>Er moest toch een manier zijn om de volledige boomstructuur van de database te
doorlopen, en zo de gegevens op het scherm te toveren? Ik moest op de één of
andere manier een interactieve sessie met <code>kpcli</code> kunnen automatiseren. Dan
restte alleen nog het 'van het scherm plukken' en wegschrijven als bestand.</p>
<h2>Expect</h2>
<p>Met de juiste zoektermen kwam ik uiteindelijk terecht bij <a href="http://expect.sourceforge.net/">Expect</a>, een
uitbreiding van de <a href="http://www.tcl.tk/">TCL</a> programmeer­taal, die ik enkel kende uit
<a href="http://shop.oreilly.com/product/9780596528126.do">Mastering Regular Expressions</a> van Jeffrey Friedl. Het is een programma dat
speciaal bedoeld is, om volgens een script te kunnen reageren en anticiperen op
bestaande commandline programma's - precies wat ik zocht!</p>
<pre><code class="tcl">#!/usr/bin/expect -f
# Een simpel voorbeeld van een Expect-script, met dank aan Wikipedia
# Open an ftp session to a remote server, and wait for a username prompt.
spawn ftp "ftp.example.com"
expect "username:"
# Send the username, and then wait for a password prompt.
send "my_user_id\r"
expect "password:"
# Send the password, and then wait for an ftp prompt.
send "my_password\r"
expect "ftp>"
# Switch to binary mode, and then wait for an ftp prompt.
send "bin\r"
expect "ftp>"
# Turn off prompting.
send "prompt\r"
expect "ftp>"
# Get all the files
send "mget *\r"
expect "ftp>"
# Exit the ftp session, and wait for a special end-of-file character.
send "bye\r"
expect eof
</code></pre>
<p>Bovenstaand voorbeeld toont de meest gebruikte Expect-specifieke commando's. In
totaal had ik drie dagen nodig om me thuis te voelen in de syntax van TCL.
Daarbij was het principe van de export relatief eenvoudig:</p>
<ol>
<li>de hoofdmap van de wachtwoorddatabase uitlezen</li>
<li>voor elk item een <code>show</code>-commando uitvoeren</li>
<li>elke submap uitlezen en items daarin verwerken</li>
</ol>
<h2>Script</h2>
<p>Ik zal hieronder het script in verkorte versie beschrijven. De
<a href="https://www.fwiep.nl/download/1295939c-4a98-45c7-b5ab-f6cfbd3b5c6d/kpcli-kdbx-csv.exp">volledige versie</a> is als download beschikbaar.</p>
<pre><code class="tcl"># Functie die een tekenreeks voorbereid om te worden gebruikt in een CSV-bestand
proc escapeCSV { s } {
regsub -all {\"} $s "\"\"" s # verdubbel alle aanhalingstekens
regsub -all {\r\n\s+} $s "\r\n" s # verwijder alle witruimte aan regelbegin
set s [ string trim $s ] # verwijder alle witruimte aan begin/einde
return \"$s\" # zet aanhalingstekens voor en achter
}
# Laat één item zien en schrijf dit weg naar het export-bestand
proc doEntry { itemT e } {
global exportFile
set j [regsub {\s*[0-9]+\.\s*(.*?)(\s{3,}.*)?} $e {\1}]
send "show \"$itemT$j\"\r"
sleep 0.3
# Hier wordt het spannend; deze reguliere expressie plukt alle gegevens
# uit de output van 'show'. Let met name op de ANSI-escape sequence die
# kpcli rondom het wachtwoord gebruikt om rode tekst op rode achtergrond te
# tonen. Dit is daardoor wel te kopiëren, maar niet visueel te zien.
expect -re "Title: (.*?)\r\nUname: (.*?)\r\n Pass: \u001b\\\[31;41m(.*?)\u001b\\\[0m\r\n URL: (.*?)\r\nNotes: (.*)\r\n\r\n.*"
set fTitle [escapeCSV $expect_out(1,string)]
set fUname [escapeCSV $expect_out(2,string)]
set fPass [escapeCSV $expect_out(3,string)]
set fURL [escapeCSV $expect_out(4,string)]
set fNotes [escapeCSV $expect_out(5,string)]
set csv "$fTitle,$fUname,$fPass,$fURL,$fNotes"
puts $exportFile $csv # schrijf de regel weg naar bestandsbuffer
flush $exportFile # schrijf de bestandsbuffer naar het bestand
}
# Main processing
proc doTotal { item } {
if [regexp {/$} $item] then {
send "ls \"$item\"\r"
sleep 0.2
expect {
-re "=== Groups ===\r\n(.*)" {
set itemS [ split $expect_out(1,string) "\r\n" ]
foreach i $itemS {
doTotal "$item$i" # Heerlijk recursief
}
}
-re "=== Entries ===\r\n(.*)" {
set itemS [ split $expect_out(1,string) "\r\n" ]
foreach i $itemS {
doEntry $item $i
}
}
}
} else {
doEntry "" $item
}
return 0
}
# Check number of arguments
# ...
# Chech for file to process
# ...
# Open export-file for writing
set exportFile [open "~/keepass-export.csv" "w"]
puts $exportFile "Title,Uname,Pass,URL,Notes" # CSV-header
flush $exportFile
# Ask user for password to open file
# ...
# Spawn an instance of kpcli
spawn kpcli --kdb [lindex $::argv 0] # Open kpcli met .kdbx-bestand
expect "Please provide the master password: "
send "$pass\r"
sleep 1
expect 'kpcli:/> ' {
# Execute main call to 'ls'
send "ls /\r"
sleep 0.2
expect -re "=== (Groups|Entries) ===\r\n(.*)"
set body $expect_out(2,string)
set itemS [ split $body "\r\n" ]
foreach i $itemS {
doTotal "$i"
}
send "\r"
sleep 0.2
expect 'kpcli:/> '
send "quit\r"
expect eof
}
# Close export-file
close $exportFile
exit
</code></pre>
<h2>Conclusie</h2>
<p>Een programma of -uitbreiding begint vaak als project dat een bepaald probleem
of tekortkoming van een bestaande applicatie oplost. Dit script is precies zo:
Ik bouwde wat ik nodig had en nergens anders kon vinden. Het was voor mij een
uitdaging om in de vorm van TCL en Expect met een nieuwe programmeertaal kennis
te maken.</p>
<p>Natuurlijk was het even doorbijten, maar het was de moeite meer dan
waard. Ik kan nu met de spreekwoor­delijke druk op de knop onder GNU/Linux
een KeePass-export maken met enkel en alleen toegankelijke CLI-programma's.</p>
</div>2017-04-28T14:52:31+02:00https://www.fwiep.nl/blog/wachtwoordbeheer-met-kpcli-keepassWachtwoordbeheer met kpcli (KeePass)2017-04-22T15:39:50+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Tijdens het computergebruik van alledag komt een gebruiker continu in aanraking
met wachtwoorden. Een wachtwoord om de computer te bedienen, een wachtwoord voor
het ophalen van e-mails, een wachtwoord voor internetbankieren… Gelukkig
bieden de meeste programma's een mogelijkheid om het ingevoerde wachtwoord te
onthouden, en zo het telkens opnieuw invoeren te vermijden.</p>
<p>Een gebruiker zou er vanuit gemakzucht voor kunnen kiezen om bij al deze
programma's, accounts en systemen dan maar hetzelfde of een licht afwijkend
wachtwoord te gebruiken. Dit is echter funest voor de veiligheid - één gekraakt
wachtwood zou betekenen dat alle anderen ook gevaar lopen.</p>
<h2>Wachtwoordbeheer</h2>
<p>Als het dan zo verstandig is, om verschillende en liefst lange en complexe
wachtwoorden te gebruiken, hoe is dit dan nog gebruiksvriendelijk te realiseren?
Met een <em>wachtwoordbeheer applicatie</em>. Het is een programma dat alle
verschillende wachtwoorden onthoudt in een database, en deze na invoer van
slechts één wachtwoord toegankelijk maakt.</p>
<h2>KeePass</h2>
<p>Er zijn talloze programma's en diensten beschikbaar om een gebruiker dit werk
uit handen te nemen. Voor mij waren de volgende eigenschappen van bepalend
belang:</p>
<ul>
<li>beschikbaarheid voor GNU/Linux, Android, Windows</li>
<li>toegankelijkheid voor schermlezers</li>
</ul>
<p>Ik koos voor <a href="https://keepass.info/">KeePass</a>, een open source applicatie die inderdaad voor
meerdere platformen beschikbaar is. Ten tijde van mijn eerste stappen met
KeePass was versie 1.x actueel. Onder GNU/Linux kon ik gebruik maken van
<a href="https://www.keepassx.org/">KeePassX</a>, een grafisch programma dat de wachtwoorddatabase kan beheren en
voorziet in globale sneltoetsen om makkelijk te kunnen inloggen.</p>
<p>Toen kwam versie 2.x van het KeePass-formaat, en kon KeePassX daar niet (meer)
mee overweg. Ik moest op zoek naar een alternatief, maar wilde wel bij KeePass
blijven. Ik experimenteerde met KeePass2, de officiële opvolger van KeePass. De
GNU/Linux-versie bleek gebouwd te zijn met het <a href="https://www.mono-project.com/">Mono-framework</a>, dat van huis
uit onder GNU/Linux <a href="https://www.mono-project.com/archived/accessibility/">(nog) niet toegankelijk</a> is. Mijn schermlezer bleef stil
en ik zocht verder.</p>
<h2>KPCLI</h2>
<p>Uiteindelijk kwam ik terecht bij <a href="https://kpcli.sourceforge.net/"><code>kpcli</code></a>, een commandline programma dat via
de terminal mijn wachtwoorden kan beheren en naadloos samenwerkt met Orca,
<a href="https://www.fwiep.nl/blog/werken-met-orca">mijn schermlezer</a>. Het kan overweg met zowel KeePass-bestanden van versie
1.x als 2.x en biedt de mogelijkheid om naar het klembord te kopiëren. Dit
laatste was echter nog wel een uitdaging, toen ik niet lang geleden mijn
besturingssysteem opnieuw installeerde.</p>
<h2>XClip</h2>
<p>Met het commando <code>xp</code> kopiëert <code>kpcli</code> het aangegeven wachtwoord naar het
klembord. Welk klembord? Zijn er meerdere klemborden, dan? Onder GNU/Linux'
grafische omgeving <code>X11</code> wel. Het onderdeel van <code>kpcli</code> dat de daadwerkelijke
interactie met de klemborden afhandelt is <a href="https://github.com/astrand/xclip"><code>XClip</code></a>, een in Perl geschreven
programma voor werken met klemborden onder <code>X11</code>.</p>
<p>De wachtwoorden van <code>kpcli</code> bleken standaard naar het <code>primary</code> klembord van <code>X</code>
te worden gekopiëerd. Het gekopiëerde is daarna met behulp van een klik op de
middelste muisknop te plakken. Maar dat is niet wat ik gewend ben als
slechtziende computergebruiker. Ik ben volledig getraind op <kbd>Ctrl</kbd> +
<kbd>V</kbd> en <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd>.</p>
<h2>Open source</h2>
<p>Dankzij het feit dat de broncode van <code>XClip</code> gewoon is in te zien, ging ik in
<code>perl5/lib/perl5/Clipboard/XClip.pm</code> op zoek naar de plek waar het te gebruiken
klembord werd geselecteerd. En waarempel, ergens rond regel 33 vond ik
onderstaand commentaar met bijbehorende definitie:</p>
<pre><code class="perl"># This ordering isn't officially verified, but so far seems to work the best:
sub all_selections { qw(primary buffer clipboard secondary) }
sub favorite_selection { my $self = shift; ($self->all_selections)[0] }
# ...
</code></pre>
<p>Ik herkende de keuze van item <code>0</code> uit een array, lees het eerste item uit de
lijst - in dit geval <code>primary</code>. Ik veranderde het naar item <code>2</code>, het derde item,
lees <code>clipboard</code>:</p>
<pre><code class="perl"># This ordering isn't officially verified, but so far seems to work the best:
# Preferred clipboard set to 'clipboard' instead of 'primary'
sub all_selections { qw(primary buffer clipboard secondary) }
sub favorite_selection { my $self = shift; ($self->all_selections)[2] }
# ...
</code></pre>
<p>Vanaf dat moment verschenen de wachtwoorden, gebruikersnamen en URLs van <code>kpcli</code>
op het 'gewone' klembord van <code>X</code> en kon ik naar hartelust met <kbd>Ctrl</kbd> +
<kbd>V</kbd> en <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd> werken.</p>
</div>2017-04-22T15:39:50+02:00https://www.fwiep.nl/blog/synchronisatie-met-google-accountSynchronisatie met Google-account2017-04-05T21:44:13+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Voor mijn zelfgebouwde adresboekapplicatie had ik van begin af aan al plannen om
de gegevens te koppelen aan mijn Google-account. Daarmee kon ik dan de groepen
en contactpersonen centraal beheren, maar ook in mijn e-mailprogramma en op mijn
smartphone gebruiken. Omdat een échte synchronisatie op dat moment boven mijn
pet ging, besloot ik met een CSV-export en -<a href="https://support.google.com/mail/answer/1069522">import</a> te beginnen.</p>
<h2>Dat kan beter</h2>
<p>Ik ben niet de enige die gebruik maakt van de genoemde adresboekapplicatie. Maar
ik kon het mijn medegebruiker niet aandoen om elke keer de omstandelijke
CSV-procedure te moeten doorlopen. Aldus zocht ik een manier om deze actie
comfortabel te automatiseren.</p>
<p>Gelukkig zijn de API's van Google goed gedocumenteerd (zie <a href="https://developers.google.com/gdata/docs/2.0/elements">GData 2.0</a> en
<a href="https://developers.google.com/google-apps/contacts/v3/">Google Contacts 3.0</a>). De meest eenvoudige, maar ook rigoreuze manier van
synchroniseren is samen te vatten als: "Alles weg, alles nieuw". De API biedt,
naast de enkele bewerking ook een reeks-verwerking, oftewel <em>Batch processing</em>
aan.</p>
<h2>Volledig</h2>
<p>In onderstaande voorbeelden is <code>A</code> de collectie die wordt uitgelezen en <code>B</code> de
collectie die wordt aangepast. De volledige synchronisatie werkt als volgt:</p>
<ol>
<li>Loop door <code>B</code>
<ul>
<li>verwijder item</li>
</ul></li>
<li>Loop door <code>A</code>
<ul>
<li>voeg item toe</li>
</ul></li>
</ol>
<p>Deze procedure heeft als voordeel dat de gegevens in <code>B</code> na afloop gegarandeerd
gelijk zijn aan die in <code>A</code>. Het nadeel is, dat er onnodig veel communicatie
plaatsvindt; zowel tussen applicatie en Google, als e-mailprogramma en Google,
en ook tussen smartphone en Google.</p>
<p>Een rondje met 1200 items die op deze manier worden verwijderd en opnieuw
toegevoegd, duurde gemiddeld meer dan een minuut. Op een zware dag voor de
server duurde het ooit zelfs meer dan twee minuten, waardoor de
<a href="http://php.net/manual/en/function.set-time-limit.php">systeem-time-out</a> van 120 seconden werd aangetikt. Dit moest beter!</p>
<h2>Update</h2>
<p>Aldus trok ik dan toch maar de stoute schoenen aan en bedacht het volgende
stappenplan:</p>
<ol>
<li>Loop door <code>B</code>
<ul>
<li>Item bestaat in <code>A</code>: update item in <code>B</code></li>
<li>Item ontbreekt in <code>A</code>: verwijder item in <code>B</code></li>
<li>onthoud verwerkt item (<code>A</code>)</li>
</ul></li>
<li>Loop door <code>A</code>
<ul>
<li>item al verwerkt: doe niets</li>
<li>item niet verwerkt: voeg item toe aan <code>B</code></li>
</ul></li>
</ol>
<p>Toen ik dit principe in code had gegoten, verliep de synchronisatie een héél
stuk sneller. Alleen de items die toegevoegd, gewijzigd of verwijderd waren,
worden daadwerkelijk overgepompt.</p>
<h2>Script</h2>
<pre><code class="php">//...
$allLabelsProcessed = array();
foreach ($googleGroups as $labelB)
{
// Skip system-groups
if (stripos($labelB->title->{'$t'}, 'System Group:') === 0) {
continue;
}
$dtGoogle = new \DateTime($labelB->updated->{'$t'});
$dtGoogle->setTimezone(new \DateTimeZone('UTC'));
// Find original label-ID using gd:extendedProperty
$labelIdOld = null;
if (property_exists($labelB, 'gd$extendedProperty')) {
foreach ($labelB->{'gd$extendedProperty'} as $z) {
if ($z->name == GDATA_SCHEMA_URL.'#label.id') {
$labelIdOld = $z->value;
break;
}
}
}
// Find original label in A to compare to
$labelA = array_filter(
$allLabels,
function($y) use ($labelIdOld) {
return $y->getID() == $labelIdOld;
}
);
$labelA = $labelA ? array_shift($labelA) : null;
// Does the item no longer exist in A?
if (is_null($labelA)) {
// delete from B
$b[] = new FR\BatchItem(FR\BatchAction::DELETE, $labelB);
// Has the item been modified in A?
} else if ($labelA->lastUpdated > $dtGoogle) {
// update in B
$b[] = new FR\BatchItem(FR\BatchAction::UPDATE, $labelA);
$allLabelsProcessed[] = $labelA;
} else {
// Unchanged, nothing to do
$allLabelsProcessed[] = $labelA;
}
}
foreach ($allLabels as $l) {
$processed = array_filter(
$allLabelsProcessed,
function($y) use ($l) {
return $y->getID() == $l->getID();
}
);
// Has the item been processed?
$toInsert = $processed ? null : $l;
if ($toInsert) {
// insert to B
$b[] = new FR\BatchItem(FR\BatchAction::CREATE, $l);
}
}
// Execute the batch of operations
$results = $google_client->batchGroups($b);
</code></pre>
<h2>Uitdagingen</h2>
<p>Een van de uitdagingen was om de items na de eenmalige volledige synchronisatie
te kunnen herkennen tijdens een update. Ook hierin kwam de API mij tegemoet; dit
maal in de vorm van een <code>gd:extendedProperty</code>. Dit is een extra veld dat je kunt
meegeven en uitlezen, maar dat niet wordt weergegeven in bijvoorbeeld de
grafische omgeving van Gmail. Ideaal om het ID uit mijn applicatie in op te
slaan.</p>
<p>Er restte nog slechts één probleem dat wachtte op een oplossing: hoe ging ik
vaststellen of een item in de applicatie gewijzigd was ten opzichte van hetzelfde
item bij Google?</p>
<p>Het object dat Google leverde bevatte een eigenschap genaamd
<code>->updated->{'$t'}</code> (let op de bijzondere PHP-syntax rond de naam met een
dollarteken). Hiervan kon ik een <code>DateTime</code>-object maken. Toen ik daarna voor
alle items in mijn applicatie de laatste mutatiedatum uit de database haalde,
kon het grote vergelijken beginnen.</p>
<h2>Tijdzones</h2>
<p>Maar wat was dat? Direct na het wijzigen van een item in mijn applicatie, werd
het elke keer opnieuw gesynchroniseerd. Telkens als ik de update uitvoerde, werd
het item weer overgepompt - alsof het elke keer opnieuw gewijzigd was.</p>
<p>Toen keek ik in detail naar de twee datums die ik met elkaar vergeleek:</p>
<pre><code class="php">$dtA = $labelA->lastUpdated; // DateTime object from database
var_dump($dtA);
/*
object(DateTime)#31337 (3) {
["date"]=>
string(26) "2017-04-03T16:31:55.000000"
["timezone_type"]=>
int(3)
["timezone"]=>
string(16) "Europe/Amsterdam"
} */
$dtB = $labelB->updated->{'$t'}; // "2017-04-03T14:31:55.899Z" from Google
var_dump(new \DateTime($dtB));
/*
object(DateTime)#36936 (3) {
["date"]=>
string(26) "2017-04-03 14:31:55.000000"
["timezone_type"]=>
int(2)
["timezone"]=>
string(1) "Z"
} */
</code></pre>
<p>Het viel me op dat de tijdzones van beide data verschillen. Met behulp van een
<a href="http://stackoverflow.com/a/17711005">vraag en antwoord</a> op internet ontdekte ik dat hier de kern van het probleem
lag. De tijd die Google aanleverde was zonder tijdzone (<code>Z</code>, Zulu). Mijn
applicatie en database werkten daarentegen met een tijdzone van
<code>Europe/Amsterdam</code>. Alleen bij vergelijken van <code>DateTime</code>s met
<code>timezone_type == 3</code> wordt correct rekening gehouden met zomer- en wintertijd.</p>
<p>Ik maakte er met behulp van <code>setTimezone(new \DateTimeZone('UTC'))</code> een type 3
datum van en waarempel: de vergelijking klopte, keer op keer!</p>
</div>2017-04-05T21:44:13+02:00https://www.fwiep.nl/blog/reset-mysql-root-wachtwoordReset MySQL root wachtwoord2017-03-06T15:57:15+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Het overkomt waarschijnlijk elke ontwikkelaar minimaal één keer in zijn loopbaan:
het wachtwoord van de hoofdgebruiker (<code>root</code>) van de MySQL databaseserver is
niet meer te achterhalen en moet worden hersteld. Gelukkig zijn er <a href="https://help.ubuntu.com/community/MysqlPasswordReset">talloze
instructies</a> te vinden op internet, die ieder voor zich claimen een werkend
stappenplan te hebben voor het resetten van het <code>root</code>-wachtwoord. Ja ja…</p>
<h2>Werkwijze</h2>
<p>Over het algemeen verloopt het herstel langs de volgende lijn:</p>
<ol>
<li>de MySQL-server stoppen</li>
<li>de MySQL-server starten zonder controle op gebruikersrechten</li>
<li>inloggen en via SQL het <code>root</code>-wachtwoord opnieuw instellen</li>
<li>de onveilige MySQL-server stoppen</li>
<li>de reguliere MySQL-server starten</li>
</ol>
<h2>Zwoegen</h2>
<p>Aldus probeerde ik de ene na de andere tutorial, maar steeds weer lag mijn
MySQL-server dwars. Ik kon handmatig geen MySQL-server starten. De logbestanden
bevatten geen enkele hint naar de oorzaak. Ik zocht opnieuw en nog een keer.
Uiteindelijk bevatte een antwoord op askubuntu.com de <a href="http://askubuntu.com/questions/878652/unable-to-reset-mysql-root-password-tried-everthing">gouden tip</a>.</p>
<h2>Script</h2>
<pre><code class="bash"># Stopt de MySQL-server
sudo service mysql stop;
# Maakt een (tijdelijke) map voor tijdens het gebruik
sudo mkdir /var/run/mysqld;
# Eigent de nieuwe map aan de gebruiker mysql toe
sudo chown mysql /var/run/mysqld;
# Start de server zonder controle op gebruikersrechten
sudo mysqld_safe --skip-grant-tables --skip-networking &
# Opent een client (zonder wachtwoord)
mysql -u root;
</code></pre>
<p>Hierna volgt het daadwerkelijke wachtwoordherstel:</p>
<pre><code class="sql">ALTER USER 'root'@'localhost' IDENTIFIED BY 'MyN3w$upeR$EcReTp@s$W0Rd';
quit;
</code></pre>
<p>Tot slot wordt de tijdelijke server afgeschoten en de 'gewone' opnieuw gestart.</p>
<pre><code class="bash"># Stop alle actieve servers
sudo killall mysqld;
# Herstart de server
sudo service mysql start;
# Opent een client (met nieuw wachtwoord)
mysql -u root -p;
</code></pre>
</div>2017-03-06T15:57:15+01:00https://www.fwiep.nl/blog/postnl-kix-code-met-fpdf-in-phpPostNL KIX-code met FPDF in PHP2017-02-22T08:12:36+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Heb je je nooit afgevraagd wat die rare streepjescode onder een adres op menige
envelop zou kunnen betekenen? Ik wel. Na een relatief korte zoektocht kwam ik op
de <a href="https://www.postnl.nl/klantenservice/bestellen-en-downloaden/documentatie-downloaden/kix-code/">website van PostNL</a> de volgende informatie tegen:</p>
<blockquote>
<p>De KIX-code is een streepjescode die u toevoegt aan uw adresgegevens. Het
bevat alle gegevens die we nodig hebben om uw post automatisch te verwerken.</p>
</blockquote>
<p>Het is dus een hulpmiddel voor het sorteerproces dat de afzender toevoegt aan
het adres op de poststukken. Ik bedacht meteen een toepassing in mijn
zelfgebouwde adresboek-webapplicatie. Hoe mooi zou het zijn om daar
geautomatiseerd voorgedrukte enveloppen uit te laten rollen?</p>
<h2>KIX formaat</h2>
<p>PostNL stelt een lettertype bestand beschikbaar waarin de cijfers <code>0</code> tot <code>9</code> en
de 26 letters van het alfabet worden weergegeven als een uniek streepjespatroon.
Dit font is beschikbaar voor meerdere platformen, zie bovengenoemde pagina. De
inhoud van de KIX-code bestaat uit twee tot drie onderdelen:</p>
<ul>
<li>de vier cijfers en twee letters van de postcode</li>
<li>de cijfers van het huisnummer</li>
<li>een eventuele toevoeging, voorafgegaan door de letter <code>X</code></li>
</ul>
<h2>FPDF</h2>
<p>Het adresboek had al ondersteuning voor het genereren van PDF-documenten door
middel van de <a href="http://www.fpdf.org/">FPDF</a>-bibliotheek. Dit is een PHP klasse die zonder verdere
externe toeters of bellen universeel leesbare documenten kan genereren. Om de
KIX code te kunnen invoegen, moet het KIX-lettertype aan FPDF worden toegevoegd.</p>
<h2>makefont</h2>
<p>Om naast de standaard lettertypen ook eigen fonts te kunnen gebruiken, moet zo'n
lettertype eerst worden voorbereid voor en door FPDF. Daartoe bevat het pakket
het <code>makefont.php</code>-script in de map <code>makefont</code>. Je kunt dit bestand op drie
manieren aanspreken (zie ook tutorial 7 in de FPDF download):</p>
<ul>
<li>door het <code>makefont.php</code>-script te <code>include</code>n en daarna de functie <code>MakeFont()</code>
aan te roepen</li>
<li>door het <code>makefont.php</code>-script via de shell op te roepen</li>
<li>door een <a href="http://www.fpdf.org/makefont/">online convertor</a> te gebruiken</li>
</ul>
<p>Ik koos voor de commandline optie:</p>
<pre><code class="bash">php /pad/naar/makefont.php /pad/naar/Kixbrg__.ttf;
</code></pre>
<h2>Script</h2>
<p>De twee gegenereerde bestanden (<code>Kixbrg__.php</code> en <code>Kixbrg__.z</code>) worden in de
<code>font</code>-map van FPDF geplaatst. Daarna volstaan de volgende commando's om de
code op het virtuele papier te krijgen:</p>
<pre><code class="php">require_once 'fpdf.php';
$pdf = new FPDF('L', 'mm', array(114, 162)); // C6 envelop
$pdf->AddPage(); // pagina toevoegen
$pdf->AddFont('KIXBarcode', '', 'Kixbrg__.php'); // font toevoegen
$pdf->SetFont('KIXBarcode', '', 10); // font activeren
$pdf->Write(8, "1234AB99XA".PHP_EOL); // postcode 1234 AB, huisnummer 99A
$pdf->Write(8, "9876ZX37".PHP_EOL); // postcode 9876 ZX, huisnummer 37
$pdf->Output('I', 'envelop.pdf');
</code></pre>
</div>2017-02-22T08:12:36+01:00https://www.fwiep.nl/blog/backup-met-raspberry-pi-en-usb-hddBackup met Raspberry Pi en USB-HDD2017-02-21T11:00:40+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>De <a href="https://www.raspberrypi.org/">Raspberry Pi</a> is een mini-computer op creditcard formaat en bedoeld als
knutsel- en leerplatform. Hij bevat bijna alle onderdelen van een 'echte' PC,
maar dan in het klein. Het printplaatje zelf kost niet meer dan € 40,-. Een paar
jaar geleden kreeg ik een model B+ cadeau. Na een aantal eerste experimenten
verdween hij in de spreekwoordelijke bureaula.</p>
<h2>Toepassing</h2>
<p>Tot het moment dat ik zocht naar een mogelijkheid om de bestanden op mijn <abbr
title="Network Attached Storage">NAS</abbr> dagelijks te back-uppen. Het moest
toch mogelijk zijn om net een netwerkverbinding en een via USB aangesloten harde
schijf kopieën te maken van een netwerkmap?</p>
<h3>CIFS-mount</h3>
<p>Om de Pi van een netwerkmap te kunnen laten lezen, moet het <code>cifs-utils</code>-pakket
worden geïnstalleerd. Daarna kan de map in bijvoorbeeld <code>/etc/fstab</code> worden
gekoppeld tijdens het opstarten. De neest eenvoudige configuratie ziet er
bijvoorbeeld zo uit:</p>
<pre><code class="bash"># my Samba-share
//192.168.1.1/share /mnt/share cifs uid=1000,gid=1000,iocharset=utf8,nobrl 0 0
</code></pre>
<h3>/etc/fstab</h3>
<p>Een regel in <code>fstab</code> bestaat uit de volgende onderdelen (zie ook de <a href="https://wiki.archlinux.org/title/Fstab">documentatie
bij Arch Linux</a>):</p>
<ol>
<li>het netwerkpad van de gedeelde map</li>
<li>de lokale map waar de netwerkmap wordt gekoppeld</li>
<li>het bestandstype</li>
<li>eventuele opties</li>
<li><code>dump</code> opties (bijna altijd <code>0</code>)</li>
<li>volgorde waarin <code>fsck</code> het bestandssysteem op fouten controleert</li>
</ol>
<p>In mijn geval was de netwerkmap alleen met gebruikersnaam en wachtwoord
toegankelijk. Het <code>options</code>-veld biedt de mogelijkheid om deze bij het koppelen
op te geven, maar dit had een veiligheidsnadeel. Normaal gesproken kan namelijk
elke gebruiker <code>/etc/fstab</code> lezen - dus ook de gebruikersnaam en het wachtwoord.
Gelukkig bestaat er minimaal één elegant en wel zo veilig alternatief.</p>
<h3>Veilig verbinden</h3>
<p>Door te verwijzen naar een apart bestand met de gebruikersnaam en wachtwoord is
het gevaar geweken. Het bestand kan bijvoorbeeld als volgt worden gemaakt:</p>
<pre><code class="bash">touch /home/pi/.smbcredentials;
sudo chown root:root /home/pi/.smbcredentials;
sudo chmod 600 /home/pi/.smbcredentials;
</code></pre>
<p>De inhoud spreekt voor zich:</p>
<pre><code class="plain">username=myuser
password=My$up3rSecReTp@s$w0rD
</code></pre>
<p>Tot slot wordt met de optie <code>credentials</code> in de <code>options</code>-kolom van <code>fstab</code> naar
het zojuist gemaakte bestand verwezen. Omdat de netwerkverbinding niet altijd
even snel beschikbaar is tijdens het opstarten van de Pi, heb ik ervoor gekozen
om met behulp van <code>noauto</code> en <code>systemd</code> het daadwerkelijke koppelen uit te
stellen totdat de map voor het eerst benaderd wordt.</p>
<pre><code class="bash"># my Samba-share
//192.168.1.1/share /mnt/share cifs noauto,x-systemd.automount,x-systemd.device-timeout=3,credentials=/home/pi/.smbcredentials,uid=1000,gid=1000,iocharset=utf8,nobrl 0 0
</code></pre>
<h3>Nukkige harde schijf</h3>
<p>Toen de netwerkmap eindelijk toegankelijk was en betrouwbaar werd gekoppeld,
leek mijn USB harde schijf niet correct te worden herkend. Ze verscheen niet
eens in de systeemlogs (<code>dmesg</code> en <code>/var/log/syslog</code>). Ik probeerde haar uit aan
een normale desktop PC, met verschillende USB-adapters... De schijf was in orde.</p>
<p>In een aantal fora op internet beschreven gebruikers soortgelijke problemen en
gaven de stroomvoorziening van de schijf de schuld. Onder geen beding mocht de
schijf zonder eigen externe voeding worden gebruikt. Tot slot werd ook nog de
optie van aansluiten via een USB-hub met externe voeding genoemd.</p>
<p>Al deze variaties leidden tot niets. In elk geval niet tot een betrouwbaar
gebruik van een aangesloten harde schijf. Gelukkig ontdekte ik dat de schijf na
een reset (warme start) wél werd herkend. Dus voegde ik de volgende regels toe
aan het backup-script:</p>
<pre><code class="bash"># Force a mount of the external drive
# (it has the noauto option set in /etc/fstab)
mount /mnt/share;
# Check whether the external drive is mounted correctly
if [ "/mnt/share" != "$( df /mnt/share/ | awk 'NR==2{print $6}' )" ]; then
echo "${STARTDT} :: External drive not mounted correctly, exiting." >> "/mnt/share/backup.log";
# Reboot the system and try again
/sbin/shutdown --reboot now &
exit 1;
fi;
</code></pre>
<p>De Pi start nu opnieuw op als de externe harde schijf niet correct is gekoppeld.
Aangezien het backup-script automatisch start bij het opstarten van de Pi,
probeert hij net zo lang totdat het lukt - of de schakelklok de voeding radicaal
uitschakelt.</p>
<h2>Conclusie</h2>
<p>Dit project heeft mij een hoop kennis en ervaring opgeleverd omtrent de Pi,
GNU/Linux, Samba en shell-scripts. Ondanks dat de oplossing niet perfect is,
vind ik haar goed genoeg. Er wordt dagelijks een backup gemaakt van de bestanden
op mijn NAS, en dat is het belangrijkste.</p>
</div>2017-02-21T11:00:40+01:00https://www.fwiep.nl/blog/afbeeldingen-als-pdf-met-watermerkAfbeeldingen als PDF met watermerk2017-02-15T10:26:41+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Soms wil je een reeks afbeeldingen samenvoegen tot één PDF document. In mijn
geval waren het scans van losse vellen bladmuziek die ik als één geheel wilde
opslaan. Tot slot wilde ik ook nog de mogelijkheid om een watermerk toe te
voegen.</p>
<h2>Tools</h2>
<p>Manipuleren van afbeeldingen, het samenvoegen van en goochelen met PDF-bestanden;
dat klinkt naar een klus voor de <a href="https://www.imagemagick.org/">ImageMagick</a> tools: <code>convert</code> en
<code>composite</code>. Deze programma's zijn zó extreem veelzijdig... Gelukkig is er een
goede documentatie en zijn er talloze voorbeelden op internet te vinden. Ik
besloot een script te schrijven dat mij het werk uit handen ging nemen.</p>
<p>Eén van de uitdagingen was het consequente papierformaat van het uiteindelijke
PDF-bestand. Met dank aan een <a href="http://unix.stackexchange.com/a/20057">antwoord op StackExchange</a> was dat snel
opgelost.</p>
<h2>Onderzoek</h2>
<p>Met de commando's zelf ben je er nog niet. Om argumenten te kunnen meegeven en
interpreteren wil ook de shell (Bash) correct en volledig worden aangesproken.
Eerst maakte ik een eenvoudige variant die één bestand in een PDF plaatste.
Daarna volgde een groep bestanden met behulp van de <code>${1}</code>, <code>${2}</code> en
<code>${@}</code>-variabelen, zie ook <a href="http://wiki.bash-hackers.org/scripting/posparams">positionele parameters</a>.</p>
<p>Tot dat moment was de bestandsnaam van het uiteindelijke PDF-bestand nog 'hard
coded', dat wil zeggen voorgeschreven vanuit het script. Dat kan variabel, toch?
Ja, maar dan wel fatsoenlijk, met behulp van <a href="http://wiki.bash-hackers.org/howto/getopts_tutorial">optie-argumenten</a>.</p>
<p>Daarna was het nog maar een kleine stap om ook het aanbrengen van een watermerk
aan het script toe te voegen. Ook nu begon ik eerst met één bestand, daarna de
integratie in het omliggende script. Hierbij wilde ik de oproep van <code>convert</code>
centraal neerleggen, om dubbele stukken code te vermijden. Ik moest een manier
vinden om de te verwerken bestanden (<code>${FILES[@]}</code>) al dan niet vanuit een
tijdelijke map te halen...</p>
<h2>Parameter expansion</h2>
<p>Ik experimenteerde met een tweede <code>array</code>, maar dat was geen goed idee.
Uiteindelijk bood Bash zelf een keurig nette oplossing in de vorm van
<a href="https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.htmla">parameter expansion</a>. Met één commando kan ik, eenmaal in de watermerk-<code>if</code>,
de bestandsnamen voorzien van een tijdelijke map (<code>${WORKDIR}</code>):</p>
<pre><code class="bash">FILES=( "${FILES[@]/#/${WORKDIR}/}" );
</code></pre>
<h2>Update</h2>
<p>Maar wat, als je een mix van afbeeldingen (JPG) en PDF-documenten van één pagina
wil samenvoegen tot één gezamenlijke PDF? Daarop was het originele script niet
voorbereid. Aldus bouwde ik met de hulp van de uitgebreide <a href="http://www.linuxjournal.com/content/bash-arrays">documentatie</a> een
extra <code>for</code>-loop waarin een eventueel PDF bestand wordt omgezet naar een
JPG-bestand met hoge kwaliteit.</p>
<h2>Script</h2>
<pre><code class="bash">#!/bin/bash
RESOLUTION=150;
OUTPUTFILE="outfile";
WATERMARKFILE=;
WORKDIR=;
CONVERT="$( which convert )";
COMPOSITE="$( which composite )";
USAGE="Usage: "$(basename ${0})" [-w watermark-file] [-o outfile]
infile [infile [...]]";
if [ ${#} -eq 0 ]; then
echo "${USAGE}";
exit 0;
fi
while getopts ":h?w:o:" opt; do
case "$opt" in
\?)
echo "${USAGE}";
exit 0;
;;
w) WATERMARKFILE="${OPTARG}";
;;
o) OUTPUTFILE="${OPTARG%%.pdf}";
;;
esac
done
shift $((OPTIND-1))
[ "$1" = "--" ] && shift
if [ ${#} -lt 1 ]; then
echo "Error: At least one infile is required.";
exit 1;
fi
if [ "${WATERMARKFILE}x" != "x" ] && [ ! -f "${WATERMARKFILE}" ]; then
echo "Error: watermark-file cannot be found.";
exit 2;
fi
FILES=("${@}");
# Convert PDF input files to high quality JPG
for i in "${!FILES[@]}"; do
if [[ ${FILES[${i}]} =~ \.pdf$ ]]; then
convert -density 150 -trim "${FILES[${i}]}" -quality 100 -flatten \
"${FILES[${i}]%%.pdf}.jpg";
FILES[${i}]="${FILES[${i}]%%.pdf}.jpg";
fi
done
# Add watermark
if [ -f "${WATERMARKFILE}" ]; then
WORKDIR="$( mktemp --directory )";
for i in "${FILES[@]}"; do
"${COMPOSITE}" -tile -gravity Center -watermark 20x50 \
"${WATERMARKFILE}" "${i}" "${WORKDIR}/${i}";
done
FILES=( "${FILES[@]/#/${WORKDIR}/}" );
fi
# Here's where the magick happens
"${CONVERT}" "${FILES[@]}" -compress jpeg -quality 70 \
-resize $((RESOLUTION*827/100))x$((RESOLUTION*1169/100)) \
-density ${RESOLUTION}x${RESOLUTION} \
-repage $((RESOLUTION*827/100))x$((RESOLUTION*1169/100)) \
"${OUTPUTFILE}.pdf";
# Clean-up
if [ ! -z "${WORKDIR}" ] && [ -d "${WORKDIR}" ]; then
rm -r "${WORKDIR}";
fi
# Exit normally
exit 0;
</code></pre>
<h2>Conclusie</h2>
<p>Opgeslagen als <code>toa4pdf.sh</code> kan ik het script als volgt oproepen:</p>
<pre><code class="bash">toa4pdf.sh -o UiteindelijkePDF -w /pad/naar/watermerk.png *.jpg *.pdf;
</code></pre>
<p>N.B.: Ik heb geen idee hoe het script reageert op PDF-documenten met meerdere
pagina's als input... :-)</p>
</div>2017-01-17T13:20:00+01:00https://www.fwiep.nl/blog/bestanden-delen-met-sftpBestanden delen met sFTP2017-02-12T16:28:54+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Toen mij laatst iemand vroeg om een aantal grote bestanden te delen, had ik een
paar opties. Voor gelegenheidsdownloads maak ik soms gebruik van
<a href="https://wetransfer.com/">WeTransfer</a>, een Amerikaans bedrijf dat (grote) bestanden aanneemt en,
eventueel voor meerdere gebruikers, als download aanbiedt. In dit geval wilde
ik echter meer controle en de data niet onnodig over de Atlantische oceaan pompen.</p>
<p>Als tweede mogelijkheid dacht ik aan het oud vertrouwde FTP. Maar dit heeft voor
mij als grootste nadeel dat er geen enkele vorm van beveiliging aan te pas komt.
Zowel het aanmelden (gebruikersnaam, wachtwoord) als het transport van de data
zelf gaan onversleuteld van A naar B. Dit moest toch beter kunnen?</p>
<h2>SSH to the rescue</h2>
<p>De veiligste methode om met een GNU/Linux server te communiceren is <em>Secure
Shell</em> (SSH). In de meeste gevallen meld je je met een gebruikersnaam en een
bijbehorend wachtwoord aan. Dan krijg je een commandoprompt (shell) alsof je
live met een terminal van de server verbonden zou zijn.</p>
<p>In dit geval wilde ik de gebruiker echter geen volledige shell-toegang geven,
maar wel de bestanden kunnen laten downloaden. Op dat moment schoot de
veelzijdigheid van het SSH-protocol te hulp. Er bestaat namelijk een soort FTP
via SSH genaamd <em>sFTP</em>. Cliëntprogramma's als <a href="https://filezilla-project.org/">FileZilla</a> en <a href="https://winscp.net/eng/index.php">WinSCP</a>
ondersteunen deze variant volledig.</p>
<h2>Installatie</h2>
<p>Als voorbeeld zal ik hieronder de gebruiker <code>arthur</code> toevoegen.</p>
<ol>
<li>Zorg dat de <code>/home</code>-map eigendom is van <code>root</code> en voor niemand anders
schrijfbaar is.</li>
</ol>
<pre><code class="bash">sudo chown root:root /home;
sudo chmod 755 /home;
</code></pre>
<ol start="2">
<li>Voeg een nieuwe gebruiker toe aan de server</li>
</ol>
<pre><code class="bash">sudo adduser --shell /bin/false arthur;
</code></pre>
<ol start="3">
<li>Maak de <code>arthur</code>-map lees- en schrijfbaar voor niemand behalve <code>arthur</code></li>
</ol>
<pre><code class="bash">sudo chmod 700 /home/arthur;
</code></pre>
<ol start="4">
<li>Installeer de OpenSSH-server</li>
</ol>
<pre><code class="bash">sudo apt install openssh-server;
</code></pre>
<ol start="5">
<li>Maak de lokale poort (meestal <code>TCP 22</code>) vanaf internet bereikbaar</li>
<li>Voeg de onderstaande regels aan <code>/etc/ssh/sshd_config</code> toe:</li>
</ol>
<pre><code class="plain">AllowUsers fwiep arthur
Subsystem sftp internal-sftp
Match User arthur
ChrootDirectory /home
AllowTCPForwarding no
X11Forwarding no
ForceCommand internal-sftp
</code></pre>
<ol start="7">
<li>Herstart de SSH service</li>
</ol>
<pre><code class="bash">sudo service ssh restart;
</code></pre>
<h2>Efficiënt delen</h2>
<p>In bovenstaande situatie wilde ik één bepaalde map op mijn harde schijf delen
met de SFTP-gebruiker. In plaats van die map te kopiëren of te verplaatsen, koos
ik voor het opnieuw <code>mount</code>-en van de map in zijn <code>/home</code>-map. Daartoe moest die
map daar wel bestaan en de juiste eigenaar hebben:</p>
<pre><code class="bash">sudo -u arthur mkdir /home/arthur/HugeFiles;
</code></pre>
<p>Daarna voegde ik aan mijn <code>/etc/rc.local</code> het volgende commando toe, zodat dit
bij elke systeemstart zou worden uitgevoerd:</p>
<pre><code class="bash"># Maak een (alleen-lezen) link tussen twee mappen
mount --bind -o ro /mnt/DATA/fwiep/HugeFiles /home/arthur/HugeFiles;
# Zorg dat onderstaand commando als laatste staat
exit 0;
</code></pre>
<h2>Beveiliging</h2>
<p>Met adviezen over adequate netwerkbeveiliging kun je complete bibliotheken
vullen. Ik niet, dus doe ik hieronder slechts een simpele poging om te vertellen
wat ik weet en wat zich in mijn praktijk heeft bewezen.</p>
<h3>Poort 22</h3>
<p>De standaard netwerkpoort van een SSH-server is 22. Dit is wereldwijd bekend en
daarom verbaasde het mij niet dat er met de regelmaat van de klok computers uit
China, Ukraine en de rest van de wereld aan mijn deur klopten. Een mogelijkheid
om het leeuwendeel van deze slechteriken buiten de deur te houden is te kiezen
voor een <em>andere TCP poort</em>.</p>
<p>Voeg de onderstaande regel aan <code>/etc/ssh/sshd_config</code> toe:</p>
<pre><code class="bash"># What ports, IPs and protocols we listen for
Port 54364
</code></pre>
<h3>Public Key Authentication</h3>
<p>In plaats van met een wachtwoord, kan SSH ook overweg met <em>Public Key
Authentication</em>, een methode om met <a href="https://nl.wikipedia.org/wiki/Asymmetrische_cryptografie">publieke en bijpassende privé sleutels</a>
te kunnen inloggen.</p>
<p>Als eerste zul je op de cliënt computer een sleutelpaar moeten aanmaken. Dat
kan met <code>ssh-keygen -b 4096</code>. Daarna moet de publieke sleutel naar de server
worden gekopieerd: <code>ssh-copy-id user@servername</code>.</p>
<p>De volgende regels in <code>/etc/ssh/sshd_config</code> schakelen daarna wachtwoorden uit,
en de veiligere manier van aanmelden in:</p>
<pre><code class="plain">PubkeyAuthentication yes
PasswordAuthentication no
ChallengeResponseAuthentication no
</code></pre>
</div>2017-02-12T16:28:54+01:00https://www.fwiep.nl/blog/domdocument-element-vs-textnodeDOMDocument: Element vs. TextNode2017-01-20T08:23:05+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Bij de <a href="https://www.fwiep.nl/blog/doe-het-zelf-weblog">bouw van dit weblog</a> koos ik voor XML als opslagformaat voor de
metadata van alle blogposts. Later volgde ook een <a href="https://www.fwiep.nl/blog/weblog-beheer-via-commandline-php">commandline beheergedeelte</a>
in PHP. Sinds de lancering schreef ik bijna dagelijks een nieuw artikel. Zo
duurde het niet lang voordat er een interessante programmeerfout aan het licht
kwam.</p>
<h2>Symptoom</h2>
<p>Ik voerde via het beheergedeelte een nieuw artikel in. Ik koos als titel
"Digitale studio met Ardour & friends", maar wat gebeurde er? In de metadata was
de XML-node <code><title /></code> leeg! Ergens in het opslaan of wegschrijven van de XML
ging iets fout, en ik ging op zoek in de broncode van mijn weblog.</p>
<h2>Diagnose</h2>
<p>De code die de metadata van een BlogPost wegschrijft bevond zich in de functie
<code>toXML()</code>. Ik beperk het voorbeeld hieronder tot één veld; <code>$this->_title</code>.</p>
<pre><code class="php"><?php
/**
* Returns this post's metadata as XML
*
* @return string
*/
public function toXML()
{
$doc = new \DOMDocument();
// ...
$root = $doc->createElement('post');
// ...
$nodeTitle = $doc->createElement('title', $this->_title);
$root->appendChild($nodeTitle);
// ...
return $doc->saveXML();
}
</code></pre>
<p>Met een <code>print_r()</code> vlak na de aanroep van <code>createElement()</code> stelde ik vast dat
hier de fout zat: "unterminated entity reference". De functie struikelde over de
ampersand (&) in de titel van de blogpost. Gelukkig waren er meer mensen met
dezelfde problematiek. Zoveel zelfs dat de ontwikkelaars van PHP dit <a href="https://php.net/manual/en/domdocument.createelement.php">in hun
documentatie</a> hebben opgenomen.</p>
<h2>Oplossing</h2>
<p>In diezelfde documentatie werd verwezen naar een functie die <em>wel</em> met
ampersands overweg kan: <a href="https://php.net/manual/en/domdocument.createtextnode.php"><code>createTextNode()</code></a>. Ik maakte een statische functie
waarmee ik met één regel code een nieuwe node kon toevoegen. Hier gebruikte ik
dan de nieuwe methode:</p>
<pre><code class="php">/**
* Adds the given value as an XML tag to the given root
*
* @param \DOMDocument &$doc the XML document
* @param \DOMElement &$root the root to add the newly created node to
* @param string $element the tag's name
* @param string $value the tag's value (will be escaped)
*
* @return void
*/
private static function _addEscaped(
\DOMDocument &$doc, \DOMElement &$root, $element, $value
) {
$node = $doc->createElement($element);
$cont = $doc->createTextNode($value);
$node->appendChild($cont);
$root->appendChild($node);
return;
}
</code></pre>
<p>Het aanroepen van deze hulpfunctie is daarna heel eenvoudig en elegant:</p>
<pre><code class="php">/**
* Returns this post's metadata as XML
*
* @return string
*/
public function toXML()
{
$doc = new \DOMDocument();
// ...
$root = $doc->createElement('post');
// ...
self::_addEscaped($doc, $root, 'title', $this->_title);
// ...
return $doc->saveXML();
}
</code></pre>
<p>Nu wordt elke waarde van de XML metadata correct geconverteerd op het moment
van toevoegen aan het document. Vanaf dat moment kon ik eindelijk beginnen aan
het artikel over <a href="https://www.fwiep.nl/blog/digitale-studio-met-ardour">Ardour & friends</a> :-)</p>
</div>2017-01-20T08:23:05+01:00https://www.fwiep.nl/blog/dvd-backup-met-vobcopy-en-handbrakeDVD backup met VOBcopy en Handbrake2017-01-18T08:20:17+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>In een <a href="https://www.fwiep.nl/blog/random-startrek-episode-selector">eerdere post</a> schreef ik trots over mijn verzameling StarTrek op dvd.
Jammer genoeg moest ik een tijd geleden vaststellen dat de plastic schijfjes te
lijden hadden van de tand des tijds - of kwalitatief slechte grondstoffen. Er
traden een groot aantal leesfouten op, nog geen 4 jaar na aankoop. Geen
programma of lezer kon er meer mee overweg. Sommige afleveringen kón ik
simpelweg niet meer bekijken.</p>
<p>Nu zou je kunnen argumenteren, dat ik alle afleveringen toch al heb gezien?
Jawel, maar het gaat om het principe. Uiteindelijk heb ik drie van de vijf
series voor een prikkie verkocht met de opmerking over de slechte kwaliteit
erbij. Ik moest en zou een alternatief vinden!</p>
<h2>Back-up</h2>
<p>Voor de dvd's die wel nog te lezen waren, zocht ik een mogelijkheid om de
video's naar een harde schijf te kopiëren. Over de legaliteit van deze actie
verschillen de meningen, maar in Nederland schijnt het <a href="https://www.iusmentis.com/auteursrecht/nl/thuiskopie/">te worden toegestaan</a>.
Wel zul je de kopieerbeveiliging moeten omzeilen met, bijvoorbeeld,
<a href="https://help.ubuntu.com/community/RestrictedFormats/PlayingDVDs"><code>libdvdcss2</code></a>.</p>
<p>Het commandline tool <a href="https://github.com/barak/vobcopy"><code>vobcopy</code></a> voldoet precies aan die wens. Met de opties
<code>--mirror</code> en <code>--output-dir</code> krijg je een volledige kopie van de schijf in de
opgegeven map:</p>
<pre><code class="bash">vobcopy --mirror --output-dir ./S1D2/
</code></pre>
<h2>Handbrake</h2>
<p>Tijdens het onderzoek naar de juiste tools kwam ik ook het grafische programma
<a href="https://handbrake.fr/">Handbrake</a> tegen. Het is een extreem veelzijdig programma om video's te
converteren vanuit en naar verschillende formaten. Daaronder zijn ook functies
om losse hoofdstukken, complete dvd's, ondertiteling en audiotracks te kopiëren.</p>
</div>2017-01-18T08:20:17+01:00https://www.fwiep.nl/blog/microdata-validatie-zonder-afbeeldingMicrodata validatie zonder afbeelding?2017-01-16T20:25:11+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Dit weblog is niet zomaar een simpele verzameling van artikelen. Veel meer is
het een programmeer­project waarin ik keer op keer nieuwe technieken ontdek en
implementeer, een geschreven verslag van wat een gemiddelde musicerende
slechtziende webontwikkelaar meemaakt.</p>
<p>Zo ontdekte ik een tijd geleden <a href="https://schema.org/docs/gs.html">microdata</a>. Het is een mogelijkheid om als
webmaster de inhoud van je website machinaal beter leesbaar te maken. Door
middel van extra stukjes broncode wordt niet alleen de inhoud, maar ook de
context van die inhoud begrijpelijk voor zoekmachines of -algoritmes.</p>
<h2>Valideren</h2>
<p>Bij het woord zoekmachine dacht ik meteen aan Google en warempel: ze hebben een
hulpmiddel online voor het <a href="https://search.google.com/structured-data/testing-tool">valideren van microdata</a>. Ik haalde één, twee van
mijn posts door de validator en wat blijkt?</p>
<pre><code class="plain">Error: A value for the image field is required.
</code></pre>
<p>Blijkbaar stelt Google voor elke BlogPost een afbeelding verplicht. Dit vind ik
onterecht. Als slechtziende zou ik extra energie moeten stoppen in het zoeken
van afbeeldingen die mijn verhaal visueel aantrekkelijk moeten maken? Nee,
bedankt. Die energie kan ik beter gebruiken.</p>
<h2>Feedback</h2>
<blockquote>
<p>Ik ben een slechtziende web-ontwikkelaar. Recent heb ik een weblog gebouwd,
waarin ik de BlogPosts graag wil voorzien van microdata. Het Google TestingTool
meldt dat bij al mijn artikelen een afbeelding (image) ontbreekt.</p>
<p>Echter, het ontbreken van de afbeeldingen is een gevolg van mijn
slechtziendheid. [ ... ] De volledige boodschap van mijn verhaal bevindt zich
in de tekst.</p>
<p>Ik vind het onterecht dat Google voor blogposts een afbeelding verplicht
stelt.</p>
</blockquote>
<p>Bovenstaand bericht heb ik via het feedback-formulier naar Google gestuurd. Ik
verwacht geen antwoord, maar ik heb wel mijn hart gelucht. En weer stof voor een
artikel in mijn zelfgebouwde weblog. Zonder afbeelding, wel te verstaan :-)</p>
</div>2017-01-16T20:25:11+01:00https://www.fwiep.nl/blog/bladmuziek-met-rumor-en-lilypondBladmuziek met Rumor en LilyPond2017-01-13T11:14:02+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>LilyPond</h2>
<p>Voor het noteren van bladmuziek maak ik gebruik van <a href="http://lilypond.org/">LilyPond</a>. Je schrijft
een soort broncode die daarna door een compiler wordt omgezet naar bladmuziek -
een heerlijk concept. Het combineert mijn twee grootste hobbies: muziek maken en
programmeren.</p>
<p>In het begin werkte ik in een simpele editor en compileerde de bladmuziek met
een commando als:</p>
<pre><code class="bash">lilypond --pdf --output=mijn-muziek /pad/naar/mijn-muziek.ly
</code></pre>
<h2>Frescobaldi</h2>
<p>Daarna ontdekte ik <a href="http://frescobaldi.org/">Frescobaldi</a> en werd het werken met LilyPond opeens een
heel stuk comfortabeler. Dit programma biedt een editor, een console (met de
commandline meldingen van LilyPond), een MIDI-speler en een groot vlak waarin de
gegenereerde bladmuziek wordt weergegeven.</p>
<p>Een extreem handige functionaliteit van laatstgenoemde vlak, is dat elke noot,
elke rust, bijna elk teken gekoppeld is met de cursor in de editor. Je kunt dus
met één klik 'op papier' een noot in de broncode opzoeken en eventueel
corrigeren.</p>
<p>Ook biedt Frescobaldi een grote verzameling kant-en-klare stukjes broncode voor
vaakvoorkomende functies. Zo wordt de layout met titel, componist en
copyright-vermelding automatisch opgemaakt. Natuurlijk moet je daarna zelf wel
nog de daadwerkelijke muziek noteren.</p>
<p>En laat dát nou juist een monnikenwerk zijn, waar ik altijd tegen opzag. Met
name complexe ritmes, die je heel makkelijk zingt of speelt, zijn een kriem om
correct op papier te krijgen. Aldus ging ik op zoek naar een opname-functie binnen
Frescobaldi. Dan kon ik het ritme en/of de melodie simpelweg op de piano inspelen
terwijl de broncode voor me werd uitgeschreven.</p>
<h2>Rumor</h2>
<p>Volgens de <a href="http://www.frescobaldi.org/uguide-1/entering">handleiding</a> bestaat onder GNU/Linux die mogelijkheid; met behulp
van <a href="http://www.volny.cz/smilauer/rumor/">Rumor</a>. Maar de instructies die ik vond, werkten niet goed. Of, ik zie
de koppeling tussen beide programma's over het hoofd - kan ook. Ben niet voor
niks <a href="https://www.fwiep.nl/slechtziendheid">slechtziend</a> :-).</p>
<p>In de <code>man</code>-page van Rumor staat beschreven dat het met andere programma's en
instrumenten samenwerkt via <a href="http://www.alsa-project.org/">ALSA</a>. Daartoe moet je wel eerst weten welke
clients je met elkaar moet verbinden:</p>
<pre><code class="bash">$ aconnect --list
client 0: 'System' [type=kernel]
0 'Timer '
1 'Announce '
client 14: 'Midi Through' [type=kernel]
0 'Midi Through Port-0'
client 24: 'Digital Piano' [type=kernel]
0 'Digital Piano MIDI 1'
</code></pre>
<p>Hier zie je in totaal drie clients, waarvan de laatste mijn digitale piano is
(24:0). Daarna start je Rumor op met het volgende commando:</p>
<pre><code class="bash">rumor --strip --explicit-duration --meter=44 \
--key=c --tempo=94 --grain=16 --alsa=24:0,24:0
</code></pre>
<p>De argumenten zijn als volgt te verklaren:</p>
<ul>
<li><code>--strip</code>: rusten vóór de eerste en ná de laatste noot worden niet genoteerd</li>
<li><code>--explicit-duration</code>: bij alle noten en rusten wordt de duur genoteerd</li>
<li><code>--meter=44</code>: de maatsoort, in 2 cijfers genoteerd</li>
<li><code>--key=c</code>: de toonsoort</li>
<li><code>--tempo=94</code>: het tempo</li>
<li><code>--grain=16</code>: de kleinste noot of rust die wordt genoteerd</li>
<li><code>--alsa=24:0,24:0</code>: de verbinding met de digitale piano; eerst input (voor het
invoeren van de noten), dan de output (voor het afspelen van de metronoom)</li>
</ul>
</div>2017-01-11T21:31:19+01:00https://www.fwiep.nl/blog/werken-met-orcaWerken met de Orca schermlezer2017-01-11T13:19:44+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Blinde en slechtziende mensen kunnen ondanks hun visuele beperking wel degelijk
van een computer gebruik maken; met behulp van hulpmiddelen zoals schermlezers,
braille en vergroting. Onder GNU/Linux is <a href="https://wiki.gnome.org/Projects/Orca">Orca</a> de schermlezer bij uitstek.
Tot op de dag van vandaag leer ik steeds meer over het gebruik daarvan. Deze
blogpost is mijn documentatie van die ervaringen.</p>
<h2>Bediening</h2>
<p>Eenmaal gestart met <kbd>Alt</kbd> + <kbd>Super</kbd> + <kbd>S</kbd> spreekt de
standaardstem van Orca je tegemoet: <q>Screenreader on</q>. Eén van de toetsen
op het toetsenbord is de zogenaamde <em>Orca-modifier</em>, meestal
<kbd>Insert</kbd>. Deze toets wordt in combinatie met andere toetsen gebruikt
om Orca te besturen. Het venster met voorkeuren (preferences) verschijnt na het
drukken van <kbd>Insert</kbd> + <kbd>Spatie</kbd>.</p>
<h2>Instellingen</h2>
<p>Binnen Orca kunnen onder andere de volgende instellingen worden aangepast:</p>
<ul>
<li>Spraak ondersteuning</li>
<li>Te spreken taal</li>
<li>Braille ondersteuning</li>
<li>Te gebruiken braille-tabel</li>
<li>Sneltoetsen</li>
</ul>
<h2>Profielen</h2>
<p>Orca kan met afwisselende instellingen worden gebruikt. Hierbij worden alle
voorkeuren in <em>profielen</em> opgeslagen. Standaard bestaat er slechts één
profiel: <code>Default</code>. Als je bijvoorbeeld tussen meerdere stemmen of talen wil
kunnen kiezen, maak je daarvoor een of meerdere nieuwe profielen aan.</p>
<h3>Back-up</h3>
<p>Alle instellingen van Orca worden opgeslagen in de <a href="https://wiki.gnome.org/Projects/dconf">DConf</a> database. Om hier
een back-up van te maken sluit je Orca af en voer je het volgende commando uit:</p>
<pre><code class="bash">dconf dump /org/gnome/orca/ > mijn-backup.ini;
</code></pre>
<h3>Terugzetten</h3>
<p>Een back-up kun je daarna terug inlezen met de volgende commando's (zorg dat
Orca niet actief is):</p>
<pre><code class="bash">dconf reset -f /org/gnome/orca/;
dconf load /org/gnome/orca/ < mijn-backup.ini;
</code></pre>
</div>2017-01-09T13:17:37+01:00https://www.fwiep.nl/blog/weblog-beheer-via-commandline-phpWeblog beheer via commandline PHP2017-01-09T10:23:38+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Schreef ik in een <a href="https://www.fwiep.nl/blog/doe-het-zelf-weblog">eerdere post</a> nog over het ontbreken van een
beheergedeelte voor mijn zelfgebouwde weblog, is dat intussen hard in de maak.
Ik heb ervoor gekozen om het beheer van dit weblog via de commandline (<abbr
title="Command Line Interface">CLI</abbr>) te doen. Dit is voor mij een nieuw
terrein; PHP, maar dan buiten een webbrowser om.</p>
<h2>Script</h2>
<p>De eerste regel van het script vertelt de shell, na een hekje en een
uitroepteken, welk programma gebruikt moet worden om de rest van het bestand uit
te voeren. Dit heet in de Unix-volksmond een <a href="https://en.wikipedia.org/wiki/Shebang_(Unix)">shebang</a>. Daarna begint de
<code>php</code>-code.</p>
<p>Als eerste wordt de functie <code>main</code> gedefinieerd, en daarna in een <code>while (true)
{ }</code>-loop opgeroepen. In deze functie wordt het scherm leeg gemaakt (<code>clear</code>),
alle posts ingelezen (<code>BlogPost::readFromDisk()</code>) en aan de gebruiker gevraagd
een keuze te maken. Op basis van die keuze wordt er één van de volgende acties
uitgevoerd:</p>
<ul>
<li>Nieuwe blogpost aanmaken</li>
<li>Bestaande blogpost bewerken</li>
<li>Bestaande blogpost verwijderen</li>
<li>Programma afsluiten</li>
</ul>
<pre><code class="php">#!/usr/bin/php
<?php
function main()
{
system('clear');
$blogs = FW\BlogPost::readFromDisk(BLOGPATH);
printTable($blogs);
$blogcount = count($blogs);
$prompt = sprintf(
" Maak uw keuze [n]ew, [e]dit, [d]elete, [q]uit: ", $blogcount
);
$line = '';
while (empty($line)) {
$line = readline($prompt);
}
switch (trim(strtolower($line)))
{
case 'n':
newPost($blogs);
break;
case 'e':
editPost($blogs);
break;
case 'd':
deletePost($blogs);
break;
case 'q':
print PHP_EOL;
exit;
break;
}
return;
}
while (true) {
main();
}
</code></pre>
<h2>Nieuwe blogpost</h2>
<p>Als eerste wordt de gebruikter gevraagd een titel op te geven. Daarna volgt een
voorstel voor de SEO-titel; de tekenreeks die in de adresbalk achter <code>/blog/</code>
komt te staan. Dit voorstel wordt afgeleid van de opgegeven titel. Tot slot
volgen nog naam en e-mailadres van de auteur, waarna de editor wordt gestart met
het nieuwe <code>.markdown</code>-document. Is de editor gesloten, dan wordt het
XML-bestand met metadata weggeschreven.</p>
<pre><code class="php">function newPost(&$blogs)
{
$prompt = sprintf(" Titel: ");
$newTitle = '';
while (empty(trim($newTitle))) {
$newTitle = readline($prompt);
if (postExists($blogs, null, $newTitle)) {
print " FOUT! Titel bestaat al. ".PHP_EOL;
$newTitle = '';
}
}
$proposalSeoTitle = F\Functions::getSeoString($newTitle);
$prompt = sprintf(" SEO-titel [%s]: ", $proposalSeoTitle);
$newSeoTitle = '';
while (empty($newSeoTitle)) {
$newSeoTitle = readline($prompt);
if (empty($newSeoTitle)) {
$newSeoTitle = $proposalSeoTitle;
} else {
$newSeoTitle = F\Functions::getSeoString($newSeoTitle);
}
if (postExists($blogs, null, null, $newSeoTitle)) {
print " FOUT! SEO-titel bestaat al. ".PHP_EOL;
$newSeoTitle = '';
}
}
$proposalAuthorName = DEFAULT_AUTHOR_NAME;
$prompt = sprintf(" Auteur [%s]: ", $proposalAuthorName);
$newAuthorName = readline($prompt);
if (empty($newAuthorName)) {
$newAuthorName = $proposalAuthorName;
}
$proposalAuthorEmail = DEFAULT_AUTHOR_EMAIL;
$prompt = sprintf(" E-mail [%s]: ", $proposalAuthorEmail);
$newAuthorEmail = '';
while (empty($newAuthorEmail)) {
$newAuthorEmail = readline($prompt);
if (!empty($newAuthorEmail)
&& false === filter_var($newAuthorEmail, FILTER_VALIDATE_EMAIL)
) {
print " FOUT! Geen geldig e-mailadres. ".PHP_EOL;
$newAuthorEmail = '';
} else if (empty($newAuthorEmail)) {
$newAuthorEmail = $proposalAuthorEmail;
}
}
$bp = new FW\BlogPost($newTitle, $newSeoTitle);
$newFile = BLOGPATH.$bp->getID().'.markdown';
file_put_contents($newFile, '## Inleiding');
print " Starting editor...";
$cmd = EDITOR.' '.$newFile.' 1>&2 2>/dev/null;';
$editOK = -1;
system($cmd, $editOK);
if ($editOK === 0) {
$bpe = new FW\BlogPostEdit($newAuthorName, $newAuthorEmail);
$bp->addEdit($bpe);
$newFile = BLOGPATH.$bp->getID().'.xml';
file_put_contents($newFile, $bp->toXML());
}
return;
}
</code></pre>
<h2>Bewerken blogpost</h2>
<p>Na de keuze van de gebruiker (niet hieronder weergegeven) wordt er een MD5-hash
berekend van de huidige metadata van de blogpost (<code>md5($bp->toXML())</code>). Deze
wordt later gebruikt om te controleren of er iets gewijzigd is.</p>
<p>Daarna worden, net als bij een nieuw blogpost, titel, seo-titel, auteur-naam en
-emailadres gevraagd. Tot slot wordt ook van het <code>.markdown</code>-bestand een
MD5-hash berekend - en na het bewerken vergeleken met de nieuwe hash.</p>
<p>Als ofwel de metadata (het XML-bestand) of de inhoud (het Markdown-bestand) is
gewijzigd, wordt er een <code><edit></code>-node aan de metadata toegevoegd en
weggeschreven.</p>
<pre><code class="php">function editPost(&$blogs)
{
// ...
$bp = $blogs[$indexToEdit];
$oldXMLhash = md5($bp->toXML());
$prompt = sprintf(" Titel [%s]: ", $bp->getTitle());
$newTitle = '';
while (empty($newTitle)) {
// ...
}
$prompt = sprintf(" SEO-titel [%s]: ", $bp->getSeoTitle());
$newSeoTitle = '';
while (empty($newSeoTitle)) {
// ...
}
$proposalAuthorName = DEFAULT_AUTHOR_NAME;
$prompt = sprintf(" Auteur [%s]: ", $proposalAuthorName);
$newAuthorName = readline($prompt);
// ...
$proposalAuthorEmail = DEFAULT_AUTHOR_EMAIL;
$prompt = sprintf(" E-mail [%s]: ", $proposalAuthorEmail);
$newAuthorEmail = '';
while (empty($newAuthorEmail)) {
// ...
}
$bp->setTitle($newTitle);
$bp->setSeoTitle($newSeoTitle);
$prompt = " Start editor? [Y/n]: ";
$line = readline($prompt);
if (empty($line)) {
$line = 'y';
}
$mdFile = BLOGPATH.$bp->getID().'.markdown';
$oldMDhash = md5_file($mdFile);
if (trim(strtolower($line)) == 'y') {
$editOK = -1;
print " Starting editor...";
$cmd = EDITOR.' '.$mdFile.' 1>&2 2>/dev/null;';
system($cmd, $editOK);
}
$newMDhash = md5_file($mdFile);
$newXMLhash = md5($bp->toXML());
if ($newXMLhash != $oldXMLhash || $newMDhash != $oldMDhash) {
$bpe = new FW\BlogPostEdit($newAuthorName, $newAuthorEmail);
$bp->addEdit($bpe);
$xmlFile = BLOGPATH.$bp->getID().'.xml';
file_put_contents($xmlFile, $bp->toXML());
}
return;
}
</code></pre>
<h2>Verwijderen blogpost</h2>
<p>Mijn moeder zei vroeger: <q>Was'te voet wurps', bis'te kwiet</q>, wat zoveel
betekent als: als je iets weggooit, ben je het kwijt. Dit gaat zeker op voor
digitale inhoud zoals blogposts. Daarom heb ik ervoor gekozen om de XML- en
Markdown-bestanden niet te verwijderen, maar te hernoemen. Zo verdwijnen ze voor
de gebruiker uit het zicht, maar kunnen ze (bijvoorbeeld via FTP) moeiteloos
worden 'teruggetoverd'.</p>
<pre><code class="php">function deletePost(&$blogs)
{
// ...
$bp = $blogs[$indexToDelete];
$prompt = sprintf(
" Weet u zeker dat u \"%s\" wil verwijderen? [y/N]: ",
$bp->getTitle()
);
$line = '';
while (empty($line)) {
$line = readline($prompt);
}
if (trim(strtolower($line)) == 'y') {
$oldFile = BLOGPATH.$bp->getID().'.xml';
$newFile = BLOGPATH.$bp->getID().'.xml.deleted';
rename($oldFile, $newFile);
$oldFile = BLOGPATH.$bp->getID().'.markdown';
$newFile = BLOGPATH.$bp->getID().'.markdown.deleted';
rename($oldFile, $newFile);
}
return;
}
</code></pre>
<h2>Foutafhandeling</h2>
<p>Bij het verwerken van invoer van gebruiker hoort altijd een gezonde dosis
wantrouwen. Soms moet je als ontwikkelaar ook de gebruiker een handje helpen om
te zorgen dat hij/zij er geen puinhoop van maakt - of <em>kan</em> maken.</p>
<h3>Duplicaten</h3>
<pre><code class="php">while (empty(trim($newTitle))) {
$newTitle = readline($prompt);
if (postExists($blogs, null, $newTitle)) {
print " FOUT! Titel bestaat al. ".PHP_EOL;
$newTitle = '';
}
}
</code></pre>
<p>Bovenstaand stuk code controleert tijdens het invoeren van een nieuwe titel of
er al een blogpost met die titel bestaat. Deze logica heb ik centraal gelegd,
zodat ze ook kan worden hergebruikt bij SEO-titel:</p>
<pre><code class="php">function postExists(&$blogs, $idToExclude = null, $title = null, $seoTitle = null)
{
if (empty($title) && empty($seoTitle)) {
return false;
}
$found = array_filter(
$blogs,
function ($bp) use ($idToExclude, $title, $seoTitle) {
return
(empty($idToExclude) || $bp->getID() != $idToExclude) &&
(empty($title) || $bp->getTitle() == $title) &&
(empty($seoTitle) || $bp->getSeoTitle() == $seoTitle);
}
);
return count($found) > 0;
}
</code></pre>
<h3>E-mailadres</h3>
<p>Schreef en hergebruikte ik vroeger nog handmatig een RegEx voor het <a href="http://www.regular-expressions.info/email.html">valideren
van e-mailadressen</a>, neemt PHP mij dit werk vanaf versie 5.2 met
<a href="http://php.net/manual/en/function.filter-var.php"><code>filter_var</code></a> dankbaar uit handen:</p>
<pre><code class="php">if (!empty($newAuthorEmail)
&& false === filter_var($newAuthorEmail, FILTER_VALIDATE_EMAIL)
) {
print " FOUT! Geen geldig e-mailadres. ".PHP_EOL;
$newAuthorEmail = '';
}
</code></pre>
</div>2017-01-09T10:23:38+01:00https://www.fwiep.nl/blog/recursieve-jukebox-met-find-en-mplayerRecursieve jukebox met find en mplayer2017-01-06T15:23:34+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Laatst vulde ik een usb-stick met mp3-bestanden voor tijdens de volgende lange
autorit. Eenmaal gereed wilde ik er graag in willekeurige volgorde doorheen
luisteren om te horen of <a href="http://normalize.nongnu.org/"><code>normalize-audio</code></a> zijn werk goed had gedaan. Het
moet toch mogelijk zijn om <a href="https://www.gnu.org/software/findutils/"><code>find</code></a>, <a href="https://www.gnu.org/software/coreutils/manual/html_node/sort-invocation.html"><code>sort</code></a> en <a href="http://www.mplayerhq.hu/design7/news.html"><code>mplayer</code></a> aan elkaar
te knopen?</p>
<h2>Onderzoek (1)</h2>
<p>Als eerste zocht ik in de handleiding van mplayer (<code>man 1 mplayer</code>) naar een
optie om een map recursief uit te lezen; tevergeefs. Ergens anders had ik
gelezen over het programma <a href="https://www.gnu.org/software/findutils/manual/html_node/find_html/Invoking-xargs.html"><code>xargs</code></a>, waarmee je grote hoeveelheden
parameters aan programma's kunt doorgeven. Ik probeerde de volgende one-liner:</p>
<pre><code class="bash">find . -type f -print0 | sort -z -R | xargs -0 mplayer
</code></pre>
<p>Het werkte! <code>find</code> zoekt in de huidige map (<code>.</code>) naar bestanden (<code>-type f</code>) en
voert deze aan <code>sort</code>, die ze in een willekeurige volgorde (<code>-R</code>) plaatst.
Uiteindelijk geeft <code>xargs</code> de lijst door aan mplayer. Om problemen met
bestandsnamen met spaties of newline karakters te vermijden, gebruik ik de
opties <code>-print0</code>, <code>-z</code> en <code>-0</code>. Hierdoor worden de namen gescheiden door een
<code>null</code>-byte in plaats van een newline.</p>
<p>Maar wat was dat? De toetsenbordbediening van <code>mplayer</code> werkte niet meer! Ik kon
niets anders dan met <kbd>Ctrl</kbd> + <kbd>C</kbd> de uitvoer afbreken.</p>
<h2>Onderzoek (2)</h2>
<p>Mplayer heeft ook een <code>-playlist</code> optie, die als argument een lijst van af te
spelen bestanden verwacht. In een ver grijs verleden had ik ooit een Bash
subshell hiervoor gebruikt in de volgende vorm:</p>
<pre><code class="bash">mplayer -playlist <( find ${PWD} -type f | sort -R );
</code></pre>
<p>Dit werkt, en ook de toetsenbordbediening van mplayer functioneert naar behoren.
Maar, wat als je buiten de huidige map wil zoeken?</p>
<h2>Script</h2>
<p>Uiteindelijk wilde ik een functie die één optionele parameter accepteert: de map
waarin moet worden gezocht. Is deze parameter leeg, dan wordt de huidige map
(<code>${PWD}</code>) gebruikt.</p>
<p>Daarna wordt een tijdelijk bestand aangemaakt (<code>mktemp</code>), waarin <code>find</code>s uitvoer
in terecht komt. <code>sort -R</code> zorgt, net als in bovenstaand script, voor de
willekeurige volgorde.</p>
<p>Omdat <code>mplayer</code> niet per sé in dezelde map start als <code>find</code>, moeten de
bestandsnamen en paden in de afspeellijst absoluut zijn. Daarom wordt voor elke
treffer een <code>-exec readlik -f \;</code> uitgevoerd.</p>
<pre><code class="bash">function mus()
{
FOLDER="$1";
if [ "${FOLDER}"x = x ]; then
FOLDER="${PWD}";
fi
TMP="$( mktemp )";
find "${FOLDER}" -type f -exec readlink -f "{}" \; | sort -R > "${TMP}";
mplayer -playlist "${TMP}";
rm "${TMP}";
}
</code></pre>
<p>Ik heb nog geprobeerd om dit script zonder tijdelijk bestand, met behulp van een
subshell te maken. Dat werd me te lastig :-) Het blijkt behoorlijk moeilijk om
variabelen vanuit de hoofd- naar de subshell door te geven.</p>
<h2>Conclusie</h2>
<p>Het bovenstaand script heb ik aan mijn <code>~/.bashrc</code>-bestand toegevoegd. Daardoor
kan ik in elk terminal­venster of tekstconsole de functie <code>mus</code> oproepen; al
dan niet met de af te spelen map als argument.</p>
<pre><code class="bash">mus ~/Music
</code></pre>
</div>2017-01-06T15:23:34+01:00https://www.fwiep.nl/blog/doe-het-zelf-weblogDoe-Het-Zelf weblog (zonder database)2017-01-01T20:13:12+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Een programmeerproject bestaat, volgens mij, uit de volgende onderdelen:</p>
<ul>
<li>het denkwerk vooraf</li>
<li>de broncode</li>
<li>de bijbehorende documentatie</li>
</ul>
<p>Met name het laatste punt leek vroeger niet zo belangrijk. Hoofdzaak is, dat het
programma werkt; toch?</p>
<p><em>Nee dus</em>. Want als je zelf (of iemand anders) na een aantal maanden of jaren
opnieuw de code induikt om een aanpassing te maken of een fout op te zoeken, dan
kom je er zonder degelijke documentatie niet uit. Maar, geldt dat ook voor
kleine huis- tuin- en keukenprojectjes?</p>
<p><em>Ja dus</em>. Zelfs het analyseren van een simpel shell-scriptje kan uren kosten,
als er nergens wordt beschreven wat er gebeurt.</p>
<p>Aldus begon ik met het schrijven van een handleiding voor mijzelf, toen ik de
<a href="https://www.fwiep.nl/blog/huawei-y550-firmware-vervangen">firmware van mijn smartphone wilde vervangen</a>. Toen ik eindelijk klaar was,
bedacht ik dat ik deze tutorial graag met de rest van de wereld wilde delen, of
op zijn minst voor mijzelf op internet wilde vereeuwigen.</p>
<h2>Het plan</h2>
<p>Dan wordt het wel een project op zich, dat documenteren! In een ver grijs
verleden stonden er op <a href="https://www.fwiep.nl">www.fwiep.nl</a> al een klein aantal artikelen, maar
sinds ik van WordPress af ben gestapt, waren die niet meer toegankelijk. Toch
was dit de stijl waarin ik wilde gaan schrijven; een weblog, maar dan 'met de
hand'.</p>
<h2>De onderdelen</h2>
<p>De artikelen zouden deel gaan uitmaken van mijn website, maar ik wilde er geen
database aan spenderen. Het moest een zogenaamd <em>flatfile-CMS</em> worden. Ik zocht
naar een manier om met simpele tekstbestanden tóch met opmaak en semantiek te
kunnen werken. Uiteindelijk kwam ik uit bij <a href="https://michelf.ca/projects/php-markdown/extra">MarkdownExtra</a> - een formaat dat
voor mensen makkelijk lees- en schrijfbaar is, en dat zonder problemen naar HTML
kan worden vertaald.</p>
<p>Omdat ik houd van overzicht, besloot ik de metadata van elke post (zoals titel,
auteur, datum van publicatie en tags) in een apart bestand op te nemen. Ik koos
voor XML, omdat het zo'n mooi universeel formaat is en alle vrijheid geeft.
Bovendien is het goed te verwerken in andere talen en te valideren (als je een
bijpassend XML-schema (XSD) schrijft).</p>
<h2>Code</h2>
<p>Hieronder zal ik een deel van de code en logica achter dit weblog bespreken.</p>
<h3>XML voorbeeld</h3>
<p>Het onderstaande bestand is een voorbeeld van een weblog <code>XML</code>-bestand. Het
bevat de titel, de SEO-titel (die in hyperlinks wordt gebruikt), van elke
wijziging de auteur en een datumtijd, en tot slot nog een verzameling
steekwoorden (tags).</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<post xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="fwiepblog.xsd" xmlns="">
<title>Doe-Het-Zelf weblog (zonder database)</title>
<seoTitle>doe-het-zelf-weblog</seoTitle>
<edits>
<edit>
<author>
<name>Voornaam Achternaam</name>
<email>email@example.com</email>
</author>
<dateTime>2017-01-01T20:13:12+01:00</dateTime>
</edit>
</edits>
<tags>
<tag>php</tag>
<tag>markdown</tag>
<tag>xml</tag>
<tag>xsd</tag>
</tags>
</post>
</code></pre>
<h3>XSD schema</h3>
<p>Hieronder staat het XML-schema waarmee de XML-bestanden voor het weblog kunnen
worden gevalideerd. Een editor die schema's ondersteunt, kan eventuele fouten
direct tijdens het typen aangeven. Ook kun je dan vaak gebruik maken van
automatisch aanvullen (autocompletion).</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="post">
<xs:complexType>
<xs:sequence>
<xs:element name="title" type="xs:string"></xs:element>
<xs:element name="seoTitle" type="SEOTitle"></xs:element>
<xs:element name="edits" type="EditsType">
<xs:unique name="edit-unique">
<xs:selector xpath="edit"></xs:selector>
<xs:field xpath="./dateTime"></xs:field>
</xs:unique>
</xs:element>
<xs:element name="tags" type="TagsType">
<xs:unique name="tag-unique">
<xs:selector xpath="tag"></xs:selector>
<xs:field xpath="."></xs:field>
</xs:unique>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<!-- ==================================================================== -->
<xs:complexType name="AuthorType">
<xs:sequence>
<xs:element name="name" type="xs:string"></xs:element>
<xs:element name="email" type="EmailAddress"></xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="EditType">
<xs:sequence>
<xs:element name="author" type="AuthorType"></xs:element>
<xs:element name="dateTime" type="xs:dateTime"></xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="EditsType">
<xs:sequence minOccurs="1" maxOccurs="unbounded">
<xs:element name="edit" type="EditType"></xs:element>
</xs:sequence>
</xs:complexType>
<xs:simpleType name="TagType">
<xs:restriction base="xs:string">
<xs:minLength value="1"></xs:minLength>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="TagsType">
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="tag" type="TagType"></xs:element>
</xs:sequence>
</xs:complexType>
<xs:simpleType name="SEOTitle">
<xs:restriction base="xs:string">
<xs:pattern value="[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]"></xs:pattern>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="EmailAddress">
<xs:restriction base="xs:string">
<xs:pattern value="[a-zA-Z0-9][a-zA-Z0-9._%+-]{0,63}@([a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?\.){1,8}[a-zA-Z]{2,63}"></xs:pattern>
</xs:restriction>
</xs:simpleType>
<!-- ==================================================================== -->
</xs:schema>
</code></pre>
<h3>PHP code</h3>
<p>Met de onderstaande PHP-code wordt het bovengenoemde XML-bestand uitgelezen en
een reeks (array <code>$blogs</code>) gevuld voor later gebruik. De klassen <code>BlogPost</code>,
<code>BlogPostEdit</code> en <code>BlogTag</code> maken eenvoudige objecten met een aantal
eigenschappen (properties) die ik hier niet nader zal beschrijven.</p>
<p>Let tijdens het uitlezen van het XML-bestand op de validatie. Niet alleen wordt
de extensie van het bestand gecontroleerd, maar het bestand wordt ook nog
gevalideerd met behulp van het XML-schema (XSD). Hierdoor kan de rest van het
script er 100% op vertrouwen dat alle velden gevuld zijn en op de juiste positie
staan.</p>
<p>Met behulp van <a href="https://php.net/manual/en/class.domdocument.php">DOMDocument</a> en <a href="https://php.net/manual/en/class.domxpath.php">DOMXPath</a> (<a href="https://www.w3schools.com/xml/xpath_syntax.asp">syntax</a>) wordt de XML-data
uitgelezen en in de properties van de betreffende objecten opgeslagen.</p>
<pre><code class="php">$blogs = array();
$blogdir = 'path/to/xml-files';
$xml = new \DOMDocument();
if ($dir = opendir($blogdir)) {
while (false !== ($f = readdir($dir))) {
if (!is_dir($f) && pathinfo($f, PATHINFO_EXTENSION) == 'xml') {
if (@$xml->load($blogdir.$f)
&& @$xml->schemaValidate($blogdir.'fwiepblog.xsd')
) {
$xp = new \DOMXPath($xml);
$edits = $xp->query('./edits/edit');
$tags = $xp->query('./tags/tag');
$blog = new BlogPost();
$blog->setID(pathinfo($f, PATHINFO_FILENAME));
$blog->setTitle(
$xp->query('./title')->item(0)->nodeValue
);
$blog->setSeoTitle(
$xp->query('./seoTitle')->item(0)->nodeValue
);
foreach ($edits as $e) {
$edit = new BlogPostEdit();
$edit->setAuthorName(
$xp->query('./author/name', $e)->item(0)->nodeValue
);
$edit->setAuthorEmail(
$xp->query('./author/email', $e)->item(0)->nodeValue
);
$edit->setDateTime(
new \DateTime(
$xp->query('./dateTime', $e)->item(0)->nodeValue
)
);
$blog->addEdit($edit);
}
foreach ($tags as $t) {
$tag = new BlogTag($t->nodeValue);
$blog->addTag($tag);
}
$blogs[] = $blog;
}
}
}
closedir($dir);
}
</code></pre>
<h2>TODO</h2>
<p>Punten waar ik al aan gedacht heb, maar die nog verder denkwerk, informatie of
ondersteuning vragen:</p>
<ul>
<li>Een zoekfunctie op auteur, inhoud, datum van plaatsing, datum laatste
aanpassing of tags...</li>
<li>Een beheermogelijkheid vanuit het weblog zelf. Om een nieuw artikel aan te
maken, moet ik nu nog op de server (of lokaal) een shell-commando uitvoeren.
Het zou mooi zijn als dit via de 'voorkant' van de applicatie zou kunnen. Maar,
dan moet die functie ook degelijk worden afgeschermd...</li>
<li>Een zinvolle toepassing van de tags bij elke post. Denk daarbij bijvoorbeeld
aan een (toegankelijke) tagcloud of post-filtering...</li>
</ul>
</div>2017-01-01T20:13:12+01:00https://www.fwiep.nl/blog/dynamische-sitemap-met-xml-en-phpDynamische sitemap met XML en PHP2016-12-29T12:33:59+01:00Frans-Willem Postinfo@fwiep.nl<div><h2>Statisch</h2>
<p>Een <em>sitemap</em> is een XML-bestand waarmee websites geautomatiseerd kunnen
worden verkend door bijvoorbeeld een zoekmachine-bot. In dit bestand worden alle
pagina's (URL's) opgenoemd, eventueel voorzien van hun datum van laatste
wijziging. Een eenvoudig voorbeeld:</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>https://example.com/</loc>
</url>
<url>
<loc>https://example.com/contact</loc>
</url>
<url>
<loc>https://example.com/weblog</loc>
</url>
</urlset>
</code></pre>
<h2>Dynamisch</h2>
<p>Een eenvoudige sitemap bestaat uit statische URL's, pagina's waarvan het adres
nooit wijzigt. Maar, wat als je een aantal dynamische pagina's - zoals een
weblog - wil laten indexeren door de zoekmachines? Een script dat zo'n sitemap
produceert zou er bijvoorbeeld als volgt kunnen uitzien:</p>
<pre><code class="php"><?php
$blogs = ...
$urlPrefix = 'https://example.com/blog';
$doc = new \DOMDocument();
$doc->formatOutput = true;
$doc->encoding = 'UTF-8';
$root = $doc->createElement('urlset');
$root->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
$root->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
$root->setAttribute(
'xsi:schemaLocation',
'http://www.sitemaps.org/schemas/sitemap/0.9 '.
'http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd'
);
$root = $doc->appendChild($root);
$nodeUrl = $doc->createElement('url');
$nodeLoc = $doc->createElement('loc', $urlPrefix);
$nodeUrl->appendChild($nodeLoc);
$root->appendChild($nodeUrl);
foreach ($blogs as $blog) {
$nodeUrl = $doc->createElement('url');
$nodeLoc = $doc->createElement('loc', $urlPrefix.'/'.$blog->getSeoTitle());
$nodeUrl->appendChild($nodeLoc);
$nodeLastMod = $doc->createElement(
'lastmod',
$blog->getDateTime()->format(\DateTime::W3C)
);
$nodeUrl->appendChild($nodeLastMod);
$root->appendChild($nodeUrl);
}
header('Content-Type:application/xml');
print $doc->saveXML();
exit;
</code></pre>
<h2>Combinatie</h2>
<p>Tot slot kun je beide varianten ook nog combineren (zie hieronder). De eerste
<code>loc</code> verwijst naar een eenvoudig XML-bestand met alle statische pagina's
(home, contact…). De laatste <code>loc</code> verwijst naar het script dat een XML-bestand
produceert op het moment van opvragen.</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://example.com/sitemap-static.xml</loc>
</sitemap>
<sitemap>
<loc>https://example.com/sitemap.php</loc>
</sitemap>
</sitemapindex>
</code></pre>
<p>Dit bestand noem je <code>sitemap.xml</code> en meldt het aan bij bijvoorbeeld <a href="https://search.google.com/search-console/">Google
Webmaster Tools</a>. Vanaf dat moment zal Google zowel de statische als
dynamische pagina's van de website bezoeken en eventueel indexeren.</p>
</div>2016-12-29T12:33:59+01:00https://www.fwiep.nl/blog/website-downloaden-met-wgetWebsite downloaden met Wget2012-09-19T18:47:01+02:00Frans-Willem Postinfo@fwiep.nl<div><h2>Inleiding</h2>
<p>Soms is het handig om een website als geheel te downloaden om later,
bijvoorbeeld offline, te bekijken. Met <a href="https://www.gnu.org/software/wget/">GNU Wget</a> is dit geen probleem. Het
is beschikbaar voor de meeste platformen en maakt standaard deel uit van de
meeste GNU/Linux distributies.</p>
<h2>Script</h2>
<p>Onderstaand commando maakt een offline kopie van een bepaalde pagina met alle
bestanden die nodig zijn om de pagina te tonen.</p>
<pre><code class="bash">wget \
--html-extension \
--recursive \
--convert-links \
--mirror \
--page-requisites "http://www.example.com/"
</code></pre>
<p>De opties met dubbele mintekens hebben ieder ook een alternatief met een enkel
minteken:</p>
<pre><code class="bash">wget -E -r -k -m -p "http://www.example.com/"
</code></pre>
</div>2012-09-19T18:47:01+02:00