kdecherf ~ % Bloghttps://kdecherf.com/blog/index.xmlKevin Decherfhttps://kdecherf.com/HugoContent under license [CC BY-NC-SA 3.0](http://creativecommons.org/licenses/by-nc-sa/3.0/)2023-12-23T14:50:03ZRécupérer un article depuis le navigateur avec wallabaggerhttps://kdecherf.com/blog/2023/08/02/recuperer-un-article-depuis-le-navigateur-avec-wallabagger/2023-08-02T20:48:29Z2023-08-02T20:48:29Z<p>An english version of this post is available <a href="https://kdecherf.com/blog/2023/08/02/fetch-content-from-browser-on-demand-with-wallabagger/">here</a>.</p>
<p><a href="https://wallabag.github.io/wallabagger/">Wallabagger</a> est une extension de navigateur permettant de sauvegarder des
pages sur votre compte wallabag.</p>
<p>Depuis la version <a href="https://github.com/wallabag/wallabagger/releases/tag/v1.14.0">v1.14.0</a>, cette extension est capable de récupérer le contenu
de la page à sauvegarder depuis le navigateur, ce qui évite au serveur wallabag
de le récupérer de lui-même. Cela permet de contourner certaines limitations
comme un rendu dynamique ou un <em>paywall</em>.</p>
<p>Jusqu'à il y a peu je maintenais une intégration personnalisée sur mon serveur
wallabag pour faire la même chose avec l'extension WebScrapBook. Mais suite à
des changements majeurs importants j'ai abandonné l'idée de continuer à la
maintenir. J'ai alors décidé de ré-essayer la nouvelle option de wallabagger.</p>
<p>La fonctionnalité fait le boulot, mais maintenir une liste de domaines
spécifiques pour la récupération de contenu me semblait assez peu pratique.</p>
<p>La version <a href="https://github.com/wallabag/wallabagger/releases/tag/v1.16.0">v1.16.0</a> a ajouté une option permettant de récupérer le contenu
depuis le navigateur <strong>par défaut</strong>, permettant ainsi de se passer de la liste
de domaines. Cette fonctionnalité aussi comporte une certaine ridigité, puisque
c'est soit l'un soit l'autre.</p>
<p>Comment pourrais-je choisir facilement d'enregistrer un article en passant le
contenu depuis le navigateur ou pas ? Il y a quelques semaines j'ai décidé de
mettre les mains dans le code de l'extension. Pendant mon exploration quelque
chose a attiré mon attention dans le manifeste.</p>
<p>Il s'avère que deux raccourcis clavier sont définis par l'extension :</p>
<p><span class="md__image">
<img src="https://kdecherf.com/blog/2023/08/02/recuperer-un-article-depuis-le-navigateur-avec-wallabagger/firefox-shortcuts.png" alt="Capture d'écran de la gestion des raccourcis Firefox pour wallabagger" />
</span>
</p>
<p>Comprendre la différence entre les deux actions m'a permis de trouver une
solution simple à mon problème.</p>
<p>En considérant que l'option "<em>Récupérer le contenu depuis le navigateur pour
tous les sites</em>" est activée :</p>
<ul>
<li>"<em>Activer le bouton de la barre d'outils</em>" permet de sauvegarder une page en
récupérant son contenu depuis le navigateur</li>
<li>"<em>Enregistrer la page…sans ouvrir la popup</em>" en revanche va uniquement
envoyer le lien au serveur wallabag, le laissant récupérer le contenu</li>
</ul>
<p>En clair, utilisez <code>Alt+W</code> si vous souhaitez sauvegarder une page en prenant le
contenu depuis le navigateur ou <code>Alt+Shift+W</code> si vous voulez que le serveur le
récupère pour vous.</p>
<p>Pour changer ces raccourcis sur Firefox, suivez <a href="https://support.mozilla.org/fr/kb/gerer-raccourcis-extensions-firefox">ces instructions</a>. Chrome
et autres dérivés doivent probablement avoir un mécanisme similaire.</p>
<p><em>Enjoy</em></p>Fetch content from browser on-demand with wallabaggerhttps://kdecherf.com/blog/2023/08/02/fetch-content-from-browser-on-demand-with-wallabagger/2023-08-02T20:48:16Z2023-08-02T20:48:16Z<p>Une traduction française est disponible <a href="https://kdecherf.com/blog/2023/08/02/recuperer-un-article-depuis-le-navigateur-avec-wallabagger/">ici</a>.</p>
<p><a href="https://wallabag.github.io/wallabagger/">Wallabagger</a> is a browser extension that let's you save pages on your
wallabag account.</p>
<p>Starting from <a href="https://github.com/wallabag/wallabagger/releases/tag/v1.14.0">v1.14.0</a> the extension is able to take the content of the
page from the browser and send it to wallabag, so that the server does not have
to fetch the page by itself. This allows you to save pages that may fail
otherwise (<em>e.g. pages with heavy javascript or behind a paywall</em>).</p>
<p>Up until recently I've used a custom integration of mine to perform the same
thing using WebScrapBook<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>. But as the related browser extension had an
update with important breaking changes, I though it was too much work to fix it
and I gave a new try to wallabagger.</p>
<p>While I was satisfied by the results, putting specific domains in a list was
pretty inflexible.</p>
<p>The release of <a href="https://github.com/wallabag/wallabagger/releases/tag/v1.16.0">v1.16.0</a> added an option to fetch content from the browser
by default, giving the ability to ignore the domain list. This is pretty cool.
However I found it nearly as inflexible as the previous version, as I still
want to save some pages the old way.</p>
<p>How could I have the ability to easily chose between either fetching content
from the browser or not when saving a page through this extension? A few weeks
ago I started to dig into the source code. And while going through the
different files, something caught my attention in the extension manifest.</p>
<p>There are two shortcuts set and available for several actions:</p>
<p><span class="md__image">
<img src="https://kdecherf.com/blog/2023/08/02/fetch-content-from-browser-on-demand-with-wallabagger/firefox-shortcuts.png" alt="Firefox extension shortcuts view of wallabagger" />
</span>
</p>
<p>Understanding the difference between these two actions actually gave me a
workaroung for what I want.</p>
<p>Considering that the "<em>Retrieve content from the browser by default</em>" option is
enabled:</p>
<ul>
<li>"<em>Activate toolbar button</em>" will save a page by using content from the browser</li>
<li>"<em>Save the page into Wallabag without opening the popup</em>" will save a
page through the background worker and thus ignore the option, letting the
server fetching the content</li>
</ul>
<p>In short, when you're on a page you want to save on wallabag, use <code>Alt+W</code> to
fetch content from browser, otherwise use <code>Alt+Shift+W</code>.</p>
<p>Follow [these instructions] to change these shortcuts on Firefox. I guess
there's a similar way to change them on Chrome-based browsers.</p>
<p><em>Enjoy</em></p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://kdecherf.com/blog/2022/03/21/wallabag-vs-the-web-in-2022-the-poors-man-solution/">wallabag vs the web in 2022: the poor's man solution</a> <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>Repenser mon utilisation des réseaux sociauxhttps://kdecherf.com/blog/2022/11/28/repenser-mon-utilisation-des-reseaux-sociaux/2022-11-28T19:36:14Z2022-11-28T19:36:14Z<div class="alertbox info">
<strong>UPDATE 2022/12/29: </strong>
Vous pouvez me retrouver sur le fediverse à cette adresse : <a href="https://n.kdecherf.com/users/kdecherf">@kdecherf@n.kdecherf.com</a>.
</div>
<p>Il y a une dizaine d'années j'arrêtais d'aller sur Facebook et je supprimais
l'application de mon téléphone. À l'époque déjà je trouvais que mon utilisation
du réseau avait une tendance à devenir toxique.</p>
<p>En revanche je suis resté actif sur un réseau d'un autre genre : Twitter. J'y
suis depuis 13 ans et je l'utilise quotidiennement à plusieurs fins :</p>
<ul>
<li>Faire de la veille, technique comme générale</li>
<li>Découvrir du contenu qui vient de l'extérieur de mon réseau</li>
<li>Partager ce que je lis et ce qui m'intéresse<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></li>
<li>Echanger avec d'autres</li>
</ul>
<p>Cela fait quelques temps que je pense de plus en plus à mon utilisation de
ce réseau.</p>
<p>En premier lieu il y a l'idée que se reposer sur une plateforme privée de ce
type pour "produire du contenu" peut nous rendre vulnérable à une disparition
du contenu ou de la plateforme et peut rendre difficile la migration d'une
communauté vers une autre plateforme (<em>manque d'interopérabilité ou de
standard, nom d'utilisateur lié à l'adresse de la plateforme</em>). L'actualité
récente autour de la reprise du réseau par Elon Musk a peut-être accéléré mon
envie de migrer vers un outil qui me permettrait d'être plus indépendant.</p>
<p>À cela s'ajoute un autre aspect, peut-être plus subjectif : mon utilisation que
je fais de ce réseau est probablement devenue néfaste pour moi, à plusieurs
égards. À commencer par le fait que j'ouvre machinalement l'application pour
avoir mon shot de dopamine et de récompense immédiate, à tel point qu'Android
me fait savoir que je l'ouvre entre 50 et 70 fois et j'y passe un peu moins
d'une heure par jour, sans compter l'utilisation des clients web depuis un
ordinateur. Deux notions peuvent accompagner cette dérive addictive : le
syndrome du <em>Fear of Missing Out</em><sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> et le concept d'économie de
l'attention<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>.</p>
<p><span class="md__image">
<img src="https://kdecherf.com/blog/2022/11/28/repenser-mon-utilisation-des-reseaux-sociaux/android-screenshot.jpg" alt="" />
</span>
<em>Captures d'écran de Android Digital Wellbeing</em></p>
<p>Vient ensuite l'anxiété. Une anxiété qui peut venir de la prédominance
d'informations à tendance négative, particulièrement depuis deux ans. Mais une
anxiété qui vient peut-être avant tout de la surcharge informationnelle<sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> :
un excès d'information qui peut devenir nuisible.</p>
<p>Cette image résume une partie de mes pensées sur Twitter :
<a href="https://framablog.org/2022/11/18/29098/">https://framablog.org/2022/11/18/29098/</a></p>
<p>Ces constats étant posés, que fais-je maintenant ?</p>
<p>Beaucoup parlent de Mastodon, un réseau social permettant de fédérer des
instances indépendantes entre elles. Cet outil repose sur le concept de
<em>fediverse</em> et est articulé autour de <em>ActivityPub</em>, un protocole standardisé
permettant à différents réseaux sociaux de communiquer entre eux. C'est ainsi
qu'un utilisateur Mastodon peut suivre et commenter d'autres utilisateurs sur
des instances Pleroma, PeerTube ou encore PixelFed.</p>
<p>La philosophie portée par ces outils me parait intéressante, rejoignant en
partie l'objectif de départ du web. Cependant je trouve ces outils trop lourds
à maintenir pour un usage mono-utilisateur, sachant que j'aimerais héberger mon
compte sous mon propre nom de domaine. Je vais peut-être me diriger vers une
solution moins <em>user-friendly</em> mais tout de même compatible avec le reste du
<em>fediverse</em>, comme microblog.pub.</p>
<p>Pourquoi publier son compte sur son propre domaine ? Principalement pour être
indépendant de la plateforme technique utilisée en dessous, que ça concerne un
site perso, des emails ou un "micro-blog".</p>
<p>Un toot de ploum à ce sujet :</p>
<blockquote>
<p>Each new generation on the web needs to learn that there’s no such thing as a permanent web identity on a commercial web service.</p>
<p>The only long-term solution to maintain your identity is:</p>
<ol>
<li>your own domain name</li>
<li>Your own website/blog</li>
<li>Several backups</li>
</ol>
<p>Everything else is temporary. Your accounts on myspace, facebook, medium, twitter, google plus, youtube, tiktok, mastodon will one day disappear or become useless.</p>
<p>You don’t have a "community" on those websites. Only ephemeral discussions.<br>
— <a href="https://mamot.fr/@ploum/109364008893158786">https://mamot.fr/@ploum/109364008893158786</a></p>
</blockquote>
<p>L'idée d'être en possession de son site, de son contenu et de son nom de
domaine rejoint les fondamentaux de la communauté IndieWeb<sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>. Parmi ceux-ci,
on y retrouve le concept de POSSE<sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup> : <em>Publish (on your) Own Site, Syndicate
Elsewhere</em>, où même si vous décidez de publier du contenu sur une instance
Mastodon, Twitter ou un autre réseau social, le contenu originel se trouve
avant tout sur votre site.</p>
<p>Même si le changement d'outil devrait me permettre de conserver cette capacité
de partager des choses, le retrait d'un réseau actif et centralisé comme
Twitter risque de rendre plus difficile la veille et la découverte de contenus
extérieurs à mon réseau. À titre d'exemple, sur 7 800 articles sauvegardés sur
mon compte Wallabag, 2 000 proviennent de tweets soit 25%.</p>
<p>En attendant je vais peut-être réussir à ne plus rager en voyant passer des
threads Twitter, parce que je trouve ce format très inefficace et frustrant.</p>
<p><em>See you somewhere on the web.</em></p>
<p>Quelques citations et quelques liens en vrac pour compléter ce billet :</p>
<blockquote>
<p>At some level, I am relieved that Elon Musk is destroying Twitter. At
another, I am horrified that one of the main ways that I communicate on the
Internet is being destroyed<br>
— <a href="https://xeiaso.net/blog/rip-twitter">https://xeiaso.net/blog/rip-twitter</a></p>
</blockquote>
<blockquote>
<p>D’abord, je crois profondément que l’on ne convainc plus grand monde sur un
réseau social comme Twitter. Ce dernier n’est simplement pas designé pour
cela. Les limites de caractère, la nature des fonctions retweet et j’aime,
l’algorithme, etc. Tout est fait pour viraliser le clash plutôt que valoriser
l’échange constructif et apaisé. Ce n’est pas prêt de s’arranger, et ça peut
empirer.<br>
— <a href="https://louisderrac.com/2022/10/31/au-revoir-twitter/">https://louisderrac.com/2022/10/31/au-revoir-twitter/</a></p>
</blockquote>
<blockquote>
<p>Twitter. Unquestionably designed to maximize usage, with all of the cognitive
tricks some of the most clever scientists have ever engineered. I could write
a whole book about Twitter. The tl;dr is that I used Twitter for all of the
above (News, Work, Stock) as well as my primary means of interacting with
other people/”friends.” I didn’t often consciously think about how much it
messed me up to go from interacting with a large number of people every day
(working at Microsoft) to engaging with almost no one in person except [my
ex] and the kids. Over seven years, there were days at Telerik, Google, and
Microsoft where I didn’t utter a word for nine workday hours at a time.
That’s plainly not healthy, and Twitter was one crutch I tried to use to
mitigate that.<br>
— <a href="https://textslashplain.com/2022/11/17/thoughts-on-twitter/">https://textslashplain.com/2022/11/17/thoughts-on-twitter/</a></p>
</blockquote>
<blockquote>
<p>The original internet was set up decentralized, with the goal to be resilient
to failing parts and to attacks. A lot of this property has been undone by
re-centralisation: if AWS has an outage, half the internet goes down. When
Twitter is run into the ground by some Billionaire Chaos Monkey, the whole
world sees it's journalist and government communication break down. A
(lifted) ban on Twitter has actual effect on democracy and society<br>
— <a href="https://berk.es/2022/11/08/fediverse-inefficiencies/">https://berk.es/2022/11/08/fediverse-inefficiencies/</a></p>
</blockquote>
<ul>
<li><a href="https://berk.es/2022/11/08/fediverse-inefficiencies/">The Fediverse is Inefficient (but that's a good trade-off)</a></li>
<li><a href="https://atthis.link/blog/2021/rss.html">Why I Still Use RSS</a></li>
<li><a href="https://www.lemonde.fr/pixels/article/2019/12/27/chez-framasoft-des-chatons-pour-sortir-des-gafa_6024230_4408996.html">Pour lutter contre les GAFA, Framasoft veut aller plus loin dans la décentralisation du Web</a></li>
<li><a href="https://larrysanger.org/2019/06/declaration-of-digital-independence/">Declaration of Digital Independence</a></li>
<li><a href="https://ruben.verborgh.org/articles/redecentralizing-the-web/">Re-decentralizing the Web, for good this time</a></li>
<li><a href="https://www.vice.com/en/article/vbanny/we-should-replace-facebook-with-personal-websites">We Should Replace Facebook With Personal Websites</a></li>
<li><a href="https://www.theguardian.com/commentisfree/2018/dec/12/its-time-to-take-back-your-data-from-google-and-facebooks-server-farms?CMP=share_btn_tw">It's time to take back your data from Google and Facebook's server farms</a></li>
<li><a href="https://www.cnbc.com/2018/12/01/social-media-detox-christina-farr-quits-instagram-facebook.html">I quit Instagram and Facebook and it made me a lot happier — and that’s a big problem for social media companies</a></li>
<li><a href="https://www.simounet.net/nettoyer-son-compte-twitter/">Nettoyer son compte Twitter</a></li>
</ul>
<p>Les différents outils du <em>fediverse</em> cités dans l'article :</p>
<ul>
<li><a href="https://joinmastodon.org/">https://joinmastodon.org/</a></li>
<li><a href="https://pleroma.social/">https://pleroma.social/</a></li>
<li><a href="https://pixelfed.org/">https://pixelfed.org/</a></li>
<li><a href="https://joinpeertube.org/">https://joinpeertube.org/</a></li>
<li><a href="https://microblog.pub/">https://microblog.pub/</a></li>
</ul>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://kdecherf.com/blog/2018/04/29/sharing-links/">https://kdecherf.com/blog/2018/04/29/sharing-links/</a> <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:2">
<p><a href="https://fr.wikipedia.org/wiki/Syndrome_FOMO">https://fr.wikipedia.org/wiki/Syndrome_FOMO</a> <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:3">
<p><a href="https://fr.wikipedia.org/wiki/%C3%89conomie_de_l%27attention">https://fr.wikipedia.org/wiki/%C3%89conomie_de_l%27attention</a> <a href="#fnref:3" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:4">
<p><a href="https://fr.wikipedia.org/wiki/Surcharge_informationnelle">https://fr.wikipedia.org/wiki/Surcharge_informationnelle</a> <a href="#fnref:4" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:5">
<p><a href="https://indieweb.org/IndieWeb">https://indieweb.org/IndieWeb</a> <a href="#fnref:5" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:6">
<p><a href="https://indieweb.org/POSSE">https://indieweb.org/POSSE</a> <a href="#fnref:6" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>Having fun with RFC 1342 and "Q" encoded-words in mail headershttps://kdecherf.com/blog/2022/11/14/having-fun-with-rfc-1342-and-q-encoded-words-in-mail-headers/2022-11-14T14:05:00Z2022-11-14T14:05:00Z<p>A few months ago I went to investigate why some automated emails from our
platform eventually ended into the spam folder for a specific user.</p>
<p>While investigating we've finally found the peculiar case of reproduction. Were
marked as spam emails meeting all these criterias:</p>
<ul>
<li>They are sent to recipients hosted on Yahoo-owned services (<em>e.g. Yahoo Mail</em>)</li>
<li>They contain both a <code>@</code> and a non-ASCII character in the display name of the sender</li>
</ul>
<p>Using <code>Foo @® Bar <something@example.com></code> as the sender, our library sent an
email with the following From header:</p>
<pre tabindex="0"><code>From: =?utf-8?Q?Foo=20@=C2=AE=20Bar?= <something@example.com>
</code></pre><p>Wait, what's this format? We'll see in a moment because right now doing more
tests makes me notice that the issue can also be triggered by using this
display name as the recipient. Let's see how some well-known providers handle
this.</p>
<p>The next test is to send an email from Gmail and FastMail with a crafted
display name and see if it lands in the inbox of our Yahoo Mail user. For that,
I'll use the following text: <code>Kévin @ Blah —</code> (<em>last char being U+2014 EM
DASH</em>).</p>
<p>In both cases the email was correctly delivered to the inbox. Let's check what
was put in the To header.</p>
<p>First, what did FastMail send?</p>
<pre tabindex="0"><code>To: =?UTF-8?Q?K=C3=A9vin_=40_Blah_=E2=80=94?= <kdecherf@ymail.com>
</code></pre><p>And what did Gmail send?</p>
<pre tabindex="0"><code>To: =?UTF-8?B?S8OpdmluIEAgQmxhaCDigJQ=?= <something@ymail.com>
</code></pre><p>Wait, what's this? Have a seat and let me introduce you to RFC 1342<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> <em>aka</em>
<em>"Representation of Non-ASCII Text in Internet Message Headers"</em>.</p>
<p>Here is a short summary of this standard:</p>
<blockquote>
<p>RFC 1341 describes a mechanism for denoting textual body parts which are
coded in various character sets, as well as methods for encoding such body
parts as sequences of printable ASCII characters.</p>
<p>This memo describes similar techniques to allow the encoding of non-ASCII
text in various portions of a RFC 822<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> message header, in a manner which
is unlikely to confuse existing message handling software</p>
</blockquote>
<p>It defines two different encodings: "Q" for 'Quoted-printable' encoding and "B"
for Base 64 encoding.</p>
<p>Now, we know that Gmail is using "B" encoded-words, thus we'll ignore it for the
rest of our investigation.</p>
<p>Let's focus on the difference between what FastMail actually sent and what we
sent: the <code>@</code>. FastMail encoded it in the display name as part of the whole "Q"
encoded-phrase using <code>=40</code> whereas we've let the <code>@</code> untouched.</p>
<p>Interesting.</p>
<p>What happens if we remove the non-ASCII character from the display name?
Sending an email through our library results in the following encoding, without
deliverability issue:</p>
<pre tabindex="0"><code>From: "Foo @ Bar" <address...>
</code></pre><p>If we summarize, our library handles From/To header depending on the presence
of non-ASCII characters:</p>
<ul>
<li>If present, "Q" encode the phrase</li>
<li>Else, quote the phrase</li>
</ul>
<p>Can a plain <code>@</code> in a q encoded-word cause issue? What says the RFC?</p>
<p>Section <em>"Use of encoded-words in message headers"</em><sup id="fnref1:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> states:</p>
<blockquote>
<p>["Q" encoded-words can be used] As a replacement for a "word" entity within a
"phrase", for example, one that precedes an address in a From, To, or Cc
header. The EBNF definition for phrase from RFC 822 thus becomes:</p>
<p>phrase = 1*(encoded-word / word)</p>
<p>In this case the set of characters that may be used in a "Q"-encoded
encoded-word is restricted to: <upper and lower case ASCII letters, decimal
digits, "!", "*", "+", "-", "/", "=", and "_" (underscore, ASCII 95.)>.</p>
</blockquote>
<p>That being said, <code>@</code> is not considered as an accepted character in our case and
must be encoded as part of the q encoded-word.</p>
<p>So it looks like there was a bug.</p>
<p>And here we are, it is the end of yet another day reading a RFC; and I must
admit that I still enjoy learning weird things from thirty years old
standards<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><em>"Representation of Non-ASCII Text in Internet Message Headers"</em> <a href="https://datatracker.ietf.org/doc/html/rfc1342">https://datatracker.ietf.org/doc/html/rfc1342</a> <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a> <a href="#fnref1:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:2">
<p><em>"Standard for the format of ARPA Internet Text Message"</em> <a href="https://datatracker.ietf.org/doc/html/rfc822">https://datatracker.ietf.org/doc/html/rfc822</a> <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:3">
<p>Shit, I'm not even younger than this one <a href="#fnref:3" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>Importer des transactions Fortuneo dans HomeBank avec woobhttps://kdecherf.com/blog/2022/11/13/importer-des-transactions-fortuneo-dans-homebank-avec-woob/2022-11-13T19:02:17Z2022-11-13T19:02:17Z<p>Cela fait maintenant une dizaine d'années que j'utilise HomeBank pour suivre
régulièrement mes dépenses et mes budgets. J'ai déjà eu l'occasion d'en parler
par le passé, notamment pour importer l'historique de transactions PayPal<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>
ou encore gérer un historique de transactions avec un encours de carte bancaire
à débit différé<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>.</p>
<p>Me voilà de retour pour vous partager un script permettant d'importer un
historique de transactions Fortuneo, avec une nouveauté : l'utilisation de <code>woob</code><sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>
pour récupérer l'historique.</p>
<p>Sans <code>woob</code> les historiques seraient à récupérer en téléchargeant une à une les
archives zip depuis l'interface client de Fortuneo, et cela ne me motivait pas
tellement.</p>
<p>Ayant également une carte à débit différé auprès de cette banque, j'appelle
deux fois woob : une première fois pour exporter les transactions par carte à
venir (<em>encours carte, avec</em> <code>woob bank coming</code>) et la deuxième pour exporter
les transactions du compte courant (<code>woob bank history</code>). Je choisis également
d'en faire un export csv avec des champs sélectionnés, <code>rdate</code> pour la date de
transaction par carte et <code>date</code> pour les autres types de transaction.</p>
<pre tabindex="0"><code>woob bank -b fortuneo coming <accountnumber>@fortuneo -f csv -s "rdate,label,amount" > HistoriqueFortuneoComing.csv
woob bank -b fortuneo history <accountnumber>@fortuneo -f csv -s "date,label,amount" > HistoriqueFortuneoCourant.csv
</code></pre><p>Une fois les fichiers récupérés, nous pouvons nous servir du script awk suivant
pour rendre l'export compatible avec le format d'import<sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> de HomeBank :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-awk" data-lang="awk"><span class="line"><span class="cl"><span class="c1">#!/usr/bin/awk -f</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">BEGIN</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nb">FS</span><span class="o">=</span><span class="s2">";"</span>
</span></span><span class="line"><span class="cl"> <span class="nb">RS</span><span class="o">=</span><span class="s2">"\r\n"</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nb">NR</span> <span class="o">></span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="o">$</span><span class="mi">2</span> <span class="o">!~</span> <span class="sr">/^CARTE [0-9]{2}\/[0-9]{2}/</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">printf</span><span class="p">(</span><span class="s2">"%s;0;;%s;%s;%s;;\n"</span><span class="p">,</span> <span class="o">$</span><span class="mi">1</span><span class="p">,</span> <span class="o">$</span><span class="mi">2</span><span class="p">,</span> <span class="o">$</span><span class="mi">2</span><span class="p">,</span> <span class="o">$</span><span class="mi">3</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">buckets</span><span class="p">[</span><span class="o">$</span><span class="mi">1</span><span class="p">]</span> <span class="o">+=</span> <span class="o">$</span><span class="mi">3</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">END</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="nx">pos</span> <span class="o">in</span> <span class="nx">buckets</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">label</span> <span class="o">=</span> <span class="s2">"Cb Diff"</span>
</span></span><span class="line"><span class="cl"> <span class="kr">printf</span><span class="p">(</span><span class="s2">"%s;0;;%s;%s;%.2f;;\n"</span><span class="p">,</span> <span class="nx">pos</span><span class="p">,</span> <span class="nx">label</span><span class="p">,</span> <span class="nx">label</span><span class="p">,</span> <span class="nx">buckets</span><span class="p">[</span><span class="nx">pos</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><em>Note : comme pour le précédent article traitant d'une carte à débit différé,
je rajoute une logique chargée de fusionner tous les mouvements par carte
apparaissant sur le compte courant car ils correspondent au moment où l'encours
est appliqué.</em></p>
<p>Je fais une petite boucle pour passer sur tous les fichiers :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">for</span> i in HistoriqueFortuneo*csv <span class="p">;</span> <span class="k">do</span> hb-fortuneo.awk <span class="nv">$i</span> > fortuneo-<span class="nv">$i</span> <span class="p">;</span> <span class="k">done</span>
</span></span></code></pre></div><p>Les fichiers qui en résultant peuvent maintenant être importés dans HomeBank.</p>
<p><em>Enjoy!</em></p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://kdecherf.com/blog/2013/12/18/import-paypal-transaction-history-in-homebank/">https://kdecherf.com/blog/2013/12/18/import-paypal-transaction-history-in-homebank/</a> <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:2">
<p><a href="https://kdecherf.com/blog/2020/04/18/gerer-les-encours-de-cb-a-debit-differe-avec-homebank/">https://kdecherf.com/blog/2020/04/18/gerer-les-encours-de-cb-a-debit-differe-avec-homebank/</a> <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:3">
<p><a href="https://woob.tech/">https://woob.tech/</a> <a href="#fnref:3" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:4">
<p>Format de fichier CSV pour HomeBank <a href="http://homebank.free.fr/help/misc-csvformat.html">http://homebank.free.fr/help/misc-csvformat.html</a> <a href="#fnref:4" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>La petite gare sous la Gare de l'Esthttps://kdecherf.com/blog/2022/09/21/la-petite-gare-sous-la-gare-de-lest/2022-09-21T11:08:19Z2022-09-21T11:08:19Z<p>Défendre et faire connaître le chemin de fer. Voilà l'objectif affiché par
l'Association Française des Amis des Chemins de Fer, aussi appelée AFAC. Cette
association, fondée en 1929, est installée dans un local mis à disposition sous
la gare de l'Est à Paris.</p>
<p><span class="md__image">
<img src="https://kdecherf.com/blog/2022/09/21/la-petite-gare-sous-la-gare-de-lest/52024921306_240780ee31_c.jpg" alt="afac" />
</span>
</p>
<p>Outre la présence d'une bibliothèque et de membres permettant de s'instruire
sur l'univers ferroviaire, une petite pépite se cache dans ces locaux. Ou
plutôt trois. En effet, l'association maintient trois réseaux de chemins de fer
miniatures aux échelles HO<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, 0 et Im dans deux pièces dédiées.</p>
<p>La construction du réseau HO aurait débuté vers 1937, pour s'étaler sur une
dizaine d'années. Quelques améliorations sont apportées à la décoration au fil
du temps mais le gros du réseau n'a pas bougé depuis. On peut y voir une grande
variété d'éléments comme des postes d'aiguillage, des passages à niveau ou
encore des gares très détaillées, le tout avec 200 mètres de voirie ouverte à
la circulation.</p>
<p><span class="md__image">
<img src="https://kdecherf.com/blog/2022/09/21/la-petite-gare-sous-la-gare-de-lest/52024925106_44b802158e_c.jpg" alt="afac" />
</span>
</p>
<p><span class="md__image">
<img src="https://kdecherf.com/blog/2022/09/21/la-petite-gare-sous-la-gare-de-lest/52024957348_45d509acfc_c.jpg" alt="afac" />
</span>
</p>
<p>Les locomotives sont alimentées par le caténaire et par un troisième rail. Ce
dernier sert également pour le block automatique lumineux (BAL) assurant la
circulation automatique des trains et l'affichage de l'état des cantons sur les
signaux lumineux.</p>
<p><span class="md__image">
<img src="https://kdecherf.com/blog/2022/09/21/la-petite-gare-sous-la-gare-de-lest/52024956588_4c8d3725a6_c.jpg" alt="afac" />
</span>
</p>
<p>La deuxième salle est composée des deux autres réseaux à l'échelle 0, d'environ
200 mètres et I, bien plus petit.</p>
<p><span class="md__image">
<img src="https://kdecherf.com/blog/2022/09/21/la-petite-gare-sous-la-gare-de-lest/52023887802_bc3155a2b5_c.jpg" alt="afac" />
</span>
</p>
<p><span class="md__image">
<img src="https://kdecherf.com/blog/2022/09/21/la-petite-gare-sous-la-gare-de-lest/52025441485_98db81b59a_c.jpg" alt="afac" />
</span>
</p>
<p>Votre âme d'enfant vous signale une envie irrésistible de voir ces réseaux en
vrai ? Vous pouvez, le siège est ouvert au public tous les samedis de 14h30 à
18h. Pour vous y rendre, vous devrez descendre au parking P1 de la gare de
l'Est et sonner à la porte N°9.</p>
<p>D'autres photos sont visibles sur Flickr: <a href="https://www.flickr.com/photos/kdecherf/albums/72177720298342454/">https://www.flickr.com/photos/kdecherf/albums/72177720298342454/</a></p>
<p><strong>Aller plus loin</strong></p>
<ul>
<li><a href="https://www.ina.fr/ina-eclaire-actu/video/g1791552_001_014/mag-6-passionnes-de-trains-miniatures">Reportage France 3 Centre - Val de Loire, archive INA</a></li>
<li><a href="http://www.afac.asso.fr/index.php/menu-afac-siege">Page Siège, AFAC</a></li>
<li><a href="https://fr.wikipedia.org/wiki/Association_fran%C3%A7aise_des_amis_des_chemins_de_fer">AFAC, Wikipedia</a></li>
<li><a href="http://marc-andre-dubout.org/cf/lvdc/lvdc0228/asso1.htm">Une page perso sur l'AFAC</a></li>
</ul>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Pour les profanes comme moi, HO représente une échelle 1:87, 0 1:43.5 et I 1:32. <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>TSW2 : Conduire un TGV Duplex 200 sur la LGV Méditerranéehttps://kdecherf.com/blog/2022/08/10/tsw2-conduire-un-tgv-duplex-200-sur-la-lgv-mediterranee/2022-08-10T17:35:22Z2022-08-10T17:35:22Z<p>Au détour des offres gratuites hebdomadaires sur l'Epic Store je me suis
découvert voilà maintenant quelques mois une nouvelle passion pour… la conduite
de trains avec <em>Train Sim World 2</em>.</p>
<p>Je fais ce billet comme anti-sèche pour la conduite du TGV Duplex 200 sur la
ligne Marseille - Avignon avec l'extension <em>LGV Méditerranée: Marseille -
Avignon Route Add-On</em>.</p>
<p><span class="md__image">
<img src="https://kdecherf.com/blog/2022/08/10/tsw2-conduire-un-tgv-duplex-200-sur-la-lgv-mediterranee/tsw2-header-lgv.jpg" alt="Train Sim World 2: LGV Méditerranée" />
</span>
</p>
<h2 id="commandes-au-clavier">Commandes au clavier</h2>
<table>
<thead>
<tr>
<th>Opérations</th>
<th style="text-align:center"></th>
<th style="text-align:center"></th>
</tr>
</thead>
<tbody>
<tr>
<td>Interrupteur principal</td>
<td style="text-align:center"></td>
<td style="text-align:center"><code>Ctrl+W</code></td>
</tr>
<tr>
<td>Activer/désactiver KVB, TVM, Crocodile</td>
<td style="text-align:center"></td>
<td style="text-align:center"><code>Ctrl+Enter</code></td>
</tr>
<tr>
<td>Activer/désactiver VACMA</td>
<td style="text-align:center"></td>
<td style="text-align:center"><code>Shift+Enter</code></td>
</tr>
<tr>
<td>Activer/désactiver disjoncteur</td>
<td style="text-align:center"></td>
<td style="text-align:center"><code>Ctrl+P</code></td>
</tr>
<tr>
<td>Réarmer disjoncteur</td>
<td style="text-align:center"></td>
<td style="text-align:center"><code>Ctrl+Shift+P</code></td>
</tr>
<tr>
<td>Pantographe (<em>lever / baisser</em>)</td>
<td style="text-align:center"><code>P</code></td>
<td style="text-align:center"><code>Shift+P</code></td>
</tr>
<tr>
<td>Action porte (<em>gauche / droite</em>)</td>
<td style="text-align:center"><code>Y</code></td>
<td style="text-align:center"><code>U</code></td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th>Conduite</th>
<th style="text-align:center"></th>
<th style="text-align:center"></th>
</tr>
</thead>
<tbody>
<tr>
<td>Mode de conduite (+ / -)</td>
<td style="text-align:center"><code>Ctrl+R</code></td>
<td style="text-align:center"><code>Ctrl+Shift+R</code></td>
</tr>
<tr>
<td>Sélecteur de vitesse (<em>+ / -</em>)</td>
<td style="text-align:center"><code>R</code></td>
<td style="text-align:center"><code>F</code></td>
</tr>
<tr>
<td>Inverseur (<em>+ / -</em>)</td>
<td style="text-align:center"><code>Z</code></td>
<td style="text-align:center"><code>S</code></td>
</tr>
<tr>
<td>Manipulateur de traction (<em>+ / -</em>)</td>
<td style="text-align:center"><code>Q</code></td>
<td style="text-align:center"><code>D</code></td>
</tr>
<tr>
<td>Frein (<em>desserage / serrage</em>)</td>
<td style="text-align:center"><code>M</code></td>
<td style="text-align:center"><code>%</code></td>
</tr>
<tr>
<td>Frein d'urgence</td>
<td style="text-align:center"></td>
<td style="text-align:center"><code>Backslash</code></td>
</tr>
<tr>
<td>Ton (<em>bas / haut</em>)</td>
<td style="text-align:center"><code>Space</code></td>
<td style="text-align:center"><code>N</code></td>
</tr>
<tr>
<td>Alarme</td>
<td style="text-align:center"></td>
<td style="text-align:center"><code>B</code></td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th>Eclairage</th>
<th style="text-align:center"></th>
<th style="text-align:center"></th>
</tr>
</thead>
<tbody>
<tr>
<td>Luminosité des phares</td>
<td style="text-align:center"></td>
<td style="text-align:center"><code>H</code></td>
</tr>
<tr>
<td>Lumière (<em>cabine / pupitre</em>)</td>
<td style="text-align:center"><code>L</code></td>
<td style="text-align:center"><code>Ctrl+L</code></td>
</tr>
<tr>
<td>Eclairage des instruments</td>
<td style="text-align:center"></td>
<td style="text-align:center"><code>I</code></td>
</tr>
<tr>
<td>Intensité éclairage des instruments (<em>+ / -</em>)</td>
<td style="text-align:center"><code>Ctrl+I</code></td>
<td style="text-align:center"><code>Ctrl+Shift+I</code></td>
</tr>
<tr>
<td>Eclairage couloirs</td>
<td style="text-align:center"></td>
<td style="text-align:center"><code>K</code></td>
</tr>
<tr>
<td>Réglage essui-glace (<em>+ / -</em>)</td>
<td style="text-align:center"><code>V</code></td>
<td style="text-align:center"><code>Shift+V</code></td>
</tr>
<tr>
<td>Sablage, annuler</td>
<td style="text-align:center"><code>X</code></td>
<td style="text-align:center"><code>Shift+X</code></td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th>Divers</th>
<th style="text-align:center"></th>
<th style="text-align:center"></th>
</tr>
</thead>
<tbody>
<tr>
<td>Capture d'écran</td>
<td style="text-align:center"></td>
<td style="text-align:center"><code>Ctrl+F12</code></td>
</tr>
</tbody>
</table>
<p>Certaines commandes ne semblent pas disponibles au clavier, parmi lesquelles :</p>
<ul>
<li>Annuler le maintien de service</li>
<li>Sélectionner le mode d'alimentation de la ligne</li>
<li>Maintien du frein</li>
<li>Neutre frein</li>
</ul>
<h2 id="procédures">Procédures</h2>
<h3 id="démarrage">Démarrage</h3>
<pre tabindex="0"><code>- Appuyer sur le bouton Annuler le maintien de service
- Activer l'interrupteur principal (Ctrl+W)
- Sélectionner le mode d'alimentation correspondant à la ligne
- Lever le pantographe (P)
- Activer le disjoncteur (Ctrl+P)
- Armer le disjoncteur (Ctrl+Shift+P)
- Activer le maintien du frein
- Presser le frein d'urgence (Retour arrière)
- Activer le neutre frein
- Relâcher le frein d'urgence (Retour arrière)
- Désactiver le neutre frein
- Desserer le frein jusqu'à 5 bars (M)
- Serrer le frein jusqu'à 4 bars (%)
- Positionner l'inverseur sur marche avant (Z)
- Sélectionner le mode de conduite manuelle ou vitesse sélectionnée (Ctrl+R)
- Désactiver le maintien du frein
- Desserer le frein (M)
- Appliquer une traction (Q)
</code></pre><h3 id="changement-dalimentation">Changement d'alimentation</h3>
<pre tabindex="0"><code>- Positionner le manipulateur de traction sur Off (D)
- Ouvrir puis fermer le disjoncteur (Ctrl+P)
- Baisser le pantographe (Shift+P)
- Sélectionner le mode d'alimentation souhaité
- Au signal REV, lever le pantographe (P)
- Au signal embarqué panto, réarmer le disjoncteur (Ctrl+Shift+P)
- Appliquer une traction (Q)
</code></pre><h3 id="récupération-dun-freinage-durgence">Récupération d'un freinage d'urgence</h3>
<pre tabindex="0"><code>- Annuler l'alarme active (A)
- Positionner le manipulateur de traction sur Off (D)
- Positionner l'inverseur sur Neutre (S)
- Réarmer le disjoncteur (Ctrl+Shift+P)
- Positionner l'inverseur sur Marche avant (Z)
- Appliquer une légère traction (Q)
- Relâcher le frein (M)
</code></pre><h2 id="parcours">Parcours</h2>
<p>L'extension permet de rouler sur la ligne LGV entre les gares
Marseille-St-Charles et Avignon TGV, avec un arrêt possible à la gare de
Aix-en-Provence TGV.</p>
<p>Au départ de la gare de Marseille-St-Charles les limitations de vitesse ne sont
pas signalées, au delà des restrictions du système KVB. La vitesse limite passe
de 30 à 60 km/h aux alentours du PK 860.7 lorsque vous êtes en double-rames, à
110 au PK 860 puis à 140 au PK 858.7. A l'entrée de la ligne 752000 et du
changement d'alimentation, TVM-430 prend le relai pour afficher les limitations
en cabine.</p>
<p>Références :</p>
<ul>
<li><a href="https://media.dovetailgames.com/Train%20Sim%20World%202%20LGV%20M%C3%A9diterran%C3%A9e%20Driver%27s%20Manual%20EN.pdf">TSW2 LGV Méditerranée Driver Manual (EN)</a></li>
<li><a href="https://www.openrailwaymap.org/?style=maxspeed&lat=43.33182710828102&lon=5.3768205642700195&zoom=14">Max speed on OpenRailwayMap</a></li>
<li><a href="https://carto.graou.info/43.33082/5.3791/12.85105/0/0">Carto GRAOU</a></li>
</ul>GitLab CI: can an interruptible job corrupt its cache?https://kdecherf.com/blog/2022/03/28/gitlab-ci-can-an-interruptible-job-corrupt-its-cache/2022-03-28T14:22:19Z2022-03-28T14:22:19Z<p>We had a question at work about the <code>interruptible</code> keyword<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> in Gitlab CI
jobs. It lets Gitlab automatically cancel pipelines<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> if a more recent commit
triggers a new pipeline for the same branch.</p>
<p>On one of our projects we have an early job in the pipeline which builds a
cache of dependencies to speed up the other jobs. As the resulting cache may be
shared between different pipelines (<em>thanks to the lockfile key feature</em>), our
concern was: can we safely make this job interruptible without risking cache
corruption?</p>
<p>Unfortunately the GitLab documentation is unclear about what happens to the
different job phases when a cancel is received, so I decided to find the answer
by digging into the gitlab-runner source code. After struggling for hours
trying to understand its codebase, I decided to take a more offensive approach
by causing a corruption.</p>
<p>I spawned a dummy project, a S3 bucket and a gitlab runner on a Scaleway
instance with the <em>docker</em> executor. If you want to try it, here is the
<em>docker-compose</em> file:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s1">'3.3'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">runner</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">gitlab/gitlab-runner:v14.9.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">/var/run/docker.sock:/var/run/docker.sock</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">CI_SERVER_URL</span><span class="p">:</span><span class="w"> </span><span class="s1">'https://gitlab.com'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">CACHE_TYPE</span><span class="p">:</span><span class="w"> </span><span class="s1">'s3'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">CACHE_SHARED</span><span class="p">:</span><span class="w"> </span><span class="s1">'true'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">CACHE_S3_BUCKET_NAME</span><span class="p">:</span><span class="w"> </span><span class="s1">'sandbox-gitlab-runner-cache'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">CACHE_S3_SERVER_ADDRESS</span><span class="p">:</span><span class="w"> </span><span class="s1">'sandbox-gitlab-runner-cache.s3.fr-par.scw.cloud'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">CACHE_S3_BUCKET_LOCATION</span><span class="p">:</span><span class="w"> </span><span class="s1">'fr-par'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">CACHE_S3_ACCESS_KEY</span><span class="p">:</span><span class="w"> </span><span class="s1">'…'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">CACHE_S3_SECRET_KEY</span><span class="p">:</span><span class="w"> </span><span class="s1">'…'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">REGISTER_NON_INTERACTIVE</span><span class="p">:</span><span class="w"> </span><span class="s1">'true'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">REGISTER_RUN_UNTAGGED</span><span class="p">:</span><span class="w"> </span><span class="s1">'true'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">RUNNER_EXECUTOR</span><span class="p">:</span><span class="w"> </span><span class="s1">'docker'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">DOCKER_IMAGE</span><span class="p">:</span><span class="w"> </span><span class="s1">'ubuntu:20.04'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">DOCKER_PRIVILEGED</span><span class="p">:</span><span class="w"> </span><span class="s1">'false'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">DOCKER_DISABLE_CACHE</span><span class="p">:</span><span class="w"> </span><span class="s1">'true'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">REGISTRATION_TOKEN</span><span class="p">:</span><span class="w"> </span><span class="s1">'…'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd"> bash -c 'gitlab-runner register && sed -i -e "1s/^/log_level = \"debug\"\n/" /etc/gitlab-runner/config.toml && gitlab-runner run'</span><span class="w">
</span></span></span></code></pre></div><p>Then we create the following <code>.gitlab-ci.yml</code> configuration file:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">stages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">check</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">push</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">checksum</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">check</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">before_script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">mkdir -p .cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">find .cache -type f -exec sha1sum {} \;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">interruptible</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">cache</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">.cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">policy</span><span class="p">:</span><span class="w"> </span><span class="l">pull</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">push</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">before_script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">mkdir -p .cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">find .cache -type f -exec sha1sum {} \;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">dd if=/dev/urandom of=.cache/${CI_PIPELINE_ID} bs=1M count=64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">find .cache -type f -exec sha1sum {} \;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">interruptible</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">cache</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">.cache</span><span class="w">
</span></span></span></code></pre></div><p>In short, this file will create a pipeline with two jobs: the first will pull
the cache and print a SHA-1 sum of all its files while the latter will create a
new 64M file and add it to the cache. It will also print a SHA-1 sum of the
files in order to compare it with the next pipeline.</p>
<p>We can easily trigger pipeline auto-cancel by pushing empty commits, using <code>git commit --allow-empty -m "Empty commit" && git push</code>.</p>
<p>We can also make it easier to cancel a pipeline during the <em>cache-archiver</em>
phase by slowing down the upload speed (<em>e.g. 8 Mbps</em>) with the help of <em>tc</em>:</p>
<pre tabindex="0"><code>tc qdisc add dev ens2 root handle 1: htb default 12
tc class add dev ens2 parent 1: classid 1:10 htb rate 8192kbps
tc filter add dev ens2 protocol ip parent 1:0 prio 1 u32 match ip dst <bucket ip> flowid 1:10
</code></pre><p>We let some pipelines run completely to have a few files in the cache:</p>
<pre tabindex="0"><code>$ find .cache -type f -exec sha1sum {} \;
99748c2e426216f712baa9ef07e108aca21b4d76 .cache/501872479
f30f3d9ed363e397d64a774c7d501bd3eef7a8ad .cache/501873411
c497406e883e190876c6d89fb0bc42a85ac1c196 .cache/501869952
</code></pre><p>Then we cancel a running job during the upload phase; the script phase may
produce<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> an output like this:</p>
<pre tabindex="0"><code>$ find .cache -type f -exec sha1sum {} \;
dd2d1d2ecf655ba2a3223cdba413c5ed4f8cbb18 .cache/501874243
99748c2e426216f712baa9ef07e108aca21b4d76 .cache/501872479
f30f3d9ed363e397d64a774c7d501bd3eef7a8ad .cache/501873411
c497406e883e190876c6d89fb0bc42a85ac1c196 .cache/501869952
</code></pre><p>This output indicates that a fourth file was intended to integrate the cache
archive.</p>
<p>Finally we create a last pipeline to run the <em>check</em> job; it should show that
the archive does not contain the fourth file (<em><code>.cache/501874243</code> here</em>).
Runner debug logs show that it waits the end of the upload phase to cancel it
but does not seem to "commit" it on the distributed cache.</p>
<p>This tends to show that cancelling a job during the <code>cache-archiver</code> phase does
not corrupt the cache.</p>
<p>As a side note I would be more than happy to learn more about how this cancel
mechanism works if you are familiar with the <em>gitlab-runner</em> codebase.</p>
<p><em>Enjoy!</em></p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://docs.gitlab.com/ee/ci/yaml/index.html#interruptible">https://docs.gitlab.com/ee/ci/yaml/index.html#interruptible</a> <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:2">
<p><a href="https://docs.gitlab.com/ee/ci/pipelines/settings.html#auto-cancel-redundant-pipelines">https://docs.gitlab.com/ee/ci/pipelines/settings.html#auto-cancel-redundant-pipelines</a> <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn:3">
<p>Logs are not always uploaded to the GitLab server when jobs are cancelled <a href="#fnref:3" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>wallabag vs the web in 2022: the poor's man solutionhttps://kdecherf.com/blog/2022/03/21/wallabag-vs-the-web-in-2022-the-poors-man-solution/2022-03-21T12:16:59Z2022-03-21T12:16:59Z<div class="alertbox info">
<strong>Disclaimer: </strong>
I made this dirty implementation several months ago; while this blog post was
still a draft, someone implemented the missing feature in wallabagger and it
was shipped in
<a href="https://github.com/wallabag/wallabagger/releases/tag/v1.14.0">v1.14.0</a>.
However I've decided to publish this post anyway, for the record.
</div>
<p>This post is the first of a series about wallabag and the dirty things I do
with it.</p>
<p>It has been mostly 6 years since I started to use wallabag as my main tool to
read content on the web; and I quickly started contributing to it on my sparse
time. wallabag does not only save pages I want to read, it also saves me time
by avoiding all these pesky pop-ups, ads and other cookiewalls.</p>
<p>In order to do that, wallabag retrieves the content of an article by making a
basic HTTP call and parsing the HTML file it receives in response. This works
in most cases but it can be defeated by bot protection or if the page loads its
content using javascript, e.g. on Bloomberg or websites behind Cloudflare.</p>
<p>For a while I accepted this fate but lately I decided to find a workaround in
order to continue reading 'cleaned' articles.</p>
<p>The easiest way to achieve that is to save the content directly from the browser
as you surf through it. Okay, we know the destination but how do we go there?</p>
<p><a href="https://github.com/wallabag/wallabagger">Wallabagger</a> is a browser extension that lets you save pages to
your wallabag's account. <del>As of now it only takes the url of the active page and
sends it to wallabag, letting the server fetching the content.</del></p>
<p>The logical path would be to update the extension to add the ability to capture
and send the actual content in addition to the url.</p>
<p>Spoiler alert: I decided to go over another way; my willingness to do
JavaScript was pretty low 🙃. While searching for browser extensions capable of
saving pages, I came accross several interesting ones:</p>
<ul>
<li><a href="https://github.com/danny0838/webscrapbook">WebScrapBook</a></li>
<li><a href="https://github.com/gildas-lormeau/SingleFile">SingleFile</a></li>
</ul>
<p>They both save the content of the page locally and do it well; but WebScrapBook
has a feature that attracted my attention: the support of a backend to store
the saved content remotely. I found a <a href="https://github.com/danny0838/PyWebScrapBook">working backend implementation in
Python</a>.</p>
<p>A few hours later I had a very limited but working WebScrapBook backend API
support in wallabag. It basically relies on the import mechanism and
on <a href="https://github.com/j0k3r/graby/pull/274">a recent change</a> that landed in Graby.</p>
<p>Here is the raw patch:</p>
<p><details >
<summary markdown="span">Click to expand</summary>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gh">diff --git a/src/Wallabag/CoreBundle/Helper/ContentProxy.php b/src/Wallabag/CoreBundle/Helper/ContentProxy.php
</span></span></span><span class="line"><span class="cl"><span class="gh">index ed706680b..1c990c47c 100644
</span></span></span><span class="line"><span class="cl"><span class="gh"></span><span class="gd">--- a/src/Wallabag/CoreBundle/Helper/ContentProxy.php
</span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+++ b/src/Wallabag/CoreBundle/Helper/ContentProxy.php
</span></span></span><span class="line"><span class="cl"><span class="gi"></span><span class="gu">@@ -26,6 +26,7 @@ class ContentProxy
</span></span></span><span class="line"><span class="cl"><span class="gu"></span> protected $fetchingErrorMessage;
</span></span><span class="line"><span class="cl"> protected $eventDispatcher;
</span></span><span class="line"><span class="cl"> protected $storeArticleHeaders;
</span></span><span class="line"><span class="cl"><span class="gi">+ protected $offlineMode = false;
</span></span></span><span class="line"><span class="cl"><span class="gi"></span>
</span></span><span class="line"><span class="cl"> public function __construct(Graby $graby, RuleBasedTagger $tagger, RuleBasedIgnoreOriginProcessor $ignoreOriginProcessor, ValidatorInterface $validator, LoggerInterface $logger, $fetchingErrorMessage, $storeArticleHeaders = false)
</span></span><span class="line"><span class="cl"> {
</span></span><span class="line"><span class="cl"><span class="gu">@@ -50,11 +51,14 @@ class ContentProxy
</span></span></span><span class="line"><span class="cl"><span class="gu"></span> public function updateEntry(Entry $entry, $url, array $content = [], $disableContentUpdate = false)
</span></span><span class="line"><span class="cl"> {
</span></span><span class="line"><span class="cl"> $this->graby->toggleImgNoReferrer(true);
</span></span><span class="line"><span class="cl"><span class="gd">- if (!empty($content['html'])) {
</span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+ if (!empty($content['html']) && !$this->offlineMode) {
</span></span></span><span class="line"><span class="cl"><span class="gi"></span> $content['html'] = $this->graby->cleanupHtml($content['html'], $url);
</span></span><span class="line"><span class="cl"> }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> if ((empty($content) || false === $this->validateContent($content)) && false === $disableContentUpdate) {
</span></span><span class="line"><span class="cl"><span class="gi">+ if ($this->offlineMode) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $this->graby->setContentAsPrefetched($content['html']);
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi"></span> $fetchedContent = $this->graby->fetchContent($url);
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> $fetchedContent['title'] = $this->sanitizeContentTitle(
</span></span><span class="line"><span class="cl"><span class="gu">@@ -158,6 +162,10 @@ class ContentProxy
</span></span></span><span class="line"><span class="cl"><span class="gu"></span> }
</span></span><span class="line"><span class="cl"> }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gi">+ public function setOfflineMode(bool $offlineMode) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $this->offlineMode = $offlineMode;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi"></span> /**
</span></span><span class="line"><span class="cl"> * Helper to extract and save host from entry url.
</span></span><span class="line"><span class="cl"> */
</span></span><span class="line"><span class="cl"><span class="gh">diff --git a/src/Wallabag/ImportBundle/Controller/WebScrapBookController.php b/src/Wallabag/ImportBundle/Controller/WebScrapBookController.php
</span></span></span><span class="line"><span class="cl"><span class="gh"></span>new file mode 100644
</span></span><span class="line"><span class="cl"><span class="gh">index 000000000..4ab2ba6f2
</span></span></span><span class="line"><span class="cl"><span class="gh"></span><span class="gd">--- /dev/null
</span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+++ b/src/Wallabag/ImportBundle/Controller/WebScrapBookController.php
</span></span></span><span class="line"><span class="cl"><span class="gi"></span><span class="gu">@@ -0,0 +1,118 @@
</span></span></span><span class="line"><span class="cl"><span class="gu"></span><span class="gi">+<?php
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+namespace Wallabag\ImportBundle\Controller;
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
</span></span></span><span class="line"><span class="cl"><span class="gi">+use Symfony\Component\HttpFoundation\Request;
</span></span></span><span class="line"><span class="cl"><span class="gi">+use Symfony\Component\Routing\Annotation\Route;
</span></span></span><span class="line"><span class="cl"><span class="gi">+use Symfony\Component\HttpFoundation\JsonResponse;
</span></span></span><span class="line"><span class="cl"><span class="gi">+use JMS\Serializer\SerializationContext;
</span></span></span><span class="line"><span class="cl"><span class="gi">+use Symfony\Component\HttpFoundation\File\UploadedFile;
</span></span></span><span class="line"><span class="cl"><span class="gi">+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+class WebScrapBookController extends Controller
</span></span></span><span class="line"><span class="cl"><span class="gi">+{
</span></span></span><span class="line"><span class="cl"><span class="gi">+ private $token = "v3ry53cur3w0w";
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ /**
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * @Route("/wsb/")
</span></span></span><span class="line"><span class="cl"><span class="gi">+ */
</span></span></span><span class="line"><span class="cl"><span class="gi">+ public function indexAction(Request $request) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ switch ($request->query->get('a')) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ case 'config':
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $config = [
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "app" => [
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "name" => "WebScrapBook (Wallabag)",
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "theme" => "default",
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "locale" => "",
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "base" => "/import/wsb",
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "is_local" => true,
</span></span></span><span class="line"><span class="cl"><span class="gi">+ ],
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "book" => [
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "" => [
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "name" => "scrapbook",
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "top_dir" => "",
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "data_dir" => "",
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "tree_dir" => ".wsb/tree",
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "index" => ".wsb/tree/map.html",
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "no_tree" => true,
</span></span></span><span class="line"><span class="cl"><span class="gi">+ ],
</span></span></span><span class="line"><span class="cl"><span class="gi">+ ],
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "VERSION" => "0.44.1",
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "WSB_DIR" => ".wsb",
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "WSB_CONFIG" => "config.ini",
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "WSB_EXTENSION_MIN_VERSION" => "0.79.0",
</span></span></span><span class="line"><span class="cl"><span class="gi">+ ];
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return $this->sendResponse($config);
</span></span></span><span class="line"><span class="cl"><span class="gi">+ break;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ case 'token':
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return $this->sendResponse(password_hash($this->token, \PASSWORD_BCRYPT));
</span></span></span><span class="line"><span class="cl"><span class="gi">+ break;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ default:
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return $this->sendResponse(new \StdClass(), false);
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ /**
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * @Route("/wsb/{page}.html", name="wsb_get_page", requirements={"page"="\S+"}, methods={"GET"})
</span></span></span><span class="line"><span class="cl"><span class="gi">+ */
</span></span></span><span class="line"><span class="cl"><span class="gi">+ public function getPageAction(Request $request) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $obj = [
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "name" => $request->query->get('page') . ".html",
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "type" => null,
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "size" => null,
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "last_modified" => null,
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "mime" => "text/html",
</span></span></span><span class="line"><span class="cl"><span class="gi">+ ];
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return $this->sendResponse($obj);
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ /**
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * @Route("/wsb/{page}.html", name="wsb_save_page", requirements={"page"="\S+"}, methods={"POST"})
</span></span></span><span class="line"><span class="cl"><span class="gi">+ */
</span></span></span><span class="line"><span class="cl"><span class="gi">+ public function postPageAction(Request $request) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $uploadedFile = $request->files->get('upload');
</span></span></span><span class="line"><span class="cl"><span class="gi">+ if (!($uploadedFile instanceof UploadedFile)) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ throw new BadRequestHttpException();
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $wsb = $this->get('wallabag_import.wsb.import');
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $wsb->setUser($this->getUser());
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $i = $wsb->setFilepath($uploadedFile->getPathname())->import();
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ if (false === $i) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ throw new BadRequestHttpException();
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return $this->sendResponse("Command run successfully.");
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ /**
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * @Route("/wsb/{path}")
</span></span></span><span class="line"><span class="cl"><span class="gi">+ */
</span></span></span><span class="line"><span class="cl"><span class="gi">+ public function catchAllAction() {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return $this->sendResponse(new \StdClass());
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ /**
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * Shortcut to send data serialized in json.
</span></span></span><span class="line"><span class="cl"><span class="gi">+ *
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * @param mixed $data
</span></span></span><span class="line"><span class="cl"><span class="gi">+ *
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * @return JsonResponse
</span></span></span><span class="line"><span class="cl"><span class="gi">+ */
</span></span></span><span class="line"><span class="cl"><span class="gi">+ protected function sendResponse($data, $success = true)
</span></span></span><span class="line"><span class="cl"><span class="gi">+ {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ // https://github.com/schmittjoh/JMSSerializerBundle/issues/293
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $context = new SerializationContext();
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $context->setSerializeNull(true);
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $obj = [
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "success" => $success,
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "data" => $data,
</span></span></span><span class="line"><span class="cl"><span class="gi">+ ];
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $json = $this->get('jms_serializer')->serialize($obj, 'json', $context);
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return (new JsonResponse())->setJson($json);
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+}
</span></span></span><span class="line"><span class="cl"><span class="gi"></span><span class="gh">diff --git a/src/Wallabag/ImportBundle/Import/WebScrapBookImport.php b/src/Wallabag/ImportBundle/Import/WebScrapBookImport.php
</span></span></span><span class="line"><span class="cl"><span class="gh"></span>new file mode 100644
</span></span><span class="line"><span class="cl"><span class="gh">index 000000000..0c7cd5496
</span></span></span><span class="line"><span class="cl"><span class="gh"></span><span class="gd">--- /dev/null
</span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+++ b/src/Wallabag/ImportBundle/Import/WebScrapBookImport.php
</span></span></span><span class="line"><span class="cl"><span class="gi"></span><span class="gu">@@ -0,0 +1,162 @@
</span></span></span><span class="line"><span class="cl"><span class="gu"></span><span class="gi">+<?php
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+namespace Wallabag\ImportBundle\Import;
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+use Wallabag\CoreBundle\Entity\Entry;
</span></span></span><span class="line"><span class="cl"><span class="gi">+use Wallabag\CoreBundle\Event\EntrySavedEvent;
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+class WebScrapBookImport extends AbstractImport
</span></span></span><span class="line"><span class="cl"><span class="gi">+{
</span></span></span><span class="line"><span class="cl"><span class="gi">+ protected $filepath;
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ /**
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * {@inheritdoc}
</span></span></span><span class="line"><span class="cl"><span class="gi">+ */
</span></span></span><span class="line"><span class="cl"><span class="gi">+ public function getName()
</span></span></span><span class="line"><span class="cl"><span class="gi">+ {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return 'WebScrapBook';
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ /**
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * {@inheritdoc}
</span></span></span><span class="line"><span class="cl"><span class="gi">+ */
</span></span></span><span class="line"><span class="cl"><span class="gi">+ public function getUrl()
</span></span></span><span class="line"><span class="cl"><span class="gi">+ {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return 'import_wsb';
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ /**
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * {@inheritdoc}
</span></span></span><span class="line"><span class="cl"><span class="gi">+ */
</span></span></span><span class="line"><span class="cl"><span class="gi">+ public function getDescription()
</span></span></span><span class="line"><span class="cl"><span class="gi">+ {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return 'import.wsb.description';
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ /**
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * {@inheritdoc}
</span></span></span><span class="line"><span class="cl"><span class="gi">+ */
</span></span></span><span class="line"><span class="cl"><span class="gi">+ public function validateEntry(array $importedEntry)
</span></span></span><span class="line"><span class="cl"><span class="gi">+ {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ if (empty($importedEntry['uri'])) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return false;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return true;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ /**
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * {@inheritdoc}
</span></span></span><span class="line"><span class="cl"><span class="gi">+ */
</span></span></span><span class="line"><span class="cl"><span class="gi">+ protected function prepareEntry(array $entry = [])
</span></span></span><span class="line"><span class="cl"><span class="gi">+ {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $data = [
</span></span></span><span class="line"><span class="cl"><span class="gi">+ 'title' => false,
</span></span></span><span class="line"><span class="cl"><span class="gi">+ 'html' => $entry['html'],
</span></span></span><span class="line"><span class="cl"><span class="gi">+ 'url' => $entry['url'],
</span></span></span><span class="line"><span class="cl"><span class="gi">+ 'is_archived' => false,
</span></span></span><span class="line"><span class="cl"><span class="gi">+ 'is_starred' => false,
</span></span></span><span class="line"><span class="cl"><span class="gi">+ 'tags' => '',
</span></span></span><span class="line"><span class="cl"><span class="gi">+ ];
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ if (\array_key_exists('tags', $entry) && '' !== $entry['tags']) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $data['tags'] = $entry['tags'];
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return $data;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ /**
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * {@inheritdoc}
</span></span></span><span class="line"><span class="cl"><span class="gi">+ */
</span></span></span><span class="line"><span class="cl"><span class="gi">+ public function import()
</span></span></span><span class="line"><span class="cl"><span class="gi">+ {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ if (!file_exists($this->filepath) || !is_readable($this->filepath)) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $this->logger->error('WebScrapBookImport: unable to read file', ['filepath' => $this->filepath]);
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return false;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $data = file_get_contents($this->filepath);
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ if (empty($data)) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $this->logger->error('WebScrapBookImport: empty content');
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return false;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ if (!($source = $this->getScrapSource($data))) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $this->logger->error('WebScrapBookImport: scrap source not found');
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return false;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $entry = $this->parseEntry([
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "url" => $source,
</span></span></span><span class="line"><span class="cl"><span class="gi">+ "html" => $data,
</span></span></span><span class="line"><span class="cl"><span class="gi">+ ]);
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return null !== $entry;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ private function getScrapSource($data) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ if (!preg_match('/data-scrapbook-source="([^\"]+)"/i', $data, $scrap)) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return false;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return array_key_exists(1, $scrap) ? $scrap[1] : false;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ /**
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * {@inheritdoc}
</span></span></span><span class="line"><span class="cl"><span class="gi">+ */
</span></span></span><span class="line"><span class="cl"><span class="gi">+ public function parseEntry(array $importedEntry)
</span></span></span><span class="line"><span class="cl"><span class="gi">+ {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ /* $existingEntry = $this->em
</span></span></span><span class="line"><span class="cl"><span class="gi">+ ->getRepository('WallabagCoreBundle:Entry')
</span></span></span><span class="line"><span class="cl"><span class="gi">+ ->findByUrlAndUserId($importedEntry['url'], $this->user->getId());
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ if (false !== $existingEntry) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ ++$this->skippedEntries;
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ } */
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $data = $this->prepareEntry($importedEntry);
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $entry = new Entry($this->user);
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $entry->setUrl($data['url']);
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $entry->setTitle($data['title']);
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ // update entry with content (in case fetching failed, the given entry will be return)
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $this->contentProxy->setOfflineMode(true);
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $this->fetchContent($entry, $data['url'], $data);
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $this->em->persist($entry);
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $this->em->flush();
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $this->eventDispatcher->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return $entry;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ /**
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * {@inheritdoc}
</span></span></span><span class="line"><span class="cl"><span class="gi">+ */
</span></span></span><span class="line"><span class="cl"><span class="gi">+ protected function setEntryAsRead(array $importedEntry)
</span></span></span><span class="line"><span class="cl"><span class="gi">+ {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $importedEntry['is_archived'] = 1;
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return $importedEntry;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ /**
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * Set file path to the json file.
</span></span></span><span class="line"><span class="cl"><span class="gi">+ *
</span></span></span><span class="line"><span class="cl"><span class="gi">+ * @param string $filepath
</span></span></span><span class="line"><span class="cl"><span class="gi">+ */
</span></span></span><span class="line"><span class="cl"><span class="gi">+ public function setFilepath($filepath)
</span></span></span><span class="line"><span class="cl"><span class="gi">+ {
</span></span></span><span class="line"><span class="cl"><span class="gi">+ $this->filepath = $filepath;
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ return $this;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span><span class="line"><span class="cl"><span class="gi">+}
</span></span></span><span class="line"><span class="cl"><span class="gi"></span><span class="gh">diff --git a/src/Wallabag/ImportBundle/Resources/config/services.yml b/src/Wallabag/ImportBundle/Resources/config/services.yml
</span></span></span><span class="line"><span class="cl"><span class="gh">index d824da4ab..5a7895e07 100644
</span></span></span><span class="line"><span class="cl"><span class="gh"></span><span class="gd">--- a/src/Wallabag/ImportBundle/Resources/config/services.yml
</span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+++ b/src/Wallabag/ImportBundle/Resources/config/services.yml
</span></span></span><span class="line"><span class="cl"><span class="gi"></span><span class="gu">@@ -119,6 +119,18 @@ services:
</span></span></span><span class="line"><span class="cl"><span class="gu"></span> tags:
</span></span><span class="line"><span class="cl"> - { name: wallabag_import.import, alias: chrome }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gi">+ wallabag_import.wsb.import:
</span></span></span><span class="line"><span class="cl"><span class="gi">+ class: Wallabag\ImportBundle\Import\WebScrapBookImport
</span></span></span><span class="line"><span class="cl"><span class="gi">+ arguments:
</span></span></span><span class="line"><span class="cl"><span class="gi">+ - "@doctrine.orm.entity_manager"
</span></span></span><span class="line"><span class="cl"><span class="gi">+ - "@wallabag_core.content_proxy"
</span></span></span><span class="line"><span class="cl"><span class="gi">+ - "@wallabag_core.tags_assigner"
</span></span></span><span class="line"><span class="cl"><span class="gi">+ - "@event_dispatcher"
</span></span></span><span class="line"><span class="cl"><span class="gi">+ calls:
</span></span></span><span class="line"><span class="cl"><span class="gi">+ - [ setLogger, [ "@logger" ]]
</span></span></span><span class="line"><span class="cl"><span class="gi">+ tags:
</span></span></span><span class="line"><span class="cl"><span class="gi">+ - { name: wallabag_import.import, alias: wsb }
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi"></span> wallabag_import.command.import:
</span></span><span class="line"><span class="cl"> class: Wallabag\ImportBundle\Command\ImportCommand
</span></span><span class="line"><span class="cl"> tags: ['console.command']
</span></span></code></pre></div>
</details></p>
<p>As you can see, there is a hardcoded token which acts as a password for
WebScrapBook. It also means that this only works for single-user instances.</p>
<p>Now that wallabag can act as a WebScrapBook remote backend, we configure the
browser extension accordingly:</p>
<p><span class="md__image">
<img src="https://kdecherf.com/blog/2022/03/21/wallabag-vs-the-web-in-2022-the-poors-man-solution/screenshot.png" alt="screenshot" />
</span>
</p>
<p>The address is your wallabag's address with <code>/import/wsb/</code> appended. Use your
account's email address as the user and the token you set earlier as the
password. You can also change some settings if you want to handle images, fonts
and other things in a specific way.</p>
<p>Now, when you hit <em>"Capture tabs"</em>, WebScrapBook grabs the loaded content and
saves it to wallabag.</p>
<p>This solution is not bulletproof however; up until now I encountered two
drawbacks:</p>
<ol>
<li>Some websites alter the page content using JavaScript after initial loading,
baring the browser extension from grabbing the page fully. "Capture tabs
(source)" can fix this but not always</li>
<li>Objects like iframes and SVG are still missing in the saved entry (<em>and it
really annoys me</em>)</li>
</ol>
<p>Anyway, here we are, with a dirty but working solution to capture pages that
can't be normally saved by wallabag.</p>
<p>I guess you're telling yourself that adding the missing feature to wallabagger
would probably be more relevant. I will be honest: I won't take time to do it.
<del>But if you want to contribute, feel free to open a PR on its repository.</del><br>
<em>Edit: an equivalent feature has been released in last wallabagger version.</em></p>
<p><del>Regarding the WebScrapBook backend support in wallabag, I'm considering it for
inclusion in upstream. Feel free to give your feedback on GitHub, Twitter or
Reddit about it.</del><br>
<em>Edit: now that wallabagger is able to do the same thing, I don't think it's
relevant to push this implementation upstream.</em></p>
<p>Last but not least, I would be pleased to hear you if you have ideas or already
worked on a generic way to capture embedded contents like SVGs (<em>e.g. charts on
medias like The Guardian</em>) in a way that would fit wallabag.</p>
<p><em>Enjoy!</em></p>Imprimer un timbre sur une enveloppehttps://kdecherf.com/blog/2022/02/17/imprimer-un-timbre-sur-une-enveloppe/2022-02-17T19:12:24Z2022-02-17T19:12:24Z<p>Bien qu'on soit en 2022 il m'arrive encore quelques fois de devoir envoyer des
papiers par courrier postal. J'ai pour habitude d'avoir un carnet de timbres à
disposition pour ce cas d'usage, sauf aujourd'hui.</p>
<p>Plutôt que de sortir acheter un nouveau carnet de timbres, je me suis motivé à
experimenter l'achat de timbres à imprimer soi-même. Ce service de La Poste
existe depuis 13 ans et, outre le côté pratique, il permet également d'obtenir
des timbres à un tarif légèrement inférieur au prix des timbres en carnet.</p>
<p>N'ayant pas de plaquettes d'autocollants pour imprimante mais ayant une
imprimante (<em>Canon PIXMA MX 495</em>) supportant le chargement d'enveloppes, je
suis parti sur l'achat d'un timbre à imprimer directement sur l'enveloppe. Le
service en soi est très simple d'utilisation, vous choisissez le type
d'enveloppe (<em>DL dans mon cas, le classique 220x110</em>), les adresses et vous
validez votre panier. Vous téléchargez alors un PDF à imprimer. Vous avez même
accès à un spécimen si vous souhaitez faire des tests d'impression.</p>
<p><span class="md__image">
<img src="https://kdecherf.com/blog/2022/02/17/imprimer-un-timbre-sur-une-enveloppe/specimen.jpg" alt="" />
</span>
</p>
<p>L'impression, c'est là que l'aventure commence. Un petit tour sur internet pour
vérifier comment je charge l'enveloppe puis je décide d'imprimer le specimen
après avoir soigneusement configuré les options d'impression pour du papier au
format DL. Et là c'est le drame, une marge mystérieuse coupe une partie du
contenu :</p>
<p><span class="md__image">
<img src="https://kdecherf.com/blog/2022/02/17/imprimer-un-timbre-sur-une-enveloppe/print-dl.jpg" alt="" />
</span>
</p>
<p>Après des recherches infructueuses et des tests, j'ai fini par trouver une
solution de contournement : personnaliser la taille du papier pour indiquer les
mêmes dimensions que le papier DL (<em>110 mm de largeur pour 220 mm de hauteur</em>)
en laissant l'orientation en mode portrait. Dans le panier de l'imprimante
l'enveloppe semble se charger ouverture vers la doite.</p>
<p><span class="md__image">
<img src="https://kdecherf.com/blog/2022/02/17/imprimer-un-timbre-sur-une-enveloppe/properties-custom.jpg" alt="" />
</span>
</p>
<p>A ce moment l'aperçu semble meilleur :</p>
<p><span class="md__image">
<img src="https://kdecherf.com/blog/2022/02/17/imprimer-un-timbre-sur-une-enveloppe/print-custom.jpg" alt="" />
</span>
</p>
<p>J'imprime, tout est bon. Bon voyage petit courrier.</p>