From 420ceb49b53a8fa9d8ba8443e42e50cd7bd7cea9 Mon Sep 17 00:00:00 2001 From: Philip Wittamore Date: Sun, 12 Jul 2026 11:08:50 +0200 Subject: update --- README.md | 7 + articles/2021/Alacritty.html | 37 + articles/2021/Arch-LTS-kernel.html | 45 + articles/2021/Arch-Linux.html | 36 + .../Biblioth\303\250que-Nationale-de-France.html" | 27 + "articles/2021/Bol-en-ch\303\252ne-de-28cm.html" | 28 + articles/2021/Boxe-pour-chevaux-jouets.html | 26 + .../Ce-que-l'Europe-\303\240-fait-pour-moi.html" | 37 + articles/2021/Certbot-delete.html | 30 + articles/2021/Coupure-microphone-X220.html | 53 + .../2021/Deux-maisons-de-poup\303\251es.html" | 27 + "articles/2021/Exp\303\251riment.html" | 28 + .../Fabriquez-votre-propre-ordinateur-portable.htm | 53 + articles/2021/Fugitive-Telemetry.html | 29 + articles/2021/GeoIP2-et-Goaccess.html | 33 + "articles/2021/Grande-vase-en-ch\303\252ne.html" | 29 + articles/2021/Intel-Thermald.html | 46 + ...s---une-interface-alternative-pour-Youtube.html | 28 + "articles/2021/Jitsi-vid\303\251oconference.html" | 37 + articles/2021/La-grande-illusion.html | 28 + .../2021/Le-Jardin-de-Tonton-Lo\303\257c.html" | 28 + .../Lnav---ma\303\256triser-vos-journaux.html" | 36 + articles/2021/Mbsync-push.html | 34 + ...ec-but\303\251e-d\342\200\231arr\303\252t.html" | 27 + ...ur-sur-la-r\303\251gulation-du-processeur.html" | 79 + articles/2021/Nerdfonts-in-i3.html | 34 + articles/2021/Nginx-permission-denied.html | 32 + .../2021/Nouvel-\303\251tabli-pour-l'atelier.html" | 28 + articles/2021/Odysee.html | 36 + articles/2021/Photo-de-mon-atelier.html | 27 + ...-\303\240-d\303\251couper-en-bois-de-bout.html" | 26 + articles/2021/Project-Hail-Mary.html | 28 + articles/2021/Pyradio.html | 30 + .../2021/Scie-\303\240-ruban-fait-maison-(1).html" | 31 + .../2021/Scie-\303\240-ruban-fait-maison-(2).html" | 27 + .../2021/Scie-\303\240-ruban-fait-maison-(3).html" | 26 + .../2021/Scie-\303\240-ruban-fait-maison-(4).html" | 28 + .../2021/Scie-\303\240-ruban-fait-maison-(5).html" | 26 + .../2021/Scie-\303\240-ruban-fait-maison-(6).html" | 30 + articles/2021/Science-et-Vie.html | 27 + ...Sous-plat-avec-incrustation-epoxy-Milliput.html | 26 + articles/2021/TLS-handshake-failure.html | 31 + articles/2021/Thinkpad-X200.html | 31 + articles/2021/Thinkpad-X220.html | 38 + ...51l\303\251charger-musique-depuis-Youtube.html" | 29 + articles/2021/Un-espion-dans-ma-poche.html | 41 + articles/2021/Une-balancoire-pour-Barbie.html | 26 + articles/2021/Update.html | 35 + articles/2021/Wiby.html | 26 + articles/2021/ZRAM.html | 34 + articles/2021/Zellij-Terminal-Workspace.html | 30 + articles/2021/erreurs-spamass-milter.html | 43 + articles/2021/i3-Blocks.html | 50 + articles/2021/spamass-milter-error.html | 56 + articles/2021/web-bloat.html | 43 + articles/2021/wittr.in.html | 33 + "articles/2021/\303\211tau-de-recup.html" | 28 + .../Scie-\303\240-ruban-fait-maison-(10).html" | 29 + .../Scie-\303\240-ruban-fait-maison-(11).html" | 29 + .../Scie-\303\240-ruban-fait-maison-(12).html" | 27 + .../2022/Scie-\303\240-ruban-fait-maison-(7).html" | 30 + .../2022/Scie-\303\240-ruban-fait-maison-(8).html" | 27 + .../2022/Scie-\303\240-ruban-fait-maison-(9).html" | 27 + articles/2022/mailcow.html | 31 + articles/2023/1vyrain.html | 85 + articles/2023/Bitwarden-RBW-blues.html | 50 + articles/2023/Chimera's-Star.html | 27 + articles/2023/Death-of-the-web.html | 36 + .../2023/Firefox-Multi-Account-Containers.html | 45 + .../Forget-VTech,-get-an-old-laptop-instead.html | 32 + .../2023/Gemini---The-protocol-not-the-AI.html | 60 + .../2023/In-2024,-please-switch-to-Firefox.html | 25 + articles/2023/Little-Doggies.html | 39 + articles/2023/Marginalia-search-engine.html | 34 + articles/2023/Need-a-long-walk.html | 29 + articles/2023/Web-1.5.html | 86 + .../2023/Web-site-stats-better-without-Google.html | 28 + articles/2023/ascii-art.html | 38 + articles/2023/connerie-des-dates-limites.html | 25 + articles/2023/debian-or-arch.html | 38 + articles/2023/drm-et-firefox.html | 39 + articles/2023/evil-empire.html | 38 + articles/2023/fixing-25-second-firefox-delay.html | 45 + articles/2023/hardening-firefox.html | 47 + articles/2023/images-in-xterm-emulators.html | 45 + articles/2023/jersey-before.html | 26 + articles/2023/kind-of-blue.html | 52 + articles/2023/linux-paradise.html | 28 + articles/2023/map-of-langourla.html | 35 + articles/2023/mer-des-faluns.html | 30 + articles/2023/micro-text-editor.html | 34 + articles/2023/minimalist-blog.html | 87 + articles/2023/ms-eu-fine.html | 34 + articles/2023/new-television.html | 41 + .../2023/newsboat-strip-junk-from-web-pages.html | 46 + articles/2023/noyb-files-gdpr-comp.html | 42 + articles/2023/nuclear-sabotage.html | 47 + articles/2023/obscurantism.html | 26 + articles/2023/pacman-yay-notes.html | 33 + articles/2023/password-store-remote.html | 38 + articles/2023/password-store-to-bitwarden.html | 79 + articles/2023/periodic-fstrim-on-ssd-drives.html | 40 + articles/2023/prettier-when-it-works.html | 50 + ...protecting-your-computer-with-a-hosts-file.html | 42 + articles/2023/quad-9.html | 30 + articles/2023/radio-jazz-soul.html | 27 + articles/2023/saint-catherine.html | 37 + articles/2023/searxng-a-private-search-engine.html | 35 + articles/2023/searxng-on-my-laptop.html | 59 + articles/2023/system-collapse.html | 33 + articles/2023/t430-cpu-upgrade.html | 41 + articles/2023/t430-cpu-upgrade.html~ | 38 + articles/2023/thinkpad-t430.html | 48 + .../2023/updating-my-thinkpad-t430-wifi-card.html | 67 + articles/2023/windowmaker-0.96.html | 48 + articles/2023/windowmaker-in-1998.html | 35 + articles/2024/A-new-lathe.html | 32 + articles/2024/Another-Oland-tool.html | 29 + ...ort-perc\303\251-pour-bougie-chauffe-plat.html" | 27 + .../2025/A-fledgling-bash-function-library.html | 33 + .../2025/AC7260-wifi-card-on-Thinkpad-X220.html | 36 + articles/2025/Amazon-playing-silly-buggers.html | 39 + articles/2025/Anacron-on-Artix.html | 93 + articles/2025/Annoying-touchpad.html | 35 + articles/2025/Archwiki-on-my-laptop.html | 38 + articles/2025/Back-to-the-future.html | 37 + articles/2025/Bash-prompt-without-scripts.html | 37 + articles/2025/Betterfox.html | 28 + articles/2025/Blog-scripts.html | 44 + articles/2025/Bringing-back-the-fun.html | 34 + articles/2025/Browser-choices.html | 45 + articles/2025/Carbide-tools-for-wood-turning.html | 45 + articles/2025/Chaotic-Aur.html | 43 + .../2025/Closing-laptop-lid-without-suspend.html | 44 + .../Cpu-scaling-governors-on-Thinkpax-X220.html | 31 + .../2025/DNS4U-European-DNS-servers-go-live.html | 35 + .../2025/DWM-Dunst-and-Thinkpad-audio-keys.html | 113 + articles/2025/DWM-circulate-through-tags.html | 196 + articles/2025/DWM-slstatus.html | 46 + articles/2025/Daft-poems.html | 34 + articles/2025/Debian-12-to-13-upgrade.html | 32 + articles/2025/Detrumpez-vous.html | 34 + articles/2025/Dmenu-updates.html | 46 + articles/2025/Dwmblocks.html | 38 + .../Enable-Arch-Extra-repository-in-Artix.html | 49 + articles/2025/Eza.html | 32 + .../Fixing-my-dad's-Stanley-8-CFW-lock-plane.html | 38 + articles/2025/Flowers-for-the-bees.html | 35 + articles/2025/France-rejects-backdoors.html | 26 + articles/2025/Ghostty.html | 35 + articles/2025/Git-repository.html | 32 + articles/2025/Gopher-it.html | 34 + ...ers-list-of-Text-only-minimalist-web-sites.html | 26 + articles/2025/Hard-disk-fail.html | 28 + articles/2025/Headless-Arch-Install.html | 40 + articles/2025/Html-center-element.html | 42 + .../Install-the-Chawan-text-browser-on-Artix.html | 42 + articles/2025/Jazz-radio-recording-from-1976.html | 47 + articles/2025/Koreader-for-e-ink-devices.html | 32 + articles/2025/Ladybird-web-browser.html | 35 + articles/2025/Les-Oiseaux-des-Jardins.html | 32 + articles/2025/Linux-System-Performance-Tuning.html | 27 + articles/2025/Linux-power-consumption-control.html | 35 + articles/2025/Lynx,-Gopher,-and-video-files.html | 102 + .../2025/Mailcow-Docker-too-many-requests.html | 38 + ...Mailcow-with-Nginx-reverse-proxy-(updated).html | 31 + .../2025/Mailcow-with-Nginx-reverse-proxy.html | 207 + articles/2025/Menu-editor-for-XFCE4.html | 26 + .../Microphone-distortion-on-Thinkpad-X220.html | 38 + articles/2025/Modified-dwm-bar-battery-script.html | 80 + articles/2025/My-gopher-site.html | 35 + articles/2025/Naked-DWM.html | 101 + articles/2025/Neomutt-as-default-in-XFCE4.html | 47 + articles/2025/New-builds-of-dwm-and-st.html | 35 + articles/2025/Newsboat-and-gopher-RSS-feeds.html | 36 + articles/2025/Oddly-useful-Linux-tools.html | 50 + articles/2025/Pipewire.html | 62 + articles/2025/Power-notifications.html | 31 + articles/2025/Powerline-Bash.html | 27 + .../RSS-feeds-that-don't-suck-on-terminals.html | 83 + articles/2025/Reducing-web-font-size.html | 39 + articles/2025/Replace-sudo-with-doas-on-Artix.html | 92 + .../2025/Rsync-backup-to-Synology-Diskstation.html | 46 + articles/2025/Screen-scaling-with-xrandr.html | 44 + articles/2025/Shellcheck-your-bash-scripts.html | 36 + .../Simplex-a-decentralised-private-messenger.html | 29 + articles/2025/Slim-fork-login-manager.html | 35 + .../Slstatus-date-with-alternative-locale.html | 59 + articles/2025/Starship-prompt.html | 34 + articles/2025/Stick-it-to-the-man.html | 36 + .../TLP-recalibration-error-on-Thinkpad-X220.html | 29 + articles/2025/Taking-the-leap-to-Artix.html | 45 + articles/2025/Terminal-Trove.html | 29 + articles/2025/The-Mote-in-God's-eye.html | 36 + articles/2025/The-last-thing-the-EU-needs-....html | 31 + articles/2025/The-universe-is-spinning.html | 55 + articles/2025/Thinkpad-T430-screen-upgrade.html | 33 + articles/2025/Thinkpad-X220-Fan-control.html | 30 + articles/2025/Thinkvantage-button-in-DWM.html | 41 + articles/2025/Thoughts-on-suckless.html | 46 + articles/2025/Thoughts-on-zsh.html | 40 + articles/2025/Toggle-screen-resolution-in-DWM.html | 35 + articles/2025/Touchpad-deactivation.html | 58 + .../2025/Traditional-screenshot-of-my-X220.html | 28 + .../Try-different-versions-of-Linux-online.html | 33 + articles/2025/Updated-cgit-server.html | 35 + .../2025/Upload-modified-blog-files-with-scp.html | 65 + .../2025/Using-Goaccess-to-view-web-stats.html | 32 + articles/2025/VPS-management.html | 33 + .../2025/What-to-do-with-an-unused-Kindle.html | 33 + .../Why-does-everything-feel-so-boring-now.html | 48 + articles/2025/Wifi-region.html | 33 + articles/2025/Window-manager-thoughts.html | 47 + articles/2025/Workshop-Air-Cleaner.html | 29 + .../2025/X220-internal-Bluetooth-controller.html | 38 + articles/2025/Xlibre-on-my-X220.html | 34 + articles/2025/dwm-windowmanager-1.html | 46 + articles/2025/dwm-windowmanager-2.html | 64 + articles/2025/dwmblocks-with-emoji-icons.html | 50 + articles/2026/De-Gaulle---Two-films.html | 63 + articles/2026/Filtering-the-web.html | 62 + articles/2026/Freedos-thoughts.html | 43 + articles/2026/From-minimalist-to-smolweb.html | 43 + articles/2026/Funeral-blues.html | 57 + articles/2026/Ghostbsd-installation.html | 92 + articles/2026/Hebdogiciel.html | 51 + .../2026/Installation-of-Snac2-on-Artix-Linux.html | 144 + .../2026/Ladybird-turns-into-a-squashed-bug.html | 45 + articles/2026/Living-on-Snac's-bleeding-edge.html | 60 + articles/2026/More-on-Dillo.html | 36 + articles/2026/My-own-private-search-engine.html | 54 + articles/2026/Oops!-I-did-it-again.html | 46 + .../2026/This-blog-wasn't-so-bad-after-all.html | 46 + articles/2026/Updated-blogthis-script.html | 38 + articles/2026/Updated-poem.html | 45 + .../2026/Web-sites-that-work-well-in-Dillo.html | 60 + .../2026/displaying-MATE-on-a-1366x768-screen.html | 73 + articles/2026/semware-tse-editor.html | 44 + articles/2026/smolweb-validation-in-Dillo.html | 57 + articles/2026/upgrade-to-dwm-6.8.html | 49 + articles/test.html | 3 + deltachat.html | 22 + favicon-32x32.png | Bin 0 -> 2505 bytes favicon.ico | Bin 0 -> 1150 bytes feeds/fediverse-philip/feed | 1 + feeds/fediverse-philip/title | 1 + feeds/gemini-vigrey.com/feed | 1 + feeds/gemini-vigrey.com/index.php | 32 + feeds/gemini-vigrey.com/title | 1 + feeds/gopher-cosmic-voyage/feed | 1 + feeds/gopher-cosmic-voyage/index.php | 41 + feeds/gopher-cosmic-voyage/title | 1 + feeds/gopher-lfa/feed | 1 + feeds/gopher-lfa/index.php | 32 + feeds/gopher-lfa/title | 1 + feeds/gopher-mattos/feed | 1 + feeds/gopher-mattos/index.php | 27 + feeds/gopher-mattos/title | 1 + feeds/gopher-wittamore/feed | 1 + feeds/gopher-wittamore/index.html | 14 + feeds/gopher-wittamore/index.php | 27 + feeds/gopher-wittamore/title | 1 + feeds/http-20minutes/feed | 2 + feeds/http-20minutes/index.php | 42 + feeds/http-20minutes/title | 1 + feeds/http-adele/feed | 1 + feeds/http-adele/index.php | 28 + feeds/http-adele/title | 1 + feeds/http-bloggings/feed | 1 + feeds/http-bloggings/title | 1 + feeds/http-eff/feed | 1 + feeds/http-eff/index.php | 52 + feeds/http-eff/title | 1 + feeds/http-petergartner/feed | 1 + feeds/http-petergartner/index.php | 37 + feeds/http-petergartner/title | 2 + feeds/http-pluralistic/feed | 1 + feeds/http-pluralistic/index.php | 32 + feeds/http-pluralistic/title | 1 + feeds/http-scitechdaily/feed | 1 + feeds/http-scitechdaily/title | 1 + feeds/http-smolweb/feed | 1 + feeds/http-smolweb/index.php | 33 + feeds/http-smolweb/title | 1 + feeds/index.php | 42 + feeds/news-euronews/feed | 1 + feeds/news-euronews/index.php | 52 + feeds/news-euronews/title | 1 + feeds/news-guardian-europe/feed | 1 + feeds/news-guardian-europe/index.php | 52 + feeds/news-guardian-europe/title | 2 + feeds/youtube-mental-outlaw/feed | 1 + feeds/youtube-mental-outlaw/index.php | 37 + feeds/youtube-mental-outlaw/title | 1 + feeds/youtube-naomi-brockwell/feed | 1 + feeds/youtube-naomi-brockwell/index.php | 37 + feeds/youtube-naomi-brockwell/title | 1 + feeds/youtube-veronica-explains/feed | 1 + feeds/youtube-veronica-explains/index.php | 37 + feeds/youtube-veronica-explains/title | 1 + feeds/youtube-yousuckatprogramming/feed | 1 + feeds/youtube-yousuckatprogramming/index.php | 37 + feeds/youtube-yousuckatprogramming/title | 1 + files/bulletins/bulletin_032015_WEB.pdf | Bin 0 -> 3178278 bytes files/bulletins/bulletin_052014_006_WEB.pdf | Bin 0 -> 3590284 bytes files/bulletins/bulletin_062015_web.pdf | Bin 0 -> 2864143 bytes files/bulletins/bulletin_092014_01_WEB.pdf | Bin 0 -> 3157841 bytes files/bulletins/bulletin_122014_WEB.pdf | Bin 0 -> 3453515 bytes files/langourla.pdf | Bin 0 -> 634690 bytes gopher/de/index.html | 124 + gopher/de/smolnet.html | 43 + gopher/en/index.html | 121 + gopher/en/smolnet.html | 43 + gopher/es/index.html | 133 + gopher/es/smolnet.html | 43 + gopher/fr/index.html | 123 + gopher/fr/smolnet.html | 43 + gopher/gdpr.html | 106 + gopher/gopherfr-translations.tar.gz | Bin 0 -> 5871 bytes gopher/images/chawan_1.png | Bin 0 -> 50728 bytes gopher/images/chawan_2.png | Bin 0 -> 48633 bytes gopher/images/gopher-dark.png | Bin 0 -> 43406 bytes gopher/images/gopher.png | Bin 0 -> 33116 bytes gopher/images/gopher_IGICv1.12..png | Bin 0 -> 69536 bytes gopher/images/gopher_elpher.png | Bin 0 -> 99137 bytes gopher/images/gopher_gophercle_android.jpg | Bin 0 -> 22918 bytes gopher/images/gopher_little_gopher_client_mac.png | Bin 0 -> 55181 bytes gopher/images/gopherus.png | Bin 0 -> 51249 bytes gopher/images/lagrange.png | Bin 0 -> 54025 bytes gopher/images/lynx_1.png | Bin 0 -> 44699 bytes gopher/index.html | 26 + gopher/it/index.html | 135 + gopher/it/smolnet.html | 43 + gopher/style.css | 23 + images/1.png | Bin 0 -> 124801 bytes images/Lenovo-ThinkPad-X220.JPEG | Bin 0 -> 22195 bytes images/Lenovo-ThinkPad-X220.webp | Bin 0 -> 5648 bytes images/ac-7620-spot.jpg | Bin 0 -> 60338 bytes images/alacritty.JPEG | Bin 0 -> 33182 bytes images/alacritty.webp | Bin 0 -> 16324 bytes images/applixware-labelmaker_0.jpg | Bin 0 -> 58279 bytes images/applixware-labelmaker_1.jpg | Bin 0 -> 74880 bytes images/arch-logo.JPEG | Bin 0 -> 39271 bytes images/arch-logo.webp | Bin 0 -> 21114 bytes images/balancoire.JPEG | Bin 0 -> 76014 bytes images/balancoire.webp | Bin 0 -> 36278 bytes images/bandsaw-frame-pieces.JPEG | Bin 0 -> 75191 bytes images/bandsaw-frame-pieces.webp | Bin 0 -> 33760 bytes images/bandsaw_1.JPEG | Bin 0 -> 30768 bytes images/bandsaw_1.jpg | Bin 0 -> 28544 bytes images/bandsaw_1.webp | Bin 0 -> 6972 bytes images/blog/Lenovo-ThinkPad-X220.JPEG | Bin 0 -> 8853 bytes images/blog/Lenovo-ThinkPad-X220.webp | Bin 0 -> 2540 bytes images/blog/Screenshot_2025-04-08_14-20-40.png | Bin 0 -> 94868 bytes images/blog/alacritty.JPEG | Bin 0 -> 33190 bytes images/blog/alacritty.webp | Bin 0 -> 21400 bytes images/blog/arch-logo.JPEG | Bin 0 -> 97199 bytes images/blog/arch-logo.webp | Bin 0 -> 53260 bytes images/blog/balancoire.JPEG | Bin 0 -> 149591 bytes images/blog/balancoire.webp | Bin 0 -> 92386 bytes images/blog/bandsaw-frame-pieces.JPEG | Bin 0 -> 154811 bytes images/blog/bandsaw-frame-pieces.webp | Bin 0 -> 99140 bytes images/blog/bandsaw_1.JPEG | Bin 0 -> 72666 bytes images/blog/bandsaw_1.jpg | Bin 0 -> 74757 bytes images/blog/bandsaw_1.webp | Bin 0 -> 34470 bytes images/blog/bash-prompt-pywal.png | Bin 0 -> 154248 bytes images/blog/battery.c.png | Bin 0 -> 20065 bytes images/blog/bonneannee.gif | Bin 0 -> 3295112 bytes images/blog/box-chevaux.JPEG | Bin 0 -> 131079 bytes images/blog/box-chevaux.webp | Bin 0 -> 83608 bytes images/blog/carbide-tools.jpg | Bin 0 -> 156858 bytes images/blog/cha-wittamore-1.png | Bin 0 -> 107039 bytes images/blog/chawan-ghostty.png | Bin 0 -> 297745 bytes images/blog/classic-keyboard.JPEG | Bin 0 -> 15277 bytes images/blog/classic-keyboard.webp | Bin 0 -> 7318 bytes images/blog/core-i5-cpu.JPEG | Bin 0 -> 44814 bytes images/blog/core-i5-cpu.png | Bin 0 -> 138168 bytes images/blog/core-i5-cpu.webp | Bin 0 -> 25396 bytes images/blog/dane-success.png | Bin 0 -> 3436 bytes images/blog/dwm-btop.png | Bin 0 -> 183797 bytes images/blog/dwm_bar.png | Bin 0 -> 11582 bytes images/blog/dwmblocks-emoji.png | Bin 0 -> 153964 bytes images/blog/espion-dans-notre-poche.JPEG | Bin 0 -> 14100 bytes images/blog/espion-dans-notre-poche.webp | Bin 0 -> 7922 bytes images/blog/etau.JPEG | Bin 0 -> 158425 bytes images/blog/etau.webp | Bin 0 -> 132498 bytes images/blog/eu-flag.JPEG | Bin 0 -> 10484 bytes images/blog/eu-flag.webp | Bin 0 -> 3794 bytes images/blog/eza.png | Bin 0 -> 14354 bytes images/blog/first-box.JPEG | Bin 0 -> 57352 bytes images/blog/first-box.webp | Bin 0 -> 33690 bytes images/blog/fugitive-telemetry.JPEG | Bin 0 -> 23994 bytes images/blog/fugitive-telemetry.webp | Bin 0 -> 10862 bytes images/blog/ghostty.gif | Bin 0 -> 7160729 bytes images/blog/grande-vase.JPEG | Bin 0 -> 113788 bytes images/blog/grande-vase.webp | Bin 0 -> 51126 bytes images/blog/gsimplecal.JPEG | Bin 0 -> 17774 bytes images/blog/gsimplecal.webp | Bin 0 -> 10064 bytes images/blog/hail-mary-wier.JPEG | Bin 0 -> 27458 bytes images/blog/hail-mary-wier.webp | Bin 0 -> 12452 bytes images/blog/happy-new-year.gif | Bin 0 -> 973676 bytes images/blog/i3-blocks.JPEG | Bin 0 -> 10315 bytes images/blog/i3-blocks.webp | Bin 0 -> 8786 bytes images/blog/kbca-jazz-radio.png | Bin 0 -> 151384 bytes images/blog/kobo-clara-colour.png | Bin 0 -> 113364 bytes images/blog/la-grande-illusion.JPEG | Bin 0 -> 14082 bytes images/blog/la-grande-illusion.webp | Bin 0 -> 5234 bytes images/blog/librefox-wittamore-1.png | Bin 0 -> 105277 bytes images/blog/links-wittamore-1.png | Bin 0 -> 207751 bytes images/blog/lnav.JPEG | Bin 0 -> 85111 bytes images/blog/lnav.webp | Bin 0 -> 69800 bytes images/blog/lower-blade-guide-1.JPEG | Bin 0 -> 139551 bytes images/blog/lower-blade-guide-1.webp | Bin 0 -> 92116 bytes images/blog/maison-poupee.JPEG | Bin 0 -> 93809 bytes images/blog/maison-poupee.webp | Bin 0 -> 57246 bytes images/blog/mini-chariot.JPEG | Bin 0 -> 332037 bytes images/blog/mini-chariot.webp | Bin 0 -> 169122 bytes images/blog/mygophersite.png | Bin 0 -> 38753 bytes images/blog/neomutt-screenshot.png | Bin 0 -> 65460 bytes images/blog/new-bench.JPEG | Bin 0 -> 286375 bytes images/blog/new-bench.webp | Bin 0 -> 141800 bytes images/blog/newsboat-cha-1.png | Bin 0 -> 105522 bytes images/blog/newsboat-rdrview-1.png | Bin 0 -> 110598 bytes images/blog/oak-bowl-28cm.JPEG | Bin 0 -> 101019 bytes images/blog/oak-bowl-28cm.webp | Bin 0 -> 82158 bytes images/blog/odysee.JPEG | Bin 0 -> 8529 bytes images/blog/odysee.webp | Bin 0 -> 8182 bytes images/blog/pense-bete.JPEG | Bin 0 -> 3569 bytes images/blog/pense-bete.webp | Bin 0 -> 2208 bytes ...anche-\303\240-d\303\251couper-jacqueline.JPEG" | Bin 0 -> 323129 bytes ...anche-\303\240-d\303\251couper-jacqueline.webp" | Bin 0 -> 166780 bytes images/blog/poster-oiseaux-des-jardins.png | Bin 0 -> 538863 bytes images/blog/powerline_bash.png | Bin 0 -> 3322 bytes images/blog/powermenu.png | Bin 0 -> 14094 bytes images/blog/pyradio-again.png | Bin 0 -> 129646 bytes images/blog/pyradio.JPEG | Bin 0 -> 184683 bytes images/blog/pyradio.webp | Bin 0 -> 66036 bytes images/blog/pywal16-screenshot.png | Bin 0 -> 1905958 bytes images/blog/resistance.jpg | Bin 0 -> 360601 bytes images/blog/riddings-web-site.JPEG | Bin 0 -> 81183 bytes images/blog/riddings-web-site.webp | Bin 0 -> 44296 bytes images/blog/screw-jig.JPEG | Bin 0 -> 197234 bytes images/blog/screw-jig.webp | Bin 0 -> 119550 bytes images/blog/semware.png | Bin 0 -> 167231 bytes images/blog/shadocks-pumping.gif | Bin 0 -> 146286 bytes images/blog/slim-theme.png | Bin 0 -> 11026 bytes images/blog/slstatus-french.png | Bin 0 -> 3047 bytes images/blog/slstatus.png | Bin 0 -> 3519 bytes images/blog/sous-plat-incrustation.JPEG | Bin 0 -> 297573 bytes images/blog/sous-plat-incrustation.webp | Bin 0 -> 206466 bytes images/blog/starship-gruvbox-rainbow.png | Bin 0 -> 12072 bytes images/blog/stop-block.JPEG | Bin 0 -> 170163 bytes images/blog/stop-block.webp | Bin 0 -> 112624 bytes images/blog/support-roue-superior.JPEG | Bin 0 -> 475128 bytes images/blog/support-roue-superior.webp | Bin 0 -> 248290 bytes images/blog/support-trunnion.JPEG | Bin 0 -> 247888 bytes images/blog/support-trunnion.webp | Bin 0 -> 145738 bytes images/blog/table-support-trunions.JPEG | Bin 0 -> 83031 bytes images/blog/table-support-trunions.webp | Bin 0 -> 47812 bytes images/blog/thinkpad-x220-mic-button.JPEG | Bin 0 -> 13020 bytes images/blog/thinkpad-x220-mic-button.webp | Bin 0 -> 6240 bytes images/blog/thinkvantage.png | Bin 0 -> 150844 bytes images/blog/tprg.JPEG | Bin 0 -> 1696624 bytes images/blog/tprg.webp | Bin 0 -> 1241428 bytes images/blog/trunion-shim.JPEG | Bin 0 -> 152598 bytes images/blog/trunion-shim.webp | Bin 0 -> 96510 bytes images/blog/trunions-chevilles.JPEG | Bin 0 -> 306973 bytes images/blog/trunions-chevilles.webp | Bin 0 -> 166002 bytes images/blog/upper-blade-guide-1.JPEG | Bin 0 -> 238279 bytes images/blog/upper-blade-guide-1.webp | Bin 0 -> 123302 bytes images/blog/w3m-wittamore-1.png | Bin 0 -> 184275 bytes images/blog/workshop-update.JPEG | Bin 0 -> 267825 bytes images/blog/workshop-update.webp | Bin 0 -> 108728 bytes images/blog/wttr.JPEG | Bin 0 -> 14719 bytes images/blog/wttr.webp | Bin 0 -> 9618 bytes images/blog/x200.JPEG | Bin 0 -> 8479 bytes images/blog/x200.webp | Bin 0 -> 2240 bytes images/blog/xfce4-panel-cpu-fan.png | Bin 0 -> 5846 bytes images/blog/zellij.JPEG | Bin 0 -> 101301 bytes images/blog/zellij.webp | Bin 0 -> 40134 bytes images/bloggings.jpg | Bin 0 -> 492262 bytes images/bonneannee.gif | Bin 0 -> 3295112 bytes images/bowl.jpg | Bin 0 -> 41907 bytes images/box-chevaux.JPEG | Bin 0 -> 68512 bytes images/box-chevaux.webp | Bin 0 -> 31618 bytes images/browsers/dillo.jpg | Bin 0 -> 90129 bytes images/browsers/firefox.jpg | Bin 0 -> 78247 bytes images/browsers/index.php | 28 + images/browsers/links.jpg | Bin 0 -> 95441 bytes images/browsers/lynx.jpg | Bin 0 -> 85994 bytes images/browsers/w3m.jpg | Bin 0 -> 73760 bytes images/chantournage/animaux-ferme.webp | Bin 0 -> 52648 bytes images/chantournage/boite-coeur.webp | Bin 0 -> 54902 bytes images/chantournage/feuilles-plumes.webp | Bin 0 -> 106650 bytes .../f\303\252te-des-grandparents.webp" | Bin 0 -> 60678 bytes images/chantournage/index.php | 33 + "images/chantournage/moto-ch\303\252ne.webp" | Bin 0 -> 67364 bytes .../sous-plat-ch\303\252ne-33cm-triskele.webp" | Bin 0 -> 37928 bytes images/chantournage/sous-plat-hermine.webp | Bin 0 -> 7032 bytes images/chantournage/triskele-cp-bouleau.webp | Bin 0 -> 51158 bytes images/chimera.jpg | Bin 0 -> 112543 bytes images/classic-keyboard.JPEG | Bin 0 -> 35871 bytes images/classic-keyboard.webp | Bin 0 -> 12952 bytes images/core-i5-cpu.JPEG | Bin 0 -> 44815 bytes images/core-i5-cpu.png | Bin 0 -> 138984 bytes images/core-i5-cpu.webp | Bin 0 -> 25368 bytes images/coronet-herald.jpg | Bin 0 -> 168388 bytes images/corse-1975.jpg | Bin 0 -> 46644 bytes images/dates-limites.jpg | Bin 0 -> 13738 bytes images/debian-vs-arch.jpg | Bin 0 -> 15546 bytes images/deltachatqr.jpg | Bin 0 -> 101997 bytes images/espion-dans-notre-poche.JPEG | Bin 0 -> 24037 bytes images/espion-dans-notre-poche.webp | Bin 0 -> 9908 bytes images/etau.JPEG | Bin 0 -> 47178 bytes images/etau.webp | Bin 0 -> 17008 bytes images/eu-flag.JPEG | Bin 0 -> 24400 bytes images/eu-flag.webp | Bin 0 -> 5974 bytes images/evil-empire.jpg | Bin 0 -> 18622 bytes images/firefox-drm.jpg | Bin 0 -> 3783 bytes images/first-box.JPEG | Bin 0 -> 42600 bytes images/first-box.webp | Bin 0 -> 17838 bytes images/fugitive-telemetry.JPEG | Bin 0 -> 34519 bytes images/fugitive-telemetry.webp | Bin 0 -> 11440 bytes images/grande-vase.JPEG | Bin 0 -> 56783 bytes images/grande-vase.webp | Bin 0 -> 21022 bytes images/gsimplecal.JPEG | Bin 0 -> 28247 bytes images/gsimplecal.webp | Bin 0 -> 12086 bytes images/hail-mary-wier.JPEG | Bin 0 -> 40246 bytes images/hail-mary-wier.webp | Bin 0 -> 14140 bytes images/happy-new-year.gif | Bin 0 -> 973676 bytes images/histoire/1740.jpg | Bin 0 -> 193535 bytes images/histoire/1866.jpg | Bin 0 -> 182400 bytes images/histoire/Allee-couverte-la-rocher.png | Bin 0 -> 381162 bytes ...Ch\303\252ne-mill\303\251naire-saint-jacut.jpg" | Bin 0 -> 598475 bytes images/histoire/Menhir-de-la-Touche-article.png | Bin 0 -> 130493 bytes images/histoire/arrete-confitures-1942.webp | Bin 0 -> 102010 bytes images/histoire/bas_fournaux.jpg | Bin 0 -> 28279 bytes images/histoire/bourg-1824.png | Bin 0 -> 888374 bytes images/histoire/chateau-de-la-couaille.jpg | Bin 0 -> 381050 bytes images/histoire/cocantine.png | Bin 0 -> 5097516 bytes images/histoire/cour-de-queneleue.png | Bin 0 -> 303303 bytes .../delivrance-cartes-alimentaires-1941.webp | Bin 0 -> 65118 bytes images/histoire/eglise_saint_eutrope.jpg | Bin 0 -> 65301 bytes images/histoire/haches-langourla.png | Bin 0 -> 122785 bytes images/histoire/index.php | 33 + images/histoire/vieux-chatet-de-coelan.png | Bin 0 -> 170469 bytes images/i3-blocks.JPEG | Bin 0 -> 7450 bytes images/i3-blocks.webp | Bin 0 -> 3414 bytes images/index.php | 28 + images/informatique/amstrad-cpc-464.jpg | Bin 0 -> 23964 bytes images/informatique/burroghs_tc500.jpg | Bin 0 -> 70015 bytes images/informatique/ibmpc.jpg | Bin 0 -> 29389 bytes images/informatique/index.php | 33 + "images/informatique/no\303\253l-magasin.jpg" | Bin 0 -> 585125 bytes images/informatique/shop.jpg | Bin 0 -> 57896 bytes images/informatique/tandy1000hx.jpg | Bin 0 -> 30145 bytes images/informatique/zx81.jpg | Bin 0 -> 26894 bytes images/jersey-before.jpg | Bin 0 -> 40309 bytes images/kobo-clara-colour.png | Bin 0 -> 113364 bytes images/la-grande-illusion.JPEG | Bin 0 -> 21730 bytes images/la-grande-illusion.webp | Bin 0 -> 5820 bytes images/langourla_map.jpg | Bin 0 -> 22540 bytes images/linux-servers-cloud.jpg | Bin 0 -> 8352 bytes images/lnav.JPEG | Bin 0 -> 85057 bytes images/lnav.webp | Bin 0 -> 51120 bytes images/logos/aposit.jpg | Bin 0 -> 33583 bytes images/logos/applixware.jpg | Bin 0 -> 25219 bytes images/logos/avenir.jpg | Bin 0 -> 21793 bytes images/logos/emess.jpg | Bin 0 -> 28889 bytes images/logos/greenhouse.jpg | Bin 0 -> 21436 bytes images/logos/idinan.jpg | Bin 0 -> 46257 bytes images/logos/index.php | 33 + images/logos/langourlavie.jpg | Bin 0 -> 36548 bytes images/logos/quilting.jpg | Bin 0 -> 21179 bytes images/logos/vistasource.jpg | Bin 0 -> 25439 bytes images/lower-blade-guide-1.JPEG | Bin 0 -> 51095 bytes images/lower-blade-guide-1.webp | Bin 0 -> 22722 bytes images/mailcow-logo.png | Bin 0 -> 55712 bytes images/maison-poupee.JPEG | Bin 0 -> 60633 bytes images/maison-poupee.webp | Bin 0 -> 27260 bytes images/me.jpg | Bin 0 -> 158576 bytes .../cartes-postales/1937-ancien-mairie.webp | Bin 0 -> 27326 bytes .../cartes-postales/avenues-saint-joseph-2.webp | Bin 0 -> 130630 bytes .../cartes-postales/avenues-saint-joseph.webp | Bin 0 -> 104372 bytes .../cartes-postales/bourg-et-monument.webp | Bin 0 -> 36684 bytes images/mene/langourla/cartes-postales/bourg.webp | Bin 0 -> 69024 bytes .../cartes-postales/caf\303\251-du-centre.webp" | Bin 0 -> 74100 bytes .../cartes-postales/champ-du-ch\303\252ne.webp" | Bin 0 -> 113480 bytes .../cartes-postales/chapel-et-ch\303\252ne.webp" | Bin 0 -> 67768 bytes .../cartes-postales/chapelle-saint-joseph.webp | Bin 0 -> 48942 bytes .../cartes-postales/chateau-de-coelan.webp | Bin 0 -> 74884 bytes .../cartes-postales/ch\303\252ne-miraculeux.webp" | Bin 0 -> 91960 bytes images/mene/langourla/cartes-postales/ecole.webp | Bin 0 -> 38214 bytes images/mene/langourla/cartes-postales/eglise.webp | Bin 0 -> 489022 bytes images/mene/langourla/cartes-postales/index.php | 33 + .../mene/langourla/cartes-postales/multivues.webp | Bin 0 -> 122974 bytes .../langourla/cartes-postales/rocher-le-celte.webp | Bin 0 -> 118058 bytes .../tour-saint-eutrop-et-cimiti\303\250re.webp" | Bin 0 -> 44338 bytes .../cartes-postales/tour-saint-eutrop.webp | Bin 0 -> 57440 bytes .../cartes-postales/vieux-moulin-de-cadeuc.webp | Bin 0 -> 105590 bytes .../cartes-postales/villa-des-rosiers.webp | Bin 0 -> 73120 bytes .../langourla/cartes-postales/vue-aerienne.webp | Bin 0 -> 60962 bytes .../mene/langourla/cartes-postales/vue-eglise.webp | Bin 0 -> 710618 bytes .../Chemin-de-saint-gilles-\303\240-Tangaie.webp" | Bin 0 -> 274016 bytes .../langourla/photos/La-roche-de-la-Jument.webp | Bin 0 -> 133930 bytes .../photos/Pied-de-boeuf-\303\240-Tangaie.webp" | Bin 0 -> 168012 bytes images/mene/langourla/photos/index.php | 33 + images/mene/logos/collinee.png | Bin 0 -> 107165 bytes images/mene/logos/index.php | 33 + images/mene/logos/langourla.png | Bin 0 -> 84934 bytes images/mene/logos/le gouray.png | Bin 0 -> 51032 bytes images/mene/logos/plessala.png | Bin 0 -> 154012 bytes images/mene/logos/saint gilles.png | Bin 0 -> 53870 bytes images/mene/logos/saint goueno.png | Bin 0 -> 95473 bytes "images/mene/logos/saint jacut du men\303\251.png" | Bin 0 -> 70796 bytes images/mene/saint-jacut-du-mene/cartes-postale | Bin 0 -> 157928 bytes .../All\303\251e-couverte-du-Rocher.webp" | Bin 0 -> 691056 bytes .../Ch\303\252ne-mill\303\251naire.webp" | Bin 0 -> 589952 bytes .../cartes-postales/Menhir-de-la-Touche.webp | Bin 0 -> 358346 bytes .../cartes-postales/chapelle-et-monument.webp | Bin 0 -> 162476 bytes .../chapelle-n.d-de-bon-reconfort.webp | Bin 0 -> 122246 bytes .../chapelle-saint-jean-baptiste.webp | Bin 0 -> 157928 bytes .../cartes-postales/chateau-du-parc.webp | Bin 0 -> 137320 bytes .../cartes-postales/eglise.webp | Bin 0 -> 119546 bytes .../saint-jacut-du-mene/cartes-postales/index.php | 33 + .../cartes-postales/la-batterie.webp | Bin 0 -> 133590 bytes .../cartes-postales/recette-buraliste.webp | Bin 0 -> 145198 bytes .../cartes-postales/une-batterie.webp | Bin 0 -> 146854 bytes .../saint-jacut-du-mene/photos/bois-du-moulin.webp | Bin 0 -> 131980 bytes .../photos/ch\303\252ne-du-parc-1.webp" | Bin 0 -> 122508 bytes .../photos/ch\303\252ne-du-parc-2.webp" | Bin 0 -> 63064 bytes .../photos/ch\303\252ne-du-parc-3.webp" | Bin 0 -> 125818 bytes .../photos/eglise-vue-de-la-mairie.webp | Bin 0 -> 218342 bytes images/mene/saint-jacut-du-mene/photos/index.php | 33 + images/menuiserie/abris jeux enfants.webp | Bin 0 -> 23188 bytes images/menuiserie/abris-jardin.webp | Bin 0 -> 51142 bytes images/menuiserie/abris-jeux-enfants.webp | Bin 0 -> 23188 bytes images/menuiserie/avion.webp | Bin 0 -> 21122 bytes "images/menuiserie/berceau-poup\303\251e.webp" | Bin 0 -> 31364 bytes images/menuiserie/bonhomme-neige.webp | Bin 0 -> 22640 bytes images/menuiserie/chariot-scie-table.webp | Bin 0 -> 24822 bytes images/menuiserie/compas-400mm.webp | Bin 0 -> 21350 bytes images/menuiserie/dominos-cumaru.jpg | Bin 0 -> 265134 bytes images/menuiserie/dominos.jpg | Bin 0 -> 98201 bytes images/menuiserie/etau.webp | Bin 0 -> 26572 bytes images/menuiserie/index.php | 33 + images/menuiserie/planche-en-bois-de-bout.webp | Bin 0 -> 49938 bytes images/menuiserie/support-roulettes.webp | Bin 0 -> 47574 bytes images/menuiserie/table-atelier.webp | Bin 0 -> 28744 bytes images/menuiserie/voiture-oliver.webp | Bin 0 -> 37382 bytes images/mer-des-faluns.jpg | Bin 0 -> 21978 bytes images/mini-chariot.JPEG | Bin 0 -> 51962 bytes images/mini-chariot.webp | Bin 0 -> 23032 bytes images/mugshot-02-12-2023.png | Bin 0 -> 124801 bytes images/mugshot-09-11-2023.jpg | Bin 0 -> 87187 bytes images/new-bench.JPEG | Bin 0 -> 55604 bytes images/new-bench.webp | Bin 0 -> 21238 bytes images/new-oland-tool.jpg | Bin 0 -> 175912 bytes images/oak-bowl-28cm.JPEG | Bin 0 -> 65094 bytes images/oak-bowl-28cm.webp | Bin 0 -> 30516 bytes images/obscurantism.jpg | Bin 0 -> 30300 bytes images/odysee.JPEG | Bin 0 -> 14206 bytes images/odysee.webp | Bin 0 -> 14556 bytes images/oland-tools.jpg | Bin 0 -> 17116 bytes images/pense-bete.JPEG | Bin 0 -> 8191 bytes images/pense-bete.webp | Bin 0 -> 5120 bytes images/phil_from_france.jpg | Bin 0 -> 99422 bytes ...anche-\303\240-d\303\251couper-jacqueline.JPEG" | Bin 0 -> 57932 bytes ...anche-\303\240-d\303\251couper-jacqueline.webp" | Bin 0 -> 21538 bytes images/prettier.jpg | Bin 0 -> 6773 bytes images/pyradio.JPEG | Bin 0 -> 36325 bytes images/pyradio.webp | Bin 0 -> 14970 bytes images/pyrogravure/chaise-clara.webp | Bin 0 -> 55324 bytes images/pyrogravure/chestnut-bowl.webp | Bin 0 -> 41602 bytes .../pyrogravure/graines-tonton-lo\303\257c.webp" | Bin 0 -> 45304 bytes images/pyrogravure/guitar-ian.webp | Bin 0 -> 90548 bytes images/pyrogravure/index.php | 33 + .../pyrogravure/logo-fer-\303\240-marquer.webp" | Bin 0 -> 11644 bytes .../triskel-sous-plat-sign\303\251.webp" | Bin 0 -> 105694 bytes .../vase-ch\303\252ne-d\303\251cor\303\251.webp" | Bin 0 -> 90180 bytes images/recordpower_AC400.jpg | Bin 0 -> 65988 bytes images/riddings-web-site.JPEG | Bin 0 -> 39894 bytes images/riddings-web-site.webp | Bin 0 -> 14598 bytes images/rss.png | Bin 0 -> 4139 bytes images/saint-catherine.jpg | Bin 0 -> 22973 bytes images/scie-alignment-1.jpg | Bin 0 -> 193309 bytes images/scie-alignment-2.jpg | Bin 0 -> 157516 bytes images/scie-completed-1.jpg | Bin 0 -> 127385 bytes images/scie-completed-2.jpg | Bin 0 -> 162684 bytes images/scie-completed-3.jpg | Bin 0 -> 159447 bytes images/scie-completed-4.jpg | Bin 0 -> 173225 bytes images/scie-completed-5.jpg | Bin 0 -> 145993 bytes images/scie-frame-1.jpg | Bin 0 -> 155942 bytes images/scie-frame-2.jpg | Bin 0 -> 176584 bytes images/scie-frame-3.jpg | Bin 0 -> 165813 bytes images/scie-frame-4.jpg | Bin 0 -> 194889 bytes images/scie-improvement-1.jpg | Bin 0 -> 157516 bytes images/scie-parts-1.jpg | Bin 0 -> 149939 bytes images/scie-wheel-1.jpg | Bin 0 -> 202762 bytes images/screw-jig.JPEG | Bin 0 -> 48192 bytes images/screw-jig.webp | Bin 0 -> 15494 bytes images/searxng.jpg | Bin 0 -> 4784 bytes images/shadocks-pumping-300.gif | Bin 0 -> 73115 bytes images/shadocks-pumping.gif | Bin 0 -> 11255 bytes images/shadocks.gif | Bin 0 -> 150537 bytes images/shop.jpg | Bin 0 -> 35131 bytes images/sous-plat-incrustation.JPEG | Bin 0 -> 51558 bytes images/sous-plat-incrustation.webp | Bin 0 -> 25592 bytes images/stop-block.JPEG | Bin 0 -> 65144 bytes images/stop-block.webp | Bin 0 -> 30472 bytes images/support-roue-superior.JPEG | Bin 0 -> 66892 bytes images/support-roue-superior.webp | Bin 0 -> 30486 bytes images/support-trunnion.JPEG | Bin 0 -> 49140 bytes images/support-trunnion.webp | Bin 0 -> 21364 bytes images/systemcollapse.jpg | Bin 0 -> 52481 bytes images/t430-cpu-upgrade.jpg | Bin 0 -> 10911 bytes images/table-support-trunions.JPEG | Bin 0 -> 51563 bytes images/table-support-trunions.webp | Bin 0 -> 22208 bytes images/tcl.webp | Bin 0 -> 11798 bytes images/thinkpad-x220-mic-button.JPEG | Bin 0 -> 29678 bytes images/thinkpad-x220-mic-button.webp | Bin 0 -> 9416 bytes "images/tour/1er-id\303\251e.webp" | Bin 0 -> 37750 bytes "images/tour/2i\303\250me-id\303\251e.webp" | Bin 0 -> 34966 bytes "images/tour/3i\303\250me-id\303\251e.webp" | Bin 0 -> 76248 bytes "images/tour/4-poup\303\251e-fixe.webp" | Bin 0 -> 46454 bytes .../tour/5-porte-outil-poup\303\251e-mobile.webp" | Bin 0 -> 44236 bytes .../tour/6-id\303\251e-poup\303\251e-mobile.webp" | Bin 0 -> 32808 bytes "images/tour/7-poup\303\251e-mobile.webp" | Bin 0 -> 61572 bytes images/tour/8-contre-pointe-tournante.webp | Bin 0 -> 38510 bytes images/tour/9-ecrou-de-blocage.webp | Bin 0 -> 28292 bytes .../91-cliquet-blocage-poup\303\251e-mobile.webp" | Bin 0 -> 28138 bytes images/tour/92-broche.webp | Bin 0 -> 24344 bytes images/tour/93-assemblage.webp | Bin 0 -> 33800 bytes images/tour/931-serrage--porte-outils.webp | Bin 0 -> 18354 bytes images/tour/932-blocage-broche.webp | Bin 0 -> 26034 bytes images/tour/94-300-tpm.webp | Bin 0 -> 71080 bytes images/tour/95-600-tpm.webp | Bin 0 -> 62040 bytes images/tour/96-1400-tpm.webp | Bin 0 -> 76048 bytes images/tour/97-finale.webp | Bin 0 -> 42496 bytes images/tour/index.php | 33 + images/tournages/3-bols-chataigner.webp | Bin 0 -> 30714 bytes images/tournages/4-bols-cerisier.webp | Bin 0 -> 59192 bytes images/tournages/Pot-en-charme.webp | Bin 0 -> 20036 bytes images/tournages/bol-cerisier.webp | Bin 0 -> 28170 bytes "images/tournages/bol-ch\303\242taigner.webp" | Bin 0 -> 14616 bytes "images/tournages/bol-h\303\252tre-12-02-20.webp" | Bin 0 -> 27088 bytes "images/tournages/bol-h\303\252tre-15-02-20.webp" | Bin 0 -> 21634 bytes "images/tournages/bol-h\303\252tre-21-02-20.webp" | Bin 0 -> 13684 bytes "images/tournages/bol-h\303\252tre-epoxy.webp" | Bin 0 -> 26206 bytes images/tournages/coquetiers-modernes.webp | Bin 0 -> 34620 bytes images/tournages/dauphin.webp | Bin 0 -> 33572 bytes images/tournages/deux-vases-thuya.webp | Bin 0 -> 35816 bytes images/tournages/ebauche-bol-30cm.webp | Bin 0 -> 31328 bytes "images/tournages/exp\303\251rimentation.webp" | Bin 0 -> 32906 bytes images/tournages/fleurs.webp | Bin 0 -> 19946 bytes images/tournages/flowers.webp | Bin 0 -> 17784 bytes images/tournages/grande-vase.webp | Bin 0 -> 51126 bytes images/tournages/index.php | 33 + images/tournages/mortier-cerisier.webp | Bin 0 -> 50404 bytes images/tournages/mortier-pilon-cerisier.webp | Bin 0 -> 20884 bytes images/tournages/oak-bowl-28cm.webp | Bin 0 -> 82158 bytes images/tournages/oak_bowl_experiment_1.webp | Bin 0 -> 34182 bytes images/tournages/petit-vase-thuya.webp | Bin 0 -> 16552 bytes images/tournages/plat-ceriser.webp | Bin 0 -> 76788 bytes images/tournages/porte-bougie-1.jpg | Bin 0 -> 112494 bytes images/tournages/pot-charme-pyro.webp | Bin 0 -> 15940 bytes images/tournages/pot-open.webp | Bin 0 -> 20690 bytes images/tournages/pot.webp | Bin 0 -> 19282 bytes images/tournages/tall-vase.webp | Bin 0 -> 16306 bytes images/tournages/two-vases.webp | Bin 0 -> 30536 bytes "images/tournages/vase-ch\303\252ne.webp" | Bin 0 -> 50782 bytes images/tournages/vase-moabi.webp | Bin 0 -> 36662 bytes images/tournages/vase-thuya.webp | Bin 0 -> 19868 bytes images/tprg.JPEG | Bin 0 -> 72967 bytes images/tprg.webp | Bin 0 -> 35522 bytes images/trunion-shim.JPEG | Bin 0 -> 51254 bytes images/trunion-shim.webp | Bin 0 -> 22798 bytes images/trunions-chevilles.JPEG | Bin 0 -> 55498 bytes images/trunions-chevilles.webp | Bin 0 -> 24530 bytes images/upper-blade-guide-1.JPEG | Bin 0 -> 36569 bytes images/upper-blade-guide-1.webp | Bin 0 -> 10104 bytes images/w3m-display-options.png | Bin 0 -> 35018 bytes images/windowmaker096.png | Bin 0 -> 26177 bytes images/workshop-update.JPEG | Bin 0 -> 62934 bytes images/workshop-update.webp | Bin 0 -> 23036 bytes images/wttr.JPEG | Bin 0 -> 22044 bytes images/wttr.webp | Bin 0 -> 11006 bytes images/x200.JPEG | Bin 0 -> 19240 bytes images/x200.webp | Bin 0 -> 5224 bytes images/xterm-images-400.jpg | Bin 0 -> 34308 bytes images/zellij.JPEG | Bin 0 -> 51313 bytes images/zellij.webp | Bin 0 -> 24656 bytes index.php | 149 + logo.png | Bin 0 -> 3474 bytes nginx.conf | 70 + robots.txt | 3 + rss.xml | 850 + sandbox/webproxy.php | 7 + scripts/SimplePie.compiled.php | 20130 +++++++++++++++++++ scripts/getfeeds_http.php | 167 + search.php | 104 + sitemap.xml | 5299 +++++ static/140-html-color-names.html | 227 + static/Interesting-links.html | 114 + static/Mention-Legal.html | 63 + static/Neomutt.html | 95 + static/about.html | 98 + static/index.php | 82 + static/le-mene.html | 181 + static/summer-of-75.html | 50 + static/woodworking.html | 43 + .../Scie-\303\240-ruban-fait-maison.html" | 52 + static/woodworking/oland-tools.html | 56 + "static/woodworking/tour-\303\240-bois.html" | 43 + style.css | 172 + txt/account_philip.txt | 51 + txt/display_filter.txt | 53 + txt/dwm-config.txt | 139 + txt/get-mailboxes.txt | 17 + txt/key.txt | 41 + txt/mailcap.txt | 11 + txt/mutt-trim.txt | 171 + txt/neomuttrc.txt | 173 + txt/poems.txt | 70 + txt/print_unicode.txt | 37 + txt/translate.txt | 14 + txt/update-fingerprint | 14 + txt/venelles.txt | 77 + 828 files changed, 41310 insertions(+) create mode 100644 README.md create mode 100755 articles/2021/Alacritty.html create mode 100755 articles/2021/Arch-LTS-kernel.html create mode 100755 articles/2021/Arch-Linux.html create mode 100755 "articles/2021/Biblioth\303\250que-Nationale-de-France.html" create mode 100755 "articles/2021/Bol-en-ch\303\252ne-de-28cm.html" create mode 100755 articles/2021/Boxe-pour-chevaux-jouets.html create mode 100755 "articles/2021/Ce-que-l'Europe-\303\240-fait-pour-moi.html" create mode 100755 articles/2021/Certbot-delete.html create mode 100755 articles/2021/Coupure-microphone-X220.html create mode 100755 "articles/2021/Deux-maisons-de-poup\303\251es.html" create mode 100755 "articles/2021/Exp\303\251riment.html" create mode 100755 articles/2021/Fabriquez-votre-propre-ordinateur-portable.htm create mode 100755 articles/2021/Fugitive-Telemetry.html create mode 100755 articles/2021/GeoIP2-et-Goaccess.html create mode 100755 "articles/2021/Grande-vase-en-ch\303\252ne.html" create mode 100755 articles/2021/Intel-Thermald.html create mode 100755 articles/2021/Invidious---une-interface-alternative-pour-Youtube.html create mode 100755 "articles/2021/Jitsi-vid\303\251oconference.html" create mode 100755 articles/2021/La-grande-illusion.html create mode 100755 "articles/2021/Le-Jardin-de-Tonton-Lo\303\257c.html" create mode 100755 "articles/2021/Lnav---ma\303\256triser-vos-journaux.html" create mode 100755 articles/2021/Mbsync-push.html create mode 100755 "articles/2021/Mini-chariot-avec-but\303\251e-d\342\200\231arr\303\252t.html" create mode 100755 "articles/2021/Mise-\303\240-jour-sur-la-r\303\251gulation-du-processeur.html" create mode 100755 articles/2021/Nerdfonts-in-i3.html create mode 100755 articles/2021/Nginx-permission-denied.html create mode 100755 "articles/2021/Nouvel-\303\251tabli-pour-l'atelier.html" create mode 100755 articles/2021/Odysee.html create mode 100755 articles/2021/Photo-de-mon-atelier.html create mode 100755 "articles/2021/Planche-\303\240-d\303\251couper-en-bois-de-bout.html" create mode 100755 articles/2021/Project-Hail-Mary.html create mode 100755 articles/2021/Pyradio.html create mode 100755 "articles/2021/Scie-\303\240-ruban-fait-maison-(1).html" create mode 100755 "articles/2021/Scie-\303\240-ruban-fait-maison-(2).html" create mode 100755 "articles/2021/Scie-\303\240-ruban-fait-maison-(3).html" create mode 100755 "articles/2021/Scie-\303\240-ruban-fait-maison-(4).html" create mode 100755 "articles/2021/Scie-\303\240-ruban-fait-maison-(5).html" create mode 100755 "articles/2021/Scie-\303\240-ruban-fait-maison-(6).html" create mode 100755 articles/2021/Science-et-Vie.html create mode 100755 articles/2021/Sous-plat-avec-incrustation-epoxy-Milliput.html create mode 100755 articles/2021/TLS-handshake-failure.html create mode 100755 articles/2021/Thinkpad-X200.html create mode 100755 articles/2021/Thinkpad-X220.html create mode 100755 "articles/2021/T\303\251l\303\251charger-musique-depuis-Youtube.html" create mode 100755 articles/2021/Un-espion-dans-ma-poche.html create mode 100755 articles/2021/Une-balancoire-pour-Barbie.html create mode 100755 articles/2021/Update.html create mode 100755 articles/2021/Wiby.html create mode 100755 articles/2021/ZRAM.html create mode 100755 articles/2021/Zellij-Terminal-Workspace.html create mode 100755 articles/2021/erreurs-spamass-milter.html create mode 100755 articles/2021/i3-Blocks.html create mode 100755 articles/2021/spamass-milter-error.html create mode 100755 articles/2021/web-bloat.html create mode 100755 articles/2021/wittr.in.html create mode 100755 "articles/2021/\303\211tau-de-recup.html" create mode 100755 "articles/2022/Scie-\303\240-ruban-fait-maison-(10).html" create mode 100755 "articles/2022/Scie-\303\240-ruban-fait-maison-(11).html" create mode 100755 "articles/2022/Scie-\303\240-ruban-fait-maison-(12).html" create mode 100755 "articles/2022/Scie-\303\240-ruban-fait-maison-(7).html" create mode 100755 "articles/2022/Scie-\303\240-ruban-fait-maison-(8).html" create mode 100755 "articles/2022/Scie-\303\240-ruban-fait-maison-(9).html" create mode 100755 articles/2022/mailcow.html create mode 100755 articles/2023/1vyrain.html create mode 100755 articles/2023/Bitwarden-RBW-blues.html create mode 100755 articles/2023/Chimera's-Star.html create mode 100755 articles/2023/Death-of-the-web.html create mode 100755 articles/2023/Firefox-Multi-Account-Containers.html create mode 100755 articles/2023/Forget-VTech,-get-an-old-laptop-instead.html create mode 100755 articles/2023/Gemini---The-protocol-not-the-AI.html create mode 100755 articles/2023/In-2024,-please-switch-to-Firefox.html create mode 100755 articles/2023/Little-Doggies.html create mode 100755 articles/2023/Marginalia-search-engine.html create mode 100755 articles/2023/Need-a-long-walk.html create mode 100755 articles/2023/Web-1.5.html create mode 100755 articles/2023/Web-site-stats-better-without-Google.html create mode 100755 articles/2023/ascii-art.html create mode 100755 articles/2023/connerie-des-dates-limites.html create mode 100755 articles/2023/debian-or-arch.html create mode 100755 articles/2023/drm-et-firefox.html create mode 100755 articles/2023/evil-empire.html create mode 100755 articles/2023/fixing-25-second-firefox-delay.html create mode 100755 articles/2023/hardening-firefox.html create mode 100755 articles/2023/images-in-xterm-emulators.html create mode 100755 articles/2023/jersey-before.html create mode 100755 articles/2023/kind-of-blue.html create mode 100755 articles/2023/linux-paradise.html create mode 100755 articles/2023/map-of-langourla.html create mode 100755 articles/2023/mer-des-faluns.html create mode 100755 articles/2023/micro-text-editor.html create mode 100755 articles/2023/minimalist-blog.html create mode 100755 articles/2023/ms-eu-fine.html create mode 100755 articles/2023/new-television.html create mode 100755 articles/2023/newsboat-strip-junk-from-web-pages.html create mode 100755 articles/2023/noyb-files-gdpr-comp.html create mode 100755 articles/2023/nuclear-sabotage.html create mode 100755 articles/2023/obscurantism.html create mode 100755 articles/2023/pacman-yay-notes.html create mode 100755 articles/2023/password-store-remote.html create mode 100755 articles/2023/password-store-to-bitwarden.html create mode 100755 articles/2023/periodic-fstrim-on-ssd-drives.html create mode 100755 articles/2023/prettier-when-it-works.html create mode 100755 articles/2023/protecting-your-computer-with-a-hosts-file.html create mode 100755 articles/2023/quad-9.html create mode 100755 articles/2023/radio-jazz-soul.html create mode 100755 articles/2023/saint-catherine.html create mode 100755 articles/2023/searxng-a-private-search-engine.html create mode 100755 articles/2023/searxng-on-my-laptop.html create mode 100755 articles/2023/system-collapse.html create mode 100755 articles/2023/t430-cpu-upgrade.html create mode 100755 articles/2023/t430-cpu-upgrade.html~ create mode 100755 articles/2023/thinkpad-t430.html create mode 100755 articles/2023/updating-my-thinkpad-t430-wifi-card.html create mode 100755 articles/2023/windowmaker-0.96.html create mode 100755 articles/2023/windowmaker-in-1998.html create mode 100755 articles/2024/A-new-lathe.html create mode 100755 articles/2024/Another-Oland-tool.html create mode 100755 "articles/2024/support-perc\303\251-pour-bougie-chauffe-plat.html" create mode 100755 articles/2025/A-fledgling-bash-function-library.html create mode 100755 articles/2025/AC7260-wifi-card-on-Thinkpad-X220.html create mode 100755 articles/2025/Amazon-playing-silly-buggers.html create mode 100755 articles/2025/Anacron-on-Artix.html create mode 100755 articles/2025/Annoying-touchpad.html create mode 100755 articles/2025/Archwiki-on-my-laptop.html create mode 100755 articles/2025/Back-to-the-future.html create mode 100755 articles/2025/Bash-prompt-without-scripts.html create mode 100755 articles/2025/Betterfox.html create mode 100755 articles/2025/Blog-scripts.html create mode 100755 articles/2025/Bringing-back-the-fun.html create mode 100755 articles/2025/Browser-choices.html create mode 100755 articles/2025/Carbide-tools-for-wood-turning.html create mode 100755 articles/2025/Chaotic-Aur.html create mode 100755 articles/2025/Closing-laptop-lid-without-suspend.html create mode 100755 articles/2025/Cpu-scaling-governors-on-Thinkpax-X220.html create mode 100755 articles/2025/DNS4U-European-DNS-servers-go-live.html create mode 100755 articles/2025/DWM-Dunst-and-Thinkpad-audio-keys.html create mode 100755 articles/2025/DWM-circulate-through-tags.html create mode 100755 articles/2025/DWM-slstatus.html create mode 100755 articles/2025/Daft-poems.html create mode 100755 articles/2025/Debian-12-to-13-upgrade.html create mode 100755 articles/2025/Detrumpez-vous.html create mode 100755 articles/2025/Dmenu-updates.html create mode 100755 articles/2025/Dwmblocks.html create mode 100755 articles/2025/Enable-Arch-Extra-repository-in-Artix.html create mode 100755 articles/2025/Eza.html create mode 100755 articles/2025/Fixing-my-dad's-Stanley-8-CFW-lock-plane.html create mode 100755 articles/2025/Flowers-for-the-bees.html create mode 100755 articles/2025/France-rejects-backdoors.html create mode 100755 articles/2025/Ghostty.html create mode 100755 articles/2025/Git-repository.html create mode 100755 articles/2025/Gopher-it.html create mode 100755 articles/2025/Greycoders-list-of-Text-only-minimalist-web-sites.html create mode 100755 articles/2025/Hard-disk-fail.html create mode 100755 articles/2025/Headless-Arch-Install.html create mode 100755 articles/2025/Html-center-element.html create mode 100755 articles/2025/Install-the-Chawan-text-browser-on-Artix.html create mode 100755 articles/2025/Jazz-radio-recording-from-1976.html create mode 100755 articles/2025/Koreader-for-e-ink-devices.html create mode 100755 articles/2025/Ladybird-web-browser.html create mode 100755 articles/2025/Les-Oiseaux-des-Jardins.html create mode 100755 articles/2025/Linux-System-Performance-Tuning.html create mode 100755 articles/2025/Linux-power-consumption-control.html create mode 100755 articles/2025/Lynx,-Gopher,-and-video-files.html create mode 100755 articles/2025/Mailcow-Docker-too-many-requests.html create mode 100755 articles/2025/Mailcow-with-Nginx-reverse-proxy-(updated).html create mode 100755 articles/2025/Mailcow-with-Nginx-reverse-proxy.html create mode 100755 articles/2025/Menu-editor-for-XFCE4.html create mode 100755 articles/2025/Microphone-distortion-on-Thinkpad-X220.html create mode 100755 articles/2025/Modified-dwm-bar-battery-script.html create mode 100755 articles/2025/My-gopher-site.html create mode 100755 articles/2025/Naked-DWM.html create mode 100755 articles/2025/Neomutt-as-default-in-XFCE4.html create mode 100755 articles/2025/New-builds-of-dwm-and-st.html create mode 100755 articles/2025/Newsboat-and-gopher-RSS-feeds.html create mode 100755 articles/2025/Oddly-useful-Linux-tools.html create mode 100755 articles/2025/Pipewire.html create mode 100755 articles/2025/Power-notifications.html create mode 100755 articles/2025/Powerline-Bash.html create mode 100755 articles/2025/RSS-feeds-that-don't-suck-on-terminals.html create mode 100755 articles/2025/Reducing-web-font-size.html create mode 100755 articles/2025/Replace-sudo-with-doas-on-Artix.html create mode 100755 articles/2025/Rsync-backup-to-Synology-Diskstation.html create mode 100755 articles/2025/Screen-scaling-with-xrandr.html create mode 100755 articles/2025/Shellcheck-your-bash-scripts.html create mode 100755 articles/2025/Simplex-a-decentralised-private-messenger.html create mode 100755 articles/2025/Slim-fork-login-manager.html create mode 100755 articles/2025/Slstatus-date-with-alternative-locale.html create mode 100755 articles/2025/Starship-prompt.html create mode 100755 articles/2025/Stick-it-to-the-man.html create mode 100755 articles/2025/TLP-recalibration-error-on-Thinkpad-X220.html create mode 100755 articles/2025/Taking-the-leap-to-Artix.html create mode 100755 articles/2025/Terminal-Trove.html create mode 100755 articles/2025/The-Mote-in-God's-eye.html create mode 100755 articles/2025/The-last-thing-the-EU-needs-....html create mode 100755 articles/2025/The-universe-is-spinning.html create mode 100755 articles/2025/Thinkpad-T430-screen-upgrade.html create mode 100755 articles/2025/Thinkpad-X220-Fan-control.html create mode 100755 articles/2025/Thinkvantage-button-in-DWM.html create mode 100755 articles/2025/Thoughts-on-suckless.html create mode 100755 articles/2025/Thoughts-on-zsh.html create mode 100755 articles/2025/Toggle-screen-resolution-in-DWM.html create mode 100755 articles/2025/Touchpad-deactivation.html create mode 100755 articles/2025/Traditional-screenshot-of-my-X220.html create mode 100755 articles/2025/Try-different-versions-of-Linux-online.html create mode 100755 articles/2025/Updated-cgit-server.html create mode 100755 articles/2025/Upload-modified-blog-files-with-scp.html create mode 100755 articles/2025/Using-Goaccess-to-view-web-stats.html create mode 100755 articles/2025/VPS-management.html create mode 100755 articles/2025/What-to-do-with-an-unused-Kindle.html create mode 100755 articles/2025/Why-does-everything-feel-so-boring-now.html create mode 100755 articles/2025/Wifi-region.html create mode 100755 articles/2025/Window-manager-thoughts.html create mode 100755 articles/2025/Workshop-Air-Cleaner.html create mode 100755 articles/2025/X220-internal-Bluetooth-controller.html create mode 100755 articles/2025/Xlibre-on-my-X220.html create mode 100755 articles/2025/dwm-windowmanager-1.html create mode 100755 articles/2025/dwm-windowmanager-2.html create mode 100755 articles/2025/dwmblocks-with-emoji-icons.html create mode 100644 articles/2026/De-Gaulle---Two-films.html create mode 100644 articles/2026/Filtering-the-web.html create mode 100644 articles/2026/Freedos-thoughts.html create mode 100644 articles/2026/From-minimalist-to-smolweb.html create mode 100644 articles/2026/Funeral-blues.html create mode 100644 articles/2026/Ghostbsd-installation.html create mode 100644 articles/2026/Hebdogiciel.html create mode 100644 articles/2026/Installation-of-Snac2-on-Artix-Linux.html create mode 100644 articles/2026/Ladybird-turns-into-a-squashed-bug.html create mode 100644 articles/2026/Living-on-Snac's-bleeding-edge.html create mode 100644 articles/2026/More-on-Dillo.html create mode 100644 articles/2026/My-own-private-search-engine.html create mode 100644 articles/2026/Oops!-I-did-it-again.html create mode 100644 articles/2026/This-blog-wasn't-so-bad-after-all.html create mode 100644 articles/2026/Updated-blogthis-script.html create mode 100644 articles/2026/Updated-poem.html create mode 100644 articles/2026/Web-sites-that-work-well-in-Dillo.html create mode 100644 articles/2026/displaying-MATE-on-a-1366x768-screen.html create mode 100644 articles/2026/semware-tse-editor.html create mode 100644 articles/2026/smolweb-validation-in-Dillo.html create mode 100644 articles/2026/upgrade-to-dwm-6.8.html create mode 100755 articles/test.html create mode 100644 deltachat.html create mode 100644 favicon-32x32.png create mode 100644 favicon.ico create mode 100644 feeds/fediverse-philip/feed create mode 100644 feeds/fediverse-philip/title create mode 100644 feeds/gemini-vigrey.com/feed create mode 100644 feeds/gemini-vigrey.com/index.php create mode 100644 feeds/gemini-vigrey.com/title create mode 100644 feeds/gopher-cosmic-voyage/feed create mode 100644 feeds/gopher-cosmic-voyage/index.php create mode 100644 feeds/gopher-cosmic-voyage/title create mode 100644 feeds/gopher-lfa/feed create mode 100644 feeds/gopher-lfa/index.php create mode 100644 feeds/gopher-lfa/title create mode 100644 feeds/gopher-mattos/feed create mode 100644 feeds/gopher-mattos/index.php create mode 100644 feeds/gopher-mattos/title create mode 100644 feeds/gopher-wittamore/feed create mode 100644 feeds/gopher-wittamore/index.html create mode 100644 feeds/gopher-wittamore/index.php create mode 100644 feeds/gopher-wittamore/title create mode 100644 feeds/http-20minutes/feed create mode 100644 feeds/http-20minutes/index.php create mode 100644 feeds/http-20minutes/title create mode 100644 feeds/http-adele/feed create mode 100644 feeds/http-adele/index.php create mode 100644 feeds/http-adele/title create mode 100644 feeds/http-bloggings/feed create mode 100644 feeds/http-bloggings/title create mode 100644 feeds/http-eff/feed create mode 100644 feeds/http-eff/index.php create mode 100644 feeds/http-eff/title create mode 100644 feeds/http-petergartner/feed create mode 100644 feeds/http-petergartner/index.php create mode 100644 feeds/http-petergartner/title create mode 100644 feeds/http-pluralistic/feed create mode 100644 feeds/http-pluralistic/index.php create mode 100644 feeds/http-pluralistic/title create mode 100644 feeds/http-scitechdaily/feed create mode 100644 feeds/http-scitechdaily/title create mode 100644 feeds/http-smolweb/feed create mode 100644 feeds/http-smolweb/index.php create mode 100644 feeds/http-smolweb/title create mode 100644 feeds/index.php create mode 100644 feeds/news-euronews/feed create mode 100644 feeds/news-euronews/index.php create mode 100644 feeds/news-euronews/title create mode 100644 feeds/news-guardian-europe/feed create mode 100644 feeds/news-guardian-europe/index.php create mode 100644 feeds/news-guardian-europe/title create mode 100644 feeds/youtube-mental-outlaw/feed create mode 100644 feeds/youtube-mental-outlaw/index.php create mode 100644 feeds/youtube-mental-outlaw/title create mode 100644 feeds/youtube-naomi-brockwell/feed create mode 100644 feeds/youtube-naomi-brockwell/index.php create mode 100644 feeds/youtube-naomi-brockwell/title create mode 100644 feeds/youtube-veronica-explains/feed create mode 100644 feeds/youtube-veronica-explains/index.php create mode 100644 feeds/youtube-veronica-explains/title create mode 100644 feeds/youtube-yousuckatprogramming/feed create mode 100644 feeds/youtube-yousuckatprogramming/index.php create mode 100644 feeds/youtube-yousuckatprogramming/title create mode 100755 files/bulletins/bulletin_032015_WEB.pdf create mode 100755 files/bulletins/bulletin_052014_006_WEB.pdf create mode 100755 files/bulletins/bulletin_062015_web.pdf create mode 100755 files/bulletins/bulletin_092014_01_WEB.pdf create mode 100755 files/bulletins/bulletin_122014_WEB.pdf create mode 100755 files/langourla.pdf create mode 100755 gopher/de/index.html create mode 100755 gopher/de/smolnet.html create mode 100755 gopher/en/index.html create mode 100755 gopher/en/smolnet.html create mode 100755 gopher/es/index.html create mode 100755 gopher/es/smolnet.html create mode 100755 gopher/fr/index.html create mode 100755 gopher/fr/smolnet.html create mode 100755 gopher/gdpr.html create mode 100755 gopher/gopherfr-translations.tar.gz create mode 100755 gopher/images/chawan_1.png create mode 100755 gopher/images/chawan_2.png create mode 100755 gopher/images/gopher-dark.png create mode 100755 gopher/images/gopher.png create mode 100755 gopher/images/gopher_IGICv1.12..png create mode 100755 gopher/images/gopher_elpher.png create mode 100755 gopher/images/gopher_gophercle_android.jpg create mode 100755 gopher/images/gopher_little_gopher_client_mac.png create mode 100755 gopher/images/gopherus.png create mode 100755 gopher/images/lagrange.png create mode 100755 gopher/images/lynx_1.png create mode 100755 gopher/index.html create mode 100755 gopher/it/index.html create mode 100755 gopher/it/smolnet.html create mode 100755 gopher/style.css create mode 100755 images/1.png create mode 100755 images/Lenovo-ThinkPad-X220.JPEG create mode 100755 images/Lenovo-ThinkPad-X220.webp create mode 100755 images/ac-7620-spot.jpg create mode 100755 images/alacritty.JPEG create mode 100755 images/alacritty.webp create mode 100755 images/applixware-labelmaker_0.jpg create mode 100755 images/applixware-labelmaker_1.jpg create mode 100755 images/arch-logo.JPEG create mode 100755 images/arch-logo.webp create mode 100755 images/balancoire.JPEG create mode 100755 images/balancoire.webp create mode 100755 images/bandsaw-frame-pieces.JPEG create mode 100755 images/bandsaw-frame-pieces.webp create mode 100755 images/bandsaw_1.JPEG create mode 100755 images/bandsaw_1.jpg create mode 100755 images/bandsaw_1.webp create mode 100755 images/blog/Lenovo-ThinkPad-X220.JPEG create mode 100755 images/blog/Lenovo-ThinkPad-X220.webp create mode 100755 images/blog/Screenshot_2025-04-08_14-20-40.png create mode 100755 images/blog/alacritty.JPEG create mode 100755 images/blog/alacritty.webp create mode 100755 images/blog/arch-logo.JPEG create mode 100755 images/blog/arch-logo.webp create mode 100755 images/blog/balancoire.JPEG create mode 100755 images/blog/balancoire.webp create mode 100755 images/blog/bandsaw-frame-pieces.JPEG create mode 100755 images/blog/bandsaw-frame-pieces.webp create mode 100755 images/blog/bandsaw_1.JPEG create mode 100755 images/blog/bandsaw_1.jpg create mode 100755 images/blog/bandsaw_1.webp create mode 100755 images/blog/bash-prompt-pywal.png create mode 100755 images/blog/battery.c.png create mode 100755 images/blog/bonneannee.gif create mode 100755 images/blog/box-chevaux.JPEG create mode 100755 images/blog/box-chevaux.webp create mode 100755 images/blog/carbide-tools.jpg create mode 100755 images/blog/cha-wittamore-1.png create mode 100755 images/blog/chawan-ghostty.png create mode 100755 images/blog/classic-keyboard.JPEG create mode 100755 images/blog/classic-keyboard.webp create mode 100755 images/blog/core-i5-cpu.JPEG create mode 100755 images/blog/core-i5-cpu.png create mode 100755 images/blog/core-i5-cpu.webp create mode 100755 images/blog/dane-success.png create mode 100755 images/blog/dwm-btop.png create mode 100755 images/blog/dwm_bar.png create mode 100755 images/blog/dwmblocks-emoji.png create mode 100755 images/blog/espion-dans-notre-poche.JPEG create mode 100755 images/blog/espion-dans-notre-poche.webp create mode 100755 images/blog/etau.JPEG create mode 100755 images/blog/etau.webp create mode 100755 images/blog/eu-flag.JPEG create mode 100755 images/blog/eu-flag.webp create mode 100755 images/blog/eza.png create mode 100755 images/blog/first-box.JPEG create mode 100755 images/blog/first-box.webp create mode 100755 images/blog/fugitive-telemetry.JPEG create mode 100755 images/blog/fugitive-telemetry.webp create mode 100755 images/blog/ghostty.gif create mode 100755 images/blog/grande-vase.JPEG create mode 100755 images/blog/grande-vase.webp create mode 100755 images/blog/gsimplecal.JPEG create mode 100755 images/blog/gsimplecal.webp create mode 100755 images/blog/hail-mary-wier.JPEG create mode 100755 images/blog/hail-mary-wier.webp create mode 100755 images/blog/happy-new-year.gif create mode 100755 images/blog/i3-blocks.JPEG create mode 100755 images/blog/i3-blocks.webp create mode 100755 images/blog/kbca-jazz-radio.png create mode 100755 images/blog/kobo-clara-colour.png create mode 100755 images/blog/la-grande-illusion.JPEG create mode 100755 images/blog/la-grande-illusion.webp create mode 100755 images/blog/librefox-wittamore-1.png create mode 100755 images/blog/links-wittamore-1.png create mode 100755 images/blog/lnav.JPEG create mode 100755 images/blog/lnav.webp create mode 100755 images/blog/lower-blade-guide-1.JPEG create mode 100755 images/blog/lower-blade-guide-1.webp create mode 100755 images/blog/maison-poupee.JPEG create mode 100755 images/blog/maison-poupee.webp create mode 100755 images/blog/mini-chariot.JPEG create mode 100755 images/blog/mini-chariot.webp create mode 100755 images/blog/mygophersite.png create mode 100755 images/blog/neomutt-screenshot.png create mode 100755 images/blog/new-bench.JPEG create mode 100755 images/blog/new-bench.webp create mode 100755 images/blog/newsboat-cha-1.png create mode 100755 images/blog/newsboat-rdrview-1.png create mode 100755 images/blog/oak-bowl-28cm.JPEG create mode 100755 images/blog/oak-bowl-28cm.webp create mode 100755 images/blog/odysee.JPEG create mode 100755 images/blog/odysee.webp create mode 100755 images/blog/pense-bete.JPEG create mode 100755 images/blog/pense-bete.webp create mode 100755 "images/blog/planche-\303\240-d\303\251couper-jacqueline.JPEG" create mode 100755 "images/blog/planche-\303\240-d\303\251couper-jacqueline.webp" create mode 100755 images/blog/poster-oiseaux-des-jardins.png create mode 100755 images/blog/powerline_bash.png create mode 100755 images/blog/powermenu.png create mode 100755 images/blog/pyradio-again.png create mode 100755 images/blog/pyradio.JPEG create mode 100755 images/blog/pyradio.webp create mode 100755 images/blog/pywal16-screenshot.png create mode 100755 images/blog/resistance.jpg create mode 100755 images/blog/riddings-web-site.JPEG create mode 100755 images/blog/riddings-web-site.webp create mode 100755 images/blog/screw-jig.JPEG create mode 100755 images/blog/screw-jig.webp create mode 100644 images/blog/semware.png create mode 100755 images/blog/shadocks-pumping.gif create mode 100755 images/blog/slim-theme.png create mode 100755 images/blog/slstatus-french.png create mode 100755 images/blog/slstatus.png create mode 100755 images/blog/sous-plat-incrustation.JPEG create mode 100755 images/blog/sous-plat-incrustation.webp create mode 100755 images/blog/starship-gruvbox-rainbow.png create mode 100755 images/blog/stop-block.JPEG create mode 100755 images/blog/stop-block.webp create mode 100755 images/blog/support-roue-superior.JPEG create mode 100755 images/blog/support-roue-superior.webp create mode 100755 images/blog/support-trunnion.JPEG create mode 100755 images/blog/support-trunnion.webp create mode 100755 images/blog/table-support-trunions.JPEG create mode 100755 images/blog/table-support-trunions.webp create mode 100755 images/blog/thinkpad-x220-mic-button.JPEG create mode 100755 images/blog/thinkpad-x220-mic-button.webp create mode 100755 images/blog/thinkvantage.png create mode 100755 images/blog/tprg.JPEG create mode 100755 images/blog/tprg.webp create mode 100755 images/blog/trunion-shim.JPEG create mode 100755 images/blog/trunion-shim.webp create mode 100755 images/blog/trunions-chevilles.JPEG create mode 100755 images/blog/trunions-chevilles.webp create mode 100755 images/blog/upper-blade-guide-1.JPEG create mode 100755 images/blog/upper-blade-guide-1.webp create mode 100755 images/blog/w3m-wittamore-1.png create mode 100755 images/blog/workshop-update.JPEG create mode 100755 images/blog/workshop-update.webp create mode 100755 images/blog/wttr.JPEG create mode 100755 images/blog/wttr.webp create mode 100755 images/blog/x200.JPEG create mode 100755 images/blog/x200.webp create mode 100755 images/blog/xfce4-panel-cpu-fan.png create mode 100755 images/blog/zellij.JPEG create mode 100755 images/blog/zellij.webp create mode 100755 images/bloggings.jpg create mode 100755 images/bonneannee.gif create mode 100755 images/bowl.jpg create mode 100755 images/box-chevaux.JPEG create mode 100755 images/box-chevaux.webp create mode 100755 images/browsers/dillo.jpg create mode 100755 images/browsers/firefox.jpg create mode 100755 images/browsers/index.php create mode 100755 images/browsers/links.jpg create mode 100755 images/browsers/lynx.jpg create mode 100755 images/browsers/w3m.jpg create mode 100755 images/chantournage/animaux-ferme.webp create mode 100755 images/chantournage/boite-coeur.webp create mode 100755 images/chantournage/feuilles-plumes.webp create mode 100755 "images/chantournage/f\303\252te-des-grandparents.webp" create mode 100755 images/chantournage/index.php create mode 100755 "images/chantournage/moto-ch\303\252ne.webp" create mode 100755 "images/chantournage/sous-plat-ch\303\252ne-33cm-triskele.webp" create mode 100755 images/chantournage/sous-plat-hermine.webp create mode 100755 images/chantournage/triskele-cp-bouleau.webp create mode 100755 images/chimera.jpg create mode 100755 images/classic-keyboard.JPEG create mode 100755 images/classic-keyboard.webp create mode 100755 images/core-i5-cpu.JPEG create mode 100755 images/core-i5-cpu.png create mode 100755 images/core-i5-cpu.webp create mode 100755 images/coronet-herald.jpg create mode 100755 images/corse-1975.jpg create mode 100755 images/dates-limites.jpg create mode 100755 images/debian-vs-arch.jpg create mode 100644 images/deltachatqr.jpg create mode 100755 images/espion-dans-notre-poche.JPEG create mode 100755 images/espion-dans-notre-poche.webp create mode 100755 images/etau.JPEG create mode 100755 images/etau.webp create mode 100755 images/eu-flag.JPEG create mode 100755 images/eu-flag.webp create mode 100755 images/evil-empire.jpg create mode 100755 images/firefox-drm.jpg create mode 100755 images/first-box.JPEG create mode 100755 images/first-box.webp create mode 100755 images/fugitive-telemetry.JPEG create mode 100755 images/fugitive-telemetry.webp create mode 100755 images/grande-vase.JPEG create mode 100755 images/grande-vase.webp create mode 100755 images/gsimplecal.JPEG create mode 100755 images/gsimplecal.webp create mode 100755 images/hail-mary-wier.JPEG create mode 100755 images/hail-mary-wier.webp create mode 100755 images/happy-new-year.gif create mode 100755 images/histoire/1740.jpg create mode 100755 images/histoire/1866.jpg create mode 100755 images/histoire/Allee-couverte-la-rocher.png create mode 100755 "images/histoire/Ch\303\252ne-mill\303\251naire-saint-jacut.jpg" create mode 100755 images/histoire/Menhir-de-la-Touche-article.png create mode 100755 images/histoire/arrete-confitures-1942.webp create mode 100755 images/histoire/bas_fournaux.jpg create mode 100755 images/histoire/bourg-1824.png create mode 100755 images/histoire/chateau-de-la-couaille.jpg create mode 100755 images/histoire/cocantine.png create mode 100755 images/histoire/cour-de-queneleue.png create mode 100755 images/histoire/delivrance-cartes-alimentaires-1941.webp create mode 100755 images/histoire/eglise_saint_eutrope.jpg create mode 100755 images/histoire/haches-langourla.png create mode 100755 images/histoire/index.php create mode 100755 images/histoire/vieux-chatet-de-coelan.png create mode 100755 images/i3-blocks.JPEG create mode 100755 images/i3-blocks.webp create mode 100755 images/index.php create mode 100755 images/informatique/amstrad-cpc-464.jpg create mode 100755 images/informatique/burroghs_tc500.jpg create mode 100755 images/informatique/ibmpc.jpg create mode 100755 images/informatique/index.php create mode 100755 "images/informatique/no\303\253l-magasin.jpg" create mode 100755 images/informatique/shop.jpg create mode 100755 images/informatique/tandy1000hx.jpg create mode 100755 images/informatique/zx81.jpg create mode 100755 images/jersey-before.jpg create mode 100755 images/kobo-clara-colour.png create mode 100755 images/la-grande-illusion.JPEG create mode 100755 images/la-grande-illusion.webp create mode 100755 images/langourla_map.jpg create mode 100755 images/linux-servers-cloud.jpg create mode 100755 images/lnav.JPEG create mode 100755 images/lnav.webp create mode 100755 images/logos/aposit.jpg create mode 100755 images/logos/applixware.jpg create mode 100755 images/logos/avenir.jpg create mode 100755 images/logos/emess.jpg create mode 100755 images/logos/greenhouse.jpg create mode 100755 images/logos/idinan.jpg create mode 100755 images/logos/index.php create mode 100755 images/logos/langourlavie.jpg create mode 100755 images/logos/quilting.jpg create mode 100755 images/logos/vistasource.jpg create mode 100755 images/lower-blade-guide-1.JPEG create mode 100755 images/lower-blade-guide-1.webp create mode 100755 images/mailcow-logo.png create mode 100755 images/maison-poupee.JPEG create mode 100755 images/maison-poupee.webp create mode 100755 images/me.jpg create mode 100755 images/mene/langourla/cartes-postales/1937-ancien-mairie.webp create mode 100755 images/mene/langourla/cartes-postales/avenues-saint-joseph-2.webp create mode 100755 images/mene/langourla/cartes-postales/avenues-saint-joseph.webp create mode 100755 images/mene/langourla/cartes-postales/bourg-et-monument.webp create mode 100755 images/mene/langourla/cartes-postales/bourg.webp create mode 100755 "images/mene/langourla/cartes-postales/caf\303\251-du-centre.webp" create mode 100755 "images/mene/langourla/cartes-postales/champ-du-ch\303\252ne.webp" create mode 100755 "images/mene/langourla/cartes-postales/chapel-et-ch\303\252ne.webp" create mode 100755 images/mene/langourla/cartes-postales/chapelle-saint-joseph.webp create mode 100755 images/mene/langourla/cartes-postales/chateau-de-coelan.webp create mode 100755 "images/mene/langourla/cartes-postales/ch\303\252ne-miraculeux.webp" create mode 100755 images/mene/langourla/cartes-postales/ecole.webp create mode 100755 images/mene/langourla/cartes-postales/eglise.webp create mode 100755 images/mene/langourla/cartes-postales/index.php create mode 100755 images/mene/langourla/cartes-postales/multivues.webp create mode 100755 images/mene/langourla/cartes-postales/rocher-le-celte.webp create mode 100755 "images/mene/langourla/cartes-postales/tour-saint-eutrop-et-cimiti\303\250re.webp" create mode 100755 images/mene/langourla/cartes-postales/tour-saint-eutrop.webp create mode 100755 images/mene/langourla/cartes-postales/vieux-moulin-de-cadeuc.webp create mode 100755 images/mene/langourla/cartes-postales/villa-des-rosiers.webp create mode 100755 images/mene/langourla/cartes-postales/vue-aerienne.webp create mode 100755 images/mene/langourla/cartes-postales/vue-eglise.webp create mode 100755 "images/mene/langourla/photos/Chemin-de-saint-gilles-\303\240-Tangaie.webp" create mode 100755 images/mene/langourla/photos/La-roche-de-la-Jument.webp create mode 100755 "images/mene/langourla/photos/Pied-de-boeuf-\303\240-Tangaie.webp" create mode 100755 images/mene/langourla/photos/index.php create mode 100755 images/mene/logos/collinee.png create mode 100755 images/mene/logos/index.php create mode 100755 images/mene/logos/langourla.png create mode 100755 images/mene/logos/le gouray.png create mode 100755 images/mene/logos/plessala.png create mode 100755 images/mene/logos/saint gilles.png create mode 100755 images/mene/logos/saint goueno.png create mode 100755 "images/mene/logos/saint jacut du men\303\251.png" create mode 100755 images/mene/saint-jacut-du-mene/cartes-postale create mode 100755 "images/mene/saint-jacut-du-mene/cartes-postales/All\303\251e-couverte-du-Rocher.webp" create mode 100755 "images/mene/saint-jacut-du-mene/cartes-postales/Ch\303\252ne-mill\303\251naire.webp" create mode 100755 images/mene/saint-jacut-du-mene/cartes-postales/Menhir-de-la-Touche.webp create mode 100755 images/mene/saint-jacut-du-mene/cartes-postales/chapelle-et-monument.webp create mode 100755 images/mene/saint-jacut-du-mene/cartes-postales/chapelle-n.d-de-bon-reconfort.webp create mode 100755 images/mene/saint-jacut-du-mene/cartes-postales/chapelle-saint-jean-baptiste.webp create mode 100755 images/mene/saint-jacut-du-mene/cartes-postales/chateau-du-parc.webp create mode 100755 images/mene/saint-jacut-du-mene/cartes-postales/eglise.webp create mode 100755 images/mene/saint-jacut-du-mene/cartes-postales/index.php create mode 100755 images/mene/saint-jacut-du-mene/cartes-postales/la-batterie.webp create mode 100755 images/mene/saint-jacut-du-mene/cartes-postales/recette-buraliste.webp create mode 100755 images/mene/saint-jacut-du-mene/cartes-postales/une-batterie.webp create mode 100755 images/mene/saint-jacut-du-mene/photos/bois-du-moulin.webp create mode 100755 "images/mene/saint-jacut-du-mene/photos/ch\303\252ne-du-parc-1.webp" create mode 100755 "images/mene/saint-jacut-du-mene/photos/ch\303\252ne-du-parc-2.webp" create mode 100755 "images/mene/saint-jacut-du-mene/photos/ch\303\252ne-du-parc-3.webp" create mode 100755 images/mene/saint-jacut-du-mene/photos/eglise-vue-de-la-mairie.webp create mode 100755 images/mene/saint-jacut-du-mene/photos/index.php create mode 100755 images/menuiserie/abris jeux enfants.webp create mode 100755 images/menuiserie/abris-jardin.webp create mode 100755 images/menuiserie/abris-jeux-enfants.webp create mode 100755 images/menuiserie/avion.webp create mode 100755 "images/menuiserie/berceau-poup\303\251e.webp" create mode 100755 images/menuiserie/bonhomme-neige.webp create mode 100755 images/menuiserie/chariot-scie-table.webp create mode 100755 images/menuiserie/compas-400mm.webp create mode 100755 images/menuiserie/dominos-cumaru.jpg create mode 100755 images/menuiserie/dominos.jpg create mode 100755 images/menuiserie/etau.webp create mode 100755 images/menuiserie/index.php create mode 100755 images/menuiserie/planche-en-bois-de-bout.webp create mode 100755 images/menuiserie/support-roulettes.webp create mode 100755 images/menuiserie/table-atelier.webp create mode 100755 images/menuiserie/voiture-oliver.webp create mode 100755 images/mer-des-faluns.jpg create mode 100755 images/mini-chariot.JPEG create mode 100755 images/mini-chariot.webp create mode 100755 images/mugshot-02-12-2023.png create mode 100755 images/mugshot-09-11-2023.jpg create mode 100755 images/new-bench.JPEG create mode 100755 images/new-bench.webp create mode 100755 images/new-oland-tool.jpg create mode 100755 images/oak-bowl-28cm.JPEG create mode 100755 images/oak-bowl-28cm.webp create mode 100755 images/obscurantism.jpg create mode 100755 images/odysee.JPEG create mode 100755 images/odysee.webp create mode 100755 images/oland-tools.jpg create mode 100755 images/pense-bete.JPEG create mode 100755 images/pense-bete.webp create mode 100755 images/phil_from_france.jpg create mode 100755 "images/planche-\303\240-d\303\251couper-jacqueline.JPEG" create mode 100755 "images/planche-\303\240-d\303\251couper-jacqueline.webp" create mode 100755 images/prettier.jpg create mode 100755 images/pyradio.JPEG create mode 100755 images/pyradio.webp create mode 100755 images/pyrogravure/chaise-clara.webp create mode 100755 images/pyrogravure/chestnut-bowl.webp create mode 100755 "images/pyrogravure/graines-tonton-lo\303\257c.webp" create mode 100755 images/pyrogravure/guitar-ian.webp create mode 100755 images/pyrogravure/index.php create mode 100755 "images/pyrogravure/logo-fer-\303\240-marquer.webp" create mode 100755 "images/pyrogravure/triskel-sous-plat-sign\303\251.webp" create mode 100755 "images/pyrogravure/vase-ch\303\252ne-d\303\251cor\303\251.webp" create mode 100755 images/recordpower_AC400.jpg create mode 100755 images/riddings-web-site.JPEG create mode 100755 images/riddings-web-site.webp create mode 100755 images/rss.png create mode 100755 images/saint-catherine.jpg create mode 100755 images/scie-alignment-1.jpg create mode 100755 images/scie-alignment-2.jpg create mode 100755 images/scie-completed-1.jpg create mode 100755 images/scie-completed-2.jpg create mode 100755 images/scie-completed-3.jpg create mode 100755 images/scie-completed-4.jpg create mode 100755 images/scie-completed-5.jpg create mode 100755 images/scie-frame-1.jpg create mode 100755 images/scie-frame-2.jpg create mode 100755 images/scie-frame-3.jpg create mode 100755 images/scie-frame-4.jpg create mode 100755 images/scie-improvement-1.jpg create mode 100755 images/scie-parts-1.jpg create mode 100755 images/scie-wheel-1.jpg create mode 100755 images/screw-jig.JPEG create mode 100755 images/screw-jig.webp create mode 100755 images/searxng.jpg create mode 100755 images/shadocks-pumping-300.gif create mode 100755 images/shadocks-pumping.gif create mode 100644 images/shadocks.gif create mode 100755 images/shop.jpg create mode 100755 images/sous-plat-incrustation.JPEG create mode 100755 images/sous-plat-incrustation.webp create mode 100755 images/stop-block.JPEG create mode 100755 images/stop-block.webp create mode 100755 images/support-roue-superior.JPEG create mode 100755 images/support-roue-superior.webp create mode 100755 images/support-trunnion.JPEG create mode 100755 images/support-trunnion.webp create mode 100755 images/systemcollapse.jpg create mode 100755 images/t430-cpu-upgrade.jpg create mode 100755 images/table-support-trunions.JPEG create mode 100755 images/table-support-trunions.webp create mode 100755 images/tcl.webp create mode 100755 images/thinkpad-x220-mic-button.JPEG create mode 100755 images/thinkpad-x220-mic-button.webp create mode 100755 "images/tour/1er-id\303\251e.webp" create mode 100755 "images/tour/2i\303\250me-id\303\251e.webp" create mode 100755 "images/tour/3i\303\250me-id\303\251e.webp" create mode 100755 "images/tour/4-poup\303\251e-fixe.webp" create mode 100755 "images/tour/5-porte-outil-poup\303\251e-mobile.webp" create mode 100755 "images/tour/6-id\303\251e-poup\303\251e-mobile.webp" create mode 100755 "images/tour/7-poup\303\251e-mobile.webp" create mode 100755 images/tour/8-contre-pointe-tournante.webp create mode 100755 images/tour/9-ecrou-de-blocage.webp create mode 100755 "images/tour/91-cliquet-blocage-poup\303\251e-mobile.webp" create mode 100755 images/tour/92-broche.webp create mode 100755 images/tour/93-assemblage.webp create mode 100755 images/tour/931-serrage--porte-outils.webp create mode 100755 images/tour/932-blocage-broche.webp create mode 100755 images/tour/94-300-tpm.webp create mode 100755 images/tour/95-600-tpm.webp create mode 100755 images/tour/96-1400-tpm.webp create mode 100755 images/tour/97-finale.webp create mode 100755 images/tour/index.php create mode 100755 images/tournages/3-bols-chataigner.webp create mode 100755 images/tournages/4-bols-cerisier.webp create mode 100755 images/tournages/Pot-en-charme.webp create mode 100755 images/tournages/bol-cerisier.webp create mode 100755 "images/tournages/bol-ch\303\242taigner.webp" create mode 100755 "images/tournages/bol-h\303\252tre-12-02-20.webp" create mode 100755 "images/tournages/bol-h\303\252tre-15-02-20.webp" create mode 100755 "images/tournages/bol-h\303\252tre-21-02-20.webp" create mode 100755 "images/tournages/bol-h\303\252tre-epoxy.webp" create mode 100755 images/tournages/coquetiers-modernes.webp create mode 100755 images/tournages/dauphin.webp create mode 100755 images/tournages/deux-vases-thuya.webp create mode 100755 images/tournages/ebauche-bol-30cm.webp create mode 100755 "images/tournages/exp\303\251rimentation.webp" create mode 100755 images/tournages/fleurs.webp create mode 100755 images/tournages/flowers.webp create mode 100755 images/tournages/grande-vase.webp create mode 100755 images/tournages/index.php create mode 100755 images/tournages/mortier-cerisier.webp create mode 100755 images/tournages/mortier-pilon-cerisier.webp create mode 100755 images/tournages/oak-bowl-28cm.webp create mode 100755 images/tournages/oak_bowl_experiment_1.webp create mode 100755 images/tournages/petit-vase-thuya.webp create mode 100755 images/tournages/plat-ceriser.webp create mode 100755 images/tournages/porte-bougie-1.jpg create mode 100755 images/tournages/pot-charme-pyro.webp create mode 100755 images/tournages/pot-open.webp create mode 100755 images/tournages/pot.webp create mode 100755 images/tournages/tall-vase.webp create mode 100755 images/tournages/two-vases.webp create mode 100755 "images/tournages/vase-ch\303\252ne.webp" create mode 100755 images/tournages/vase-moabi.webp create mode 100755 images/tournages/vase-thuya.webp create mode 100755 images/tprg.JPEG create mode 100755 images/tprg.webp create mode 100755 images/trunion-shim.JPEG create mode 100755 images/trunion-shim.webp create mode 100755 images/trunions-chevilles.JPEG create mode 100755 images/trunions-chevilles.webp create mode 100755 images/upper-blade-guide-1.JPEG create mode 100755 images/upper-blade-guide-1.webp create mode 100755 images/w3m-display-options.png create mode 100755 images/windowmaker096.png create mode 100755 images/workshop-update.JPEG create mode 100755 images/workshop-update.webp create mode 100755 images/wttr.JPEG create mode 100755 images/wttr.webp create mode 100755 images/x200.JPEG create mode 100755 images/x200.webp create mode 100755 images/xterm-images-400.jpg create mode 100755 images/zellij.JPEG create mode 100755 images/zellij.webp create mode 100755 index.php create mode 100644 logo.png create mode 100644 nginx.conf create mode 100755 robots.txt create mode 100644 rss.xml create mode 100644 sandbox/webproxy.php create mode 100644 scripts/SimplePie.compiled.php create mode 100644 scripts/getfeeds_http.php create mode 100755 search.php create mode 100755 sitemap.xml create mode 100755 static/140-html-color-names.html create mode 100755 static/Interesting-links.html create mode 100755 static/Mention-Legal.html create mode 100755 static/Neomutt.html create mode 100755 static/about.html create mode 100755 static/index.php create mode 100755 static/le-mene.html create mode 100755 static/summer-of-75.html create mode 100755 static/woodworking.html create mode 100755 "static/woodworking/Scie-\303\240-ruban-fait-maison.html" create mode 100755 static/woodworking/oland-tools.html create mode 100755 "static/woodworking/tour-\303\240-bois.html" create mode 100755 style.css create mode 100755 txt/account_philip.txt create mode 100755 txt/display_filter.txt create mode 100755 txt/dwm-config.txt create mode 100755 txt/get-mailboxes.txt create mode 100755 txt/key.txt create mode 100755 txt/mailcap.txt create mode 100755 txt/mutt-trim.txt create mode 100755 txt/neomuttrc.txt create mode 100755 txt/poems.txt create mode 100755 txt/print_unicode.txt create mode 100755 txt/translate.txt create mode 100755 txt/update-fingerprint create mode 100755 txt/venelles.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..4dd74ab --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Bloggings + +This is my personal minimalist blog written in php and bash. + +It has no Javascript, cookies, trackers, or bloat, and can be viewed in any browser. + +It is also validated using the [smolweb validator](https://smolweb.org/validator/) diff --git a/articles/2021/Alacritty.html b/articles/2021/Alacritty.html new file mode 100755 index 0000000..a31d039 --- /dev/null +++ b/articles/2021/Alacritty.html @@ -0,0 +1,37 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Alacritty

+

2021-05-12

+

image alacritty

+

Lorsque j'ai commencé à utiliser i3 la semaine dernière, +j'ai opté d'utiliser xfce4-terminal pendant que je farfouillais. +Aujourd'hui, je suis passé à Alacritty. Il est écrit en Rust, et est très intéressant.

+

Par exemple , vous pouvez sélectionner du texte dans le terminal sans souris. +Démarrez le mode vi avec Ctrl+Shift+Space et vous pouvez déplacer le curseur avec +les flèches ou hjkl. Pour démarrer la sélection, appuyez sur v, pour copier, +appuyez sur y, pour coller, appuyez sur Ctrl+Shift+v. +Le texte copié va dans le presse-papiers primaire.

+

Si vous utilisez Ranger comme gestionnaire de fichiers, vous pouvez visualiser les images +dans le terminal Alacritty, et c'est vraiment rapide, je peux feuilleter ma collection +de photos sur mon NAS Synology (monté via cifs) avec facilité.

+

Installer avec pacman -S alacritty sur Manjaro ou Arch Linux

+
+
+ + diff --git a/articles/2021/Arch-LTS-kernel.html b/articles/2021/Arch-LTS-kernel.html new file mode 100755 index 0000000..095ecd9 --- /dev/null +++ b/articles/2021/Arch-LTS-kernel.html @@ -0,0 +1,45 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Arch LTS kernel

+

2021-05-28

+

Arch Linux est parfois étiqueté comme étant instable, +mais en réalité ce sont les options de pointe qui peuvent +le rendre instable. J'ai opté pour la stabilité, j'utilise +donc le noyau LTS.

+

Installer le noyeau LTS

+
sudo pacman -S linux-lts
+sudo grub-mkconfig -o /boot/grub/grub.cfg
+sudo reboot
+
+

Retirer le noyeau de pointe

+
sudo pacman -R linux
+sudo grub-mkconfig -o /boot/grub/grub.cfg
+sudo reboot
+
+

+J'ai dû supprimer acpi_call et tp-smapi avant de +retirer le noyau de pointe, et les réinstaller en +utilisant les versions DKMS dans l'AUR. +

+

Why and How to install the LTS kernel in Arch +Linux

+
+
+ + diff --git a/articles/2021/Arch-Linux.html b/articles/2021/Arch-Linux.html new file mode 100755 index 0000000..5803339 --- /dev/null +++ b/articles/2021/Arch-Linux.html @@ -0,0 +1,36 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Arch Linux

+

2021-05-28

+

arch logo

+

Banzaï! Je suis passé de Manjaro Linux à Arch Linux +sur mon X220

+

Manjaro est basé sur Arch, +et est génial pour les nouveaux utilisateurs, mais je voulais la vraie +article.

+

J'ai un peu triché en utilisant l'installateur +calamares +pour éviter 4 jours de configuration manuelle. Bien que vous deviez encore +effectuer un certain nombre d'étapes après avoir utilisé calamares pour +avoir une jolie Linux, l'installation de base est effectuée proprement.

+

J'ai maintenant une installation Arch pure avec le windowmanager i3, Firefox, Telegram, Neomutt, etc.

+
+
+ + diff --git "a/articles/2021/Biblioth\303\250que-Nationale-de-France.html" "b/articles/2021/Biblioth\303\250que-Nationale-de-France.html" new file mode 100755 index 0000000..35ae03b --- /dev/null +++ "b/articles/2021/Biblioth\303\250que-Nationale-de-France.html" @@ -0,0 +1,27 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Bibliothèque Nationale de France

+

2021-04-07

+

La BNF est vraiment une mine d'or. J'étais à la recherche d'informations historiques via Gallica. J'ai trouvé les données très bien présentées et très agréables à utiliser. Voici un exemple de recherche +: Gallica

+

+

+
+ + diff --git "a/articles/2021/Bol-en-ch\303\252ne-de-28cm.html" "b/articles/2021/Bol-en-ch\303\252ne-de-28cm.html" new file mode 100755 index 0000000..91f7751 --- /dev/null +++ "b/articles/2021/Bol-en-ch\303\252ne-de-28cm.html" @@ -0,0 +1,28 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Bol en chêne de 28cm

+

2021-05-14

+

image

+

+De nouveau dans le garage. Fabriqué avec du chêne que mon ami Joël m'a donné. +

+
+
+ + diff --git a/articles/2021/Boxe-pour-chevaux-jouets.html b/articles/2021/Boxe-pour-chevaux-jouets.html new file mode 100755 index 0000000..bd19b12 --- /dev/null +++ b/articles/2021/Boxe-pour-chevaux-jouets.html @@ -0,0 +1,26 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Boxe pour chevaux jouets

+

2021-12-16

+

image

+

le père noël fait des heures supplémentaires.

+
+
+ + diff --git "a/articles/2021/Ce-que-l'Europe-\303\240-fait-pour-moi.html" "b/articles/2021/Ce-que-l'Europe-\303\240-fait-pour-moi.html" new file mode 100755 index 0000000..1d6732b --- /dev/null +++ "b/articles/2021/Ce-que-l'Europe-\303\240-fait-pour-moi.html" @@ -0,0 +1,37 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Ce que l'Europe à fait pour moi

+

2021-05-09

+

Eu Flag

+

Sans l'Union européenne, je n'aurais pas pu vivre et prospérer en France depuis 1995. +Voyager à travers l'Europe pendant mon adolescence m'a ouvert les yeux sur d'autres façons +de vivre et a calmé mon cerveau turbulent. J'ai bénéficié d'une vie sans guerre +(pour laquelle l'UE a reçu le prix Nobel), de la possibilité de voyager et de travailler +librement dans d'autres pays de l'UE, d'une économie stable grâce à l'euro, +et d'un environnement autre que le piège mortel du nationalisme.

+

Joyeux anniversaire l'Europe!

+ +
+
+ + diff --git a/articles/2021/Certbot-delete.html b/articles/2021/Certbot-delete.html new file mode 100755 index 0000000..1ccf064 --- /dev/null +++ b/articles/2021/Certbot-delete.html @@ -0,0 +1,30 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Certbot delete

+

2021-06-03

+

Ce matin, j'ai remarqué que le renouvellement des certificats letsencrypt +sur mon serveur de messagerie a avorté à cause d'un domaine qui n'existe plus. +La suppression manuel du domaine dans /etc/letsencrypt a aggravé la situation, +mais j'ai finalement découvert qu'il existe une commande simple pour le faire +:

+

certbot delete

+
+
+ + diff --git a/articles/2021/Coupure-microphone-X220.html b/articles/2021/Coupure-microphone-X220.html new file mode 100755 index 0000000..9f465b9 --- /dev/null +++ b/articles/2021/Coupure-microphone-X220.html @@ -0,0 +1,53 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Coupure microphone X220

+

2021-05-09

+

Dans mon Thinkpad X22O fonctionnant sous Manjaro i3, la bouton de coupure du micro est +désactivée, car il n'est pas pris en charge par thinkpad-acpi.
+Pour l'activer, voici ce que j'ai fait:

+

Premièrement, vérifier que l'acpi est installé:

+
sudo pacman -S acpi
+

1) créer le fichier /etc/acpi/events/lenovo-mutemic

+
+event=bouton/f20
+action=/etc/acpi/lenovo-mutemic.sh
+
+

2) créez le fichier /etc/acpi/lenovo-mutemic.sh

+
+#!/bin/bash
+INPUT_DEVICE=" 'Capture'"
+VOTRE_USERNAME="philip"
+if amixer sget $INPUT_DEVICE,0 | grep '\[on\]' ; then
+    amixer sset $INPUT_DEVICE,0 toggle
+    echo "0 blink" | sudo tee /proc/acpi/ibm/led
+    DISPLAY=":0.0" notify-send -t 1000 -i microphone-sensibilité-muted-symbolic "Mic MUTED"
+else
+    amixer sset $INPUT_DEVICE,0 toggle                       
+    DISPLAY=":0.0" notify-send -t 1000 -i microphone-sensitivity-high-symbolic "Mic ON"
+    echo "0 on" | sudo tee /proc/acpi/ibm/led 
+fi
+
+

3) redémarrer acpi

+
systemctl restart acpid.service
+

Adapté de réponseAskubuntu.

+
+
+ + diff --git "a/articles/2021/Deux-maisons-de-poup\303\251es.html" "b/articles/2021/Deux-maisons-de-poup\303\251es.html" new file mode 100755 index 0000000..e0934f1 --- /dev/null +++ "b/articles/2021/Deux-maisons-de-poup\303\251es.html" @@ -0,0 +1,27 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Deux maisons de poupées

+

2021-12-06

+

image

+

Deux maisons de poupées pour deux sœurs, l'une droitière, l'autre gauchère. Je les ai fabriquées à partir d'étagères en pin 18mm bon marché et de quelques chutes de bois +dur.

+
+
+ + diff --git "a/articles/2021/Exp\303\251riment.html" "b/articles/2021/Exp\303\251riment.html" new file mode 100755 index 0000000..ce22529 --- /dev/null +++ "b/articles/2021/Exp\303\251riment.html" @@ -0,0 +1,28 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Expériment

+

2021-08-02

+

expérimentation

+

+Je joue avec différentes formes pour voir ce que je peux faire. Ce morceau de chêne a été tourné deux fois, avec 5 mois de séchage entre les deux tournages. +

+
+
+ + diff --git a/articles/2021/Fabriquez-votre-propre-ordinateur-portable.htm b/articles/2021/Fabriquez-votre-propre-ordinateur-portable.htm new file mode 100755 index 0000000..009ce74 --- /dev/null +++ b/articles/2021/Fabriquez-votre-propre-ordinateur-portable.htm @@ -0,0 +1,53 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +

Fabriquez votre propre ordinateur portable?

+
2021-05-11
+

classic keyboard photo

+

Aujourd'hui, nous sommes tous victimes d'ordinateurs portables ultraminces avec des claviers de +pacotille, des boîtiers en plastique bon marché et l'impossibilité de les mettre à niveau en +raison de la soudure de la RAM sur la carte mère, ainsi que la batterie, +etc.

+

Il est si difficile de trouver un ordinateur portable avec un bon clavier que +les anciens ordinateurs portables tels que les Thinkpads X220, T420, W520 etc. +s'arrachent dans le monde entier. Oui, un X220 de 10 ans vaut son pesant d'or.

+

Pourquoi cela s'est-il produit ? L'avidité - supprimer des choses pour augmenter la marge bénéficiaire +sur le dos d'achéteurs peu méfiantes, répondre aux demandes de fashionistas +(je vous regarde, Apple) plutôt qu'à la qualité d'utilisation et de mise à jour ne sont que deux raisons. +Au diable ceux qui veulent des solutions écologiques à longue durée de vie.

+

Cela ne laisse pas beaucoup d'espoir pour l'avenir, à moins que les ordinateurs portables +auto-assemblés ne deviennent plus populaires, +ou que la production de petites séries devienne plus facile et financièrement rentable.

+

Il y a peut-être de l'espoir, Olitec +par exemple est une entreprise bulgare qui fait exactement cela avec son ordinateur portable TERES. +Il y a aussi le Pinebook. +J'en cherche d'autres et je les ajouterai dans ce blog lorsque je le trouve.

+

References:

+ + +

 

+ +
+ A minimalist blog by + Philip Wittamore +
+ + diff --git a/articles/2021/Fugitive-Telemetry.html b/articles/2021/Fugitive-Telemetry.html new file mode 100755 index 0000000..152500d --- /dev/null +++ b/articles/2021/Fugitive-Telemetry.html @@ -0,0 +1,29 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Fugitive Telemetry

+

2021-04-29

+

book cover

+

J'ai pleinement apprécié cette série jusqu'à présent, +l'histoire est racontée par un cyborg qui est plus humain que les humains qui l'entourent. +J'espère que ce livre sera tout aussi bon. +

+
+
+ + diff --git a/articles/2021/GeoIP2-et-Goaccess.html b/articles/2021/GeoIP2-et-Goaccess.html new file mode 100755 index 0000000..58c45a1 --- /dev/null +++ b/articles/2021/GeoIP2-et-Goaccess.html @@ -0,0 +1,33 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

GeoIP2 et Goaccess

+

2021-04-09

+

Aujourd'hui, j'ai mis à jour goaccess pour qu'il fonctionne avec GeoIP2. +J'ai d'abord téléchargé et installé la dernière version de geoipupdate, +puis créé un compte gratuit avec Maxmind +et obtenu un identifiant de compte et une clé de licence. +J'ai ajouté ceux-ci au fichier /etc/GeoIP.conf, et j'ai ajouté une entrée crontab avec +@weekly /usr/local/bin/geoipupdate +geoipupdate a mis à jour 2 fichiers dans /usr/share/GeoIP/.
+J'ai ensuite modifié goaccess.conf avec geoip-database /usr/share/GeoIP/GeoLite2-City.mmdb +et goaccess affiche maintenant les villes correspondant aux adresses IP.

+
+
+ + diff --git "a/articles/2021/Grande-vase-en-ch\303\252ne.html" "b/articles/2021/Grande-vase-en-ch\303\252ne.html" new file mode 100755 index 0000000..faf9632 --- /dev/null +++ "b/articles/2021/Grande-vase-en-ch\303\252ne.html" @@ -0,0 +1,29 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Grande vase en chêne

+

2021-04-26

+

image

+

J'ai fabriqué ce grand vase à partir de couches de chêne récupéré. +J'ai percé des trous de 40 mm dans chaque couche sauf deux, +et j'ai fait un bouchon de 40 mm pour pouvoir le tourner entre les deux extrémités du +tour. Voir dans tournages

+
+
+ + diff --git a/articles/2021/Intel-Thermald.html b/articles/2021/Intel-Thermald.html new file mode 100755 index 0000000..10c6b6c --- /dev/null +++ b/articles/2021/Intel-Thermald.html @@ -0,0 +1,46 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Intel Thermald

+

2021-05-14

+

core i5 cpu image

+

J'ai remarqué aujourd'hui que gcc a monté mon cpu i5 à +96° en compilant un paquet AUR. Le reconditionneur BackMarket m'a +assuré qu'il renouvelait la pâte thermique sur toutes ses machines, je +ne l'ai donc pas encore vérifiée. En cherchant une protection thermique +logicielle pour mon vénérable X220, je suis tombé sur thermald.

+

Depuis le wiki debian:

+

"Le démon thermique Linux (thermald) surveille et contrôle la température dans les +ordinateurs portables, les tablettes PC avec le dernier sandy bridge d'Intel +et les dernières versions de CPU Intel. Une fois que la température du +système atteint un certain seuil, le démon Linux active diverses méthodes +de refroidissement pour essayer de refroidir le système. "

+

Je ne sais pas si cela est suffisant, ou si une approche +plus compliquée utilisant cpupower sera nécessaire. Affaire à suivre..

+

Installation:

+
sudo pacman -S thermald
+sudo systemctl enable thermald
+sudo systemctl start thermald
+
+ +
+
+ + diff --git a/articles/2021/Invidious---une-interface-alternative-pour-Youtube.html b/articles/2021/Invidious---une-interface-alternative-pour-Youtube.html new file mode 100755 index 0000000..6cce186 --- /dev/null +++ b/articles/2021/Invidious---une-interface-alternative-pour-Youtube.html @@ -0,0 +1,28 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Invidious - une interface alternative pour Youtube

+

2021-04-17

+

Invidious vous permet de visionner Youtube sans pistage ni publicité.

+

Vous pouvez aussi télécharger les videos facilement

+

https://yewtu.be/

+

D'autres instances

+
+
+ + diff --git "a/articles/2021/Jitsi-vid\303\251oconference.html" "b/articles/2021/Jitsi-vid\303\251oconference.html" new file mode 100755 index 0000000..5ef5b9a --- /dev/null +++ "b/articles/2021/Jitsi-vid\303\251oconference.html" @@ -0,0 +1,37 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Jitsi vidéoconference

+

2021-04-05

+

Il y a un an, on m'a demandé de trouver si notre famille pouvait faire des +rencontres en vidéo pendant que nous étions confinés.

+ +

J'ai testé Jitsi, et j'ai découvert qu'il était idéal +pour les personnes peu familiarisées avec l'informatique, +car il suffit de partager un lien vers le site Web de la réunion Jitsi. +https://meet.jit.si/ et d'ajouter un nom unique, par exemple : +https://meet.jit.si/mon_nom_unique

+ +

Si vous souhaitez organiser une réunion régulière, +réutiliser simplement le même nom à chaque fois, +pensez tout de même à utiliser un nom que personne d'autre ne peut trouver par +hasard.

+
+
+ + diff --git a/articles/2021/La-grande-illusion.html b/articles/2021/La-grande-illusion.html new file mode 100755 index 0000000..cfaaa9a --- /dev/null +++ b/articles/2021/La-grande-illusion.html @@ -0,0 +1,28 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

La grande illusion

+

2021-05-10

+

couverture du livre

+

J'ai commencé à lire les notes de Michel Barnier sur les négociations du Brexit. +Il semble être impressionné par les fonctionnaires britanniques, mais beaucoup moins +par les politiciens auxquels ils doivent répondre. C'est très intéressant de connaître les coulisses de l'histoire.

+
+
+ + diff --git "a/articles/2021/Le-Jardin-de-Tonton-Lo\303\257c.html" "b/articles/2021/Le-Jardin-de-Tonton-Lo\303\257c.html" new file mode 100755 index 0000000..6a3ac97 --- /dev/null +++ "b/articles/2021/Le-Jardin-de-Tonton-Lo\303\257c.html" @@ -0,0 +1,28 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Le Jardin de Tonton Loïc

+

2021-04-14

+

Le Jardin de
+Loïc

+

Un peu de pyrogravure sur une boîte en bois de chez Amazon pour une +amie.

+
+
+ + diff --git "a/articles/2021/Lnav---ma\303\256triser-vos-journaux.html" "b/articles/2021/Lnav---ma\303\256triser-vos-journaux.html" new file mode 100755 index 0000000..48367c6 --- /dev/null +++ "b/articles/2021/Lnav---ma\303\256triser-vos-journaux.html" @@ -0,0 +1,36 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Lnav - maîtriser vos journaux

+

2021-06-13

+lnav screenshot +

Aujourd'hui, j'ai découvert LNAV, un excellent moyen de visualiser les logs de mon serveur.

+

Sous Centos:

+

+


+sudo dnf install lnav
+lnav /var/log
+
+

+

Ceci affiche tous les logs dans /var/logs, et en couleur.

+ +

Voir Lnav ici : lnav.org.

+
+
+ + diff --git a/articles/2021/Mbsync-push.html b/articles/2021/Mbsync-push.html new file mode 100755 index 0000000..2d5894b --- /dev/null +++ b/articles/2021/Mbsync-push.html @@ -0,0 +1,34 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Mbsync push

+

2021-05-04

+

J'ai bêtement copié mon .mbsyncrc sur mon nouvel ordinateur portable avant de copier le dossier .mail. Le résultat a été que mbsync a supprimé tout le courrier sur mon serveur +IMAP.

+

J'ai donc dû tout repousser depuis le dossier .mail de mon ancien ordinateur portable. Heureusement, il y a un bon guide en ligne +ici

+

L'astuce consiste à remettre à zéro le dossier .mail local en supprimant toutes les informations précédentes de mbsync.

+

retirer toutes les fichiers .mbsyncstate et .uidvalidity:

+find . -type f -name '.??*' -delete +

Ensuite retirer les identifiants mbsync des noms des fichiers mail:

+find . -type f -name '*U=*' | { while read f; do mv "$f" "`echo $f | sed -e 's/,U=[0-9]*//'`"; done } +

J'ai ensuite supprimé tous les dossiers sur mon serveur IMAP et j'ai procédé à leur restauration avec un push mbsync.

+
+
+ + diff --git "a/articles/2021/Mini-chariot-avec-but\303\251e-d\342\200\231arr\303\252t.html" "b/articles/2021/Mini-chariot-avec-but\303\251e-d\342\200\231arr\303\252t.html" new file mode 100755 index 0000000..2d0f9b8 --- /dev/null +++ "b/articles/2021/Mini-chariot-avec-but\303\251e-d\342\200\231arr\303\252t.html" @@ -0,0 +1,27 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Mini chariot avec butée d’arrêt

+

2021-12-20

+

image

+

Mini chariot avec butée d’arrêt pour scie sous table. Pour empêcher le chariot de dépasser la lame de la scie, j'ai ajouté une petite butée au bout d'un ressort en frêne. La butée descend dans le trou de la plaque de la lame à dégagement +zéro.

+
+
+ + diff --git "a/articles/2021/Mise-\303\240-jour-sur-la-r\303\251gulation-du-processeur.html" "b/articles/2021/Mise-\303\240-jour-sur-la-r\303\251gulation-du-processeur.html" new file mode 100755 index 0000000..54d766e --- /dev/null +++ "b/articles/2021/Mise-\303\240-jour-sur-la-r\303\251gulation-du-processeur.html" @@ -0,0 +1,79 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Mise à jour sur la régulation du processeur

+

2021-05-15

+

Old article, I now use tlp,tlp-gui and cpupower

+

Mise à jour sur la régulation du processeur pour limiter les problèmes de température

+

Depuis le Arch Linux Wiki:

+

+

Le processeur de mon Thinkpad X220 est un Intel i5-2520M (4) @ 3,200GHz, +je ne dois donc pas utiliser le module acpi-cpufreq, mais le module intel-pstate intégré.

+

Note : Le pilote intel_pstate +ne prend en charge que deux gouverneurs : powersave et performance. +Bien qu'ils partagent le même nom que les gouverneurs génériques, +ils ne fonctionnent pas de la même manière que ces derniers. Les deux +gouverneurs intel_pstate fournissent une mise à l'échelle dynamique +similaire à celle des gouverneurs génériques schedutil ou ondemand. +Le gouverneur de performance fourni par intel_pstate devrait donner une +meilleure fonctionnalité d'économie d'énergie que l'ancien gouverneur à la +demande.

+

J'ai donc installé cpupower en plus de +thermald:

+sudo pacman -S cpupower cpupower-gui +sudo systemctl enable cpupower +sudo systemctl start cpupower + +

Pour activer pstate au demarrage j'ai ajouté

+GRUB_CMDLINE_LINUX_DEFAULT="intel_pstate=active ... +

dans /etc/default/grub et lancé sudo update-grub.

+

J'ai aussi edité /etc/default/cpupower avec:

+governor='performance' +max_freq="3.2GHz" + +

Le script:

+#!/bin/sh +# mettre le gouverneur en mode powersave s'il est déconnecté du secteur. +ACPI=`acpi -a | cut -d ' ' -f 3` +if [ "$ACPI" == "off-line" ] ; then + cpupower frequency-set -g powersave; +else + cpupower frequency-set -g performance ; +fi + +

Créer un regle UDEV

+/etc/udev/rules.d/99-power.rules +ACTION=="change",SUBSYSTEM=="power_supply",RUN+="/usr/bin/bash +/home/philip/.local/bin/power-adapt" +

Aussi, lorsque on reboot en batterie, le default est "performance", donc +créer un service systemd pour lancer power-adapt au boot

+/etc/systemd/system/power-adapt.service + +[Unit] +Description=your description +[Service] +ExecStart=/home/philip/.local/bin/power-adapt +[Install] +WantedBy=multi-user.target + +

et l'installer

+sudo systemd enable power-adapt.service +
+
+ + diff --git a/articles/2021/Nerdfonts-in-i3.html b/articles/2021/Nerdfonts-in-i3.html new file mode 100755 index 0000000..5f909c6 --- /dev/null +++ b/articles/2021/Nerdfonts-in-i3.html @@ -0,0 +1,34 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Nerdfonts in i3

+

2021-05-08

+

J'ai utilisé des symboles de police dans i3 et Neomutt, mais j'ai remarqué qu'ils +ont tendance à se planter si vous installez des polices différentes.

+

+

La solution que j'ai utilisée est d'installer une Nerdfont, qui contient tous +les symboles nécessaires en plus du font de base. J'ai téléchargé les polices dejavu +sans mono nerd sur Nerdfonts, et copié +les polices dans /usr/share/fonts/TTF.

+

J'ai ensuite lancé

fc-cache -f -v
pour mettre à jour le cache des polices.

+

Pour utiliser cette police dans i3, j'ai modifié la police dans le fichier de configuration de i3 :

+

font pango:DejavuSansMono nerd font mono 11

+
+
+ + diff --git a/articles/2021/Nginx-permission-denied.html b/articles/2021/Nginx-permission-denied.html new file mode 100755 index 0000000..de31e0e --- /dev/null +++ b/articles/2021/Nginx-permission-denied.html @@ -0,0 +1,32 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Nginx permission denied

+

2021-04-08

+

Ça m'a pris un moment pour comprendre. J'ai généré un rapport à partir de +goaccess, mais nginx a refusé de l'afficher. +J'ai vérifié les permissions des fichiers mais rien ne clochait. +Il s'avère que c'était SElinux. Je l'ai résolu avec :

+ +setsebool -P httpd_can_network_connect on
+chcon -Rt httpd_sys_content_t /path/to/webroot +
+
+
+ + diff --git "a/articles/2021/Nouvel-\303\251tabli-pour-l'atelier.html" "b/articles/2021/Nouvel-\303\251tabli-pour-l'atelier.html" new file mode 100755 index 0000000..33f9e66 --- /dev/null +++ "b/articles/2021/Nouvel-\303\251tabli-pour-l'atelier.html" @@ -0,0 +1,28 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Nouvel établi pour l'atelier

+

2021-08-29

+

new bench pic

+

Je me suis débarrassé des étagères bon marché que j'utilisais dans le garage, +et je me suis fabriqué un nouveau banc d'atelier de 4 mètres le long du mur avec +du bois de charpente. Plus à venir...

+
+
+ + diff --git a/articles/2021/Odysee.html b/articles/2021/Odysee.html new file mode 100755 index 0000000..f3d4067 --- /dev/null +++ b/articles/2021/Odysee.html @@ -0,0 +1,36 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Odysee

+

2021-04-23

+

Odysee logo

+

Odysee est un site de vidéos basé sur le protocole blockchain LBRY.

+

Comme Peertube, il propose un moyen de partager des vidéos sans publicités ni règles +arbitraires destinées à favoriser les annonceurs. +Il n'est pas aussi important que Youtube, et il y a quelques vidéos vraiment bizarres, mais il a de plus en plus d'adeptes et un avenir intéressant.

+

Quelques-unes de mes chaînes préférées :

+ +

A vous de voir!

+
+
+ + diff --git a/articles/2021/Photo-de-mon-atelier.html b/articles/2021/Photo-de-mon-atelier.html new file mode 100755 index 0000000..2600f5e --- /dev/null +++ b/articles/2021/Photo-de-mon-atelier.html @@ -0,0 +1,27 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Photo de mon atelier

+

2021-09-03

+

workshop update pic +

Mon petit nirvana. Sur la table, une planche à découper en bois de bout est en train d'être collée. Mon tour fait maison est au bout de la table. Le nouveau banc de 4 mètres rend les choses tellement plus faciles dans un petit espace. Il me manque encore des choses, comme une scie à ruban de 40 cm pour découper des ébauches de bol dans du bois vert, et une raboteuse, mais c'est bien d'avoir des projets quand on est à la retraite +😜.

+
+
+ + diff --git "a/articles/2021/Planche-\303\240-d\303\251couper-en-bois-de-bout.html" "b/articles/2021/Planche-\303\240-d\303\251couper-en-bois-de-bout.html" new file mode 100755 index 0000000..7e08c60 --- /dev/null +++ "b/articles/2021/Planche-\303\240-d\303\251couper-en-bois-de-bout.html" @@ -0,0 +1,26 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Planche à découper en bois de bout

+

2021-09-03

+

pic

+

Planche à couper en chêne avec bois de bout pour plus de résistance à la coupe. Finition huile de noix, 42x24cm

+
+
+ + diff --git a/articles/2021/Project-Hail-Mary.html b/articles/2021/Project-Hail-Mary.html new file mode 100755 index 0000000..4c9c320 --- /dev/null +++ b/articles/2021/Project-Hail-Mary.html @@ -0,0 +1,28 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Project Hail Mary

+

2021-05-14

+

book cover +

Voici un nouveau livre d'Andy Weir, le célèbre auteur de "The Martian". +Je l'ai placé dans ma liste de lecture, car je n'ai pas encore terminé le livre de Michel +Barnier.

+
+
+ + diff --git a/articles/2021/Pyradio.html b/articles/2021/Pyradio.html new file mode 100755 index 0000000..d22a3f3 --- /dev/null +++ b/articles/2021/Pyradio.html @@ -0,0 +1,30 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Pyradio

+

2021-05-25

+

pyradio screenshot

+

Je joue avec Pyradio et Cava en ce moment.

+

Pyradio est un sympathique lecteur radio pour terminal de coderholic, et cava est une application simple d'affichage de barres audio, également pour le terminal.

+

Afin de remplir votre liste de stations radio dans ~/.config/pyradio/stations.csv, +vous pouvez trouver de nombreuses URL ici : Listenlive.nl

+

Je suis donc en train de surfer sur le canapé en écoutant de la Bossa Nova, il y a pire dans la vie.

+
+
+ + diff --git "a/articles/2021/Scie-\303\240-ruban-fait-maison-(1).html" "b/articles/2021/Scie-\303\240-ruban-fait-maison-(1).html" new file mode 100755 index 0000000..42638a2 --- /dev/null +++ "b/articles/2021/Scie-\303\240-ruban-fait-maison-(1).html" @@ -0,0 +1,31 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Scie à ruban fait maison (1)

+

2021-12-10

+

image

+

+Mon nouveau projet est de construire une scie à ruban de 16 pouces à partir des plans de +Matthias Wandel. +Ici, j'ai coupé du chêne pour fabriquer le cadre supérieur du support de roue. +

+

Woodgears plans

+
+
+ + diff --git "a/articles/2021/Scie-\303\240-ruban-fait-maison-(2).html" "b/articles/2021/Scie-\303\240-ruban-fait-maison-(2).html" new file mode 100755 index 0000000..d7139a9 --- /dev/null +++ "b/articles/2021/Scie-\303\240-ruban-fait-maison-(2).html" @@ -0,0 +1,27 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Scie à ruban fait maison (2)

+

2021-12-16

+

image

+

Support de roue supérieur. J'ai utilisé des chutes de chêne récupérées. Le cadre de tension est renforcé par des +splines dans les coins.

+
+
+ + diff --git "a/articles/2021/Scie-\303\240-ruban-fait-maison-(3).html" "b/articles/2021/Scie-\303\240-ruban-fait-maison-(3).html" new file mode 100755 index 0000000..b14a22d --- /dev/null +++ "b/articles/2021/Scie-\303\240-ruban-fait-maison-(3).html" @@ -0,0 +1,26 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Scie à ruban fait maison (3)

+

2021-12-20

+

image

+

Poutre support de pivots d'articulation de ta table.

+
+
+ + diff --git "a/articles/2021/Scie-\303\240-ruban-fait-maison-(4).html" "b/articles/2021/Scie-\303\240-ruban-fait-maison-(4).html" new file mode 100755 index 0000000..fb0aacf --- /dev/null +++ "b/articles/2021/Scie-\303\240-ruban-fait-maison-(4).html" @@ -0,0 +1,28 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Scie à ruban fait maison (4)

+

2021-12-24

+

image

+

+Guide de la lame inférieure fixé au support du pivot. J'ai légèrement adapté le design de Matthias car je vais utiliser un roulement de 28mm derrière la lame. Le guide est fixé au support du pivot avec un boulon de 8mm dans une douille filetée. Les guides lateraux sont faites en ébene de Gabon. +

+
+
+ + diff --git "a/articles/2021/Scie-\303\240-ruban-fait-maison-(5).html" "b/articles/2021/Scie-\303\240-ruban-fait-maison-(5).html" new file mode 100755 index 0000000..53dad31 --- /dev/null +++ "b/articles/2021/Scie-\303\240-ruban-fait-maison-(5).html" @@ -0,0 +1,26 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Scie à ruban fait maison (5)

+

2021-12-27

+

image

+

Guide de lame supérieur

+
+
+ + diff --git "a/articles/2021/Scie-\303\240-ruban-fait-maison-(6).html" "b/articles/2021/Scie-\303\240-ruban-fait-maison-(6).html" new file mode 100755 index 0000000..088d226 --- /dev/null +++ "b/articles/2021/Scie-\303\240-ruban-fait-maison-(6).html" @@ -0,0 +1,30 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Scie à ruban fait maison (6)

+

2021-12-31

+

image

+

+Les supports de pivot sont fixés à la poutre avec des chevilles de 10mm de diamètre et de 90mm de long. Je n'avais pas de chevilles de cette taille, j'ai donc fabriqué un gabarit rapide avec un morceau de cornière, et j'ai poussé des morceaux de chêne a travers le trou avec une perceuse à main. +

+Ma première tentative avec des chevilles cachées était impossible à aligner correctement, donc je l'ai refait en collant d'abord les supports et puis en perçant directement à travers. +

+
+
+ + diff --git a/articles/2021/Science-et-Vie.html b/articles/2021/Science-et-Vie.html new file mode 100755 index 0000000..75f385a --- /dev/null +++ b/articles/2021/Science-et-Vie.html @@ -0,0 +1,27 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Science et Vie

+

2021-04-15

+

J'ai reçu mon mensuel "Science et vie" aujourd'hui. Je suis un lecteur régulier depuis 40 ans (je me souviens même de l'époque où Antoine de Caunes y écrivait parfois des articles, et où chaque mois avait un schéma électronique à tester). J'aime son objectivité et son large éventail de sujets. Très bon rapport +qualité-prix.

+

Site web Science et Vie

+
+
+ + diff --git a/articles/2021/Sous-plat-avec-incrustation-epoxy-Milliput.html b/articles/2021/Sous-plat-avec-incrustation-epoxy-Milliput.html new file mode 100755 index 0000000..c4e5960 --- /dev/null +++ b/articles/2021/Sous-plat-avec-incrustation-epoxy-Milliput.html @@ -0,0 +1,26 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Sous-plat avec incrustation epoxy Milliput

+

2021-09-08

+

pic

+

Fait avec des chutes de bois.

+
+
+ + diff --git a/articles/2021/TLS-handshake-failure.html b/articles/2021/TLS-handshake-failure.html new file mode 100755 index 0000000..77d31ec --- /dev/null +++ b/articles/2021/TLS-handshake-failure.html @@ -0,0 +1,31 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

TLS handshake failure

+

2021-04-11

+

J'ai continué à obtenir des erreurs de handshake TLS +lorsque jai essayé d'envoyé des emails +après avoir configuré postfix. Après m'être +frappé la tête contre le mur en essayant de trouver une solution, +j'ai finalement réalisé que j'avais supprimé le port 465 sur mon serveur, +mais msmtp avait tls_starttls off dans .msmtprc, qui ne s'applique qu'au port 465. +Problème résolu.

+
+
+ + diff --git a/articles/2021/Thinkpad-X200.html b/articles/2021/Thinkpad-X200.html new file mode 100755 index 0000000..70a6aed --- /dev/null +++ b/articles/2021/Thinkpad-X200.html @@ -0,0 +1,31 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Thinkpad X200

+

2021-04-22

+

Image Thinkpad X200

+

Mon matériel informatique actuel est assez simple. L'époque où je gérais des serveurs à la maison est révolue. +Mon ordinateur portable est un MSI GP60 2QE : résolution d'écran 1380 x 768, RAM 16Gb, 2 disques durs SSD 120Gb et 250Gb, OS Manjaro Linux +XFCE.

+

C'est une super machine, mais meilleure sur une table. J'aimerais bien avoir un vieux Thinkpad X200 à utiliser +dans un fauteuil. Même 10 ans après leur sortie, ils sont toujours le fantasme des nerds minimalistes avec un écran de 12 pouces et un magnifique clavier mécanique. Solide comme un roc +aussi.

+
+
+ + diff --git a/articles/2021/Thinkpad-X220.html b/articles/2021/Thinkpad-X220.html new file mode 100755 index 0000000..2e98c5c --- /dev/null +++ b/articles/2021/Thinkpad-X220.html @@ -0,0 +1,38 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Thinkpad X220

+

2021-04-27

+

Thinkpad X220 image

+

Donc je l'ai fait, je me suis acheté un Thinkpad X220 en parfait état avec 8Gb de RAM +et un disque SSD de 240Gb. Cela fait quelques années que je n'ai pas eu deux ordinateurs +à la maison, et maintenant je vais avoir le nirvana des geeks, un Thinkpad fabriqué +avant que Lenovo ne les saborde.

+

Il y a aussi un grand choix de pièces détachées, vous pourriez en construire une. +J'ai choisi le 12" X220 parce qu'il est destiné à être utilisé sur mes genoux dans un fauteuil, +vous trouverez peut-être que le 14" T420 est un meilleur choix, +il est de la même génération et de la même qualité de construction.

+

Il y a beaucoup de ressources en ligne pour le X220, par exemple :

+ +
+
+ + diff --git "a/articles/2021/T\303\251l\303\251charger-musique-depuis-Youtube.html" "b/articles/2021/T\303\251l\303\251charger-musique-depuis-Youtube.html" new file mode 100755 index 0000000..5f0141b --- /dev/null +++ "b/articles/2021/T\303\251l\303\251charger-musique-depuis-Youtube.html" @@ -0,0 +1,29 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Télécharger musique depuis Youtube

+

2021-05-16

+

pense-bete

+

Installer youtube-dl et ffmpeg, ensuite créer le alias youtube-dl-audio :

+alias youtube-dl-audio='youtube-dl --ignore-errors --output "%(title)s.%(ext)s" --extract-audio --audio-format mp3' +

Utilisation :

+
youtube-dl-audio lien
+
+
+ + diff --git a/articles/2021/Un-espion-dans-ma-poche.html b/articles/2021/Un-espion-dans-ma-poche.html new file mode 100755 index 0000000..94049eb --- /dev/null +++ b/articles/2021/Un-espion-dans-ma-poche.html @@ -0,0 +1,41 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Un espion dans ma poche

+

2021-04-30

+

image de téléphone portable

+

Nous savons que nos précieux téléphones portables enregistrent presque tout ce que nous leur confions : +fréquentations, déplacements, achats, mots de passe, goûts, etc. +Mais jusqu'où va leur connaissance de notre intimité ? Et à qui transmettent-ils ces informations privées ? +Quel est le niveau de notre surveillance +?

+

(ré)Voir l'emission +d'Envoyé spécial (avancer à +40mn)

+

J'ajouterai que mon téléphone portable n'exécute pas de logiciels Google ou Apple pour ces raisons, il exécute plutôt Lineageos.

+

+Tant que je suis sur le sujet de freiner les sociétés GAFA de syphonner mes informations +privées: mon ordinateur portable fonctionne sous Manjaro Linux, +et j'utilise le moteur de recherche Duckduckgo +au lieu de la recherche Google ou Bing.

+

+

+
+ + diff --git a/articles/2021/Une-balancoire-pour-Barbie.html b/articles/2021/Une-balancoire-pour-Barbie.html new file mode 100755 index 0000000..76d9c64 --- /dev/null +++ b/articles/2021/Une-balancoire-pour-Barbie.html @@ -0,0 +1,26 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Une balancoire pour Barbie

+

2021-12-19

+

image

+

Quelques baguettes pour brochettes, de la colle à bois, et voila.

+
+
+ + diff --git a/articles/2021/Update.html b/articles/2021/Update.html new file mode 100755 index 0000000..0697a2a --- /dev/null +++ b/articles/2021/Update.html @@ -0,0 +1,35 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Update

+

2021-07-25

+

Après avoir attrapé le Covid fin mars, j'ai eu des problèmes de dos, d'où la pause silencieuse +ici.

+ +

Cependant, j'ai des projets :

+ +

réorganiser l'espace de travail du garage maintenant que le réservoir de chauffage a été déplacé à l'extérieur, +construire une scie à ruban 16" à partir des plans de Mathias Wandel, +fabriquer un mandrin Longworth pour les bols, +et bien sûr, fabriquer encore plus de bols.

+ +

Surveillez cet espace!

+
+
+ + diff --git a/articles/2021/Wiby.html b/articles/2021/Wiby.html new file mode 100755 index 0000000..f69f10a --- /dev/null +++ b/articles/2021/Wiby.html @@ -0,0 +1,26 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Wiby

+

2021-04-02

+

Wiby est un moteur de recherche web qui ne répertorie que des sites web rapides, simples et sans bavures. Comme avec DuckDuckGo, vous pouvez utiliser les commandes bang pour obtenir des résultats d'autres moteurs de recherche : par exemple, en tapant !g avant votre recherche, vous obtiendrez des données de Google Text Search. Consultez la rubrique "paramètres" pour connaître les autres commandes +disponibles.

+
+
+ + diff --git a/articles/2021/ZRAM.html b/articles/2021/ZRAM.html new file mode 100755 index 0000000..e96afc3 --- /dev/null +++ b/articles/2021/ZRAM.html @@ -0,0 +1,34 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

ZRAM

+

2021-05-29

+

Si vous avez un disque dur SSD, et suffisamment de RAM, c'est une bonne idée d'installer zram car il peut réduire l'écriture sur le SSD.

+

Le paquet zramswap dans Arch Linux fournit un script automatisé pour mettre en place un swap avec une priorité plus élevée et une taille par défaut de 20% de la taille de la RAM de votre système. Pour le faire automatiquement à chaque démarrage, activez zramswap.service.

+
yay -S zramswap
+sudo systemctl enable zramswap.service
+sudo systemctl start zramswap.service
+cat /proc/swaps
+
+

+Improving +performance

+
+
+ + diff --git a/articles/2021/Zellij-Terminal-Workspace.html b/articles/2021/Zellij-Terminal-Workspace.html new file mode 100755 index 0000000..6815250 --- /dev/null +++ b/articles/2021/Zellij-Terminal-Workspace.html @@ -0,0 +1,30 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Zellij Terminal Workspace

+

2021-04-25

+

zellij image

+

Voici un espace de travail terminal prometteur pour les personnes qui aiment les applications basées sur le texte. La version bêta est disponible, ce qui signifie qu'elle fonctionne bien pour un usage quotidien, mais qu'elle peut subir de rares +plantages.

+

Je le trouve très facile à utiliser. Il occupe maintenant de façon permanente un bureau virtuel dans mon +manjaro/xfce4.

+

Jetez-y un coup d'oeil ici : Zellij

+
+
+ + diff --git a/articles/2021/erreurs-spamass-milter.html b/articles/2021/erreurs-spamass-milter.html new file mode 100755 index 0000000..3bbc640 --- /dev/null +++ b/articles/2021/erreurs-spamass-milter.html @@ -0,0 +1,43 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

erreurs-spamass-milter

+

2021-05-05

+

Je continue à voir de temps en temps /run/spamass-milter/spamass-milter.sock; No such file +or directory dans /var/log/maillog alors que tout semble fonctionner correctement.

+

Pour le resoudre, je test actuellement:

+
    +
  • chown sa-milt:postfix /run/spamass-milter
  • +
  • -g postfix dans /etc/sysconfig/spamass-milter EXTRA_FLAGS
  • +
  • chmod 775 /run/spamass-milter
  • +
+

Mise à jour

+

Probleme résolu, submission etait en chroot, master.cf est maintenant +comme ceci:

+
+# ==========================================================================
+# service type  private unpriv  chroot  wakeup  maxproc command + args
+#               (yes)   (yes)   (no)    (never) (100)
+# ==========================================================================
+smtp        inet  n       -       n       -       -       smtpd
+submission  inet  n       -       n       -       -       smtpd
+
+
+
+ + diff --git a/articles/2021/i3-Blocks.html b/articles/2021/i3-Blocks.html new file mode 100755 index 0000000..058d999 --- /dev/null +++ b/articles/2021/i3-Blocks.html @@ -0,0 +1,50 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

i3 Blocks

+

2021-05-05

+

Mon X220 tourne maintenant sous Manjaro i3, et j'ai réussi à obtenir une jolie barre d'état avec +i3-Blocks

+i3 status image +

Afin d'obtenir un calendrier pop-up lorsque je clique sur la date dans la barre, j'ai installé +gsimplecal

+gsimplecal image +

pacman -S gsimplecalendar

+

et modifié .config/I3blocks/config comme suit:

+

+[gsimplecal]
+command=date '+%d/%m %H:%M:%S';if [ $button ]; then gsimplecal; fi
+interval=1 +

+

Pour faire disparaître le calendrier lorsque le curseur ne le survole plus, +éditez

+

+~/.config/gsimplecal/config +

+

et ajoutez la ligne

+

+close_on_unfocus = 1 +

+

puis redémarrez la barre avec

+

+i3-msg restart +

+
+
+ + diff --git a/articles/2021/spamass-milter-error.html b/articles/2021/spamass-milter-error.html new file mode 100755 index 0000000..8d162e3 --- /dev/null +++ b/articles/2021/spamass-milter-error.html @@ -0,0 +1,56 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

spamass-milter error

+

2021-04-15

+

Lorsque j'a regardé si tout allait bien avec spamassasin,

+systemctl status spamass-milter +

m'affichait le message d'erreur suivant :

+spamass-milter could not retrieve sendmail macro "_" !". +

Cela n'a aucun effet sur la fonction de spamass_milter, mais je n'aime pas que les choses soient +désordonnées. Il s'avère que spamass-milter a été construit pour sendmail, +donc si vous utilisez Postfix, la macro milter_connect_macro doit être modifiée.

+postconf -d milter_connect_macros +

renvoie

+milter_connect_macros = j {nom_du_démon} {addr_du_démon} v + +

Le "_" etait bien manquant, ansi que la macro i.
+J'ai donc ajouté cette ligne au fichier /etc/postfix/main.cf

+ +milter_connect_macros = "i j {daemon_name} v {if_name} _" + +

et voila, plus d'erreurs? Non. Une message d'erreur pour le macro "i" subsiste, mais c'est une bogue sans conséquence.

+ +

http://www.postfix.org/MILTER_README.html

+
+Some Milter applications log a warning that looks like this:
+
+    sid-filter[36540]: WARNING: sendmail symbol 'i' not available
+
+And they may insert an ugly message header with "unknown-msgid" like this:
+
+    X-SenderID: Sendmail Sender-ID Filter vx.y.z host.example.com 
+
+The problem is that Milter applications expect that the queue ID is known before the MTA accepts the MAIL FROM
+ (sender) command. Postfix does not choose a queue ID, which is used as the queue file name, until after it 
+ accepts the first valid RCPT TO (recipient) command. 
+
+
+
+ + diff --git a/articles/2021/web-bloat.html b/articles/2021/web-bloat.html new file mode 100755 index 0000000..b49d673 --- /dev/null +++ b/articles/2021/web-bloat.html @@ -0,0 +1,43 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+ +
+ + diff --git a/articles/2021/wittr.in.html b/articles/2021/wittr.in.html new file mode 100755 index 0000000..284e7bb --- /dev/null +++ b/articles/2021/wittr.in.html @@ -0,0 +1,33 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+ +
+ + diff --git "a/articles/2021/\303\211tau-de-recup.html" "b/articles/2021/\303\211tau-de-recup.html" new file mode 100755 index 0000000..f8e2dc5 --- /dev/null +++ "b/articles/2021/\303\211tau-de-recup.html" @@ -0,0 +1,28 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Étau de recup

+

2021-08-12

+

etau

+

Voici un étau d'origine soviétique que j'ai trouvé dans une brocante pour 20€.

+

Un bon bain de vinaigre, une brosse metallique, de la peinture verte, et +du WD40, et voila.

+
+
+ + diff --git "a/articles/2022/Scie-\303\240-ruban-fait-maison-(10).html" "b/articles/2022/Scie-\303\240-ruban-fait-maison-(10).html" new file mode 100755 index 0000000..b4a9392 --- /dev/null +++ "b/articles/2022/Scie-\303\240-ruban-fait-maison-(10).html" @@ -0,0 +1,29 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Scie à ruban fait maison (10)

+

2022-02-01

+

Le cadre est fait avec 10 planches de sapin du nord, 2000x120x18mm

+

+

+

+

+
+
+ + diff --git "a/articles/2022/Scie-\303\240-ruban-fait-maison-(11).html" "b/articles/2022/Scie-\303\240-ruban-fait-maison-(11).html" new file mode 100755 index 0000000..6985bd6 --- /dev/null +++ "b/articles/2022/Scie-\303\240-ruban-fait-maison-(11).html" @@ -0,0 +1,29 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Scie à ruban fait maison (11)

+

2022-02-15

+

+

+

+

+

+
+
+ + diff --git "a/articles/2022/Scie-\303\240-ruban-fait-maison-(12).html" "b/articles/2022/Scie-\303\240-ruban-fait-maison-(12).html" new file mode 100755 index 0000000..6062f78 --- /dev/null +++ "b/articles/2022/Scie-\303\240-ruban-fait-maison-(12).html" @@ -0,0 +1,27 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Scie à ruban fait maison (12)

+

2022-02-20

+

Amelioration

+

J'ai fabriqué un autre guide lame inférieur qui fonctionne mieux. Celui d'origine présentait la tranche du roulement vers la lame ce qui nécessite un support plus compliqué et moins solide. L'autre avantage d'appuyer la lame sur le côté du roulement est que c'est plus silencieux. Les blocs des guides latéraux vient d'un morceaux d'ébène de Gabon qu'on m'avait donné.

+ +
+
+ + diff --git "a/articles/2022/Scie-\303\240-ruban-fait-maison-(7).html" "b/articles/2022/Scie-\303\240-ruban-fait-maison-(7).html" new file mode 100755 index 0000000..519d319 --- /dev/null +++ "b/articles/2022/Scie-\303\240-ruban-fait-maison-(7).html" @@ -0,0 +1,30 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Scie à ruban fait maison (7)

+

2022-01-01

+

+

+Après avoir monté les pivots, je me suis rendu compte qu'il leur manquait un millimètre au milieu. Plutôt que de tout démonter, j'ai collé de fines cales en chêne, et je les ai poncées avec du papier de verre collé sur l'autre partie du pivot. +Si jamais je recommence, j'utiliserai une toupie sur un gabarit de compas pour obtenir un ajustement parfait. +

+

+
+
+ + diff --git "a/articles/2022/Scie-\303\240-ruban-fait-maison-(8).html" "b/articles/2022/Scie-\303\240-ruban-fait-maison-(8).html" new file mode 100755 index 0000000..9160166 --- /dev/null +++ "b/articles/2022/Scie-\303\240-ruban-fait-maison-(8).html" @@ -0,0 +1,27 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Scie à ruban fait maison (8)

+

2022-01-01

+

+

Support de table montée sur les pivots. La table de la scie à ruban est placée sur le support, ce qui la renforce. Le support est fabriqué avec une chute d'étagère en chêne +laminé.

+
+
+ + diff --git "a/articles/2022/Scie-\303\240-ruban-fait-maison-(9).html" "b/articles/2022/Scie-\303\240-ruban-fait-maison-(9).html" new file mode 100755 index 0000000..d02e94c --- /dev/null +++ "b/articles/2022/Scie-\303\240-ruban-fait-maison-(9).html" @@ -0,0 +1,27 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Scie à ruban fait maison (9)

+

2022-01-19

+

+

Toutes les pieces pour le cadre du scie à ruban découpés. Il m'a fallu 10 planches d'épicéa +2000x120x18mm

+
+
+ + diff --git a/articles/2022/mailcow.html b/articles/2022/mailcow.html new file mode 100755 index 0000000..5b29b53 --- /dev/null +++ b/articles/2022/mailcow.html @@ -0,0 +1,31 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Mailcow

+

24-12-2022

+ +

+ Very happy with Mailcow, it seems to be the best solution currently for a self hosted email server. It allows me to have perfectly authenticated email with DKMS, ARC, SPF and DMARC. I've set it up on an OVH vps and it's been running for a couple of weeks now. +

+ In the past I have maintained by hand email servers, but - been there got the t-shirt. So I tested a few solutions such as Post.Io, iRedmail, etc. but settled on Mailcow as it is the most advanced and complete. Installation took only a few minutes. +

+ https://mailcow.email +

+
+ + diff --git a/articles/2023/1vyrain.html b/articles/2023/1vyrain.html new file mode 100755 index 0000000..e04c7e3 --- /dev/null +++ b/articles/2023/1vyrain.html @@ -0,0 +1,85 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

1vyrain - unlock your Thinkpad T430

+

01-09-2023

+
+ d888                                      d8b          
+d8888                                      Y8P          
+  888                                                   
+  888   888  888 888  888 888d888  8888b.  888 88888b.  
+  888   888  888 888  888 888P"       "88b 888 888 "88b 
+  888   Y88  88P 888  888 888     .d888888 888 888  888 
+  888    Y8bd8P  Y88b 888 888     888  888 888 888  888 
+8888888   Y88P    "Y88888 888     "Y888888 888 888  888 
+                      888                               
+                 Y8b d88P                               
+                  "Y88P"                                
+    
+ +

+The Thinkpad 430 comes with an older wifi card that doesn't support 802.11ac. My machine also didn't come with a Bluetooth card. +

+In order to fit a replacement card such as the AC 7260, the BIOS must be altered as by default Lenovo only allows you to install hardware they have whitelisted, and as the T430 dates a bit this list is defunct. +

+Thankfully, 1vyrain has created a BIOS that adds an advanced tab in th BIOS and wipes the whitelist, and allows you to install alternative hardware and deactivate Intel ME among other things. +

+I followed the following steps: +

+

1

+

+For this you'll need to be in Windows 64 bit +

+First downgrade the BIOS to one that is hackable, for the T430 this is version 2.64 +

+You can either download and install the BIOS from the Official Lenovo page +

+or use 1vprep: +

+https://github.com/n4ru/IVprep +

+Click "code"> Download ZIP. Unzip the package and click "downgrade.bat" +

+The system will reboot and install the BIOS +

+

2

+

+Boot into the BIOS and make sure Secure boot is disabled, and startup set to Both, UEFI first. +

+Boot back into Windows +

+Download the 1vyrain.iso from https://github.com/n4ru/1vyrain +

+Currently available from
https://drive.google.com/open?id=1yusq98ja6NmI4G4txKVueFqY_ZEwaZvO +

+Burn the iso to a USB drive using Rufus https://rufus.ie (select DD mode) +

+Reboot from the USB drive and follow the instructions. The computer will go into sleep mode, just hit the power button to get back into 1vyrain +

+Note: I had to remove the SATA drive where windows was installed to be able to select the USB drive to boot from. +

+The computer will reboot, it might recycle a few times. It might also display a CRC error, you can ignore this and reboot. +

+Boot into the BIOS again, you should now see a new tab "Advanced". In this tab you can disable Intel ME. +

+You are all set! +

+
+
+ + diff --git a/articles/2023/Bitwarden-RBW-blues.html b/articles/2023/Bitwarden-RBW-blues.html new file mode 100755 index 0000000..67d53db --- /dev/null +++ b/articles/2023/Bitwarden-RBW-blues.html @@ -0,0 +1,50 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Bitwarden RBW blues

+

2023-12-05

+

+ Unfortunately the CLI app for Bitwarden is clunky and difficult to use as + it is stateless. A kind soul wrote an unofficial CLI app called + RBW that uses a daemon, but it + stopped working a couple of weeks ago. Issues are pending but no reaction + from the author so far. +

+

The official Bitwarden CLI app is still working though.

+

+ My current solution is to use password-store on the CLI and Bitwarden in + Firefox and Android. +

+

+ There's a dirty hack called "B" here: + https://github.com/lebarondecharlus/b, but it has the master password in clear in the script. +

+

+ I'm wondering if it's possible to script a bridge between the two, but + it's looking like I need to use password-store for CLI apps, and Bitwarden + for the rest. +

+

Update:

+

RBW has been fixed. The .eu bitwarden domains have been integrated. Tested in rbw-1.9.0-1 arch package

+

Check https://github.com/doy/rbw#configuration for usage info.

+
+
+ + diff --git a/articles/2023/Chimera's-Star.html b/articles/2023/Chimera's-Star.html new file mode 100755 index 0000000..26d066c --- /dev/null +++ b/articles/2023/Chimera's-Star.html @@ -0,0 +1,27 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+ +
+ + diff --git a/articles/2023/Death-of-the-web.html b/articles/2023/Death-of-the-web.html new file mode 100755 index 0000000..ff0462d --- /dev/null +++ b/articles/2023/Death-of-the-web.html @@ -0,0 +1,36 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Death of the web

+

2023-12-07

+

I'm wondering where we are going with all this.

+

Searching today's web I feel like I'm scrabbling around in a trash can for a half eaten sandwich.

+

Content is there somewhere, but you have to swallow the glitz, blitz, frills, lies, and adverts before you find it.

+

With the advent of AI written content it can only get much worse, after all, who owns these AI's ? Who's world view will they push?

+

In a few years time web browsers will be based on the same web engine, +asking if you need some help with that infection you mentioned to your doctor, chiding you for not checking +the local election results, and sugggesting a diet change because of the contents of your last supermarket bill.

+

Seems like the only way out will require a rupture.

+

Perhaps take a step back and use older text based browsers with just a smattering of images.

+

Local BBS's maybe, or Gopher/Gemini servers on everyone's laptop with a decentralized search system.

+

It's inevitable there will be a backlash, the form it will take should be interesting.

+ +
+
+ + diff --git a/articles/2023/Firefox-Multi-Account-Containers.html b/articles/2023/Firefox-Multi-Account-Containers.html new file mode 100755 index 0000000..03423a2 --- /dev/null +++ b/articles/2023/Firefox-Multi-Account-Containers.html @@ -0,0 +1,45 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Firefox Multi Account Containers

+

20-11-2023

+

+ Firefox Mutli-Account Containers works by isolating your identity into a + separate container that makes it harder to track your visits to other + websites with third-party cookies. +

+

+ For example, you can sign in to your Facebook account inside of a + container, and Facebook will never know who you are whenever you aren’t + inside the container. +

+

+ I use it to contain unsavory web sites like Facebook and Google, Banks and + Shopping sites +

+

+ multi-account-containers +

+ +
+
+ + diff --git a/articles/2023/Forget-VTech,-get-an-old-laptop-instead.html b/articles/2023/Forget-VTech,-get-an-old-laptop-instead.html new file mode 100755 index 0000000..d7d5531 --- /dev/null +++ b/articles/2023/Forget-VTech,-get-an-old-laptop-instead.html @@ -0,0 +1,32 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Forget VTech, get an old laptop instead

+

2023-12-29

+

I had the dubious joy of setting up a miserable low powered android excuse for a kids computer from VTech over christmas.

+

It's so low powered the slow reaction time made me believe it wasn't working for a while. The screen is tiny, the ergonomy crappy, and the software is gahh...

+

To give the kids something they could actually use without bringing tears to the kids and their parents, I set up an old laptop with Linux Mint Cinnamon.

+

A nice, easy, and quick installation.

+

I added Seb's IP hosts file, and in Firefox, in addition to the basics (Ublock Origin,Decentraleyes,etc.) I set the search engine and default page to Qwant Junior. I also added some bookmarks like WebJunior.

+

With Mint's library of games and educational software, their mum can add more things later as required as only she has the root password.

+

For the same price of the VTech toy, you can get a refurbished Thinkpad X240 from Backmarket, set it up the same way, and actually have something useable.

+

UPDATE: The kid loves the old laptop, and the VTech ...thing has been relegated to the dustbin of Christmas past. Win!

+
+
+ + diff --git a/articles/2023/Gemini---The-protocol-not-the-AI.html b/articles/2023/Gemini---The-protocol-not-the-AI.html new file mode 100755 index 0000000..ed725dc --- /dev/null +++ b/articles/2023/Gemini---The-protocol-not-the-AI.html @@ -0,0 +1,60 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Gemini - The protocol not the AI

+

2023-12-09

+

Playing with a Gemini server today. I'm not entirely convinced, but in the interest of research, here goes:

+

First I opened port 1965 on the server

+ufw allow 1965 + +

I created the user gemini on the server and created the folders bin, certs, and gemini in /var/gemini

+

I downloaded the x86_64 linux binary from github and moved it to /var/gemini/bin.

+

In /var/gemini/content I creatd the text file index.gmi with just "hello world" in it.

+

Then I did a first run of Gemini

+/var/gemini/bin/agate --content /var/gemini/content/ --certs /var/gemini/certs --hostname wittamore.fr --lang en-GB +

Agate Auto created it's required certificates, and using the gemini browser Amfora I saw the "hello world!" text.

+

So I added a systemd file to manage the execution of Agate:

+[Unit] +Description=agate +After=network.target + +[Service] +User=gemini +Type=simple +ExecStart=/var/gemini/bin/agate --content /var/gemini/content/ --certs /var/gemini/certs --hostname wittamore.fr --lang en-GB + +[Install] +WantedBy=default.target + +

And launched it

+systemctl enable agate.service +systemctl start agate.service + + +

Agate is a very simple Gemini server. It automatically creates self-signed certificates for TLS in the .der format, +which makes it clumsy if you have letsencrypt certificates in .pem format available.

+openssl rsa -outform der -in privkey.pem -out key.der +openssl x509 -outform der -in cert.pem -out cert.der + +

Unlike Gopher, none of the servers available appear to be mature yet, which is probably due to the relatively new protocol.

+

The Flounder site shows some interesting possibilities for Gemini.

+ +
+
+ + diff --git a/articles/2023/In-2024,-please-switch-to-Firefox.html b/articles/2023/In-2024,-please-switch-to-Firefox.html new file mode 100755 index 0000000..4b584a3 --- /dev/null +++ b/articles/2023/In-2024,-please-switch-to-Firefox.html @@ -0,0 +1,25 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+ +
+ + diff --git a/articles/2023/Little-Doggies.html b/articles/2023/Little-Doggies.html new file mode 100755 index 0000000..18bdb95 --- /dev/null +++ b/articles/2023/Little-Doggies.html @@ -0,0 +1,39 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Little Doggies

+

2023-12-12

+

+ If such a thing as gods really existed, they wouldn't let little doggies + suffer and have such short lives. +

+

+ When gods are omnipotent but at the same time vicious, callous, genocidal, + bloodlusting entities, they can only have been made by man, in the image + of man. +

+

+ Bobby was my dog. He had enough character for a pack of dogs, and although + only 7kgs of mostly hair, was the boss, but a boss who melted in my arms whenever + he could. He took an enormous place in our house and hearts, and he had a soul. + Rest in peace Bobby. +

+
+
+ + diff --git a/articles/2023/Marginalia-search-engine.html b/articles/2023/Marginalia-search-engine.html new file mode 100755 index 0000000..d513d6b --- /dev/null +++ b/articles/2023/Marginalia-search-engine.html @@ -0,0 +1,34 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Marginalia search engine

+

2023-12-18

+

Marginalia is an independent DIY search engine that attempts to show you sites you perhaps weren't aware of.

+

It does this by up-ranking web sites that have text content, and downranking the typical image heavy, content-poor, advert ridden web sites other search engines will favour. +

+https://search.marginalia.nu +

+

Comments

+
+

Maginalia is a nice search engine for finding small and obscure sites with good content. Another good one is wiby.me

+-Mike S. +
+
+
+ + diff --git a/articles/2023/Need-a-long-walk.html b/articles/2023/Need-a-long-walk.html new file mode 100755 index 0000000..735529f --- /dev/null +++ b/articles/2023/Need-a-long-walk.html @@ -0,0 +1,29 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Need a long walk

+

2023-12-02

+

+ This time of year I need long walks to clear my brain of the winter grey. The + lack of sunlight and short days isn't good for me. I must have southern + genes somewhere. Roll on March. +

+
+
+ + diff --git a/articles/2023/Web-1.5.html b/articles/2023/Web-1.5.html new file mode 100755 index 0000000..19b0711 --- /dev/null +++ b/articles/2023/Web-1.5.html @@ -0,0 +1,86 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Web 1.5 and Berries

+

2023-12-20

+

+ I was searching this morning for web 1.0 ressources in French. It was + interesting to see the amount of belittleing phrases, from "passive web" + through "documentary web", "Spectator web", etc. +

+

+ Web 2.0 is referred to as the "social web", where large companies found + how to make money from this thing through centralisation, javascript, + social networks, online sales, and the massive use of berries. The + descriptions of Web 3.0 and 4.0 are so full of bullshit, acronyms, + marketing speak, and futurism, it's not worth commenting on. +

+ +

Berries? you might ask.

+ +

+ Human evolution has included colour sight to enable the picking of ripe + fruit and avoidance of dangerous meats or insects. +

+

+ Icons tap into this evolutionary advantage. Images that target our + attention in order to vehicule an emotional response are another. Their + use sometimes has the side effect of manipulating you, the reader. Today + they are also used to track you or vehicule code. +

+ +

+ The current web plunges the reader in a (sometimes pretty) morass of + background images, icons, adverts, gadgets, etc. that distract you from + noticing that perhaps there's not much actual content. +

+

+ It also slows web sites dramatically. Try looking at most recipe web sites + for an example. Now visit Luke Smith's + Based Cooking (He had a rant about + this a while back) +

+

+ Scrolling through Facebook's litany of meme's, repeated jokes, and plain + rubbish, will also make you realise that there's very little content. What + remains, is formatted and presented in an identical fashion, much like + visiting adjacent prison cells. It also dissapears quickly, buried under + more meme's, repeated jokes, and rubbish. And while you are rotting your + brain in this manner, you are being tracked mercilessly while your soul is + sold for a few deniers. +

+

+ In a nutshell, web 1.0 wasn't something to simply discard, and while + technological advances are indiscutable, (UTF-8, CSS, backend + scripting,..) the uses they have been put to sometimes definitely are. +

+

+ I'm no monk: I still need to get to my bank, make the odd online purchase, + etc. but I think it's possible to have in parallel a reasoned web simply + by editing web sites responsably. Not by throwing out the baby with the + bathwater, but eliminating the chaff, reducing the C02 footprint, reducing + the illisibility, making it faster, making it easier for people with + reduced sight to enjoy the content, making sure it can be read in all + browsers, stop tracking in it's... tracks, and getting to the point. +

+

How about calling it Web 1.5 ?

+

 

+
+
+ + diff --git a/articles/2023/Web-site-stats-better-without-Google.html b/articles/2023/Web-site-stats-better-without-Google.html new file mode 100755 index 0000000..02b4167 --- /dev/null +++ b/articles/2023/Web-site-stats-better-without-Google.html @@ -0,0 +1,28 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Web site stats better without Google

+

2023-12-25

+

Merry Christmas!

+

I used to run this blog on a CMS with all the "required" google schema code included.

+

Funnily though, since I moved to using my own stripped down html blog with zero search engine optimisation nor shemas, the stats are much better.

+

Go figure...

+
+
+ + diff --git a/articles/2023/ascii-art.html b/articles/2023/ascii-art.html new file mode 100755 index 0000000..5c0dcfc --- /dev/null +++ b/articles/2023/ascii-art.html @@ -0,0 +1,38 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Ascii Art

+

27-11-2023

+
+ ____   _                       _                    
+| __ ) | |  ___    __ _   __ _ (_) _ __    __ _  ___ 
+|  _ \ | | / _ \  / _` | / _` || || '_ \  / _` |/ __|
+| |_) || || (_) || (_| || (_| || || | | || (_| |\__ \
+|____/ |_| \___/  \__, | \__, ||_||_| |_| \__, ||___/
+                  |___/  |___/            |___/      
+
+

Old as the hills, and still amusing...

+ + +
+
+ + diff --git a/articles/2023/connerie-des-dates-limites.html b/articles/2023/connerie-des-dates-limites.html new file mode 100755 index 0000000..d92efd5 --- /dev/null +++ b/articles/2023/connerie-des-dates-limites.html @@ -0,0 +1,25 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

La connerie des dates limites

+

20-08-2023

+ +
+
+ + diff --git a/articles/2023/debian-or-arch.html b/articles/2023/debian-or-arch.html new file mode 100755 index 0000000..185768a --- /dev/null +++ b/articles/2023/debian-or-arch.html @@ -0,0 +1,38 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Debian 12 or Arch

+

03-09-2023

+ +

+ I installed Debian 12 on my T430, as that's what I use on my vps servers, but after using Arch Linux on my X220 I'm a bit dubious. +

+ For example, Firefox. The stable release of Debian has a much older version, if you want to use the latest version you either have to get the unstable packages via the pin system or use a snap package. +

+ Using pin with an unstable package messes up your installation because afterwards I needed to install curl, and guess what, it has to be unstable too otherwise you can't install it because of modified dependancies. +

+ For Neomutt, a 2023 version isn't even in unstable, so the only solution is to download a ton of development dependancies and compile it myself. Been there done that but it leads me to doubt the system. Maybe there will be tears down the road with some other unforseen problem. +

+ Conclusion, Debian is the best for servers, but maybe less so for my Laptop. +

+ Back to Arch then. +

+
+
+ + diff --git a/articles/2023/drm-et-firefox.html b/articles/2023/drm-et-firefox.html new file mode 100755 index 0000000..9e56430 --- /dev/null +++ b/articles/2023/drm-et-firefox.html @@ -0,0 +1,39 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

DRM dans Firefox

+

03-09-2023

+

+ Ma Freebox m'empêche de regarder sans problème les vidéos d'Amazon Prime. Les chaînes de télévision normales fonctionnent bien, mais Amazon Video ne cesse de se couper. +

+ Sur ma smartphone ou sur ordinateur pas de problem. +

+ Je regarde donc actuellement Amazon Prime Video sur mon ordinateur portable avec Firefox. +

+ Il suffit d'activer le contenu DRM dans les paramètres : +

+ +

+ Et la, ça marche plutôt bien. +

+ Je pense que le logiciel du boîtier TV de Free doit passer par la fenêtre pour obtenir une mise à jour. +

+
+
+ + diff --git a/articles/2023/evil-empire.html b/articles/2023/evil-empire.html new file mode 100755 index 0000000..eff8071 --- /dev/null +++ b/articles/2023/evil-empire.html @@ -0,0 +1,38 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Evil Empire

+

10-11-2023

+ evil empire +

So facebook has thrown down the sword. Pay up or bend over.

+

Thanks but no thanks.

+

+ There are many other solutions that respect your privacy, you don't need + to use stealth slot machines like Facebook or X rated Twitter. +

+

+ Check out Mastodon for example +

+

+

Update

+ Maybe not going the way Meta wants, see: noyb files GDPR complaint +

+
+
+
+

 

§

+ diff --git a/articles/2023/fixing-25-second-firefox-delay.html b/articles/2023/fixing-25-second-firefox-delay.html new file mode 100755 index 0000000..13edab4 --- /dev/null +++ b/articles/2023/fixing-25-second-firefox-delay.html @@ -0,0 +1,45 @@ + + + + + + + Bloggings + + +
+

Bloggings

+
Back + +
+ + +
+
+

Fixing a 25 second startup delay in Firefox

+

06-09-2023

+

+ I found Firefox was taking a while to launch in XFCE4. I tested removing + the plugins, emptying the cache, etc. but nothing worked. +

+

+ Eventually I ran "strace Firefox" and found it was hanging polling + something for 25 seconds. +

+

+ So armed with the readout from strace, I searched and found the solution : +

+ +sudo pacman -Rndd xdg-desktop-portal + +

+ I kept xdg-desktop-portal-gtk as the bug isn't in there. Apparently + xdg-desktop-portal-gnome has the same bug, but I don't use Gnome so... +

+

Both Firefox and Libreoffice now launch in milliseconds!

+

I hope this helps someone else...

+
+
+ + diff --git a/articles/2023/hardening-firefox.html b/articles/2023/hardening-firefox.html new file mode 100755 index 0000000..67ea172 --- /dev/null +++ b/articles/2023/hardening-firefox.html @@ -0,0 +1,47 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Hardening Firefox

+

19-11-2023

+

+ I don't like spyware disguised as web browsers, so I don't use Google + Chrome, Microsoft Edge, Opera, etc. +

+

+ Instead I use Firefox with a hardened user.js , which is the configuration + file for Firefox settings +

+

+ arkenfox/user.js +

+

and the following add-ons:

+
    +
  • Ublock Origin
  • +
  • Privacy Badger
  • +
  • Decentraleyes
  • +
+

+ I also use a private search engine, + SearXNG + to avoid tracking and profiling. +

+ +
+
+ + diff --git a/articles/2023/images-in-xterm-emulators.html b/articles/2023/images-in-xterm-emulators.html new file mode 100755 index 0000000..4066ad7 --- /dev/null +++ b/articles/2023/images-in-xterm-emulators.html @@ -0,0 +1,45 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Images in terminal emulators

+

13-11-2023

+ xterm-images +

+ Playing with newsboat and W3M as pager, I've been checking Terminals that support W3M images without flickering when scrolling, or other display artifacts. Unfortunately I've only found one, Xterm. +

+ There are a lot of publications on the web proposing arcane hacks, might not work for you's, unmaintained stuff, etc. but in reality there's no cut and dried solution for W3M's own image rendering that works on all terminal emulators. +

+
    +
  • Xterm - Images display correctly
  • +
  • Kconsole - Images display but bar across the screen at the cursor position
  • +
  • Xfce4-terminal - Images display only when scrolling with flicker
  • +
  • Alacritty - Images display only when scrolling with flicker
  • +
  • Mate Terminal - Images display only when scrolling with flicker .etc. etc.
  • +
  • Kitty - has it's own image routines, but I was unable to get it to work with W3M 's kitty display option
  • +
+

+ The working solution I found was sixel support, see arewesixelyet.com +

+ Today the winner is: Alacritty with W3M's img2sixel option +

+ w3m-options + +
+
+ + diff --git a/articles/2023/jersey-before.html b/articles/2023/jersey-before.html new file mode 100755 index 0000000..7becb1c --- /dev/null +++ b/articles/2023/jersey-before.html @@ -0,0 +1,26 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Jersey before 541 a.d.

+

24-10-2023

+ + +
+
+ + diff --git a/articles/2023/kind-of-blue.html b/articles/2023/kind-of-blue.html new file mode 100755 index 0000000..83c1f7b --- /dev/null +++ b/articles/2023/kind-of-blue.html @@ -0,0 +1,52 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back +
+ +
+
+

Kind of Blue

+

11-11-2023

+

+ The best selling Jazz album of all time. 1959, the year everything + changed. +

+
+ +
+

+ https://youtu.be/j9QXpfvgSVk +

+
+
+ + + diff --git a/articles/2023/linux-paradise.html b/articles/2023/linux-paradise.html new file mode 100755 index 0000000..7f18aee --- /dev/null +++ b/articles/2023/linux-paradise.html @@ -0,0 +1,28 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Linux paradise

+

15-08-2023

+ +

Dad, what are clouds made of?

+

Linux servers, mostly

+ +
+
+ + diff --git a/articles/2023/map-of-langourla.html b/articles/2023/map-of-langourla.html new file mode 100755 index 0000000..6830f76 --- /dev/null +++ b/articles/2023/map-of-langourla.html @@ -0,0 +1,35 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

A map of Langourla

+

24-08-2023

+ +

+ A few years ago I made this map of Langourla using Inkscape. +

+ Notably it shows the old roman roads and an estimation of the chemin de St.Gilles from some of the documents found at the BNF. +

+ A friend asked me for it last week, so I thought I'd post it here. +

+

+ langourla.pdf +

+
+
+ + diff --git a/articles/2023/mer-des-faluns.html b/articles/2023/mer-des-faluns.html new file mode 100755 index 0000000..8425c2b --- /dev/null +++ b/articles/2023/mer-des-faluns.html @@ -0,0 +1,30 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Mer des Faluns

+

11-11-2023

+ mer-des-faluns +

+ Quand la Bretagne était une île +

+ Wikipaedia + +
+
+ + diff --git a/articles/2023/micro-text-editor.html b/articles/2023/micro-text-editor.html new file mode 100755 index 0000000..1be9026 --- /dev/null +++ b/articles/2023/micro-text-editor.html @@ -0,0 +1,34 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

+ Micro text editor

+

28-08-2023

+

+ I've been using Joe as my go-to shell text editor for years, writing web sites with it, and basically all text writing. Recently I've been looking at Micro as it has a Prettier plugin. This plugin uses Prettier to format your code upon saving your file. Micro is vastly superior to Nano and has a number of useful plugins. +

+ I have never been able to be productive with Vim. +

+ I'm so set in my ways regarding Joe that I'll have to alias joe to micro in order to use it, but I think it's the way forward. +

+ https://micro-editor.github.io +

+
+
+ + diff --git a/articles/2023/minimalist-blog.html b/articles/2023/minimalist-blog.html new file mode 100755 index 0000000..714e790 --- /dev/null +++ b/articles/2023/minimalist-blog.html @@ -0,0 +1,87 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

A Minimalist blog

+

23-11-2023

+
Updated 07-09-2025
+

+ In the past I've used several blog engines, CMS, and rolled my own solutions, but at + the end of the day, I find writing simple html is faster and easier than using a blog + engine. A blog engine or CMS makes you dependant on other softwareand may no longer exist + or be obsolete in a couple of years. Plus you usually have to hack the html and css + of these engines to get what you want anyway, which defeats their purpose. +

+

+ After playing with 8 bit computers, my IT career debuted writing invoicing + applications using Multiplan on MS-DOS, so plain text has a nostalgic pull + for me. Searching for a reasonable solution, I looked at + Gopher and + Gemini. +

+

+ Both are nice although Gopher is old and quirky and Gemini still a bit experimental, + but the fundemental problem is that they both require specific browsers, html bridges or plugins to be viewed, + limiting their use and content availability to a few initiates, and I really mean a few. + Lets face it, the www is the common denominator today. +

+

+ Ok, you can't put the djinn back in the bottle. You can do a load of stupid stuff + with a web site, and there are billions of them out there producing carbon, wasting energy, + and displaying a marked lack of content behind a show of technological sleight of hand. +

+

+ Instead, the onus is on you to write a universally readable web site, + not on the reader to posess the right browser. +

+

+ Of course, you can't force people to be responsible, and major search engines + don't offer to filter out crap websites. In the mean time, I'll practice what I preach. +

+

+ So here's a blog with articles that just uses simple html. + There are no Javascript, grids, or + fancy formats. Articles are plain html and can be read in a terminal text browser like + Lynx, or a simple browser + graphical browser like Dillo, or a state + of the art browser like + Firefox. +

+

+ The only extras are the use of some basic css for browsers that can use + it, and UTF-8 character encoding rather than ASCII. Behind the scenes + there's some PHP for generating article listings and search, and a couple of + bash scripts to edit new articles and upload them. I think + this arrangement will be pretty future resistant.

+

You can download the scripts here +

+

+ A Google only web is on the horizon. Let them have it. Instead let's go + back to enjoying a fast, pertinent, content-rich space where you are not + the product. +

+

+ For info, I use Librewolf, + a more secure version of Firefox, and + Chawan, + perhaps the best terminal web browser to date. +

+ +
+
+ + diff --git a/articles/2023/ms-eu-fine.html b/articles/2023/ms-eu-fine.html new file mode 100755 index 0000000..e3c7656 --- /dev/null +++ b/articles/2023/ms-eu-fine.html @@ -0,0 +1,34 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+ +
+ + diff --git a/articles/2023/new-television.html b/articles/2023/new-television.html new file mode 100755 index 0000000..6d9ff57 --- /dev/null +++ b/articles/2023/new-television.html @@ -0,0 +1,41 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ + +
+
+

New Television

+ +

05-10-2023

+ +

+My old 43" Panasonic bought in 2007 for 2500 was a bit tired, or was it me tired of it? +

+Whatever the reason, I decided to change it. After visiting the local shops and getting asked questions like "what is a bluetooth remote?" by salespeople, I ended up buying online. +

+If people turn to buying online, traditional shops are largely to blame in my humbler opinion. +

+The TCL 43C641 arrived, the first thing I noticed was it's weight, so much lighter than the old Panasonic. I bought a separate central column stand as I don't like the diddly feet that require a support larger than the television to not fall off. +

+Setup was a breeze, although android TV boxes go out of their way to stop you from running apps upon launch. For example my provider is Free, and so I use the OQEE app instead of the Free TV box, but impossible to have it automatically run when the telly starts. +

+The strangest thing is the definition. We were so used to the Panasonic, the higher definition somehow makes films or series look as if they were shot by an amateur. I guess it's habit. +

+ +
+
+ + diff --git a/articles/2023/newsboat-strip-junk-from-web-pages.html b/articles/2023/newsboat-strip-junk-from-web-pages.html new file mode 100755 index 0000000..d3ea26f --- /dev/null +++ b/articles/2023/newsboat-strip-junk-from-web-pages.html @@ -0,0 +1,46 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Newsboat: Strip junk from webpages

+

17-11-2023

+

+ Install Python readability +

+ yay -S python-readability-lxml +

+ Test output +

+ + python3 \ -m readability.readability -u \ 'https://www.servethehome.com/amd-psb-vendor-locks-epyc-cpus-for-enhanced-security-at-a-cost/' \ 2> /dev/null \ 1> output.html + +

+ In ~/.newsboat/config +

+ set macro - french keyboard comma semicolon is practical +

+ + macro ; set browser "python3 -m readability.readability -u %u 2> /dev/null | w3m -T text/html"; open-in-browser ; set browser "w3m %u" + +

+ Source +

+ +
+
+ + diff --git a/articles/2023/noyb-files-gdpr-comp.html b/articles/2023/noyb-files-gdpr-comp.html new file mode 100755 index 0000000..5c9d131 --- /dev/null +++ b/articles/2023/noyb-files-gdpr-comp.html @@ -0,0 +1,42 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

GDPR complaint against Meta

+

28-11-2023

+

+ Today, noyb filed a complaint against Meta with the Austrian data protection + authority. European users now have the “choice” to either consent to being + tracked for personalized advertising – or pay up to €251.88 a year to retain + their fundamental right to data protection on Instagram and Facebook. Not only + is the cost unacceptable, but industry numbers suggest that only 3 percent of + people want to be tracked – while more than 99 percent decide against a + payment when faced with a “privacy fee”. If Meta gets away with this, + competitors will soon follow in its footsteps. Given that the average phone + has 35 apps installed, keeping your phone private could soon cost around € + 8,815 a year. +

+

+ https://noyb.eu +

+
+
+ + diff --git a/articles/2023/nuclear-sabotage.html b/articles/2023/nuclear-sabotage.html new file mode 100755 index 0000000..3ec563a --- /dev/null +++ b/articles/2023/nuclear-sabotage.html @@ -0,0 +1,47 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back +
+ +
+
+

Nuclear Sabotage

+

14-09-2023

+

+ How Russia incited German and French politicians to sabotage French + nuclear power. It nearly worked. +

+
+ +
+
+
+ + + diff --git a/articles/2023/obscurantism.html b/articles/2023/obscurantism.html new file mode 100755 index 0000000..e91c9ba --- /dev/null +++ b/articles/2023/obscurantism.html @@ -0,0 +1,26 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Obscurantism

+

17-10-2023

+ + +
+
+ + diff --git a/articles/2023/pacman-yay-notes.html b/articles/2023/pacman-yay-notes.html new file mode 100755 index 0000000..f846500 --- /dev/null +++ b/articles/2023/pacman-yay-notes.html @@ -0,0 +1,33 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Arch Linux Pacman Notes

+

01-01-2023

+

+ Clean unused packages: pacman -Qdtq | pacman -Rs - +

+ Clean package cache: pacman -Sc +

+ Purge package: pacman -Runs +

+ yay -Scc +

+
+
+ + diff --git a/articles/2023/password-store-remote.html b/articles/2023/password-store-remote.html new file mode 100755 index 0000000..867c01b --- /dev/null +++ b/articles/2023/password-store-remote.html @@ -0,0 +1,38 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Password-store remote server

+

02-09-2023

+

+ I added a remote server for pass so I can share the passwords on my new-to-me laptop. +

+ see: https://gist.github.com/abtrout/d64fb11ad6f9f49fa32 +

+ and don't forget to +

+
    +
  • Add the Passff extension to Firefox
  • +
  • Add the host app
  • +
+ + curl -sSL github.com/passff/passff-host/releases/latest/download/install_host_app.sh | bash -s -- firefox + +
+
+ + diff --git a/articles/2023/password-store-to-bitwarden.html b/articles/2023/password-store-to-bitwarden.html new file mode 100755 index 0000000..8fd66a0 --- /dev/null +++ b/articles/2023/password-store-to-bitwarden.html @@ -0,0 +1,79 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

password-store to bitwarden

+

2023-12-04

+

Migrating from password-store to Bitwarden and RBW

+

+ I've been using Pass (password-store) for a couple of years now, and it works really well on the command line and + with command line apps. Browser extensions exist that while not earth + shattering, do work. Setting up a remote git repository and sharing the + repository on another computer is relatively easy, and overall it appeals to + my ideal of no-nonsense software. +

+

+ Problems arise however when you want to share your password store on Android. + Getting the android app to work with the same repository is a few steps away + from planetary orbit insertion calculations. +

+

+ After fiddling with it for a few hours, I threw in the towel. I must be too + dumb. The most recent user written howto I found dated from 2018, for an older + version. I get the impression that nobody really uses it. With probable + reason. +

+

+ So with a sigh in my heart I've opted to move to + Bitwarden for now. +

+

Bitwarden

+

+ Bitwarden is free for personal use, but also has some extras with $10 and $40 + year plans. +

+

+ Bitwarden's browser extensions are great, Android support is great, and + there's a command line app. You can even set up your own Bitwarden repository, + although that's something I don't really need. +

+

+ Bitwarden doesn't have LastPass's data breach history, and it appears to cater + to people like me who just want the job done without a ton of + commercialisation effects. +

+

Migration

+

+ There are a couple of ways to migrate from Pass to Bitwarden, but I found that + Pass can pile up a lot of obsolete and irregular data, so I've installed the Bitwarden and + Pass Browser Firefox extensions side by side, so I can build the Bitwarden vault as I go + along. +

+

Update

+

I've been using Bitwarden for two years now and I'm completely satisfied.

+

Bitwarden's unofficial command line application RBW +has come a long way. It's now easy to include RBW in the mbsync config file .mbsyncrc for +example to obtain a stored password with

+Passcmd "rbw get [folder/password]" +

I use the same synchronised vault on Android and two different laptops.

+ +
+
+ + diff --git a/articles/2023/periodic-fstrim-on-ssd-drives.html b/articles/2023/periodic-fstrim-on-ssd-drives.html new file mode 100755 index 0000000..5e22772 --- /dev/null +++ b/articles/2023/periodic-fstrim-on-ssd-drives.html @@ -0,0 +1,40 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Periodic fstrim on SSD drives

+

05-11-2023

+

+The util-linux package provides fstrim.service and fstrim.timer systemd unit files. +

+Enabling the timer will activate the service weekly. The service executes fstrim(8) on all mounted filesystems on devices that support the discard operation. +

+The timer relies on the timestamp of /var/lib/systemd/timers/stamp-fstrim.timer (which it will create upon first invocation) to know whether a week has elapsed since it last ran. +

+Therefore there is no need to worry about too frequent invocations, in an anacron-like fashion. systemctl enable fstrim.timer +

+To query the units activity and status, see journalctl. +

+To change the periodicity of the timer or the command run, edit the provided unit files. +

+https://wiki.archlinux.org/title/Solid_state_drive + + +
+
+ + diff --git a/articles/2023/prettier-when-it-works.html b/articles/2023/prettier-when-it-works.html new file mode 100755 index 0000000..8e0c196 --- /dev/null +++ b/articles/2023/prettier-when-it-works.html @@ -0,0 +1,50 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back +
+
+ +
+
+

Prettier when it works

+

24-08-2023

+ +

Prettier is a great code beautifier for PHP, once you get it to work.

+ +

Update 14 may 2025

+

For Archlinux:

+ pacman -S prettier +yay -S prettier-plugin-php +

edit ~/.prettierrc

+
+{ 
+   "plugins": ["/usr/lib/node_modules/@prettier/plugin-php/src/index.mjs"]
+}
+    
+

In order to use it in the Micro text editor, download micro-prettier:

+
+    git clone https://github.com/claromes/micro-prettier.git
+    cd micro-prettier
+    make
+    
+

If you would rather not have it running on every save, in micro:
+ Ctrl+e > set prettier.onsave false

+
+
+ + + diff --git a/articles/2023/protecting-your-computer-with-a-hosts-file.html b/articles/2023/protecting-your-computer-with-a-hosts-file.html new file mode 100755 index 0000000..8ec8edf --- /dev/null +++ b/articles/2023/protecting-your-computer-with-a-hosts-file.html @@ -0,0 +1,42 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Protecting your computer with a hosts file

+

19-11-2023

+

Sebsauvage has put together a great hosts file: + dns-blocklist +

+

+ DNS filtering is a simple way of blocking the majority of ads, trackers and malicious sites with minimum effort. Set-up is fast and easily reversible. +

+

On Linux

+sudo curl "https://sebsauvage.net/wiki/doku.php?do=export_code&id=dns-blocklist&codeblock=0" -o /etc/cron.weekly/hosts-update +sudo chmod +x /etc/cron.weekly/hosts-update +

+ Run the first update: +

+sudo /etc/cron.weekly/hosts-update +

On Windows

+

+ Windows networking is a steaming pile of manure, but you can still install this file with caveats. +

+ +
+
+ + diff --git a/articles/2023/quad-9.html b/articles/2023/quad-9.html new file mode 100755 index 0000000..1987f9d --- /dev/null +++ b/articles/2023/quad-9.html @@ -0,0 +1,30 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ + +
+
+

Quad9 and systemd-resolved

+

05-09-2023

+

+I set up systemd-resolved with the Quad9 DNS servers, with Cloudflare as backup. Quad9 is a Swiss DNS service that blocks malicious hosts and improves performance. +

+https://geekflare.com/linux-server-local-dns-caching + +
+
+ + diff --git a/articles/2023/radio-jazz-soul.html b/articles/2023/radio-jazz-soul.html new file mode 100755 index 0000000..1c2ff6d --- /dev/null +++ b/articles/2023/radio-jazz-soul.html @@ -0,0 +1,27 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+ +
+ + diff --git a/articles/2023/saint-catherine.html b/articles/2023/saint-catherine.html new file mode 100755 index 0000000..c34e17a --- /dev/null +++ b/articles/2023/saint-catherine.html @@ -0,0 +1,37 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Saint Catherine

+

17-11-2023

+ saint catherine +

+ À la Sainte-Catherine, tout bois prend racine +

+ On Saint Catherine's Day ( 25th November), all wood takes root +

+ I haven't suddenly converted to religion, it's just a saying... +

+ When the plants are not using energy to produce leaves, but the weather is mild and wet, is an ideal time for cuttings. +

+ The sun is out so I snipped and planted a load of cuttings, some nicked from the local church garden. +

+ +
+
+ + diff --git a/articles/2023/searxng-a-private-search-engine.html b/articles/2023/searxng-a-private-search-engine.html new file mode 100755 index 0000000..bd6fbb7 --- /dev/null +++ b/articles/2023/searxng-a-private-search-engine.html @@ -0,0 +1,35 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Searxng - a private search engine

+

12-11-2023

+

+ I've installed a copy of Searxng. +

+ It's also on my laptop, but my internet connection is so crappy it's faster to have it on a vps +

+ Searxng is a free meta-search engine, meaning that it does not manage the search itself but relies on search engines such as bing, google, duckduckgo, etc. that it uses anonymously. +

+ The difference is that nothing is tracked, nobody is profiling you, there are no logs, thus your searches are totally private. +

+ https://docs.searxng.org/index.php + +
+
+ + diff --git a/articles/2023/searxng-on-my-laptop.html b/articles/2023/searxng-on-my-laptop.html new file mode 100755 index 0000000..f566d65 --- /dev/null +++ b/articles/2023/searxng-on-my-laptop.html @@ -0,0 +1,59 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

SearxNG on my laptop

+

05-11-2023

+

To keep my searches private, I've installed SearxNG on the T430.

+

Here's how I did it: My OS is Arch linux.

+
+$ cd ~/Downloads 
+$ git clone https://github.com/searxng/searxng.git searxng 
+$ cd searxng sudo -H ./utils/searxng.sh install all 
+
+

+ I use Nginx as a web server on my laptop, so I added the following in + /etc/nginx/sites-available/localhost +

+
+location /searxng {
+  uwsgi_pass unix:///usr/local/searxng/run/socket;
+  include uwsgi_params;
+  uwsgi_param    HTTP_HOST             $host;
+  uwsgi_param    HTTP_CONNECTION       $http_connection;
+
+  # see flaskfix.py
+  uwsgi_param    HTTP_X_SCHEME         $scheme;
+  uwsgi_param    HTTP_X_SCRIPT_NAME    /searxng;
+  # see limiter.py 
+  uwsgi_param    HTTP_X_REAL_IP      $remote_addr;
+  uwsgi_param    HTTP_X_FORWARDED_FOR  $proxy_add_x_forwarded_for;
+}
+
+

Navigate to localhost/searxng and there's our search engine.

+

+ In order to use it as the default search engine in Firefox, right click on + the url and select searxng as a search engine. +

+

Then in settings set searxng as the default search engine.

+ +
+
+ + diff --git a/articles/2023/system-collapse.html b/articles/2023/system-collapse.html new file mode 100755 index 0000000..846523f --- /dev/null +++ b/articles/2023/system-collapse.html @@ -0,0 +1,33 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

System Collapse

+

14-11-2023

+ book cover +

+ I have this in my Kindle as a next read... +

+

Update:Not as good as the previous books, Murderbot gets closer to figuring out humans, but no real surprises.

+

+ Amazon link +

+ +
+
+ + diff --git a/articles/2023/t430-cpu-upgrade.html b/articles/2023/t430-cpu-upgrade.html new file mode 100755 index 0000000..5cda990 --- /dev/null +++ b/articles/2023/t430-cpu-upgrade.html @@ -0,0 +1,41 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Thinkpad T430 CPU Upgrade

+

27-10-2023

+ +

+I've updated the CPU of my Thinkpad T430 +

+The laptop came with a dual core i5 , but as it is a highly upgradable machine, I've added a quad core i7–3632QM +

+I also changed the cooler to increase the amount of copper acting as the heatsink. +

+The benchmark shows a nice improvement and everything feels snappier. Probably unnecessary for basic use, but I plan to do some DVD transcoding, so in all a good move. See: +The Definitive T430 Modding +Guide +

+

Update

+

+The standard 65 watt charger that comes with the T430 no longer charges while the laptop is running as the new CPU sucks more juice that the charger can handle, so I've bought a 90 watt charger. The battery itself runs for about a bit less than before due to the same cause. It's looking good ! +

+
+
+ + diff --git a/articles/2023/t430-cpu-upgrade.html~ b/articles/2023/t430-cpu-upgrade.html~ new file mode 100755 index 0000000..9cb1c56 --- /dev/null +++ b/articles/2023/t430-cpu-upgrade.html~ @@ -0,0 +1,38 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +

Thinkpad T430 CPU Upgrade

+
27-10-2023
+ +

+I've updated the CPU of my Thinkpad T430 +

+The laptop came with a dual core i5 , but as it is a highly upgradable machine, I've added a quad core i7–3632QM +

+I also changed the cooler to increase the amount of copper acting as the heatsink. +

+The benchmark shows a nice improvement and everything feels snappier. Probably unnecessary for basic use, but I plan to do some DVD transcoding, so in all a good move. See: The Definitive T430 Modding Guide +

+

Update

+

+The standard 65 watt charger that comes with the T430 no longer charges while the laptop is running as the new CPU sucks more juice that the charger can handle, so I've bought a 90 watt charger. The battery itself runs for about a bit less than before due to the same cause. It's looking good ! +

+ + +
+ A minimalist blog by Philip Wittamore +
+ + diff --git a/articles/2023/thinkpad-t430.html b/articles/2023/thinkpad-t430.html new file mode 100755 index 0000000..75390fe --- /dev/null +++ b/articles/2023/thinkpad-t430.html @@ -0,0 +1,48 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Thinkpad T430

+

27-08-2023

+

+ Since april 2021 I've been using a Thinkpad X220. I upgraded it with 16Gb ram and a 240Gb SSD hard drive. Really happy with it, solid well built no-nonsense laptop. +

+ But I have a growing problem, the 12" 1366 x 768 is a bit small for developing web sites, and my eyes aren't what they used to be. +

+ So I've bought another old Thinkpad, this time a T430. It has a 14" 1600 x 900 screen. +

+ Upgrades: 16Gb ram, a 128Gb msata hard disk, a 480Gb SSD hard disk, and AC-7260HMW wifi card. +

+ I'll upgrade the Bios with 1vyrain to unlock the limits of Ivybridge laptops and install the AC-7260. +

+ It's also possible to upgrade the screen and processor. +

+ People tend to fall in love with these laptops, because their build and upgradablilty have never been bettered, + and where else can you get a Thinklight? +

+ (Framework laptops look nice, but an older Thinkpad is waaaay cheaper!) +

+ A few links about this machine: +

+ https://www.thinkwiki.org/wiki/Category:T430 +

+ https://medium.com/@n4ru/the-definitive-t430-modding-guide-3dff3f6a8e2e +

+
+
+ + diff --git a/articles/2023/updating-my-thinkpad-t430-wifi-card.html b/articles/2023/updating-my-thinkpad-t430-wifi-card.html new file mode 100755 index 0000000..8864181 --- /dev/null +++ b/articles/2023/updating-my-thinkpad-t430-wifi-card.html @@ -0,0 +1,67 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ + +
+
+

Updating my Thinkpad T430 wifi card

+

06-09-2023

+ +

+The wifi card that was installed in my laptop was an Intel® Centrino® Advanced-N 6205 +

+The N 6205 is a dual band card but unlike the AC 7620, It doesn't have an integrated bluetooth card, nor 802.11ac +

+I previously flashed the bios with 1vyrain see article, which wipes the whitelist that would otherwise prevent me from installing hardware not on the list. So I can now install the AC 7620. +

+The T430 doesn't activate this card unless the 51st pin is removed. I removed the pin with my Dremel (some use tape). +

+This pin is the last pin on the right when the card is face up. I put the laptop back together and checked. +

+The wifi came on automatically. +

+I then installed Bluez: +

+sudo pacman -S bluez sudo pacman -S bluez-utils sudo pacman -S blueman +sudo micro /etc/bluetooth/main.conf set AutoEnable=true +sudo systemctl enable bluetooth.service +sudo systemctl start bluetooth.service + +

+I made sure pulseaudio was installed: +

+ +sudo pacman -S pulseaudio +sudo pacman -S pulseaudio-bluetooth +sudo pacman -S pavucontrol +sudo systemctl enable pulseaudio +sudo systemctl start pulseaudio + +

+Then I connected my bluetooth headset and ... it just worked +

+Sources: +

+https://medium.com/@n4ru/the-definitive-t430-modding-guide-3dff3f6a8e2e +
+https://www.jeremymorgan.com/tutorials/linux/how-to-bluetooth-arch-linux/ +
+https://www.sevarg.net/2022/12/03/thinkpad-t430s-wireless-combo-card-upgrade/ +
+

+
+ + diff --git a/articles/2023/windowmaker-0.96.html b/articles/2023/windowmaker-0.96.html new file mode 100755 index 0000000..fb1b0a5 --- /dev/null +++ b/articles/2023/windowmaker-0.96.html @@ -0,0 +1,48 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Window maker 0.96

+

06-11-2023

+ +

+ Windowmaker has had it's first update since 2000, so I had to try it out. +

+

+ Windowmaker was written in 1997. For me it's a blast from the past as I + used to run it in a commercial setting, dual booting with windows 3.11 and + running Applixware 4.41 with custom software I wrote for labeling luxury + goods. +

+

+ I chose Windowmaker as it was easy for my collegues to select an app with + a nice big button. +

+

+ After using FVWM, it was easy to set up in comparaison and a unique + paradigm straight from Steve Job's Nextstep. I loved it. +

+

+ Nowdays I use mainly the Xfce4 environment, but it's nice to have + something else to play with. I've used Tiling window managers such as i3 + but they just don't work for me. +

+ +
+
+ + diff --git a/articles/2023/windowmaker-in-1998.html b/articles/2023/windowmaker-in-1998.html new file mode 100755 index 0000000..d1ea148 --- /dev/null +++ b/articles/2023/windowmaker-in-1998.html @@ -0,0 +1,35 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Windowmaker in 1998

+

07-11-2023

+

+ I've kept a couple of screenshots of an application I wrote using + Applixware 4.41, running on Windowmaker, in 1998. +

+

+ It was used with an early HP laser printer with refillable toner pots to + produce personalised product labels that had a lot of variables. +

+ + + +
+
+ + diff --git a/articles/2024/A-new-lathe.html b/articles/2024/A-new-lathe.html new file mode 100755 index 0000000..5bf9069 --- /dev/null +++ b/articles/2024/A-new-lathe.html @@ -0,0 +1,32 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

New lathe

+

2024-02-22

+ +

I haven't updated this blog for a couple of months due to my yearly winter brain fog during the dark season, +but here I am again. I've just ordered a new lathe from Bordet, the Coronet Herald.

+

My home-made lathe works but has limitations that I hope to overcome, +and it's going to give me a well needed push to get back into making things rather than being a couch potato.

+

I chose the Coronet Herald as it is the only lathe in it's category that has a rotating headstock, +which enables turning of large pieces, and in general it is at the top of all reviews.

+

I also have a project for a new workshop, as sharing the garage with madame is far from optimal and a guy needs his man-cave, right?

+
+
+ + diff --git a/articles/2024/Another-Oland-tool.html b/articles/2024/Another-Oland-tool.html new file mode 100755 index 0000000..419369b --- /dev/null +++ b/articles/2024/Another-Oland-tool.html @@ -0,0 +1,29 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Another Oland tool

+

2024-03-18

+ +

I'm making myself some new oland tools. The ones I have are rather big, and I wanted some finer tools.

+

This one is for 5mm HSS tool bits. It has a 7mm hole in a 12mm steel shaft with a set screw to hold the tool bit, +which can be ground to any form

+

#woodturning #tools #homemade

+
+
+ + diff --git "a/articles/2024/support-perc\303\251-pour-bougie-chauffe-plat.html" "b/articles/2024/support-perc\303\251-pour-bougie-chauffe-plat.html" new file mode 100755 index 0000000..6895073 --- /dev/null +++ "b/articles/2024/support-perc\303\251-pour-bougie-chauffe-plat.html" @@ -0,0 +1,27 @@ + + + + + + + Bloggings + + +
+

Bloggings

+ Back + +
+ +
+
+

Support percé pour bougie chauffe-plat

+

2024-03-11

+

+

I was give a scrap block of Iroko to try out, and came up with this.

+

Piercing the holes took a while even with mini drill. It has a Tung oil finish.

+
+
+ + diff --git a/articles/2025/A-fledgling-bash-function-library.html b/articles/2025/A-fledgling-bash-function-library.html new file mode 100755 index 0000000..d59d1d0 --- /dev/null +++ b/articles/2025/A-fledgling-bash-function-library.html @@ -0,0 +1,33 @@ + + + + + + + A fledgling bash function library + + +
+

Bloggings

+ Back + +
+ +
+
+

A fledgling bash function library

+

2025-05-16

+

Messing with bash functions

+

I needed a function for writing colour text, so I thought why not start a library +and learn some bash scripting? Anyway, it's up on my git repository.

+

Bashlib git repository

+
+
+ + + diff --git a/articles/2025/AC7260-wifi-card-on-Thinkpad-X220.html b/articles/2025/AC7260-wifi-card-on-Thinkpad-X220.html new file mode 100755 index 0000000..0d6475a --- /dev/null +++ b/articles/2025/AC7260-wifi-card-on-Thinkpad-X220.html @@ -0,0 +1,36 @@ + + + + + + + AC7260 wifi card on Thinkpad X220 + + +
+

Bloggings

+ Back + +
+ +
+
+

AC7260 wifi/bluetooth card on Thinkpad X220

+

2025-04-05

+

Easy card replacement

+ac7260 card +

I had previously upgraded the bios +on this machine so I could whitelist future hardware.

+

The new card was easy to install: remove the keyboard by unscrewing 2 screws under the RAM trapdoor

+

Carefully remove the keyboard by sliding it towards the screen to uncover the lower tabs and unplug it, +then remove the screws holding the palmrest and remove the palmrest carefully unplugging it.

+

The wifi card socket is on the right next to the M2 ssd socket

+

I removed pin 51 on the AC7620 with a dremel rather than using tape. Pin 51 is used to activate or deactivate certain card functions, +including Bluetooth, and can be problematic on certain thinkpads. I did the same for my T430.

+

See also Deactivating the X220 internal Bluetooth controller

+ +
+
+ + diff --git a/articles/2025/Amazon-playing-silly-buggers.html b/articles/2025/Amazon-playing-silly-buggers.html new file mode 100755 index 0000000..a4ca86f --- /dev/null +++ b/articles/2025/Amazon-playing-silly-buggers.html @@ -0,0 +1,39 @@ + + + + + + + Amazon playing silly buggers + + +
+

Bloggings

+ Back + +
+ +
+
+

Amazon playing silly buggers

+

2025-03-21

+

I've dropped my Kindle for a Kobo

+

+

I downloaded all my purchased books, around 500, before Amazon terminated the possibility. Even though it says "purchase" on their site in reality they are now selling you a license, and only allowing books to be read on Kindles.

+

Enough of this.

+

Using Calibre I created a library that can be read on any ebook reader, and purchased a Kobo Clara colour.

+

Now I'm as happy as a pig in muck. Oh and Amazon, I won't be buying books from you any more. There's more than enough alternative sources.

+

** I love Glynn Stewarts science fiction stories, but most of his work is +published on Amazon. The solution was simple, I subscribed to Glynn's Patreon account and +receive advanced copies of his books that I can read on any reader.

+

https://www.patreon.com/glynnstewart

+

That said, I've read nearly all of Michael Connelly's books but some of +his ebooks on Google or Kobo are country restricted. For example I' unable to purchase the +full series of the Jack Macevoy series because I'm in France. WTF? All this +does is tell me to get a book from another author. Get a clue guys!

+ +
+
+ + diff --git a/articles/2025/Anacron-on-Artix.html b/articles/2025/Anacron-on-Artix.html new file mode 100755 index 0000000..624a53a --- /dev/null +++ b/articles/2025/Anacron-on-Artix.html @@ -0,0 +1,93 @@ + + + + + + + Anacron on Artix + + +
+

Bloggings

+ Back + +
+ +
+
+

Anacron on Artix

+

2025-06-16

+

On Arch Anacron is a systemd process

+

Anacron ensures cron scripts are run on machines that are frequently powered off

+

On Artix it can be run via cron (cronie)

+ +

The /etc/crontab file

+ +# /etc/crontab: configuration file for cron +# See cron(8) and crontab(5) for details. +# m h dom mon dow user command + +SHELL=/bin/bash +PATH=/sbin:/bin:/usr/sbin:/usr/bin +MAILTO=root +HOME=/ + +# run-parts +# (also runs the 0anacron script every hour) +12 * * * * root run-parts /etc/cron.hourly +22 10 * * * root run-parts /etc/cron.daily +32 10 * * 0 root run-parts /etc/cron.weekly +42 10 1 * * root run-parts /etc/cron.monthly + + +

The /etc/cron.hourly/0anacron script

+ +#!/bin/sh +# Check whether 0anacron was run today already +if test -r /var/spool/anacron/cron.daily; then +day=`cat /var/spool/anacron/cron.daily` +fi +if [ `date +%Y%m%d` = "$day" ]; then +exit 0; +fi + +# Do not run jobs when on battery power +if test -x /usr/bin/on_ac_power; then +/usr/bin/on_ac_power >/dev/null 2>&1 +if test $? -eq 1; then +exit 0 +fi +fi +/usr/sbin/anacron -s + + +

The /etc/anacrontab file

+ +# /etc/anacrontab: configuration file for anacron + +# See anacron(8) and anacrontab(5) for details. + +SHELL=/bin/sh +PATH=/sbin:/bin:/usr/sbin:/usr/bin +MAILTO=root +# the maximal random delay added to the base delay of the jobs +RANDOM_DELAY=45 +# the jobs will be started during the following hours only +START_HOURS_RANGE=3-22 + +#period in days delay in minutes job-identifier command +1 5 cron.daily nice run-parts /etc/cron.daily +7 25 cron.weekly nice run-parts /etc/cron.weekly +@monthly 45 cron.monthly nice run-parts /etc/cron.monthly + + +
+
+ + + diff --git a/articles/2025/Annoying-touchpad.html b/articles/2025/Annoying-touchpad.html new file mode 100755 index 0000000..e4f784c --- /dev/null +++ b/articles/2025/Annoying-touchpad.html @@ -0,0 +1,35 @@ + + + + + + + Annoying touchpad + + +
+

Bloggings

+ Back + +
+ +
+
+

Annoying touchpad

+

2025-04-28

+

My palm keeps hitting the trackpad

+

So I deactivated it. Got a red touchpoint for that!

+
+sudo pacman -S xf86-input-synaptics
+synclient TouchpadOff=1
+
+
+
+ + + diff --git a/articles/2025/Archwiki-on-my-laptop.html b/articles/2025/Archwiki-on-my-laptop.html new file mode 100755 index 0000000..bb91166 --- /dev/null +++ b/articles/2025/Archwiki-on-my-laptop.html @@ -0,0 +1,38 @@ + + + + + + + Archwiki on my laptop + + +
+

Bloggings

+ Back + +
+ +
+
+

Archwiki on my laptop

+

2025-05-15

+

Wikiman enables you to peruse wiki's on your computer

+
+pacman -S wikiman
+pacman -S arch-wiki-docs
+cp /etc/wikiman.conf ~/.config/wikiman/
+
+

edit ~/.config/wikiman/wikiman.config to your liking. I use the chawan browser instead of w3m for example.

+

git page for Wikiman

+

Note: wikiman can be used in the TTY which can be a life saver if you have borked your installation.

+
+
+ + + diff --git a/articles/2025/Back-to-the-future.html b/articles/2025/Back-to-the-future.html new file mode 100755 index 0000000..54908c1 --- /dev/null +++ b/articles/2025/Back-to-the-future.html @@ -0,0 +1,37 @@ + + + + + + + Back to the future + + +
+

Bloggings

+ Back + +
+ +
+
+

Back to the future

+

2025-04-25

+

4 years afterwards

+

Back in 2001 I was using Pyradio with Cava on Manjaro with i3

+

Today I'm using the same apps on Arch Linux with DWM

+

Not quite sure if I'm going backwards or forwards...

+pyradio screenshot +

Pyradio-m3u

+

Here's a handy app that fills pyradio with a few thousand stations

+

pyradio-m3u

+
+
+ + + diff --git a/articles/2025/Bash-prompt-without-scripts.html b/articles/2025/Bash-prompt-without-scripts.html new file mode 100755 index 0000000..cf6c42c --- /dev/null +++ b/articles/2025/Bash-prompt-without-scripts.html @@ -0,0 +1,37 @@ + + + + + + + Bash prompt without scripts + + +
+

Bloggings

+ Back + +
+ +
+
+

Bash prompt without scripts

+

2025-05-06

+

A pretty prompt in one line of my .bashrc

+

I tried several bash prompt scripts but returned to just having a single line in my .bashrc

+
+export PS1='\[\e[30;102m\] \A \[\e[92;103m\]\[\e[30m\] \u@\h \[\e[93;107m\]\[\e[30m\] \w \[\e[0;97m\]\n\[\e[0m\]\$ '
+
+

This site is a great generator

+

I used the base 16 colour palette so that it ends up displayed using the Pywal16 terminal palette.

+

+
+
+ + + diff --git a/articles/2025/Betterfox.html b/articles/2025/Betterfox.html new file mode 100755 index 0000000..cf9985c --- /dev/null +++ b/articles/2025/Betterfox.html @@ -0,0 +1,28 @@ + + + + + + + Betterfox + + +
+

Bloggings

+ Back + +
+ +
+
+

Betterfox

+

2025-03-22

+

With all the hoohaa around Firefox wanting your data I started using Floorp instead.

+

But there is hope. Betterfox makes your Firefox safer and faster by modifying user.js

+

There's a lot of super interesting information on the site too.

+

BetterFox

+
+
+ + diff --git a/articles/2025/Blog-scripts.html b/articles/2025/Blog-scripts.html new file mode 100755 index 0000000..9c418a8 --- /dev/null +++ b/articles/2025/Blog-scripts.html @@ -0,0 +1,44 @@ + + + + + + + Blog scripts + + +
+

Bloggings

+ Back + +
+ +
+
+

Blog scripts

+

2025-05-13

+

My simple blog scripts

+

I've added my blog scripts to the git server just in case someone +needs a quick simple blog like this one.

+

Blog articles are simply html, the site structure is on your computer +and there are a couple of Bash scripts to manage it:

+
    +
  • blogthis builds an articles header and footer, opens the editor
  • +
  • blogsend uses ssh to upload all recently added or modified files
  • +
  • Blogrss builds/updates the rss feed
  • +
  • Blogsitemap builds/updates the sitemap
  • +
+

Note that these Bash scripts rely on your having set up ssh correctly to allow +you to ssh to your web server without entering a password.

+

The articles are listed by three php files that include a search form.

+

get the files here

+
+
+ + + diff --git a/articles/2025/Bringing-back-the-fun.html b/articles/2025/Bringing-back-the-fun.html new file mode 100755 index 0000000..5cd95de --- /dev/null +++ b/articles/2025/Bringing-back-the-fun.html @@ -0,0 +1,34 @@ + + + + + + + Bringing back the fun + + +
+

Bloggings

+ Back + +
+ +
+
+

Bringing back the fun

+

2025-04-23

+

I installed xscreensaver and it's cool

+

Many years ago my window managers were FVWM and Windowmaker, and they were a great deal of fun.

+

Installing DWM is a breath of fresh air, giving me a bit of nostalgia for the good old days.

+

I'm trying to not fall into the trap of patching DWM to death and adding useless bling, just keeping it simple.

+

However, today I installed xscreensaver and my screen lit up reminding me that like the indie web, older stuff can be fun.

+
+
+ + + diff --git a/articles/2025/Browser-choices.html b/articles/2025/Browser-choices.html new file mode 100755 index 0000000..d8b483a --- /dev/null +++ b/articles/2025/Browser-choices.html @@ -0,0 +1,45 @@ + + + + + + + Browser choices + + +
+

Bloggings

+ Back + +
+ +
+
+

Browser choices

+

2025-04-02

+

Browser, browser, on the wall, who is the fairest of them all?

+

Here's my totally not objective shortlist to date

+
    +
  • Firefox - doing stupid things for money
  • +
  • Floorp - so many options I wound up not using
  • +
  • Chawan - works great with Ghostty, Neomutt & Newsboat
  • +
  • W3M - best of the text browsers after Chawan
  • +
  • Lynx - borks on ipv6
  • +
  • Dillo - fast and simple, too simple?
  • +
  • Librewolf - a lean secure Firefox
  • +
  • Waterfox - yeah but Librefox is leaner
  • +
  • Mullvad - nice and secure, I don't need a VPN though, yet
  • +
  • Otter - nice but crashed
  • +
  • Qutebrowser - yet another barrel of keystrokes to learn
  • +
  • Nyx - yet another barrel of keystrokes to learn
  • +
  • Chrome - google's squirmy tentacles, no thanks
  • +
  • Chromium - I just use it for Jitsi
  • +
  • Brave - crypto what?
  • +
  • Vivaldi - ok but it didn't hook me
  • +
  • Edge - vade retro satana
  • +
+
+
+ + diff --git a/articles/2025/Carbide-tools-for-wood-turning.html b/articles/2025/Carbide-tools-for-wood-turning.html new file mode 100755 index 0000000..09ea597 --- /dev/null +++ b/articles/2025/Carbide-tools-for-wood-turning.html @@ -0,0 +1,45 @@ + + + + + + + Carbide tools for wood turning + + +
+

Bloggings

+ Back + +
+ +
+
+

Carbide tools for wood turning

+

2025-09-07

+

Making my own tools

+

image

+

I obtained some HRC61~63 steel rods and made my own carbide wood turning tools.

+

As the rods are 12mm with a round profile instead of square, presentation of the carbide cutters at various angles, not just 90° is enabled. +My thinking was that I needed some beefy handles to provide leverage and facilitate control..

+

The handles are made of Ash, and came from the winter woodpile. A more noble end than being thrown in the woodburner.

+

First impressions are good, specially the round cutter. It looks like these tools are shapers, and position themselves between +the roughing gouge and finition.

+

Note that you'll need a hss+cobalt drill bit to make a hole in the hardened steel shafts.

+

Shafts: Linear bearing shafts, 12mm x 600mm

+

Carbide inserts:
15mm round
+15mm square

+

I might get some diamond shaped carbide cutters too, but I have some home made hss tools that do roughly the same job.

+

See here and here +for more of my home made lathe tools

+

#woodturning #tools #homemade

+
+
+ + + diff --git a/articles/2025/Chaotic-Aur.html b/articles/2025/Chaotic-Aur.html new file mode 100755 index 0000000..44e7ad1 --- /dev/null +++ b/articles/2025/Chaotic-Aur.html @@ -0,0 +1,43 @@ + + + + + + + Chaotic Aur + + +
+

Bloggings

+ Back + +
+ +
+
+

Chaotic Aur

+

2025-03-23

+

Chaotic Aur is an automated building repo for AUR packages

+

Retrieve the primary key to enable the installation of our keyring and mirror list

+sudo pacman-key --recv-key 3056513887B78AEB --keyserver keyserver.ubuntu.com +sudo pacman-key --lsign-key 3056513887B78AEB + +

Install the chaotic-keyring and chaotic-mirrorlist packages

+sudo pacman -U 'https://cdn-mirror.chaotic.cx/chaotic-aur/chaotic-keyring.pkg.tar.zst' +sudo pacman -U 'https://cdn-mirror.chaotic.cx/chaotic-aur/chaotic-mirrorlist.pkg.tar.zst' + +

Append (adding at the end) the following to /etc/pacman.conf

+[chaotic-aur] +Include = /etc/pacman.d/chaotic-mirrorlist + + +

Run a full system update along syncing the mirrorlist

+sudo pacman -Syu + +

The repository is ready to use! 🚀

+sudo pacman -S firedragon +
+
+ + diff --git a/articles/2025/Closing-laptop-lid-without-suspend.html b/articles/2025/Closing-laptop-lid-without-suspend.html new file mode 100755 index 0000000..5890d25 --- /dev/null +++ b/articles/2025/Closing-laptop-lid-without-suspend.html @@ -0,0 +1,44 @@ + + + + + + + Closing laptop lid without suspend + + +
+

Bloggings

+ Back + +
+ +
+
+

Closing laptop lid without suspend

+

2025-07-04

+

Looking into using a laptop as a home server

+

Since I've got a fibre connection I've been musing about not paying for a VPS and using a home server instead.

+

I have an unused laptop that could fit the bill maybe. The advantage of a laptop is that 1) I have one and 2) No need for +a ups or screen.

+

I'm not really sure that I'll build a home server like this, but I'm collecting info.

+

One of the things I was wondering is how to stop a laptop from entering suspend when the lid is closed. I found a +comment on unixstackexchange that explains how to do it on a non-systemd installation. (and a systemd one too but who cares about that?)

+

Find the path of the lid switch:

+cat /proc/acpi/wakeup +

Find the line for the lid and enter the command to deactivate it, for example:

+echo 'PNP0C0D:00' | sudo tee /sys/bus/acpi/drivers/button/unbind +

If it works, you can add the above line to /etc/rc.local to make it permanent.

+

Original comment

+

On systemd installations, edit /etc/systemd/logind.conf and uncomment the following line: HandleLidSwitch=ignore.

+ +
+
+ + + diff --git a/articles/2025/Cpu-scaling-governors-on-Thinkpax-X220.html b/articles/2025/Cpu-scaling-governors-on-Thinkpax-X220.html new file mode 100755 index 0000000..72ce067 --- /dev/null +++ b/articles/2025/Cpu-scaling-governors-on-Thinkpax-X220.html @@ -0,0 +1,31 @@ + + + + + + + Cpu scaling governors on Thinkpax X220 + + +
+

Bloggings

+ Back + +
+ +
+
+

Cpu scaling governors on Thinkpad X220

+

2025-03-26

+

There are only two CPU scaling governors on my Thinkpad X220

+

Powersave, and Performance

+

Attempting to use any other in tlpui will not give the desired results

+

Check available govenors:

+cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors +

See also: Linux +power consumption control

+
+
+ + diff --git a/articles/2025/DNS4U-European-DNS-servers-go-live.html b/articles/2025/DNS4U-European-DNS-servers-go-live.html new file mode 100755 index 0000000..442985e --- /dev/null +++ b/articles/2025/DNS4U-European-DNS-servers-go-live.html @@ -0,0 +1,35 @@ + + + + + + + DNS4U European DNS servers go live + + +
+

Bloggings

+ Back + +
+ +
+
+

DNS4U European DNS servers go live

+

2025-06-16

+

The EU now has an alternative to Google and Cloudflare DNS

+

JopinDNS launched

+

JoinDNS for public

+

The offers are Unfiltered resolution, Protective resolution, Protective with Child Protection, +Protective with Ad blocking, Protective with Child protection and Ad blocking

+ +
+
+ + + diff --git a/articles/2025/DWM-Dunst-and-Thinkpad-audio-keys.html b/articles/2025/DWM-Dunst-and-Thinkpad-audio-keys.html new file mode 100755 index 0000000..ee184b8 --- /dev/null +++ b/articles/2025/DWM-Dunst-and-Thinkpad-audio-keys.html @@ -0,0 +1,113 @@ + + + + + + + DWM Dunst and Thinkpad audio keys + + +
+

Bloggings

+ Back + +
+ +
+
+

DWM Dunst and Thinkpad audio keys

+

2025-04-24

+

How I got them working, microphone mute led included

+

DWM isn't a desktop environment, so there are a few things to add, like notifications and +audio keys.

+

I installed dunst and acpid. Dunst is managed by systemd, if you edit ~/.config/dunst/dunstrc +restart dunst with systemctl --user restart dunst.service

+

Dunst web page

+

Acpid Arch page

+ + +

dwm/config.h

+

The audio keys are registered in dwm/config.h +

+...
+{ 0, XF86XK_AudioMicMute, spawn, SHCMD("~/.local/bin/dwm-audio mictoggle") },
+{ 0, XF86XK_AudioMute, spawn, SHCMD("~/.local/bin/dwm-audio voltoggle") },
+{ 0, XF86XK_AudioLowerVolume, spawn, SHCMD("~/.local/bin/dwm-audio volquieter") },
+{ 0, XF86XK_AudioRaiseVolume, spawn, SHCMD("~/.local/bin/dwm-audio vollouder") },
+...
+
+ +

~/.local/bin/dwm-audio

+

The audio keys run a bash script which sets volume, speaker mute, and microphone mute.

+
+#!/bin/bash
+# Manage Thinkpad Audio keys
+
+msgTag="Audio"
+
+# Microphone
+if [[ "$@" == "mictoggle" ]]; then
+    pamixer --default-source -t
+    micmute="$(pamixer --default-source --get-mute)"
+    if [[ "$micmute" == "true" ]]; then
+        # Mic muted notification
+        dunstify -t 1000 -a "Microphone" -u low -i audio-input-microphone \
+        -h string:x-dunst-stack-tag:$msgTag "Microphone muted"
+    else
+        # Mic unmuted notification
+        dunstify -t 1000 -a "Microphone" -u low -i audio-input-microphone \
+        -h string:x-dunst-stack-tag:$msgTag "Microphone unmuted"
+    fi
+    exit 0
+fi
+
+# Speakers
+if [[ "$@" == "voltoggle" ]]; then
+	pamixer --toggle-mute
+elif [[ "$@" == "vollouder" ]]; then
+	pamixer -i 10
+elif [[ "$@" == "volquieter" ]]; then
+    pamixer -d 10
+fi
+
+volume="$(pamixer --get-volume-human)"
+mute="$(pamixer --get-mute)"
+if [[ $volume == 0 || "$mute" == "true" ]]; then
+    # Sound muted notification
+    dunstify -t 1000 -a "changeVolume" -u low -i audio-speakers \
+    -h string:x-dunst-stack-tag:$msgTag "Volume muted" 
+else
+    # Volume notification
+    dunstify -t 1000 -a "changeVolume" -u low -i audio-speakers \
+    -h string:x-dunst-stack-tag:$msgTag -h int:value:"$volume" "Volume: ${volume}"
+fi
+
+ +

Microphone mute led

+

I'm running Arch Linux on a Thinkpad X220, so I need to make the mic/mute button's led +lights up when muted. This can't be done with normal user permissions, so I installed acpid. +On Arch linux all acpi events are passed to /etc/acpi/handler.sh, so I added another +function to this file. +

+
+...
+button/micmute)
+  if [[ "$(cat /sys/devices/platform/thinkpad_acpi/leds/platform\:\:micmute/brightness)" == "1" ]]; then
+    echo "0" > /sys/devices/platform/thinkpad_acpi/leds/platform\:\:micmute/brightness
+  else
+    echo "1" > /sys/devices/platform/thinkpad_acpi/leds/platform\:\:micmute/brightness
+  fi
+;;
+...
+
+ +
+
+ + + diff --git a/articles/2025/DWM-circulate-through-tags.html b/articles/2025/DWM-circulate-through-tags.html new file mode 100755 index 0000000..c80535f --- /dev/null +++ b/articles/2025/DWM-circulate-through-tags.html @@ -0,0 +1,196 @@ + + + + + + + DWM circulate through tags + + +
+

Bloggings

+ Back + +
+ +
+
+

DWM circulate through tags

+

2025-05-07

+

Small modifications to DWM source code so far.

+

This is intended for my own records, I don't use git yet, perhaps I should look into it.

+

I haven't added any patches to my dwm 6.5 source code, but I have modified dwm.c to enable circulating through tags, +and edited config.h to play nicely with Pywal16 and my French keyboard.

+

Super+Shift+Left/Right keys circulate through tags
+Super+left/Right keys circulate through windows in a tag.

+

dwm.c

+

I added this at line 235

+
+static void view_adjacent(const Arg *arg);
+
+

I added this function at line 2067

+
+void
+view(const Arg *arg)
+{
+	if ((arg->ui & TAGMASK) == selmon->tagset[selmon->seltags])
+		return;
+	selmon->seltags ^= 1; /* toggle sel tagset */
+	if (arg->ui & TAGMASK)
+		selmon->tagset[selmon->seltags] = arg->ui & TAGMASK;
+	focus(NULL);
+	arrange(selmon);
+}
+
+

config.h

+

Here is my modified config.h

+
+/* See LICENSE file for copyright and license details. */
+#include 
+
+/* appearance */
+static const unsigned int borderpx  = 1;        /* border pixel of windows */
+static const unsigned int snap      = 32;       /* snap pixel */
+static const int showbar            = 1;        /* 0 means no bar */
+static const int topbar             = 1;        /* 0 means bottom bar */
+static const char *fonts[]          = { "JetBrainsMono Nerd Font Mono:size=11" };
+static const char dmenufont[]       = "JetBrainsMono Nerd Font Mono:size=11";
+
+/* Pywal16 */ 
+#include "/home/philip/.cache/wal/colors-wal-dwm.h"
+
+/* tagging */
+static const char *tags[] = { "Ⅰ" ,"Ⅱ", "Ⅲ", "Ⅳ"}; //, "5", "6", "7", "8", "9" };
+
+static const Rule rules[] = {
+	/* xprop(1):
+	 *	WM_CLASS(STRING) = instance, class
+	 *	WM_NAME(STRING) = title
+	 */
+	/* class        instance    title       tags mask     isfloating   monitor */
+	{ "ghostty",    NULL,       NULL,       1 << 0,       0,           -1 },
+	{ "librewolf",  NULL,       NULL,       1 << 1,       0,           -1 },
+	{ "Chromium",   NULL,       NULL,       1 << 1,       0,           -1 },
+	{ "Gimp",       NULL,       NULL,       1 << 2,       1,           -1 },
+	{ "gThumb",     NULL,       NULL,       1 << 2,       1,           -1 },
+	{ "Galculator", NULL,       NULL,       1 << 8,       1,           -1 },
+};
+
+/* layout(s) */
+static const float mfact     = 0.55; /* factor of master area size [0.05..0.95] */
+static const int nmaster     = 1;    /* number of clients in master area */
+static const int resizehints = 1;    /* 1 means respect size hints in tiled resizals */
+static const int lockfullscreen = 1; /* 1 will force focus on the fullscreen window */
+
+static const Layout layouts[] = {
+	/* symbol     arrange function */
+	{ "Ƭ",      tile },    /* first entry is default */
+	{ "Φ",      NULL },    /* no layout function means floating behavior */
+	{ "µ",      monocle }, /* everything is maximised */
+};
+
+/* key definitions */
+/* modifiers are ShiftMask, LockMask, ControlMask, Mod1Mask, Mod2Mask, Mod3Mask, Mod4Mask and Mod5Mask */
+#define MODKEY Mod4Mask
+#define TAGKEYS(KEY,TAG) \
+	{ MODKEY,                       KEY,      view,           {.ui = 1 << TAG} }, \
+	{ MODKEY|ControlMask,           KEY,      toggleview,     {.ui = 1 << TAG} }, \
+	{ MODKEY|ShiftMask,             KEY,      tag,            {.ui = 1 << TAG} }, \
+	{ MODKEY|ControlMask|ShiftMask, KEY,      toggletag,      {.ui = 1 << TAG} },
+
+/* helper for spawning shell commands in the pre dwm-5.0 fashion */
+#define SHCMD(cmd) { .v = (const char*[]){ "/bin/sh", "-c", cmd, NULL } }
+
+/* commands */
+static char dmenumon[2] = "0"; /* component of dmenucmd, manipulated in spawn() */
+static const char *dmenucmd[] = { "dmenu_run", "-m", dmenumon, "-fn", dmenufont, "-nb", norm_bg, "-nf", norm_fg, "-sb", sel_bg, "-sf", sel_fg, NULL };
+static const char *termcmd[]   = { "ghostty", NULL };
+static const char *wwwcmd[]    = { "librewolf", NULL };
+static const char *brighter[]  = { "brightnessctl", "set", "10%+", NULL };
+static const char *dimmer[]    = { "brightnessctl", "set", "10%-", NULL };
+static const char *bluetooth[] = { "blueman-manager", NULL };
+static const char *scrotcmd[]  = { "screenshot.sh", NULL};
+
+static const Key keys[] = {
+	/* modifier                     key        function        argument */
+    { 0,            XF86XK_AudioMute,          spawn,          SHCMD("~/.local/bin/dwm-audio voltoggle") },
+    { 0,            XF86XK_AudioLowerVolume,   spawn,          SHCMD("~/.local/bin/dwm-audio volquieter") },
+    { 0,            XF86XK_AudioRaiseVolume,   spawn,          SHCMD("~/.local/bin/dwm-audio vollouder") },
+    { 0,            XF86XK_AudioMicMute,       spawn,          SHCMD("~/.local/bin/dwm-audio mictoggle") },
+	{ 0,            XF86XK_MonBrightnessDown,  spawn,          {.v = dimmer   } },
+	{ 0,            XF86XK_MonBrightnessUp,    spawn,          {.v = brighter } },
+	{ 0,            XF86XK_Launch1,            spawn,          SHCMD("rofi -show power-menu -modi power-menu:rofi-power-menu") },
+	{ 0,                            XK_Menu,   spawn,          SHCMD("rofi -show power-menu -modi power-menu:rofi-power-menu") },
+	{ 0,                            XK_Print,  spawn,          {.v = scrotcmd } },
+	{ MODKEY,                       XK_b,      togglebar,      {0} },
+	{ MODKEY,                       XK_d,      incnmaster,     {.i = -1 } },
+	{ MODKEY,                       XK_f,      setlayout,      {.v = &layouts[1]} },
+	{ MODKEY,                       XK_h,      setmfact,       {.f = -0.05} },
+	{ MODKEY,                       XK_i,      incnmaster,     {.i = +1 } },
+	{ MODKEY,                       XK_l,      setmfact,       {.f = +0.05} },
+	{ MODKEY,                       XK_m,      setlayout,      {.v = &layouts[2]} },
+	{ MODKEY,                       XK_r,      spawn,          SHCMD("rofi -show run") },
+	{ MODKEY,                       XK_p,      spawn,          {.v = dmenucmd } },
+	{ MODKEY,                       XK_t,      setlayout,      {.v = &layouts[0]} },
+	{ MODKEY,                       XK_w,      spawn,          {.v = wwwcmd   } },
+	{ MODKEY,                       XK_Return, spawn,          {.v = termcmd  } },
+	{ MODKEY,                       XK_Right,  focusstack,     {.i = +1 } },
+	{ MODKEY,                       XK_Left,   focusstack,     {.i = -1 } },
+	{ MODKEY,                       XK_Tab,    view,           {0} },
+	{ MODKEY,                       XK_space,  setlayout,      {0} },
+	{ MODKEY,                       XK_agrave, view,           {.ui = ~0 } },
+	{ MODKEY,                       XK_comma,  focusmon,       {.i = -1 } },
+	{ MODKEY,                       XK_semicolon, focusmon,    {.i = +1 } },
+	{ MODKEY|ShiftMask,             XK_b, spawn,               {.v = bluetooth } },
+	{ MODKEY|ShiftMask,             XK_c,      killclient,     {0} },
+	{ MODKEY|ShiftMask,             XK_Right,  view_adjacent,  {.i = +1 } },
+    { MODKEY|ShiftMask,             XK_Left,   view_adjacent,  {.i = -1 } },
+	{ MODKEY|ShiftMask,             XK_Return, zoom,           {0} },
+	{ MODKEY|ShiftMask,             XK_space,  togglefloating, {0} },
+	{ MODKEY|ShiftMask,             XK_agrave, tag,            {.ui = ~0 } },
+	{ MODKEY|ShiftMask,             XK_comma,  tagmon,         {.i = -1 } },
+	{ MODKEY|ShiftMask,             XK_semicolon, tagmon,      {.i = +1 } },
+	{ MODKEY|ShiftMask,             XK_q,      quit,           {0} },
+	{ MODKEY|ShiftMask,             XK_x,      spawn,          SHCMD("rofi -show power-menu -modi power-menu:rofi-power-menu") },
+	TAGKEYS(                        XK_ampersand,              0)
+	TAGKEYS(                        XK_eacute,                 1)
+	TAGKEYS(                        XK_quotedbl,               2)
+	TAGKEYS(                        XK_apostrophe,             3)
+	TAGKEYS(                        XK_parenleft,              4)
+	TAGKEYS(                        XK_minus,                  5)
+	TAGKEYS(                        XK_egrave,                 6)
+	TAGKEYS(                        XK_underscore,             7)
+	TAGKEYS(                        XK_ccedilla,               8)
+	TAGKEYS(                        XK_agrave,                 9)
+};
+
+/* button definitions */
+/* click can be ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle, ClkClientWin, or ClkRootWin */
+static const Button buttons[] = {
+	/* click                event mask      button          function        argument */
+	{ ClkLtSymbol,          0,              Button1,        setlayout,      {0} },
+	{ ClkLtSymbol,          0,              Button3,        setlayout,      {.v = &layouts[2]} },
+	{ ClkWinTitle,          0,              Button2,        zoom,           {0} },
+	{ ClkStatusText,        0,              Button2,        spawn,          {.v = termcmd } },
+	{ ClkClientWin,         MODKEY,         Button1,        movemouse,      {0} },
+	{ ClkClientWin,         MODKEY,         Button2,        togglefloating, {0} },
+	{ ClkClientWin,         MODKEY,         Button3,        resizemouse,    {0} },
+	{ ClkTagBar,            0,              Button1,        view,           {0} },
+	{ ClkTagBar,            0,              Button3,        toggleview,     {0} },
+	{ ClkTagBar,            MODKEY,         Button1,        tag,            {0} },
+	{ ClkTagBar,            MODKEY,         Button3,        toggletag,      {0} },
+	{ ClkTagBar,            0,              Button4,        view_adjacent,  {.i = -1 } },
+	{ ClkTagBar,            0,              Button5,        view_adjacent,  {.i = +1 } },
+};
+
+
+
+
+ + + diff --git a/articles/2025/DWM-slstatus.html b/articles/2025/DWM-slstatus.html new file mode 100755 index 0000000..7ebef78 --- /dev/null +++ b/articles/2025/DWM-slstatus.html @@ -0,0 +1,46 @@ + + + + + + + DWM slstatus + + +
+

Bloggings

+ Back + +
+ +
+
+

DWM slstatus

+

2025-04-25

+

KISS

+

slstatus bar image

+

https://tools.suckless.org/slstatus/

+

After fiddling with dwm-blocks and dwm-bar, I decided that the only thing I want to see +in my status bar is battery info and date/time. The rest can be done with btop and alerts.

+

So I cloned the slstatus source code from suckless.org and added the following in slstatus/config.h

+
+static const struct arg args[] = {
+        /* function format          argument */
+        { battery_remaining, "%s",           "BAT0" },
+        { battery_state,     " %s",          "BAT0" },
+        { battery_perc,      " %s%%",        "BAT0" },
+        { datetime,          " | %s", "%a %d-%m %T" },
+};
+
+

As I'm using the Jetbrains mono nerdfont in DWM I added some icons in slstatus/components/battery.c

+

battery code

+
+
+ + + diff --git a/articles/2025/Daft-poems.html b/articles/2025/Daft-poems.html new file mode 100755 index 0000000..a4b1e93 --- /dev/null +++ b/articles/2025/Daft-poems.html @@ -0,0 +1,34 @@ + + + + + + + Poems + + +
+

Bloggings

+ Back + +
+ +
+
+

Poems

+

2025-08-22

+

Playing with words

+ +

Some poems

+ +

No pretention here, just learning. Let me know if I should keep my day job :-)

+
+
+ + + diff --git a/articles/2025/Debian-12-to-13-upgrade.html b/articles/2025/Debian-12-to-13-upgrade.html new file mode 100755 index 0000000..3fddef5 --- /dev/null +++ b/articles/2025/Debian-12-to-13-upgrade.html @@ -0,0 +1,32 @@ + + + + + + + Debian 12 to 13 upgrade + + +
+

Bloggings

+ Back + +
+ +
+
+

Debian 12 to 13 upgrade

+

2025-08-27

+

Server upgrade successful

+

I have upgraded my server to the latest Debian version, Trixie (13) without problem.

+

Mailcow have posted some simple steps to follow here

+
+
+ + + diff --git a/articles/2025/Detrumpez-vous.html b/articles/2025/Detrumpez-vous.html new file mode 100755 index 0000000..f967557 --- /dev/null +++ b/articles/2025/Detrumpez-vous.html @@ -0,0 +1,34 @@ + + + + + + + Détrumpez-vous + + +
+

Bloggings

+ Back + +
+ +
+
+

Detrumpify

+

2025-05-03

+

Technology against politics

+

An interesting addition for smartphone driven boycotts

+

Web site

+

Détrumpez-vous is an app for iPhone and Android that signals products that are of American origin.

+

I'm going to test this on my weekly shopping.

+
+
+ + + diff --git a/articles/2025/Dmenu-updates.html b/articles/2025/Dmenu-updates.html new file mode 100755 index 0000000..4eba85d --- /dev/null +++ b/articles/2025/Dmenu-updates.html @@ -0,0 +1,46 @@ + + + + + + + Dmenu updates + + +
+

Bloggings

+ Back + +
+ +
+
+

Dmenu updates

+

2025-06-16

+

more patches added to dmenu

+

I've updated my dmenu fork, it now has the following patches:

+ +
    +
  • xresources
  • +
  • center
  • +
  • border
  • +
  • xyz
  • +
  • fuzzymatch
  • +
  • fuzzyhighlight
  • +
  • mouse and motions support
  • +
+ +

I've also tweaked the patches to work with xresources.

+ +

my dmenu git repository

+ +
+
+ + + diff --git a/articles/2025/Dwmblocks.html b/articles/2025/Dwmblocks.html new file mode 100755 index 0000000..1c2d2d8 --- /dev/null +++ b/articles/2025/Dwmblocks.html @@ -0,0 +1,38 @@ + + + + + + + Dwmblocks + + +
+

Bloggings

+ Back + +
+ +
+
+

Dwmblocks

+

2025-06-02

+

eating previous thoughts

+

I wanted to keep my status bar simple, and slstatus fitted the bill

+

But I was missing an indication of the CPU temperature as I got tired of using conky +as it was only visible on empty DWM tags, and I didn't want to try writing a program in C to do it.

+

So a new status monitor was needed. I liked the idea of dwmblocks firing up bash scripts, +but I tried the original and several forks without luck. I finally settled on +dwmblocks-async, because it is more recent and ... it worked.

+

Dwmblocks-async

+

An example script

+
+
+ + + diff --git a/articles/2025/Enable-Arch-Extra-repository-in-Artix.html b/articles/2025/Enable-Arch-Extra-repository-in-Artix.html new file mode 100755 index 0000000..44fc86e --- /dev/null +++ b/articles/2025/Enable-Arch-Extra-repository-in-Artix.html @@ -0,0 +1,49 @@ + + + + + + + Enable Arch Extra repository in Artix + + +
+

Bloggings

+ Back + +
+ +
+
+

Enable Arch Extra repository in Artix

+

2025-06-20

+

Extending Artix package library

+

Artix Linux is mostly Arch Linux, minus systemd of course, and plus packages that +replace it's absence.

+

In order to access packages that may not be available via Artix, it's possible to add +the Arch Linux Extra Mirror.

+ +pacman -S artix-archlinux-support +wget https://archlinux.org/mirrorlist/all/ -O /etc/pacman.d/mirrorlist-arch + + +

Edit /etc/pacman.conf and activate the mirror.

+ +[extra] +Include = /etc/pacman.d/mirrorlist-arch + +

Populate the Arch Linux keyring and synchronize the repository

+ +pacman-key --populate archlinux +pacman -Syu + +
+
+ + + diff --git a/articles/2025/Eza.html b/articles/2025/Eza.html new file mode 100755 index 0000000..8709fb3 --- /dev/null +++ b/articles/2025/Eza.html @@ -0,0 +1,32 @@ + + + + + + + Eza + + +
+

Bloggings

+ Back + +
+ +
+
+

Eza

+

2025-03-24

+

Eza is a replacement for ls that uses symbols that are found in nerdfonts

+

+

I edited my .bashrc to alias ls to eza like so:

+alias ls="eza --icons=always" +alias ll="eza --icons=always -l" +alias la="eza --icons=always -la" + +

Pretty

+
+
+ + diff --git a/articles/2025/Fixing-my-dad's-Stanley-8-CFW-lock-plane.html b/articles/2025/Fixing-my-dad's-Stanley-8-CFW-lock-plane.html new file mode 100755 index 0000000..4e5f9e6 --- /dev/null +++ b/articles/2025/Fixing-my-dad's-Stanley-8-CFW-lock-plane.html @@ -0,0 +1,38 @@ + + + + + + + Fixing my dad's Stanley 8 CFW lock plane + + +
+

Bloggings

+ Back + +
+ +
+
+

Fixing my dad's Stanley 8 CFW lock plane

+

2025-08-27

+

Cast iron fixes

+

I inherited a small low angle block plane from my dad.

+

Nice looking with brass buttons, but upon inspection the blade wasn't straight. +I removed it and checked, it was square and just needed sharpening, but when I looked at the cast iron base, +the seat of the blade at throat level was thicker on one side, making it impossible to align.

+

The sides were also both slightly more that 90° which didn't help.

+

The easiest solution was to remove the adjustable front throat and reform the blade seat with a file.

+

It worked, and after flattening the foot of the plane and sides using sandpaper on a granit slab, + then sharpening the blade I now have a very nice block plane.

+
+
+ + + diff --git a/articles/2025/Flowers-for-the-bees.html b/articles/2025/Flowers-for-the-bees.html new file mode 100755 index 0000000..daf0327 --- /dev/null +++ b/articles/2025/Flowers-for-the-bees.html @@ -0,0 +1,35 @@ + + + + + + + Flowers for the bees + + +
+

Bloggings

+ Back + +
+ +
+
+

Flowers for the bees

+

2025-04-12

+

I sowed some flower seeds

+

I wouldn't mind having half the garden for wild flowers, although I'm not +sure what the maintenance would be like, there's a lot of native flora that is +a pain here in Brittany.

+

Anyway I sowed a small patch to see what happens, a trial run. I'm sure the bees +will appreciate.

+
+
+ + + diff --git a/articles/2025/France-rejects-backdoors.html b/articles/2025/France-rejects-backdoors.html new file mode 100755 index 0000000..36488fd --- /dev/null +++ b/articles/2025/France-rejects-backdoors.html @@ -0,0 +1,26 @@ + + + + + + + France rejects backdoors + + +
+

Bloggings

+ Back + +
+ +
+
+

France rejects backdoors

+

2025-03-22

+

It's always a warm fuzzy feeling when we do something smart

+

eff.org

+
+
+ + diff --git a/articles/2025/Ghostty.html b/articles/2025/Ghostty.html new file mode 100755 index 0000000..cf55cc9 --- /dev/null +++ b/articles/2025/Ghostty.html @@ -0,0 +1,35 @@ + + + + + + + Ghostty + + +
+

Bloggings

+ Back + +
+ +
+
+

Ghostty

+

2025-03-27

+

I've been playing with Ghostty, a new terminal emulator that seems very promising

+

+

Here's Newsboat opening an article with Chawan inside Ghostty:

+

news article in chawan

+

Tips

+

When I ssh into a vps I found some keys where not working in mc (midnight commander). The solution was to edit ~/.ssh/config and add:

+

Host * + setenv Term=xterm-256color

+

Ressources

+

Ghostty

+

Awasome Ghostty

+
+
+ + diff --git a/articles/2025/Git-repository.html b/articles/2025/Git-repository.html new file mode 100755 index 0000000..ccb829a --- /dev/null +++ b/articles/2025/Git-repository.html @@ -0,0 +1,32 @@ + + + + + + + Git repository + + +
+

Bloggings

+ Back + +
+ +
+
+

Git repository

+

2025-05-07

+

I've added a git repository

+

Nothing much there yet, but I've got cgit running...

+

https://git.wittamore.fr

+
+
+ + + diff --git a/articles/2025/Gopher-it.html b/articles/2025/Gopher-it.html new file mode 100755 index 0000000..04ea5cb --- /dev/null +++ b/articles/2025/Gopher-it.html @@ -0,0 +1,34 @@ + + + + + + + Gopher it + + +
+

Bloggings

+ Back + +
+ +
+
+

Gopher it

+

2025-09-14

+

My Gopher hole

+

After all I've said against Gopher and Gemini, I've started my own Gopher hole.

+

Just proves humans are fickle and you shouldn't listen to my rants and I'm an incorrigible fiddler.

+

You'll need a gopher browser such as Lagrange (GUI) or +Lynx (TUI) for this link: gopher://nagatha.fr

+
+
+
+ +

 

+ +

§

+
+ + diff --git a/articles/2025/Greycoders-list-of-Text-only-minimalist-web-sites.html b/articles/2025/Greycoders-list-of-Text-only-minimalist-web-sites.html new file mode 100755 index 0000000..70a68c7 --- /dev/null +++ b/articles/2025/Greycoders-list-of-Text-only-minimalist-web-sites.html @@ -0,0 +1,26 @@ + + + + + + + Greycoders list of Text only minimalist web sites + + +
+

Bloggings

+
Back + +
+ +
+
+

Greycoders list of minimalist web sites 2025

+

2025-04-03

+

Avoiding web pages filled with ads, videos, and bandwidth-heavy content

+

https://greycoder.com

+
+
+ + diff --git a/articles/2025/Hard-disk-fail.html b/articles/2025/Hard-disk-fail.html new file mode 100755 index 0000000..7e341c2 --- /dev/null +++ b/articles/2025/Hard-disk-fail.html @@ -0,0 +1,28 @@ + + + + + + + Hard disk fai + + +
+

Bloggings

+ Back + +
+ +
+
+

Hard disk fail

+

2025-03-19

+

The hard disk on my X220 failed

+

and my backup machine was in an unstable state for a while and I hadn't fixed it. Examination of my collection of USB keys found no linux installer, so I was stumped through lazyness.

+

But at the bottom of a pencil holder I found another usb key with an old copy of Linux Mint which enabled me to download Arch and recover.

+

Luckily all my inportant files were saved, and I use Bitwarden too, so with a new SSD I was quickly back up and running.

+
+
+ + diff --git a/articles/2025/Headless-Arch-Install.html b/articles/2025/Headless-Arch-Install.html new file mode 100755 index 0000000..8852691 --- /dev/null +++ b/articles/2025/Headless-Arch-Install.html @@ -0,0 +1,40 @@ + + + + + + + Headless Arch Install + + +
+

Bloggings

+ Back + +
+ +
+
+

Headless Arch Install

+

2025-03-21

+

I have a micro pc I'd like to use as a web server

+

It doesn't have a screen so I'll need to do a headless installation

+

I found some simple instructions here: https://tarneo.fr/posts/arch_headless/

+
+sudo pacman -S archiso +mkdir arch-headless && cd arch-headless +cp -r /usr/share/archiso/configs/releng/ . +mkdir releng/airootfs/root/.ssh/ +cp ~/.ssh/id_rsa.pub releng/airootfs/root/.ssh/authorized_keys +sudo mkarchiso -v -o ./out ./releng + +Insert a usb key, mine is in /dev/sdc, and create a bootable usb drive +dd if=./out/archlinux-2025.03.21-x86_64.iso of=/dev/sdc bs=4M; sync +
+

Find the IP address of the machine, and ssh to it. You can then run +archinstall for example.

+
+
+ + diff --git a/articles/2025/Html-center-element.html b/articles/2025/Html-center-element.html new file mode 100755 index 0000000..50cdf83 --- /dev/null +++ b/articles/2025/Html-center-element.html @@ -0,0 +1,42 @@ + + + + + + + Html center element + + +
+

Bloggings

+ Back + +
+ +
+
+

Html center element

+

2025-04-10

+

Making things simpler still

+

The center element in html is depreciated, and validation of the page shows warnings. +Instead the use of the css style text-align:center is precognised.

+

But this is ignored in terminal text browsers, and I prefer some sort of consistency, so I've removed center elements and text-align css. +The page itself is centered setting margins. Chawan gives the best rendering on the terminal, here I'm using Ghostty.

+

Librefox

+

image of blog in Librefox

+

Chawan (terminal)

+

image of blog in Chawan

+

W3M (terminal)

+

image of blog in W3M

+

Links (terminal)

+

image of blog in Links

+
+
+ + + diff --git a/articles/2025/Install-the-Chawan-text-browser-on-Artix.html b/articles/2025/Install-the-Chawan-text-browser-on-Artix.html new file mode 100755 index 0000000..0136bb9 --- /dev/null +++ b/articles/2025/Install-the-Chawan-text-browser-on-Artix.html @@ -0,0 +1,42 @@ + + + + + + + Install the Chawan text browser on Artix + + +
+

Bloggings

+ Back + +
+ +
+
+

Install the Chawan text browser on Artix

+

2025-06-15

+

the Arch package doesn't compile, here's the solution

+

The package chawan-git in the AUR doesn't compile as the included NIM version is too old

+

Here's how I installed it in Artix Linux

+

Install the NIM compiler from https://nim-lang.org/install.html

+

The default dependancies were already installed on my hard disk: (OpenSSL, libssh2, brotli)

+cd ~/src +git clone https://git.sr.ht/~bptato/chawan && cd chawan +make +sudo make install +cha -V +

And it's done. I use mostly use chawan as newsboat's browser, it's really good.

+

Thanks to Kachick

+

Previous article: rdrview or chawan

+
+
+ + + diff --git a/articles/2025/Jazz-radio-recording-from-1976.html b/articles/2025/Jazz-radio-recording-from-1976.html new file mode 100755 index 0000000..56f8517 --- /dev/null +++ b/articles/2025/Jazz-radio-recording-from-1976.html @@ -0,0 +1,47 @@ + + + + + + + Jazz radio recording from 1976 + + +
+

Bloggings

+ Back +
+ +
+
+

Jazz radio recording from 1976

+

2025-05-24

+

KBCA Jazz Radio - Los Angeles
Recorded February 1976

+

An enjoyable 3 hour radio time machine

+
+ +
+
+
+ + + diff --git a/articles/2025/Koreader-for-e-ink-devices.html b/articles/2025/Koreader-for-e-ink-devices.html new file mode 100755 index 0000000..107c886 --- /dev/null +++ b/articles/2025/Koreader-for-e-ink-devices.html @@ -0,0 +1,32 @@ + + + + + + + Koreader for e-ink devices + + +
+

Bloggings

+ Back + +
+ +
+
+

Koreader for e-ink devices

+

2025-03-24

+

I installed Koreader on my Kobo Clara Colour last night

+

Like everything, there's a learning curve due to the multiplicity of options. The first thing I did was copy the Bookerly font to the Koreader fonts folder on the Kobo, as I find the default Noto fonts too ugly for reading.

+

The second thing I did was remove the Koreader installation log file and +logo which stuck out like a sore thumb in the file manager

+

The third thing I have to do is read the user guide...

+

Anyhow reading is a pleasure once I fixed the font, font size, and line spacing

+

Koreader +

One-Click Install Packages for KOReader & Plato

+
+
+ + diff --git a/articles/2025/Ladybird-web-browser.html b/articles/2025/Ladybird-web-browser.html new file mode 100755 index 0000000..0ce8563 --- /dev/null +++ b/articles/2025/Ladybird-web-browser.html @@ -0,0 +1,35 @@ + + + + + + + Ladybird web browser + + +
+

Bloggings

+ Back + +
+ +
+
+

Ladybird web browser

+

2025-06-26

+

L'éspoir fait vivre

+

The Ladybird web browser is truly something different, and a hope for the future.

+

It has no connection to Google, no rehash of older web engines, with no corporate influence looming over it.

+

Written transparently, W3C standards compliant, it will be available for public beta in 2026, with release is planned for 2028

+

There's an aphid eating light at the end of the tunnel.

+

https://ladybird.org/

+
+
+ + + diff --git a/articles/2025/Les-Oiseaux-des-Jardins.html b/articles/2025/Les-Oiseaux-des-Jardins.html new file mode 100755 index 0000000..a705631 --- /dev/null +++ b/articles/2025/Les-Oiseaux-des-Jardins.html @@ -0,0 +1,32 @@ + + + + + + + Les Oiseaux des Jardins + + +
+

Bloggings

+ Back + +
+ +
+
+

Les Oiseaux des Jardins

+

2025-04-04

+

Identify garden birds in Brittany

+

My bird feeder only has Blue Tits, Coal Tits, and sparrows - but you never know.

+

poster-les-oiseaux-des-jardins

+

Bretagne Vivant - comptage des oiseaux

+

Bretagne Vivante has been Brittany's leading association for the protection of nature and biodiversity since 1959. +Recognized as a non-profit organization, it works on a daily basis to improve knowledge and preservation of the region's natural heritage. +It also manages a network of protected sites and nature reserves throughout Brittany and Loire-Atlantique. +Passing on and sharing its passions is also at the heart of Bretagne Vivante's missions.

+
+
+ + diff --git a/articles/2025/Linux-System-Performance-Tuning.html b/articles/2025/Linux-System-Performance-Tuning.html new file mode 100755 index 0000000..7dcd6a4 --- /dev/null +++ b/articles/2025/Linux-System-Performance-Tuning.html @@ -0,0 +1,27 @@ + + + + + + + Linux System Performance Tuning + + +
+

Bloggings

+ Back + +
+ +
+
+

Linux System Performance Tuning

+

2025-03-26

+

Linux System Performance Tuning: Optimizing CPU, Memory, and Disk

+ +

Linuxjournal

+
+
+ + diff --git a/articles/2025/Linux-power-consumption-control.html b/articles/2025/Linux-power-consumption-control.html new file mode 100755 index 0000000..31748e0 --- /dev/null +++ b/articles/2025/Linux-power-consumption-control.html @@ -0,0 +1,35 @@ + + + + + + + Linux power consumption control + + +
+

Bloggings

+ Back + +
+ +
+
+

Linux power consumption control

+

2025-03-23

+

TLP is a feature-rich command line utility for Linux

+

saving laptop battery power without the need to delve deeper into technical details.

+

TLP’s default settings are already optimized for battery life, so you may install it and just sit back and relax. Nevertheless, TLP is completely customizable to get even more power savings or meet your exact requirements.

+sudo pacman -S tlp tlpui cpupower +sudo systemctl enable tlp.service +

I enabled the panel cpufreq plugin on my Thinkpad X220 and saw that AC/BAT power changes +were handled correctly

+

+

TLP Web site

+

Arch Linux TLP page

+ +
+
+ + diff --git a/articles/2025/Lynx,-Gopher,-and-video-files.html b/articles/2025/Lynx,-Gopher,-and-video-files.html new file mode 100755 index 0000000..6288414 --- /dev/null +++ b/articles/2025/Lynx,-Gopher,-and-video-files.html @@ -0,0 +1,102 @@ + + + + + + + Lynx, Gopher, and video files + + +
+

Bloggings

+ Back + +
+ +
+
+

Lynx, Gopher, and video files

+

2025-09-21

+

Getting Lynx to open video files

+ +

I've been using Lynx as a Gopher browser on Artix Linux, +and I like it a lot as it renders cleanly, has logical +keybindings, and enables following html links.

+

But...

+

Trying to open video files with an external application +wasn't working. Luckily on one test I saw an error +concerning "mpeg_play".

+ +

The only mention of this file is as a VIEWER entry in +lynx.cfg. But changing it to something else has no effect +at all.

+ +

Somewhere in lynx, mpeg_play is hardcoded. 🤔. Checking the Lynx source code, I found it in HTInit.c

+

Why short circuit the config file this way? Anyway, I wrote a bash script named mpeg_play:

+ +

+#!/usr/bin/env bash
+/usr/bin/mpv --title="Mpv" >/dev/null 2>&1 $1 +

+ +

chmod +x mpeg_play and put it in my PATH

+ +

and it works!
+Lynx can now play media files in Gopher holes. +(Note the --title parameter is to get it to play nice with dwm).

+ +

The question remains: why is this hard coded?

+ +

As for mpeg_play, it appears in Linux mailcap +entries in the 90's and seems to have been bundled +in ximagetools.

+ +

Mpeg_play appears to date back a long way as a UNIX application:

+ +

http://www.geom.uiuc.edu/software/mpeg_play

+ +

Update 2025-09-23

+ +

A little tweak made this script capable of opening playlists + +

----------

+

+#!/usr/bin/env bash
+# necessary for Lynx browser video files
+# chmod +x and place in your PATH
+
+# mpv can sometimes take a few seconds to open when streaming
+# so I use notification that it's happening
+
+dunstify -t 2000 -a "Video" -u low -i "media-playback-start-symbolic"\
+  -h string:x-dunst-stack-tag:"Media" "MPV Launching" &
+
+# detect if playlist
+# Lynx uses an anonymous file in /tmp, so we can only
+# detect a file type by it's contents, not it's extension
+
+if [ "$(grep -c "EXT3MU" "$1")" -eq 1 ]; then
+  # this is a playlist
+  # I use dwm, the mpv options used here make it compatible
+  mpv --geometry=1068x600 --ontop --idle=yes --force-window=yes\
+    --playlist="$1" >/dev/null 2>&1 &
+else
+  mpv --geometry=1068x600 --ontop --idle=yes --force-window=yes\
+    "$1" >/dev/null 2>&1 &
+fi
+
+exit 0
+

+

----------

+

The latest version can be found at git.wittamore.fr

+ +
+
+ + + diff --git a/articles/2025/Mailcow-Docker-too-many-requests.html b/articles/2025/Mailcow-Docker-too-many-requests.html new file mode 100755 index 0000000..619879a --- /dev/null +++ b/articles/2025/Mailcow-Docker-too-many-requests.html @@ -0,0 +1,38 @@ + + + + + + + Mailcow Docker too many requests + + +
+

Bloggings

+ Back + +
+ +
+
+

Mailcow Docker too many requests

+

2025-04-29

+

Dcker changes

+

While updating Mailcow this morning Docker gave me a "too many requests" error.

+

It seems that Docker now limits the IPv6’s subnet instead of the IPv6 address itself, which +could mean your pull limit is shared.

+

To fix this you can create a personal account at docker.com, this will allow 200 image pulls per 6 hours. +My Mailcow installation does 7 pulls for an update.

+

Docker account creation

+

Once the account created, copy the personal access token, go back to your server and enter docker login -u (login)

+

Mailcow will now update without error.

+
+
+ + + diff --git a/articles/2025/Mailcow-with-Nginx-reverse-proxy-(updated).html b/articles/2025/Mailcow-with-Nginx-reverse-proxy-(updated).html new file mode 100755 index 0000000..efbdc99 --- /dev/null +++ b/articles/2025/Mailcow-with-Nginx-reverse-proxy-(updated).html @@ -0,0 +1,31 @@ + + + + + + + Mailcow with Nginx reverse proxy (updated) + + +
+

Bloggings

+ Back + +
+ +
+
+

Mailcow with Nginx reverse proxy (updated)

+

2025-09-11

+

Improved instructions with DANE and MTS-STA info

+

-> Link to the updated post

+
+
+ + + diff --git a/articles/2025/Mailcow-with-Nginx-reverse-proxy.html b/articles/2025/Mailcow-with-Nginx-reverse-proxy.html new file mode 100755 index 0000000..657b214 --- /dev/null +++ b/articles/2025/Mailcow-with-Nginx-reverse-proxy.html @@ -0,0 +1,207 @@ + + + + + + + Mailcow with Nginx reverse proxy + + +
+

Bloggings

+ Back + +
+ +
+
+

Mailcow with Nginx reverse proxy (updated)

+

2025-04-19

+

Using other web services with Mailcow

+

This is roughly what I did to have an Nginx web server on the same machine as dockerized +Mailcow

+

Note: mail.xxxxx.com is your mail server (MX), yourdomain.com is whatever domain you wish to use.

+
    +
  1. Change the default http and https ports (for example 8480 and 8443)
    +and set SKIP_LETS_ENCRYPT=y in /opt/mailcow-dockerized/mailcow.conf
  2. +
  3. Restart Mailcow by executing
    +cd /opt/mailcow-dockerized;docker compose restart
  4. +
  5. Make sure the DNS entry mail.xxxxx.com points to the server
  6. +
  7. mkdir -p /var/www/html/letsencrypt/.well-known/acme-challenge
  8. +
  9. Create /etc/nginx/letsencrypt_path. This can be re-used in your other domains. +
    # url for letsencrypt
    +location ^~ /.well-known/acme-challenge/ {
    +  allow all;
    +  default_type "text/plain";
    +  # Path can be used for cert-validation on all domains
    +  root /var/www/html/letsencrypt;
    +  break;
    +}
    +
    +
  10. +
  11. Stop the nginx server systemctl stop nginx
  12. +
  13. Run certbot certonly -d mail.xxxxx.com (select 2 standalone)
  14. +
  15. Create /etc/nginx/sites-available/mail.xxxxx.com
  16. +
    server {
    +  listen 443 ssl;
    +  listen [::]:443 ssl;
    +  http2 on;
    +
    +  server_name mail.xxxxx.com;
    +
    +  charset UTF-8;  
    +  access_log /var/log/nginx/access.mail.xxxxx.com;  
    +  error_log /var/log/nginx/error.mail.xxxxx.com;  
    +  include snippets/error_pages.conf;
    +  ssl_certificate /etc/letsencrypt/live/mail.xxxxx.com/fullchain.pem; 
    +  ssl_certificate_key /etc/letsencrypt/live/mail.xxxxx.com/privkey.pem; 
    +  include /etc/letsencrypt/options-ssl-nginx.conf;
    +  ssl_dhparam /etc/letsencrypt/ssl-dhparams-2048.pem;  
    +
    +  include /etc/nginx/letsencrypt_path;
    +
    +  location / {
    +    proxy_pass      http://127.0.0.1:8480;
    +    proxy_buffering off;
    +    include /etc/nginx/proxy_params;
    +    
    +  }
    +
    +}
    +
    +server {
    +
    +    listen 80;
    +    listen [::]:80;
    +    server_name mail.xxxxx.com;
    +    return 301 https://mail.xxxxx.com;
    +}
    +
    +
  17. ln -s /etc/nginx/sites-available/mail.xxxxx.com
    /etc/nginx/sites-enabled/mail.xxxxx.com
  18. +
  19. Test with nginx -t, if all is well run sudo systemctl start nginx
  20. +
  21. Pointing your browser to https://mail.xxxxx.com should show you the Mailcow login page.
  22. +
+

Problem

+

The problem is that Mailcow can no longer use port 80 to update it's ssl certificates that +are used by postfix and dovecot. Instead certbot puts them in /etc/letsencrypt/live. +To fix this the following script runs from crontab daily.

+
#!/usr/bin/env bash
+
+# we are running behind an nginx proxy, where certbot is run  by systemd,
+# so the mail.xxxxx.com certificates are updated, but not copied to the mailcow folder
+# in case the certificate isn't renewed automatically, run this:
+# sudo certbot -n certonly --webroot -w /var/www/html/letsencrypt -d mail.xxxxx.com
+
+t1="/etc/letsencrypt/live/mail.xxxxx.com/fullchain.pem"
+t2="/opt/mailcow-dockerized/data/assets/ssl/mail.xxxxx.com/cert.pem" 
+
+# test if certificate has been updated
+differ=$(cmp -b $t1 $t2 | grep -c "differ")
+
+[[ "$differ" = "0" ]] && exit 0
+
+# these files are required in /data/assets/ssh/mail.xxxxx.com
+cp /etc/letsencrypt/live/mail.xxxxx.com/fullchain.pem /opt/mailcow-dockerized/data/assets/ssl/mail.xxxxx.com/cert.pem
+cp /etc/letsencrypt/live/mail.xxxxx.com/privkey.pem /opt/mailcow-dockerized/data/assets/ssl/mail.xxxxx.com/key.pem
+
+# these files are required in /data/assets/ssl/ (turns out sending failed otherwise)
+cp /etc/letsencrypt/live/mail.xxxxx.com/fullchain.pem /opt/mailcow-dockerized/data/assets/ssl/cert.pem
+cp /etc/letsencrypt/live/mail.xxxxx.com/privkey.pem /opt/mailcow-dockerized/data/assets/ssl/key.pem
+
+# update postfix & docker
+docker exec $(/usr/bin/docker ps -qaf name=postfix-mailcow) postfix reload
+docker exec $(/usr/bin/docker ps -qaf name=dovecot-mailcow) dovecot reload
+
+

Inspiration: Felix Moesbauer

+

Don't forget to check and correct if necessary your DANE and MTA-STS records if you use them

+

DANE

+

On your mail server:

+
apt install hash-slinger -y
+

Create a TLSA record:

+
tlsa --create --selector 1 -p 25 --certificate /etc/letsencrypt/live/mail.xxxxx.com/fullchain.pem mail.xxxxx.com
+
+

The last line or the result will be something like:

+
_25._tcp.mail.xxxxx.com. IN TLSA 3 1 1 443ac7c5c70fbfbc...
+

Enter this TLSA line in your mail server's DNS record.

+

Do the same for the following ports:

+
_110._tcp.mail.xxxxx.com
+_143._tcp.mail.xxxxx.com
+_465._tcp.mail.xxxxx.com
+_587._tcp.mail.xxxxx.com
+_993._tcp.mail.xxxxx.com
+_995._tcp.mail.xxxxx.com
+
+

You can check your DANE records at huque.com

+

dane success

+

MTA-STS

+

This is a fallback to DANE, you can run both.

+

Note: https access to mta-sts.yourdomain.com is obligatory

+
    +
  1. Create /var/www/html/mta-sts/.well-known/mta-sts.txt with the following content:
    +
    version: STSv1
    +mode: enforce
    +max_age: 172800
    +mx: mail.xxxxx.com
    +
    +
  2. +
  3. Create the DNS record _mta-sts.mail.xxxxx.com as TXT with "v=STSv1; id=INSERT AN ID, EX. 202511101644"
  4. +
  5. Create the DNS record _mta-sts.yourdomain.com as CNAME pointing to _mta-sts.mail.xxxxx.com (your mail server)
  6. +
  7. Create the DNS record mta-sts.yourdomain.com pointing to the ip of the web server
  8. +
  9. You may need to wait for the DNS records to propagate
  10. +
  11. Stop Nginx with systemctl stop nginx
  12. +
  13. Create a letsencrypt certificate with certbot certonly -d mta-sts.yourdomain.com (select 2 standalone)
  14. +
  15. Create mta-sts.yourdomain.com in Nginx:
  16. +
  17. /etc/sites-available/mta-sts.yourdomain.com
    +
    server {
    +   listen 443 ssl;
    +   listen [::]:443 ssl;
    +   http2 on;
    +
    +   charset UTF-8;  
    +   access_log /var/log/nginx/access.mts-sta.yourdomain.com;  
    +   error_log /var/log/nginx/error.mts-sta.yourdomain.com;  
    +   include snippets/error_pages.conf;     
    +   ssl_certificate /etc/letsencrypt/live/mta-sts.yourdomain.com/fullchain.pem;
    +   ssl_certificate_key /etc/letsencrypt/live/mta-sts.yourdomain.com/privkey.pem;
    +   include /etc/letsencrypt/options-ssl-nginx.conf;
    +   ssl_dhparam /etc/letsencrypt/ssl-dhparams-2048.pem;
    +
    +   include /etc/nginx/letsencrypt_path;
    +  
    +   root /var/www/html/mta-sts;
    +   index index.html index.htm;
    +
    +   server_name mta-sts.yourdomain.com;
    +
    +   location / {
    +      try_files $uri $uri/ =404;
    +   }
    +}
    +server {
    +    listen 80;
    +    listen [::]:80;
    +    server_name mta-sts.yourdomain.com;
    +    return 301 https://mta-sts.yourdomain.com;
    +}
    +
    +
  18. +
  19. ln -s /etc/nginx/sites-available/mta-sts.yourdomain.com
    /etc/nginx/sites-enabled/mta-sts.yourdomain.com
  20. +
  21. Test with nginx -t and if all goes well start nginx systemctl start nginx
  22. +
+

You can check your domain's MTA-STS at mxtoolbox.com

+

 

+

All this worked for me :-) I hope it does for you!

+ +
+
+ + + + + + diff --git a/articles/2025/Menu-editor-for-XFCE4.html b/articles/2025/Menu-editor-for-XFCE4.html new file mode 100755 index 0000000..8f2bedb --- /dev/null +++ b/articles/2025/Menu-editor-for-XFCE4.html @@ -0,0 +1,26 @@ + + + + + + + Menu editor for XFCE4 + + +
+

Bloggings

+ Back + +
+ +
+
+

Menu editor for XFCE4

+

2025-03-23

+

Alacarte is a gui menu editor for Gnome and XFCE

+yay -S alacarte-xfce +
+
+ + diff --git a/articles/2025/Microphone-distortion-on-Thinkpad-X220.html b/articles/2025/Microphone-distortion-on-Thinkpad-X220.html new file mode 100755 index 0000000..8b40778 --- /dev/null +++ b/articles/2025/Microphone-distortion-on-Thinkpad-X220.html @@ -0,0 +1,38 @@ + + + + + + + Microphone distortion on Thinkpad X220 + + +
+

Bloggings

+ Back + +
+ +
+
+

Microphone distortion on Thinkpad X220

+

2025-04-27

+

Trying to fix a bad microphone

+

The X220 has a notoriously bad internal microphone. It can be somewhat improved by making the input mono with module-remap-source

+

/etc/modules-load.d/microphone.conf

+
+# make micro mono
+module-remap-source master=alsa_input.pci-0000_00_1b.0.analog-stereo channels=1 master_channel_map=front-left channel_map=mono
+
+

Replace master= with your device, and master_channel_map= with the device's channel

+

It's a bit better, although an external usb microphone is the real solution.

+
+
+ + + diff --git a/articles/2025/Modified-dwm-bar-battery-script.html b/articles/2025/Modified-dwm-bar-battery-script.html new file mode 100755 index 0000000..b837c73 --- /dev/null +++ b/articles/2025/Modified-dwm-bar-battery-script.html @@ -0,0 +1,80 @@ + + + + + + + Modified dwm-bar battery script + + +
+

Bloggings

+ Back + +
+ +
+
+

Modified dwm-bar battery script

+

2025-04-23

+

adding battery charging nerdfont icons

+

This is a modification of the default dwm-battery script

+

+
+#!/bin/sh
+
+# A dwm_bar function to read the battery level and status
+# GNU GPLv3
+
+dwm_battery () {
+    # Change BAT1 to whatever your battery is identified as. Typically BAT0 or BAT1
+    CHARGE=$(cat /sys/class/power_supply/BAT0/capacity)
+    STATUS=$(cat /sys/class/power_supply/BAT0/status)
+
+    #printf "%s" "$SEP1"
+    printf " %s"
+    if [ "$IDENTIFIER" = "unicode" ]; then
+        if [ "$STATUS" = "Charging" ]; then
+           if   [ "${CHARGE}" -lt 10 ];  then ICON="󱃍󱐋"
+           elif [ "${CHARGE}" -lt 20 ];  then ICON="󰁺󱐋"
+           elif [ "${CHARGE}" -lt 30 ];  then ICON="󰁻󱐋"
+           elif [ "${CHARGE}" -lt 40 ];  then ICON="󰁼󱐋"
+           elif [ "${CHARGE}" -lt 50 ];  then ICON="󰁽󱐋"
+           elif [ "${CHARGE}" -lt 60 ];  then ICON="󰁾󱐋"
+           elif [ "${CHARGE}" -lt 70 ];  then ICON="󰁿󱐋"
+           elif [ "${CHARGE}" -lt 80 ];  then ICON="󰂀󱐋"
+           elif [ "${CHARGE}" -lt 90 ];  then ICON="󰂂󱐋"
+           elif [ "${CHARGE}" -lt 100 ]; then ICON="󰁹󱐋"
+           fi
+        else
+           if   [ "${CHARGE}" -lt 10 ];  then ICON="󱃍"
+           elif [ "${CHARGE}" -lt 20 ];  then ICON="󰁺"
+           elif [ "${CHARGE}" -lt 30 ];  then ICON="󰁻"
+           elif [ "${CHARGE}" -lt 40 ];  then ICON="󰁼"
+           elif [ "${CHARGE}" -lt 50 ];  then ICON="󰁽"
+           elif [ "${CHARGE}" -lt 60 ];  then ICON="󰁾"
+           elif [ "${CHARGE}" -lt 70 ];  then ICON="󰁿"
+           elif [ "${CHARGE}" -lt 80 ];  then ICON="󰂀"
+           elif [ "${CHARGE}" -lt 90 ];  then ICON="󰂂"
+           elif [ "${CHARGE}" -lt 100 ]; then ICON="󰁹"
+           fi
+        fi
+        printf "%s %s%% " "$ICON" "$CHARGE"
+    else
+        printf "BAT %s%% %s" "$CHARGE" "$STATUS"
+    fi
+    #printf "%s\n" "$SEP2"
+}
+
+dwm_battery
+
+
+
+ + + diff --git a/articles/2025/My-gopher-site.html b/articles/2025/My-gopher-site.html new file mode 100755 index 0000000..c372098 --- /dev/null +++ b/articles/2025/My-gopher-site.html @@ -0,0 +1,35 @@ + + + + + + + My gopher site + + +
+

Bloggings

+ Back + +
+ +
+
+

My gopher site

+

2025-09-26

+

moving to saner tech

+

my gopher site pic

+

If you haven't noticed, I'm blogging (phlogging) on my gopher site now.

+

If you have a termnal, install lynx and point it to gopher://wittamore.fr

+

If you don't, point your web browser to gopherproxy, +the results are not as good but you'll get the gist.

+
+
+ + + diff --git a/articles/2025/Naked-DWM.html b/articles/2025/Naked-DWM.html new file mode 100755 index 0000000..bb64c73 --- /dev/null +++ b/articles/2025/Naked-DWM.html @@ -0,0 +1,101 @@ + + + + + + + Naked DWM + + +
+

Bloggings

+ Back + +
+ +
+
+

Naked DWM

+

2025-05-01

+

Zero dwm patches...

+

Inspiration from Bread on Penguins and BugsWriter

+

+

I've been messing with DWM patches but I went back to naked DWM. +The only caveat is a couple of lines of code in dwm/config.h to integrate Pywal16, +that allows me to have a colour theme that matches whatever wallpaper I'm using.

+

It's possible to patch DWM with the Xresources patch but patches are version dependant and can be a pain. +I prefer simple and maintainable.

+

Install pywal16

+
+yay -S python-pywal16
+
+

Modify dwm/config.h

+

In dwm/config.h I removed the default colour settings:

+
+static const char col_gray1[]       = "#222222";
+static const char col_gray2[]       = "#444444";
+static const char col_gray3[]       = "#bbbbbb";
+static const char col_gray4[]       = "#eeeeee";
+static const char col_cyan[]        = "#005577";
+static const char *colors[][3]      = {
+	/*               fg         bg         border   */
+	[SchemeNorm] = { col_gray3, col_gray1, col_gray2 },
+	[SchemeSel]  = { col_gray4, col_cyan,  col_cyan  },
+};
+
+

and replaced them with this line:

+
+/* Pywal16 */ 
+#include "/home/philip/.cache/wal/colors-wal-dwm.h"
+
+

xinitrc

+

In my .xinitrc I have wal -Rq which recalls the last wallpaper setting.

+

Chooser script

+

This script chooses the wallpaper then recompiles dwm and dmenu.

+
+#!/usr/bin/env bash
+# Choose a wallpaper 
+# requires nsxiv, pywal16
+# ~/.local/bin is in PATH
+
+FOLDER=~/Pictures/wallpaper
+# thumbnail display of images
+CHOICE=$(nsxiv -otb $FOLDER/*) 
+
+wal -i $CHOICE
+
+# Post process
+
+# DWM
+cd ~/.local/src/dwm
+make clean ; make
+rm -f ~/.local/bin/dwm
+cp dwm ~/.local/bin
+
+# DWMMENU
+rm -f ~/.local/dmenu*
+cd ~/.local/src/dmenu
+make clean ; make
+cp {dmenu,dmenu_run,dmenu_path} ~/.local/bin
+
+
+
+ + + + + + + + + + + + + + diff --git a/articles/2025/Neomutt-as-default-in-XFCE4.html b/articles/2025/Neomutt-as-default-in-XFCE4.html new file mode 100755 index 0000000..4c7259b --- /dev/null +++ b/articles/2025/Neomutt-as-default-in-XFCE4.html @@ -0,0 +1,47 @@ + + + + + + + Neomutt as default in XFCE4 + + +
+

Bloggings

+ Back + +
+ +
+
+

Neomutt as default in XFCE4

+

2025-03-21

+

I've always loved Mutt and now Neomutt on XFCE4

+

Note: I use RBW to access my Bitwarden password vault on the command line or in +scripts, so I need to make sure it is unlocked before running Neomutt, hence +the Exec line in the desktop entry.

+

RBW sudo pacman -S rbw

+ +

Create desktop entry

+ +
~/.local/share/applications/neomutt.desktop
+ +[Desktop Entry] +Categories=Office;Network;Email; +Comment=Simple text-based Mail User Agent +Exec=xfce4-terminal -x bash -c '"rbw unblock" ; neomutt' %u +Icon=neomutt +Name=neomutt +MimeType=x-scheme-handler/mailto; +NoDisplay=false +Terminal=false +Type=Application + +

Add xdg mime type

+
xdg-mime default neomutt.desktop x-scheme-handler/mailto
+
+
+ + diff --git a/articles/2025/New-builds-of-dwm-and-st.html b/articles/2025/New-builds-of-dwm-and-st.html new file mode 100755 index 0000000..1601b4c --- /dev/null +++ b/articles/2025/New-builds-of-dwm-and-st.html @@ -0,0 +1,35 @@ + + + + + + + New builds of dwm and st + + +
+

Bloggings

+ Back + +
+ +
+
+

New builds of dwm, dmenu, and st

+

2025-05-21

+

new builds with patches

+

I've updated my builds of dwm, dmenu, and st, available on my git server

+

Although I've compiled St, Kitty and Ghostty are my favorite terminal emulators. St has many patches, but not all of them work together. St is meant to be a bare bones -suckless- terminal emulator.

+

The DWM build has the xrdb, rotatestack, adjacenttag, center, statuscmd and swallow patches.

+

I was using slstatus with time and battery indications, but dwmblocks-async adds bit of flair.

+

Dmenu has the xresources and center patches.

+
+
+ + + diff --git a/articles/2025/Newsboat-and-gopher-RSS-feeds.html b/articles/2025/Newsboat-and-gopher-RSS-feeds.html new file mode 100755 index 0000000..ec1c48f --- /dev/null +++ b/articles/2025/Newsboat-and-gopher-RSS-feeds.html @@ -0,0 +1,36 @@ + + + + + + + Newsboat and gopher RSS feeds + + +
+

Bloggings

+ Back + +
+ +
+
+

Newsboat and gopher RSS feeds

+

2025-09-24

+

How to read gopher RSS feeds in newsboat

+

curl is able to access gopher files, and newsboat has an "exec" command +that can be used run commands.

+

So to read a gopher rss feed you can simply edit .newsboat/urls and add the following line:

+

"exec:curl gopher://url"

+

For example:

+

"exec:curl gopher://lucio.albenga.es/0/lfa/en/phlog/rss.xml"

+
+
+ + + diff --git a/articles/2025/Oddly-useful-Linux-tools.html b/articles/2025/Oddly-useful-Linux-tools.html new file mode 100755 index 0000000..719dd56 --- /dev/null +++ b/articles/2025/Oddly-useful-Linux-tools.html @@ -0,0 +1,50 @@ + + + + + + + Oddly useful Linux tools + + +
+

Bloggings

+ Back +
+ +
+
+

Oddly useful Linux tools

+

2025-05-08

+

Bread on Penguins with some interesting tools

+

+ Some really interesting videos, Bread on Penguins is taking up where + Luke Smith left off +

+
+ +
+
+
+ + + diff --git a/articles/2025/Pipewire.html b/articles/2025/Pipewire.html new file mode 100755 index 0000000..dd75885 --- /dev/null +++ b/articles/2025/Pipewire.html @@ -0,0 +1,62 @@ + + + + + + + Pipewire + + +
+

Bloggings

+ Back + +
+ +
+
+

Pipewire

+

2025-04-28

+

Fixing Thinkpad X220 microphone woes

+

So with my nice new DWM installation I waltz into a Jitsi meeting only to find that my microphone +wasn't working even though it appeared to. Something was missing in pulseaudio after I removed XFCE4, or +wasn't taken into account in DWM. Then I noticed that Librewolf was looking for Pipewire. What?

+

Eventually I got it working, but the microphone was distorted and full of parasites. +Adding a module remap to +turn the two laptop microphones into mono was a tad better but I decided go for the bleeding +edge and install Pipewire instead of Pulseaudio.

+
+sudo pacman -Rdd pulseaudio
+sudo pacman -S pipewire-{jack,alsa,pulse}
+systemctl --user enable --now pipewire pipewire-pulse pipewire-media-session
+
+

Noise suppression

+
+git clone https://github.com/werman/noise-suppression-for-voice.git
+
+

Follow the instructions at Noise suppression for Voice

+

In order to change the default source:

+
+pactl list sources short
+pactl set-default-source rnnoise_source
+systemctl --user restart pipewire.service 
+
+

And that's the best I'll probably ever get out of the internal X220 microphones.

+

Why not just use Alsa? Because I use a bluetooth headset and noise suppression.

+

Why not just use an external microphone? I've ordered one :-)

+ +

Bluetooth Autoconnect

+

Just to add, Bluetooth Autoconnect was built for Pulseaudio but works fine with Pipewire.

+

Bluetooth autoconnect will connect any client that is paired and trusted, in my case a headset.

+

If you use Chaotic-Aur, pacman -S bluetooth-autoconnect will install it. Use yay -S bluetooth-autoconnect if not.

+ +
+
+ + + diff --git a/articles/2025/Power-notifications.html b/articles/2025/Power-notifications.html new file mode 100755 index 0000000..af66de7 --- /dev/null +++ b/articles/2025/Power-notifications.html @@ -0,0 +1,31 @@ + + + + + + + Power notifications + + +
+

Bloggings

+ Back + +
+ +
+
+

Power notifications

+

2025-04-26

+

Searching how to get a notification for power source changes or USB drives

+

I found the solution here: udev desktop notifications

+
+
+ + + diff --git a/articles/2025/Powerline-Bash.html b/articles/2025/Powerline-Bash.html new file mode 100755 index 0000000..9af5c90 --- /dev/null +++ b/articles/2025/Powerline-Bash.html @@ -0,0 +1,27 @@ + + + + + + + Powerline Bash + + +
+

Bloggings

+ Back + +
+ +
+
+

Powerline Bash

+

2025-03-24

+

Make your bash prompt look real pretty

+

+

powerline.bash

+
+
+ + diff --git a/articles/2025/RSS-feeds-that-don't-suck-on-terminals.html b/articles/2025/RSS-feeds-that-don't-suck-on-terminals.html new file mode 100755 index 0000000..d3757bf --- /dev/null +++ b/articles/2025/RSS-feeds-that-don't-suck-on-terminals.html @@ -0,0 +1,83 @@ + + + + + + + RSS feeds that don't suck on terminals + + +
+

Bloggings

+ Back + +
+ +
+
+

RSS feeds that don't suck on terminals

+

2025-04-05

+

RSS news in the terminal

+

I'm building a list of RSS feeds that work in the terminal. +There are a lot of feeds out there but not all of the linked articles display well.

+ +

Basically it's a question of whether the page linked in the RSS feed is displayed well in +a text browser, which isn't always the case. I find the Chawan text browser gives the best +web display in general and includes images, so I'm using it as a benchmark.

+

It's always possible to use rdrview to pre-format the text before sending to cha, +with browser "rdrview -B cha %u" in .newsboat/config, but this removes the images.

+

This is an ongoing task so it will probably end up as a static page

+

The list so far:

+ +---Linux--- +https://old.reddit.com/r/commandline/.rss +https://old.reddit.com/r/Ghostty/.rss +https://old.reddit.com/r/xfce/.rss +https://old.reddit.com/r/neomutt/.rss +https://old.reddit.com/.rss +https://itsfoss.com/rss/ +https://archlinux.org/feeds/news +https://www.linuxjournal.com/node/feed +https://www.cyberciti.biz/feed/ +https://terminaltrove.com/blog.xml +https://terminaltrove.com/new.xml +https://terminaltrove.com/totw.xml + +---News-English--- +https://www.theatlantic.com/feed/all/ + +---News-French--- +https://www.thelocal.fr/feeds/rss.php +https://www.diplomatie.gouv.fr/spip.php?page=backend-fd +https://mondediplo.com/backend +https://frenchdailynews.com/feed +https://www.euronews.com/rss +https://tempsreel.nouvelobs.com/rss.xml +https://www.lepoint.fr/rss.xml + +---Science--- +https://www.science.org/rss/news_current.xml +http://rss.sciam.com/ScientificAmerican-Global +https://www.lemonde.fr/sciences/rss_full.xml +https://www.newscientist.com/feed/home/?cmpid=RSS%7CNSNS-Home +http://feeds.feedburner.com/sciencealert-latestnews +http://awesci.com/feed/ +https://blogs.esa.int/rocketscience/feed/ + +---Tech--- +https://feeds.feedburner.com/TheHackersNews?format=xml + +---Divertissement--- +https://xkcd.com/rss.xml + +---Other--- + +
+
+ + diff --git a/articles/2025/Reducing-web-font-size.html b/articles/2025/Reducing-web-font-size.html new file mode 100755 index 0000000..e03b92f --- /dev/null +++ b/articles/2025/Reducing-web-font-size.html @@ -0,0 +1,39 @@ + + + + + + + Reducing web font size + + +
+

Bloggings

+ Back + +
+ +
+
+

Reducing web font size

+

2025-04-06

+

I stumbled on a marvellous post

+

I started using the Jetbrains font for this blog, but adding the font in the css file makes it +pretty heavy, and I've been considering removing it. BUT...

+

I stumbled on this post +and tried Neil's suggestion on the JetbrainsMono-Regular truetype font, and the font filesize was reduced from 274k to 66Kb

+pipx install fonttools +pyftsubset JetbrainsMono-Regular.ttf --output-file=JetbrainsMono-Regular-reduced.ttf --unicodes=U+0020-007E +

So I decided to try it on the JetBrainsMono Regular Nerdfont, a whopping 2.4Mb, but the result was even better, 18Kb!

+.rw-r--r-- 66k philip 6 avril 23:04 JetBrainsMono-reduced.ttf +.rw-r--r-- 18k philip 6 avril 23:20 JetBrainsMonoNerd-reduced.ttf + +

Understanding why will take a bit of forensics, in the mean time this blog has become much lighter.

+

Update

+

I wound up just using the default monospace for the main text, and Ultra (reduced) for the title .

+ +
+
+ + diff --git a/articles/2025/Replace-sudo-with-doas-on-Artix.html b/articles/2025/Replace-sudo-with-doas-on-Artix.html new file mode 100755 index 0000000..d05bb5a --- /dev/null +++ b/articles/2025/Replace-sudo-with-doas-on-Artix.html @@ -0,0 +1,92 @@ + + + + + + + Replace sudo with doas on Artix + + +
+

Bloggings

+ Back + +
+ +
+
+

Replace sudo with doas on Artix

+

2025-06-18

+

More efficient, less complicated

+

Sudo is quite bloated and not without security problems. It is intended to cover situations +that normal Linux users will never encounter, and its configuration is messy.

+

Doas is an application from OpenBSD that is simpler and cleaner.

+

Here are the steps I took to install doas and replace sudo:

+sudo pacman -S opendoas +

Add my username to the group wheel:

+sudo usermod -aG wheel philip +

Create /etc/doas.conf and add:

+permit setenv {PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin} :wheel +

Modify the ownership and permissions:

+doas chown -c root:root /etc/doas.conf +doas chmod -c 0400 /etc/doas.conf + +

You should now be able to remove sudo:

+doas pacman -Rdd sudo +

Create a symlink to sudo

+ln -s $(which doas) /usr/bin/sudo +

So now it would be nice to have the equivalent of sudoedit.

+

Create /root/scripts/doasedit (I use the micro editor):

+#!/usr/bin/env bash + +DOASDIR="/tmp/doas-$(date +%s)" +mkdir $DOASDIR +chmod 700 $DOASDIR +DOASFILE="$DOASDIR/doas.conf" + +cp /etc/doas.conf $DOASFILE +chmod 600 $DOASFILE + +micro $DOASFILE +sync + +doas -C $DOASFILE && echo "valid config" && cp $DOASFILE /etc/doas.conf && chmod 400 /etc/doas.conf || echo "invalid config" +sync + +rm -rf $DOASDIR + + +

Create /usr/local/bin/doasedit:

+ +#!/usr/bin/env bash +if [ "$(id -u)" != 0 ]; then + doas /root/script/doasedit +else + /root/script/doasedit +fi + + +

Modify the permissions

+doas chmod 700 /root/script/doasedit +doas chmod 755 /usr/local/bin/doasedit + + +

Test doasedit:

+doasedit +

add this line:

+permit nopass philip as root cmd pacman +

save and you should see:

+valid config + +

Source

+ +
+
+ + + diff --git a/articles/2025/Rsync-backup-to-Synology-Diskstation.html b/articles/2025/Rsync-backup-to-Synology-Diskstation.html new file mode 100755 index 0000000..62f9c50 --- /dev/null +++ b/articles/2025/Rsync-backup-to-Synology-Diskstation.html @@ -0,0 +1,46 @@ + + + + + + + Rsync backup to Synology Diskstation + + +
+

Bloggings

+ Back + +
+ +
+
+

Rsync backup to Synology Diskstation

+

2025-03-25

+

This could change but it works for me.

+

My Diskstation isn't always on, so I run this from the command line rather than cron.

+

To avoid errors devices and links are skipped, permissions, times, group and owner are not written and files are compared by size only.

+

Ssh is configured with ssh-copy-id for passwordless access and host:port details in .ssh/config

+

Note: I set the rsync options on the diskstation but had to chmod 755 my +diskstation's home folder before passwordless ssh worked

+#!/bin/sh + + rsync -r --no-D --no-t --no-o --no-g --no-p \ + --size-only \ + --delete \ + --update \ + --exclude .cache/ \ + --exclude Downloads/ \ + --exclude tmp/ \ + --exclude .local/share/Trash/ \ + --exclude .wine \ + --info=progress2 \ + --stats \ + /home/philip philip@dsm:backup-X220 + + +
+
+ + diff --git a/articles/2025/Screen-scaling-with-xrandr.html b/articles/2025/Screen-scaling-with-xrandr.html new file mode 100755 index 0000000..9e42c67 --- /dev/null +++ b/articles/2025/Screen-scaling-with-xrandr.html @@ -0,0 +1,44 @@ + + + + + + + Screen scaling with xrandr + + +
+

Bloggings

+ Back + +
+ +
+
+

Screen scaling with xrandr

+

2025-03-30

+

Changing screen scaling on my X220

+

The Thinkpad X220 has a screen resolution of 1366x768 (16:9), which suits the size of the 13" screen, +but sometimes you stumble on applications that have gui elements that are taller than the screen.

+

In Linux, it's possible to change the screen scaling to emulate a larger screen with xrandr. +It won't look as crisp as the real resolution, but might get you out of a bind.

+

This command will emulate a 1920x1080 (16:9) screen:

+xrandr --output LVDS1 --panning 1920x1080 --scale 1.406x1.406 +

or 1600x900:

+xrandr --output LVDS1 --panning 1600x900 --scale 1.171x1.172 +

To return to the original scaling, type:

+xrandr --output LVDS1 --panning 1366x768 --scale 1x1 +

On the other hand , if you have for example a 1920x1080 (16:9) screen but would like to +scale down to 1600x900
(which might well be the case when I receive the new 1920x1080 IPS screen for my 14" T430)

+xrandr --output LVDS1 --panning 1600x900 --scale 0.8333x0.8333 +

 

+

In XFCE4, you can also magnify the resolution in
+Settings->Appearance->Settings->Window Scaling

+

you can also do this on the command line with

+xfconf-query -c xsettings -p /Gdk/WindowScalingFactor -s SCALE +

where SCALE is either 1 or 2

+
+
+ + diff --git a/articles/2025/Shellcheck-your-bash-scripts.html b/articles/2025/Shellcheck-your-bash-scripts.html new file mode 100755 index 0000000..36ff5c7 --- /dev/null +++ b/articles/2025/Shellcheck-your-bash-scripts.html @@ -0,0 +1,36 @@ + + + + + + + Shellcheck your bash scripts + + +
+

Bloggings

+ Back +
+
+ +
+
+

Shellcheck your bash scripts

+

2025-05-14

+

Shellcheck is a handy online and command line tool

+

+ I've started using this tool to clean up my scripts. Testing via the web + interface displays links to explanations of the errors or warnings found. +

+

https://www.shellcheck.net/

+
yay -Ss shellcheck
+
+
+ + + diff --git a/articles/2025/Simplex-a-decentralised-private-messenger.html b/articles/2025/Simplex-a-decentralised-private-messenger.html new file mode 100755 index 0000000..11f6efe --- /dev/null +++ b/articles/2025/Simplex-a-decentralised-private-messenger.html @@ -0,0 +1,29 @@ + + + + + + + Simplex a decentralised private messenger + + +
+

Bloggings

+ Back + +
+ +
+
+

Simplex a decentralised private messenger

+

2025-04-04

+

A messenger app without user IDs

+

Other apps have user IDs: Signal, Matrix, Session, Briar, Jami, Cwtch, etc. +SimpleX does not, not even random numbers.This radically improves your privacy.

+

Simplex

+

Rick Carlino's blog

+
+
+ + diff --git a/articles/2025/Slim-fork-login-manager.html b/articles/2025/Slim-fork-login-manager.html new file mode 100755 index 0000000..1b55788 --- /dev/null +++ b/articles/2025/Slim-fork-login-manager.html @@ -0,0 +1,35 @@ + + + + + + + Slim-fork login manager + + +
+

Bloggings

+ Back + +
+ +
+
+

Slim-fork login manager

+

2025-04-26

+

Slim has been ressurected

+

Slim is an lightweight login manager that has been ressurected, patched, and improved.

+

Oh the power of open source software...

+

Slim fork

+

Slim Themes

+

slim greeny-dark theme screenshot

+
+
+ + + diff --git a/articles/2025/Slstatus-date-with-alternative-locale.html b/articles/2025/Slstatus-date-with-alternative-locale.html new file mode 100755 index 0000000..60d7b44 --- /dev/null +++ b/articles/2025/Slstatus-date-with-alternative-locale.html @@ -0,0 +1,59 @@ + + + + + + + Slstatus date with alternative locale + + +
+

Bloggings

+ Back + +
+ +
+
+

Slstatus date with alternative locale

+

2025-04-26

+

Modifying slstatus to display French date

+

My default locale is en-US, but I wanted to display weekdays in French in the slstatus bar.

+

The solution is to add setlocale to the function in
slstatus/components/datetime.c

+

+
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <time.h>
+#include <locale.h>
+
+#include "../slstatus.h"
+#include "../util.h"
+
+
+const char *
+datetime(const char *fmt)
+{    
+	setlocale(LC_ALL,"fr_FR.utf8");
+	time_t t;
+
+	t = time(NULL);
+
+	if (!strftime(buf, sizeof(buf), fmt, localtime(&t))) {
+		warn("strftime: Result string exceeds buffer size");
+		return NULL;
+	}
+
+	return buf;
+}
+
+
+
+ + + diff --git a/articles/2025/Starship-prompt.html b/articles/2025/Starship-prompt.html new file mode 100755 index 0000000..5371de9 --- /dev/null +++ b/articles/2025/Starship-prompt.html @@ -0,0 +1,34 @@ + + + + + + + Starship prompt + + +
+

Bloggings

+ Back + +
+ +
+
+

Starship prompt

+

2025-04-26

+

Making the terminal pretty with Starship

+

I got fed up with the powerline prompt and installed starship +with the gruvbox-rainbow preset.

+

+

And with that, I'm done for the day.

+
+
+ + + diff --git a/articles/2025/Stick-it-to-the-man.html b/articles/2025/Stick-it-to-the-man.html new file mode 100755 index 0000000..2846439 --- /dev/null +++ b/articles/2025/Stick-it-to-the-man.html @@ -0,0 +1,36 @@ + + + + + + + Stick it to the man + + +
+

Bloggings

+ Back + +
+ +
+
+

Artix: Stick it to the man

+

2025-06-29

+

Artix is the resistance: Xlibre

+

resistance.jpg

+

Xlibre released for testing on Artix Linux

+

Artix is becoming what Arch should have been. While corporations have been instrumental in Linux's progression over the years, +allowing them to forge Linux into a new sort of Windows where applications are becoming totally phagocyted by a +few pieces of software (Wayland, Systemd,...) requires solid resistance.

+

Kudos to the people making this happen.

+
+
+ + + diff --git a/articles/2025/TLP-recalibration-error-on-Thinkpad-X220.html b/articles/2025/TLP-recalibration-error-on-Thinkpad-X220.html new file mode 100755 index 0000000..6ba27a4 --- /dev/null +++ b/articles/2025/TLP-recalibration-error-on-Thinkpad-X220.html @@ -0,0 +1,29 @@ + + + + + + + TLP recalibration error on Thinkpad X220 + + +
+

Bloggings

+ Back + +
+ +
+
+

TLP recalibration error on Thinkpad X220

+

2025-04-08

+

Error: cannot read charge level of battery BAT0

+

My attempt to recalibrate the battery on my X220 failed. Hunting for info I found this on github +

It's a TLP bug that was fixed a couple of weeks ago. The solution was to install the git versions of tlp.

+yay -S tlp-git tlp-rdw-git +

Now I can plug in my X220 to the mains and run tlp recalibrate BAT0 overnight.

+
+
+ + diff --git a/articles/2025/Taking-the-leap-to-Artix.html b/articles/2025/Taking-the-leap-to-Artix.html new file mode 100755 index 0000000..6daee3b --- /dev/null +++ b/articles/2025/Taking-the-leap-to-Artix.html @@ -0,0 +1,45 @@ + + + + + + + Taking the leap to Artix + + +
+

Bloggings

+ Back + +
+ +
+
+

Taking the leap to Artix

+

2025-06-14

+

systemd no more

+

Artix Linux is a distribution based on Arch, but without systemd.

+

Seeing how systemd has become insidious within Linux it's no mean feat.

+

After watching this video, +and having read a lot of systemd warnings by old timers, +I decided to try Artix. I prefer a rolling release distribution, and I like Arch a lot, +so Artix was the logical choice.

+

After the first hour of floundering around I got it working.

+

For some reasong the default network manager was limiting network access, for example the Artix site +and my own site were ok, but reddit and duckduckgo were not allowed. I checked /etc/hosts but couldn't find +the culprit. In the end I removed ConnMan and installed networkmanager and it worked straight away. In addition +I can now use nmtui.

+

There are still things to fix, for example the SLiM desktop manager no longer works, true it's ancient, +so I'll have to find a replacement Until then I'm just typing startx after logging in.

+

I have dwm running, bluetooth and pipewire too, so it's just a question of fiddling around.

+ +
+
+ + + diff --git a/articles/2025/Terminal-Trove.html b/articles/2025/Terminal-Trove.html new file mode 100755 index 0000000..e07b7ea --- /dev/null +++ b/articles/2025/Terminal-Trove.html @@ -0,0 +1,29 @@ + + + + + + + Terminal Trove + + +
+

Bloggings

+ Back + +
+ +
+
+

Terminal Trove

+

2025-04-02

+

Terminal Trove curates and showcases all things in the terminal

+

such as command line interface tools (CLI), text mode interface tools (TUI), developer tools and more no matter what platform or medium.

+

The site also has RSS feeds, and is present on mastodon and BlueSky among others

+

https://terminaltrove.com

+

Nice site

+
+
+ + diff --git a/articles/2025/The-Mote-in-God's-eye.html b/articles/2025/The-Mote-in-God's-eye.html new file mode 100755 index 0000000..d6c779c --- /dev/null +++ b/articles/2025/The-Mote-in-God's-eye.html @@ -0,0 +1,36 @@ + + + + + + + The Mote in God's eye + + +
+

Bloggings

+ Back + +
+ +
+
+

The Mote in God's eye

+

2025-04-06

+

Science fiction by Larry Niven and Jerry Pournelle

+

I'm re-reading this book on my Kobo. It's a book I first read back in the 70's. +I probably bought it at Shakespeare & Co. a famous bookshop in the latin quarter of Paris, where +I could go and spend a couple of hours reading a book in a big leather armchair, +so it bears a touch of nostalgia for me.

+

The premise is of an intelligent species (the Moties), locked in it's own solar system, +victim of endless cycles of expansion and destruction due to it's fertility. +Larry Niven thought big when he wrote novels like Ringworld, and here it's no exception.

+

Some things seem a bit dated, for example the descendents of Russia, America or Scotland +being important in the distant future, but all in all a good thought provoking read. +To me it begs the question: what if our own species winds up becoming Moties?

+

📘 Kobo ebook

+
+
+ + diff --git a/articles/2025/The-last-thing-the-EU-needs-....html b/articles/2025/The-last-thing-the-EU-needs-....html new file mode 100755 index 0000000..05dd1d6 --- /dev/null +++ b/articles/2025/The-last-thing-the-EU-needs-....html @@ -0,0 +1,31 @@ + + + + + + + The last thing the EU needs ... + + +
+

Bloggings

+ Back + +
+ +
+
+

The last thing the EU needs ...

+

2025-04-09

+

Clickbait title for some serious points

+

The Last Thing the EU Needs Is Its Own Linux Distro

+

Having installed Linux in a couple of schools only to have the teachers showered in Microsoft freebies and propaganda, +I fully support this guy because otherwise I know how this is going to end up.

+

The roll over and beg attitude of authorities towards Microsoft lobbying is terrifying, and it has to change through top down +directives that place independant European solutions first.

+ +
+
+ + diff --git a/articles/2025/The-universe-is-spinning.html b/articles/2025/The-universe-is-spinning.html new file mode 100755 index 0000000..c537ffc --- /dev/null +++ b/articles/2025/The-universe-is-spinning.html @@ -0,0 +1,55 @@ + + + + + + + The universe is spinning + + +
+

Bloggings

+ Back +
+ +
+
+

The universe is spinning

+

2025-04-13

+

What goes around, comes around

+

+ One of my favorite Youtubers on the subject of our spinning universe +

+

No wonder I get vertigo.

+
+ +
+

+ Sabine Hossenfelder +

+
+
+ + + diff --git a/articles/2025/Thinkpad-T430-screen-upgrade.html b/articles/2025/Thinkpad-T430-screen-upgrade.html new file mode 100755 index 0000000..aa4fcdb --- /dev/null +++ b/articles/2025/Thinkpad-T430-screen-upgrade.html @@ -0,0 +1,33 @@ + + + + + + + Thinkpad T430 screen upgrade + + +
+

Bloggings

+ Back + +
+ +
+
+

Thinkpad T430 screen upgrade

+

2025-03-21

+

My T430's current screen is 1368x768, a bit crappy

+

So I've decided to upgrade it.

+

I've purchased a B140HAN01.3 screen and an LCD controller board kit from Aliexpress

+

I'll update this article when it arrives

+

Controller board

+

1920x1080 IPS screen

+

Oh poo.

+

The parts arrived, but when I installed them only the backlight works. I tried another LCD controller kit +but there's no display. I hit a wall with the screen supplier who won't replace it. Lesson learned.

+
+
+ + diff --git a/articles/2025/Thinkpad-X220-Fan-control.html b/articles/2025/Thinkpad-X220-Fan-control.html new file mode 100755 index 0000000..2e8010d --- /dev/null +++ b/articles/2025/Thinkpad-X220-Fan-control.html @@ -0,0 +1,30 @@ + + + + + + + Thinkpad X220 Fan control + + +
+

Bloggings

+ Back + +
+ +
+
+

Thinkpad X220 Fan control

+

2025-03-21

+

Blowing out the heat from my laptop

+

After installing thinkfan, sudo pacman -S thinkfan +

journalctl -p 3 -xb was showing an error concerning thinkfan, and I worked out that the thinkfan_acpi module wasn't being loaded on boot.

+

Adding thinkpad_acpi to /etc/modules-load.d/thinkpad.conf didn't work

+

What did work was editing /etc/default/grub and adding thinkpad_acpi.fan_control=1 to GRUB_CMDLINE_LINUX_DEFAULT

+

Then

sudo grub-mkconfig -o /boot/grub/grub.cfg

+
+
+ + diff --git a/articles/2025/Thinkvantage-button-in-DWM.html b/articles/2025/Thinkvantage-button-in-DWM.html new file mode 100755 index 0000000..5721c8e --- /dev/null +++ b/articles/2025/Thinkvantage-button-in-DWM.html @@ -0,0 +1,41 @@ + + + + + + + Thinkvantage button in DWM + + +
+

Bloggings

+ Back + +
+ +
+
+

Thinkvantage button in DWM

+

2025-04-27

+

Putting the Thinkvantage button to use

+

thinkvantage button

+

On my Thinkpad X220 there's a blue Thinkvantage button that only gets used when the machine boots to select different options.

+

Using xev the button is referenced as XF86XK_Launch1, here I use it to run a rofi menu for logging out, shutting down, rebooting, etc.

+

dwm/config.h

+
+...
+{ 0,            XF86XK_Launch1,            spawn,          SHCMD("rofi -show power-menu -modi power-menu:rofi-power-menu") },
+...
+
+

power-menu can be found here

+

+
+
+ + + diff --git a/articles/2025/Thoughts-on-suckless.html b/articles/2025/Thoughts-on-suckless.html new file mode 100755 index 0000000..25d9cc8 --- /dev/null +++ b/articles/2025/Thoughts-on-suckless.html @@ -0,0 +1,46 @@ + + + + + + + Thoughts on suckless + + +
+

Bloggings

+ Back + +
+ +
+
+

Thoughts on suckless

+

2025-06-12

+

Note: opinions can change

+

I've been playing with suckless tools: dwm, dmenu, st, surf

+

Dwm and dmenu are fantastic, st and surf not so much.

+

Both st and surf have patches that are incompatible, and +often outdated. The user is expected to know how to correct the patch +rejections, which is often possible, but knowledge of C is your best bet.

+

Suckless software is intended to be somewhat elite. Their web site +is frankly not suckless, perhaps intentionally? Some of the patches proposed +address problems that have already been solved in the main program.

+

Surf compiles but with GTK warnings about obsolete functions.

+

St is a headbanger if you want kittygraphics, boxdraw, and ligatures for example.

+

The educational aspect of suckless software is of course is invaluable, +and if it's the objective, then that's fine. The problem arises when you want to +build something that will fulfill your needs.

+

Because of all this I use Kitty or Ghostty instead of st, and Librewolf +instead of Surf. I just hope dwm doesn't fall into the same hole of wanting to +be elite to the point of not fulfilling it's task, which would be a terrible shame.

+
+
+ + + diff --git a/articles/2025/Thoughts-on-zsh.html b/articles/2025/Thoughts-on-zsh.html new file mode 100755 index 0000000..b0d1af6 --- /dev/null +++ b/articles/2025/Thoughts-on-zsh.html @@ -0,0 +1,40 @@ + + + + + + + Thoughts on zsh + + +
+

Bloggings

+ Back + +
+ +
+
+

Thoughts on zsh

+

2025-06-22

+

do I need it?

+

I kinda figured out that although youtubers attempt to make you feel a has-been if +you don't use all the bells and whistles that come with zsh, that Bash does everything I need, and I can extend it if needed.

+

Minimalism might be my aim, but it's not a do of die mantra, even so zsh appears rather flash and with esoteric possibilies that I will never use.

+

I'm also keeping my bash scripts, there seems to be little point in making them reliant on some other shell.

+

There are also a number of things that can improve your bash "experience" like Blesh - Bash Line Editor

+

So I'm sticking to Bash for now.

+

Edit: after adding blesh I found that Midnight Commander (mc) took a few seconds to start, I fixed it with an alias:

+alias mc="mc --nosubshell" +

Re-Edit: sometimes typing in the termnal was non responsive, I got annoyed after a while and removed Blesh from .bashrc

+

Turns out simple is best, who would have thought it?

+
+
+ + + diff --git a/articles/2025/Toggle-screen-resolution-in-DWM.html b/articles/2025/Toggle-screen-resolution-in-DWM.html new file mode 100755 index 0000000..6928414 --- /dev/null +++ b/articles/2025/Toggle-screen-resolution-in-DWM.html @@ -0,0 +1,35 @@ + + + + + + + Toggle screen resolution in DWM + + +
+

Bloggings

+ Back + +
+ +
+
+

Toggle screen resolution in DWM

+

2025-06-28

+

using X220 keys

+

Sometimes a badly written application will display a panel that is bigger than my X220's screen resolution, which is 1366x768.

+

To quickly solve this I've affected the Scroll Lock key to a script that toggles scaling between 1366x768 and 1600x900

+

The scaling adds some fuzziness but gets the job done.

+

My DWM fork

+

DWM Scale script

+
+
+ + + diff --git a/articles/2025/Touchpad-deactivation.html b/articles/2025/Touchpad-deactivation.html new file mode 100755 index 0000000..2f66a78 --- /dev/null +++ b/articles/2025/Touchpad-deactivation.html @@ -0,0 +1,58 @@ + + + + + + + Touchpad deactivation + + +
+

Bloggings

+ Back + +
+ +
+
+

Touchpad deactivation

+

2025-06-09

+

When you have big hands and a little keyboard

+

Until now I was using synclient TouchpadOff=1 in my .xinitrc, but I was getting odd +results when using the keyboard mouse buttons. It turns out oddly the lower left area of my touchpad was infrequently active +and messing things up.

+

Replacing synclient with xinput set-prop 11 172 0 fixed it.

+

I obtained the touchpad ID with xinput list and the command with xinput -list-props 11

+
+ $ xinput list
+ ⎡ Virtual core pointer                    	id=2	[master pointer  (3)]
+ ⎜   ↳ Virtual core XTEST pointer              	id=4	[slave  pointer  (2)]
+ ⎜   ↳ Logitech Wireless Mouse                 	id=9	[slave  pointer  (2)]
+ ⎜   ↳ TPPS/2 IBM TrackPoint                   	id=12	[slave  pointer  (2)]
+ ⎣ Virtual core keyboard                   	id=3	[master keyboard (2)]
+     ↳ Virtual core XTEST keyboard             	id=5	[slave  keyboard (3)]
+     ↳ Power Button                            	id=6	[slave  keyboard (3)]
+     ↳ Video Bus                               	id=7	[slave  keyboard (3)]
+     ↳ Sleep Button                            	id=8	[slave  keyboard (3)]
+     ↳ AT Translated Set 2 keyboard            	id=10	[slave  keyboard (3)]
+     ↳ ThinkPad Extra Buttons                  	id=13	[slave  keyboard (3)]
+     ↳ Logitech Wireless Mouse                 	id=14	[slave  keyboard (3)]
+ ∼ SynPS/2 Synaptics TouchPad              	id=11	[floating slave]
+ 
+$ xinput -list-props 11
+Device 'SynPS/2 Synaptics TouchPad':
+    Device Enabled (172):	1
+    ...
+	
+$ xinput set-prop 11 172 0
+ 
+
+
+ + + diff --git a/articles/2025/Traditional-screenshot-of-my-X220.html b/articles/2025/Traditional-screenshot-of-my-X220.html new file mode 100755 index 0000000..19cb8af --- /dev/null +++ b/articles/2025/Traditional-screenshot-of-my-X220.html @@ -0,0 +1,28 @@ + + + + + + + Traditional screenshot of my X220 + + +
+

Bloggings

+ Back + +
+ +
+
+

Traditional screenshot of my X220

+

2025-04-08

+

Thinkpad X22O running Arch Linux

+screenshot of X220 +

It's traditional to show off one's Thinkpad screenshot basking in all it's Linuxian glory, so here we are.

+
My name is Ozymandias, King of Kings: Look on my works, ye Mighty, and despair!
+
+
+ + diff --git a/articles/2025/Try-different-versions-of-Linux-online.html b/articles/2025/Try-different-versions-of-Linux-online.html new file mode 100755 index 0000000..eb6e25d --- /dev/null +++ b/articles/2025/Try-different-versions-of-Linux-online.html @@ -0,0 +1,33 @@ + + + + + + + Try different versions of Linux online + + +
+

Bloggings

+ Back + +
+ +
+
+

Try different distributions of Linux online

+

2025-07-14

+

Distrosea

+

Here's a way of testing various distributions of Linux using your browser

+

Artix Linux is included

+

https://distrosea.com

+
+
+ + + diff --git a/articles/2025/Updated-cgit-server.html b/articles/2025/Updated-cgit-server.html new file mode 100755 index 0000000..53ce0d7 --- /dev/null +++ b/articles/2025/Updated-cgit-server.html @@ -0,0 +1,35 @@ + + + + + + + Updated cgit server + + +
+

Bloggings

+ Back + +
+ +
+
+

Updated cgit server

+

2025-05-28

+

dwm, dmenu, st repositories updated

+

I've updated my git server with patched versions of DWM, Dmenu and ST

+

The cgit server is also fixed, I didn't realise that https cloning wasn't working.

+

It works now, for example:

+
git clone https://git.wittamore.fr/dwm
+

Link to my git server

+

+
+ + + diff --git a/articles/2025/Upload-modified-blog-files-with-scp.html b/articles/2025/Upload-modified-blog-files-with-scp.html new file mode 100755 index 0000000..9b28eed --- /dev/null +++ b/articles/2025/Upload-modified-blog-files-with-scp.html @@ -0,0 +1,65 @@ + + + + + + + Upload modified blog files with scp + + +
+

Bloggings

+ Back + +
+ +
+
+

Upload modified blog files with scp

+

2025-03-29

+

A simple bash script to update this blog using scp

+

Note

+

I have my web server aliased in ~/.ssh/config as "web"
so the remote folder is web:/folder

+#!/bin/sh + +# blogsend : scp blog files to web server +# usage : blogsend hours (if not set default is 1 hour) +# example : blogsend 72 sends files modified over the past 3 days + +# folders +rf= #add your remote ssh folder here +mf= #add your local folder here +cd $mf + +# test if option is sent +if [[ -z "$1" ]]; then + HOURS=1; +else + # test if option is a number + if [[ "$1" =~ ^[0-9]+$ ]]; then + HOURS=$1; + else + HOURS=1; + fi +fi + +MINUTES=$(($HOURS*60)) + +echo "Sending files modified in the past $HOURS hour(s)..." + +files=$(find * -path .tmp -prune -o -mmin -$MINUTES -type f) +for file in $files +do + echo -ne "-> "; + scp -r "$file" "$rf/$file"; +done + +echo "Done." + +exit 0 + +
+
+ + diff --git a/articles/2025/Using-Goaccess-to-view-web-stats.html b/articles/2025/Using-Goaccess-to-view-web-stats.html new file mode 100755 index 0000000..8312fa5 --- /dev/null +++ b/articles/2025/Using-Goaccess-to-view-web-stats.html @@ -0,0 +1,32 @@ + + + + + + + Using Goaccess to view web stats + + +
+

Bloggings

+ Back + +
+ +
+
+

Using Goaccess to view web stats

+

2025-04-03

+

Using Goaccess locally

+

I have goaccess installed on my laptop to view the web stats on a vps

+

First install Goaccess on Arch Linux

+sudo pacman -S goaccess +

View the remote stats (adjust the path for your actual log location)

+ssh USER@SERVER 'zcat -f /var/log/nginx/access.log*' | goaccess -a --log-format=COMBINED +

I've added an alias in my .bashrc :

+alias stats="ssh USER@SERVER 'zcat -f /var/log/nginx/access.log*' | goaccess -a --log-format=COMBINED" +
+
+ + diff --git a/articles/2025/VPS-management.html b/articles/2025/VPS-management.html new file mode 100755 index 0000000..77d77c6 --- /dev/null +++ b/articles/2025/VPS-management.html @@ -0,0 +1,33 @@ + + + + + + + VPS management + + +
+

Bloggings

+ Back + +
+ +
+
+

VPS management

+

2025-04-18

+

Reducing things

+

I have 2 vps's that were underused, so I've put everything on to a single vps, which has the benefit of halving costs.

+

My days of writing and hosting web sites for others are over, I just have to cater for myself now.

+

Which is quite nice really...

+
+
+ + + diff --git a/articles/2025/What-to-do-with-an-unused-Kindle.html b/articles/2025/What-to-do-with-an-unused-Kindle.html new file mode 100755 index 0000000..de8bf3c --- /dev/null +++ b/articles/2025/What-to-do-with-an-unused-Kindle.html @@ -0,0 +1,33 @@ + + + + + + + What to do with an unused Kindle + + +
+

Bloggings

+ Back + +
+ +
+
+

What do you do with an unused Kindle

+

2025-04-17

+

Early in the morning...

+

My Kindle paperwhite is sleeping in a drawer since I bought a Kobo, but there's something I can do with it.

+

Run Linux, use Koreader, play games ... +

It's time to jailbreak your Kindle

+
+
+ + + diff --git a/articles/2025/Why-does-everything-feel-so-boring-now.html b/articles/2025/Why-does-everything-feel-so-boring-now.html new file mode 100755 index 0000000..e0ef08b --- /dev/null +++ b/articles/2025/Why-does-everything-feel-so-boring-now.html @@ -0,0 +1,48 @@ + + + + + + + Why does everything feel so boring now? + + +
+

Bloggings

+ Back +
+ +
+
+

Why does everything feel so boring now?

+

2025-06-07

+

Food for thought on blandness

+

What happened to colour? Where did it go?

+

I wonder if we have reached peak blandness.

+
+ +
+
+
+ + + diff --git a/articles/2025/Wifi-region.html b/articles/2025/Wifi-region.html new file mode 100755 index 0000000..2a1957b --- /dev/null +++ b/articles/2025/Wifi-region.html @@ -0,0 +1,33 @@ + + + + + + + Wifi region + + +
+

Bloggings

+ Back + +
+ +
+
+

Wifi region

+

2025-03-26

+

If like me you reside in France, you may have wifi problems with the default frequency settings.

+

Verify the value:

+iw reg get +

If you don't see FR then it's not correctly set.

+sudo iw reg set FR +

will correct that. To make it persistant, edit /etc/default/crda and add

+REGDOMAIN=FR +
+Source

+
+
+ + diff --git a/articles/2025/Window-manager-thoughts.html b/articles/2025/Window-manager-thoughts.html new file mode 100755 index 0000000..e8dfed3 --- /dev/null +++ b/articles/2025/Window-manager-thoughts.html @@ -0,0 +1,47 @@ + + + + + + + Window manager thoughts + + +
+

Bloggings

+ Back + +
+ +
+
+

Window manager thoughts

+

2025-04-15

+

Messing with i3, Openbox, and Xfce4

+

Listening to Youtube "influencers" I learned that I was a dummy because I don't use tiling window managers.

+

I tried i3 in Manjaro before but just didn't connect with it. Perhaps it was me? +Trying again I spent the past couple of days messing with the configuration files of i3, i3blocks, polybar, etc. +again, just to see if maybe I missed out on something.

+

Turns out I didn't.

+

My Thinkpad X220 is my daily drive, it has a 13" screen, 1366x768, and a French keyboard.

+

Splitting the screen is a daft idea on such a small laptop, it doesn't add any productivity improvement +even when you cast aside the problems remembering was it Super+~ or was it Super+Shift+F2 ?, +and the resulting digital gymnastics (although possibly good for my carpal tunnels, who knows?)

+

It's understandable when you have a nice large desktop screen and a decently sized keyboard, but here no way.

+

Anyway, a stacking windowmanager, Xfce4, appears to be the best bang for my buck teeth on this laptop.

+

I have set the keystroke Super+right & left to flip through windows that I mostly have maximised +(I'll eventually use devilspie to make this automatic), +that are all simply stacked on top of each other in the same workspace. +I use tabs in Ghostty to have for example Neomutt, btop, and a command line. In Librefox I have tabs too, +and I'll probably have Libreoffice Calc or Writer open, Gimp, galculator too.

+

Simple and effective, while keeping a bottom panel and application menu available.

+
+
+ + + diff --git a/articles/2025/Workshop-Air-Cleaner.html b/articles/2025/Workshop-Air-Cleaner.html new file mode 100755 index 0000000..307a2c5 --- /dev/null +++ b/articles/2025/Workshop-Air-Cleaner.html @@ -0,0 +1,29 @@ + + + + + + + Workshop Air Cleaner + + +
+

Bloggings

+ Back + +
+ +
+
+

Workshop Air Cleaner

+

2025-03-19

+

Keeping the garage clean-ish

+ +

I've cancelled the plan to build a workshop. It's expensive, and increases my local taxes.

+

So I'm adapting the shared garage. The first step is an air cleaner, I settled for the Record Power AC400

+

I chained it to the rafters, and first results are good.

+
+
+ + diff --git a/articles/2025/X220-internal-Bluetooth-controller.html b/articles/2025/X220-internal-Bluetooth-controller.html new file mode 100755 index 0000000..dd8f337 --- /dev/null +++ b/articles/2025/X220-internal-Bluetooth-controller.html @@ -0,0 +1,38 @@ + + + + + + + X220 internal Bluetooth controller + + +
+

Bloggings

+ Back + +
+ +
+
+

X220 internal Bluetooth controller

+

2025-05-27

+

Deactivating the internal Bluetooth controller

+

The Thinkpad X220 has an internal Bluetooth controller, but +I installed the AC7260 wifi/bluetooth card.

+

Unfortunately the default card remains the internal one, so autoconnect of my headset doesn't work.

+

In order to block the internal device, I made a UDEV rule in /etc/udev/rules.d/91-bluetooth-hci.rules

+

After obtaining the id with lsusb:

+
SUBSYSTEM=="usb", ATTRS{idVendor}=="8087", ATTRS{idProduct}=="07dc", ATTR{authorized}="0"
+

Reboot, and the AC7260 Bluetooth card is now the default.

+ +
+
+ + + diff --git a/articles/2025/Xlibre-on-my-X220.html b/articles/2025/Xlibre-on-my-X220.html new file mode 100755 index 0000000..c3bc3d8 --- /dev/null +++ b/articles/2025/Xlibre-on-my-X220.html @@ -0,0 +1,34 @@ + + + + + + + Xlibre on my X220 + + +
+

Bloggings

+ Back + +
+
+
+

Xlibre on my X220

+

2025-08-07

+

Xlibre > Xorg

+

Not much to post about recently as I've been busy building myself a new shed to store my recuperated hardwood collection.

+

However, I just installed Xlibre, a fork of Xorg. It's now available in the Artix Galaxy repo, and it went without a hitch.

+pacman -S xlibre +

Allow xlibre to replace all the xorg packages, then install the video driver package you need.

+pacman -S xlibre-xf86-video-intel +

For more info on Xlibre, check out the site https://x11libre.net

+
+
+ + + diff --git a/articles/2025/dwm-windowmanager-1.html b/articles/2025/dwm-windowmanager-1.html new file mode 100755 index 0000000..5b19d06 --- /dev/null +++ b/articles/2025/dwm-windowmanager-1.html @@ -0,0 +1,46 @@ + + + + + + + test + + +
+

Bloggings

+ Back + +
+ +
+
+

DWM Window manager [1]

+

2025-04-21

+

Suckless window manager

+

Suckless web site

+

It struck me that using DWM with Monocle layout would be just what I've been looking for on my X220

+

I cloned the st, dmenu, and dwm from suckless.org and followed the instructions to compile them.

+

Right now I have Xfce4 installed, so to try out DWM I've added an entry to select it in lightdm

+

sudo micro /usr/share/xsessions/dwm.desktop

+[Desktop Entry] +Encoding=UTF-8 +Name=dwm +Comment=Dynamic window manager +Exec=dwm +Icon=dwm +Type=XSession + +

In dwm's config.h I set the monocle layout as default and recompiled dwm.

+

At first glance this looks much better than the i3 window manager, small, neat and fast.

+

More to come...

+
+
+ + + diff --git a/articles/2025/dwm-windowmanager-2.html b/articles/2025/dwm-windowmanager-2.html new file mode 100755 index 0000000..27e5a63 --- /dev/null +++ b/articles/2025/dwm-windowmanager-2.html @@ -0,0 +1,64 @@ + + + + + + + DWM window manager [2] + + +
+

Bloggings

+ Back + +
+ +
+
+

DWM window manager [2]

+

2025-04-23

+

the simpler the better

+

Mucking around with DWM proves the old adge that less is more. +So far I haven't needed to add any of the myriad of patches available for DWM, it works as it is for me.

+

I've tried dwm-blocks but right now I'm using dwm-bar as I like the battery icon to update along with the charge.

+

I have edited config.h though and changed the keybindings to work better with a French keyboard and launch a few apps.

+dwm running btop +

Editing config.h is an ongoing job, but you can see a snapshot here

+

ST is the default terminal emulator for DWM but I rather like Ghostty even if it consumes a bit more +memory. Adding window-decoration = none to .config/ghostty/config makes it look better in DWM.

+

I also added gtk-single-instance = true to make ghostty's startup faster.

+

Being able to swap between display layouts on the fly is cool. I mostly use monocle layout, +but flipping to tiling or floating comes in handy.

+

I removed the lightdm display manager and installed Slim. Here is my .xinitrc

+
+#!/bin/sh
+
+# Source global X session scripts.
+if [ -d /etc/X11/xinit/xinitrc.d ]; then
+  for f in /etc/X11/xinit/xinitrc.d/*; do
+    [ -x "$f" ] && . "$f"
+  done
+  unset f
+fi
+
+bluetooth off
+clipmenud &
+xscreensaver --no-splash &
+ghostty &
+feh --bg-scale --randomize ~/Pictures/wallpaper/* &
+~/.local/src/dwm-bar/dwm_bar.sh &
+# if dwm exits 0, restart -- this allows hot reloading of config.h
+while type dwm >/dev/null ; do dwm && continue || break ; done
+
+

And here is my DWM configuration file (it probably isn't the latest version)
+config.h

+
+
+ + + diff --git a/articles/2025/dwmblocks-with-emoji-icons.html b/articles/2025/dwmblocks-with-emoji-icons.html new file mode 100755 index 0000000..5817367 --- /dev/null +++ b/articles/2025/dwmblocks-with-emoji-icons.html @@ -0,0 +1,50 @@ + + + + + + + dwmblocks with emoji icons + + +
+

Bloggings

+ Back + +
+ +
+
+

dwmblocks with emoji icons

+

2025-06-04

+

fancy blocks for dwm

+

Just a pic of my simple dwmblocks status bar

+

+

The calendar app is Orage, opened and closed by clicking on the date which launches a dwmblocks script

+

dwm/config.def.h

+
+static const Rule rules[] = {
+	/* xprop(1):
+	 *	WM_CLASS(STRING) = instance, class
+	 *	WM_NAME(STRING) = title
+	 */
+	/* class        instance  title          tags mask iscentered  isfloating  isterminal  noswallow  monitor */
+	{ "Gimp",       NULL,     NULL,           0,        0,         1,          0,           0,        -1 },
+	{ "Firefox",    NULL,     NULL,           1 << 8,   0,         0,          0,          -1,        -1 },
+	{ "Ghostty",    NULL,     NULL,           0,        0,         0,          1,           0,        -1 },
+	{ "Orage",      NULL,     NULL,           0,        0,         1,          0,           1,        -1 },
+	{ "Galculator", NULL,     NULL,           0,        1,         1,          0,           1,        -1 },
+	{ NULL,         NULL,     "Event Tester", 0,        0,         0,          0,           1,        -1 }, /* xev */
+};
+
+ +
+
+ + + diff --git a/articles/2026/De-Gaulle---Two-films.html b/articles/2026/De-Gaulle---Two-films.html new file mode 100644 index 0000000..5be3c14 --- /dev/null +++ b/articles/2026/De-Gaulle---Two-films.html @@ -0,0 +1,63 @@ + + + + + + + + De Gaulle - Two films + + +
+

Bloggings

+ Back +
+
+
+

De Gaulle - Two films

+

2026-06-04

+

De Gaulle is more popular than ever

+

+ People who choose a country to be their home are often patriotic as + it's their choice, not mere circumstance. +

+

Personally I'm patriotic but certainly not nationalist.

+
+ “The difference between patriotism and nationalism is that the + patriot is proud of his country for what it does, and the + nationalist is proud of his country no matter what it does; the + first attitude creates a feeling of responsibility, but the second a + feeling of blind arrogance that leads to war.” + ― + Sydney J. Harris +
+

+
+ +
+
+
+ + + diff --git a/articles/2026/Filtering-the-web.html b/articles/2026/Filtering-the-web.html new file mode 100644 index 0000000..0a94d16 --- /dev/null +++ b/articles/2026/Filtering-the-web.html @@ -0,0 +1,62 @@ + + + + + + + + Filtering the web + + +
+

Bloggings

+ Back +
+
+
+

Filtering the web

+

2026-06-20

+

A browser that says no

+

+ To me the problem with the smolweb is that there's isn't a browser + that applies these rules. Dillo comes close, even though it needs a + makeover. +

+

+ Dillo also has an rdrview plugin, but you have to manually write + rdrview: in front of the url in the url bar of every page to see the + results. +

+

+ A better solution would be a smolweb plugin that filters the entire + html page rejecting anything that doesn't + validate or pass to + rdrview . Degrees of acceptance could be set in settings. +

+

+ A dedicated browser wouldn't require searching for smolweb / smallweb + / indieweb blogs, sites, or articles. (There seems to be no such thing + as a smolweb search engine, although points for wiby, marginalia, kagi + and mojeek). +

+

+ Just point the browser to any web site and if it borks, display the + "errors" or ignore it with a + + 418 I'm a teapot error: after all it's the web site author's responsibility to provide + clean, readable web sites. +

+

+ Imagine a world where this plugin would be as ubiquitous as + ublock-origin, it could even change web site development. +

+
+
+ + + diff --git a/articles/2026/Freedos-thoughts.html b/articles/2026/Freedos-thoughts.html new file mode 100644 index 0000000..d16485e --- /dev/null +++ b/articles/2026/Freedos-thoughts.html @@ -0,0 +1,43 @@ + + + + + + + + Freedos thoughts + + +
+

Bloggings

+ Back +
+
+
+

Freedos thoughts

+

2026-06-11

+

Maybe I have the best system already

+

+ I've been looking at Freedos for the retrocomputing nostalgia value + and the text-only goodness. +

+

+ I wound up using Qemu on my X270 as Freedos on bare metal means my 14 + year old Thinkpad X220 is way too modern. +

+

+ Yes there's some crazy good software, and it's fun. But using + fullscreen in Qemu means a fuzzy display, and I came to measure how + the terminal on Linux is truly light years ahead. +

+

+ In fact it pushes home that my Artix / DWM / Kitty setup is the + veritable retrocomputing bees knees +

+
+
+ + + diff --git a/articles/2026/From-minimalist-to-smolweb.html b/articles/2026/From-minimalist-to-smolweb.html new file mode 100644 index 0000000..54de0bd --- /dev/null +++ b/articles/2026/From-minimalist-to-smolweb.html @@ -0,0 +1,43 @@ + + + + + + + From minimalist to smolweb + + +
+

Bloggings

+ Back +
+ +
+
+

From minimalist to smolweb

+

2026-05-06

+

When smarter people get things done

+

+ In 2023 + I wrote an article + about what I was feeling at the time on the sorry state of the web. +

+

+ I'm happy to say that the people at + smolweb.org have made a far better + statement than mine. +

+

+ As this blog is now a few years old, I'll be working on making it pass + the + smolweb validation test. +

+
+
+ + + diff --git a/articles/2026/Funeral-blues.html b/articles/2026/Funeral-blues.html new file mode 100644 index 0000000..31f4f9b --- /dev/null +++ b/articles/2026/Funeral-blues.html @@ -0,0 +1,57 @@ + + + + + + + + Funeral blues + + +
+

Bloggings

+ Back +
+
+
+

Funeral blues

+

2026-05-07

+

A poem by W.H.Auden (1907-1973)

+

This poem was read in the film "Four marriages and a funeral"

+ +

I just stumbled upon it and wanted to write it down.

+ + +Funeral Blues + +Stop all the clocks, cut off the telephone. +Prevent the dog from barking with a juicy bone, + +Silence the pianos and with muffled drum +Bring out the coffin, let the mourners come. + +Let aeroplanes circle moaning overhead +Scribbling in the sky the message He is Dead, + +Put crêpe bows round the white necks of the public doves, +Let the traffic policemen wear black cotton gloves. + +He was my North, my South, my East and West, +My working week and my Sunday rest + +My noon, my midnight, my talk, my song; +I thought that love would last forever, I was wrong. + +The stars are not wanted now; put out every one, +Pack up the moon and dismantle the sun. + +Pour away the ocean and sweep up the wood; +For nothing now can ever come to any good. + +
+
+ + + diff --git a/articles/2026/Ghostbsd-installation.html b/articles/2026/Ghostbsd-installation.html new file mode 100644 index 0000000..cf3314e --- /dev/null +++ b/articles/2026/Ghostbsd-installation.html @@ -0,0 +1,92 @@ + + + + + + + + Ghostbsd installation + + +
+

Bloggings

+ Back +
+
+
+

Ghostbsd installation

+

2026-06-14

+

messing around on a sunday

+

+ So now we have Microsoft delivering Linux, and the Arch based AUR + getting wrecked by people who should be locked into a public pillory, + and I have some free time. Well in fact a lot of free time now that + I'm retired. +

+

+ So. I downloaded the latest #Ghostbsd Mate and cut a usb key, then + plugged it into my Thinkpad X220. +

+

+ The installation was simple and uneventful, and it booted without + problem. Everything worked out of the box. Mate seems pretty and + unobfuscated, just like me. +

+

So far I've done just two things:

+

+ (1) to get some thinkpad function keys to work I've added in + /boot/loader.conf +

+
+acpi_ibm_load="YES"
+
+

+ Printscreen works, brightness and volume controls work, vol mute + works, lid led works, but the MICMUTE button doesn't work.
+ For the X220 MICMUTE key, which is F20 or 0xBE (190), unfortunately + Ghostbsd's acpi_ibm doesn't pass on the event, so any script for it in + /etc/devd won't be fired. Checking the source code, acpi_ibm only accepts + a subset of events accepted by Linux's thinkpad_acpi, which appears to + cover them all. It's unfortunate as I'm a bit pedantic about keys but I + guess I'll have to live with it as the code is 20 years old and I + doubt it will change anytime soon. +

+ +

FYI, acpi_ibm accepts only these keyboard events:

+
+        LCD_BACKLIGHT
+        SUSPEND_TO_RAM
+        BLUETOOTH
+        SCREEN_EXPAND
+        SUSPEND_TO_DISK
+        BRIGHTNESS_UP
+        BRIGHTNESS_DOWN
+        THINKLIGHT
+        ZOOM
+        VOLUME_UP
+        VOLUME_DOWN
+        MUTE
+        ACCESS_IBM_BUTTON
+        
+

(2) to enable the automounting of usb storage media:

+
+sudo pkg install automount
+
+

+ I've replaced the default Firefox with Librewolf, and I'm not too sure + about zsh instead of bash, and why not doas instead of sudo? +

+

+ Ghostbsd is a very nice BSD distribution that's been around since + 2010. +

+

https://www.ghostbsd.org

+
+
+ + + diff --git a/articles/2026/Hebdogiciel.html b/articles/2026/Hebdogiciel.html new file mode 100644 index 0000000..fdc10e4 --- /dev/null +++ b/articles/2026/Hebdogiciel.html @@ -0,0 +1,51 @@ + + + + + + + + Hebdogiciel + + +
+

Bloggings

+ Back +
+
+
+

Hebdogiciel

+

2026-06-07

+

Flashback to the 80's

+

+ After my ZX81 I purchased an Amstrad CPC464, and oh joy I discovered + Hebdogiciel. Before the internet this magazine printed the code for + games and programs that you could painfully copy out. Today I + discovered that there's an online archive: +

+ +

+ abandonware-magazines +

+ +

A translation of the French abandonware-magazine article:

+

+ 1983–1987: Imagine a weekly magazine that, in addition to + providing us with a weekly program to type out and delivering + uncompromising hardware and software reviews (and that’s saying + something), also featured TV listings, movie, music, and comic book + releases, assembly language tutorials, a comic strip, a discount club, + walkthroughs... A tone that blew you away from the first page to the + last, Carali’s drawings filling in the gaps, lawsuits galore… Simply + extraordinary, unmatched. +

+
+
+ + + diff --git a/articles/2026/Installation-of-Snac2-on-Artix-Linux.html b/articles/2026/Installation-of-Snac2-on-Artix-Linux.html new file mode 100644 index 0000000..2b752ea --- /dev/null +++ b/articles/2026/Installation-of-Snac2-on-Artix-Linux.html @@ -0,0 +1,144 @@ + + + + + + + + Installation of Snac2 on Artix Linux + + +
+

Bloggings

+ Back +
+
+
+

Installation of Snac2 on Artix Linux

+

2026-05-20

+

Installation of snac2 on Artix Linux +Nginx +Dinit

+
+useradd -m -s /bin/bash snac
+passwd snac
+usermod -aG wheel snac
+su - snac
+mkdir src
+cd src
+git clone https://codeberg.org/grunfink/snac2.git
+cd snac2
+make
+sudo make install
+snac init ~/data
+
+

snac init will ask these questions:

+
+Host name: your.server.domain
+Path prefix: leave empty
+Network address: 127.0.0.1
+Port: 8001
+
+

add user

+
+snac adduser ~/data
+
+

In /home/snac/data/server.json edit these settings:

+
+admin_email
+admin_account
+title
+short_description
+
+and add this line:
+
+"proxy_media: true
+
+

As root, create the dinit script: /etc/dinit.d/snac2

+
+type = process
+run-as = snac
+command = /usr/local/bin/snac httpd /home/snac/data
+depends-on = NetworkManager
+restart = true
+smooth-recovery = yes
+log-type = file
+logfile = /var/log/dinit/snac2.log
+
+

enable snac2

+
+dinitcl enable snac2
+
+

NGINX Media caching

+

+see it-notes.fragas.net +

+

Nginx configuration +

+see comam.es.
+Use $host not $http_host (gixy) +

+

in /etc/nginx/nginx.conf http section add:

+
+proxy_cache_path /var/cache/nginx/snac_cache levels=1:2 keys_zone=snac:10m max_size=1g inactive=1440m use_temp_path=off;
+
+

make cache folder

+
+mkdir -p /var/cache/nginx/snac_cache
+
+

/etc/nginx/sites-available/example.com

+
+server {
+    listen 443 ssl;
+    listen [::]:443 ssl;
+    server_name example.com;
+
+    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/example.com/privkey.pem;
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_ciphers HIGH:!aNULL:!MD5;
+    # openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
+    ssl_dhparam /etc/ssl/certs/dhparam.pem;
+
+    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+
+location / {
+    proxy_pass http://127.0.0.1:8001;
+    proxy_set_header Host $host;
+    proxy_set_header X-Forwarded-For $remote_addr;
+}
+
+location ~ ^/.+/s/ {
+    proxy_cache snac;
+    proxy_pass http://127.0.0.1:8001;
+    proxy_set_header Host $host;
+    proxy_cache_valid 200 1d;
+    proxy_cache_valid 404 1h;
+    proxy_ignore_headers "Cache-Control" "Expires";
+    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
+    proxy_cache_lock on;
+    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+    add_header X-Proxy-Cache $upstream_cache_status;
+}
+}
+
+server {
+    listen 80;
+    listen [::]:80;
+    server_name example.com;
+    return 301 https://$host$request_uri;
+}
+
+ +

add pleroma relay

+

see relaylist.com

+
+su - snac
+snac adduser ~/data relay
+snac follow ~/data relay https://relay.toot.io/actor
+
+
+
+ + + diff --git a/articles/2026/Ladybird-turns-into-a-squashed-bug.html b/articles/2026/Ladybird-turns-into-a-squashed-bug.html new file mode 100644 index 0000000..c6039f7 --- /dev/null +++ b/articles/2026/Ladybird-turns-into-a-squashed-bug.html @@ -0,0 +1,45 @@ + + + + + + + + Ladybird turns into a squashed bug + + +
+

Bloggings

+ Back +
+
+ +
+ + + diff --git a/articles/2026/Living-on-Snac's-bleeding-edge.html b/articles/2026/Living-on-Snac's-bleeding-edge.html new file mode 100644 index 0000000..a17ffc6 --- /dev/null +++ b/articles/2026/Living-on-Snac's-bleeding-edge.html @@ -0,0 +1,60 @@ + + + + + + + + Living on Snac's bleeding edge + + +
+

Bloggings

+ Back +
+
+
+

Living on Snac's bleeding edge

+

2026-05-25

+

Autoupdate script

+

+ I like living on the bleeding edge of + snac2, so I added + this (banal) script to /etc/cron.daily. +

+
+#!/usr/bin/env bash
+# snaceto
+# Social Networks Are Crap Except This One
+
+cd /home/snac/src/snac2 || exit
+
+sudo -u snac git remote update
+UPDATED="$(git status -uno | grep -c 'up to date')"
+
+if [ "$UPDATED" -ne "1" ] ; then
+    echo "Updating..."
+    systemctl stop snac.service
+	# backup data before update
+	TODAY=$(date "+%Y-%m-%d-%H-%m")
+	BACKUPDIR="/opt/backups"
+	mkdir -p "$BACKUPDIR"
+	FILENAME="$BACKUPDIR/pre-update-snac-$TODAY.tar.gz"
+	tar -zcf "$FILENAME" /home/snac/data
+	# pull latest
+	sudo -u snac git pull
+	sudo -u snac make
+	make install
+	sudo -u snac snac upgrade /home/snac/data
+	systemctl start snac.service
+	VERSION="$(snac | head -n2)"
+	toot post "My bleeding edge #snac2 server has been updated
$VERSION" +fi +
+
+
+ + + diff --git a/articles/2026/More-on-Dillo.html b/articles/2026/More-on-Dillo.html new file mode 100644 index 0000000..862574e --- /dev/null +++ b/articles/2026/More-on-Dillo.html @@ -0,0 +1,36 @@ + + + + + + + + More on Dillo + + +
+

Bloggings

Back +
+
+
+

More on Dillo

+

2026-06-21

+

update on yesterday's post

+

The git + version of dillo has a new option: page_action, which + enables running rdrview on the current page if the + setting:

+
page_action="Reader mode:dilloc dump | rdrview -H | dilloc load"
+
+

is set in dillorc.

+

When you right click on a page the menu will now include + the option "Reader Mode".

+
+
+ + + diff --git a/articles/2026/My-own-private-search-engine.html b/articles/2026/My-own-private-search-engine.html new file mode 100644 index 0000000..e2c924e --- /dev/null +++ b/articles/2026/My-own-private-search-engine.html @@ -0,0 +1,54 @@ + + + + + + + + My own private search engine + + +
+

Bloggings

+ Back +
+
+
+

My own private search engine

+

2026-06-02

+

Messing around with a bad back in stormy weather

+

+ I made a bad movement in the shower this morning and made my back + scream again, so no woodworking today. One of the benefits of + retirement is that I get to choose the days activities. +

+

+ I had installed + SearxNG previously + more than two years ago, but as I'm constantly fiddling it wound up in + the bin. +

+

+ Convalescing in the armchair I saw that my current vps is mostly idle + running just snac2, my blog, xmpp, gophernicus and other bits, so I + reinstalled SearxNG. +

+

+ It's fast, responsive, and private. I have it behind an Nginx reverse + proxy, and only accepting my own IP address for now. +

+

+ Another thing I did was to play around with the + Fresh editor. Like Micro, it can + be configured to use prettier, the code formatter. Ok, it's written in + Rust, but it is a nice editor with a retro computing vibe. It won't + replace Micro as my daily editor, but if I ever need a TUI IDE it + could be a good choice. +

+
+
+ + + diff --git a/articles/2026/Oops!-I-did-it-again.html b/articles/2026/Oops!-I-did-it-again.html new file mode 100644 index 0000000..bd3bf4f --- /dev/null +++ b/articles/2026/Oops!-I-did-it-again.html @@ -0,0 +1,46 @@ + + + + + + + + Oops! I did it again + + +
+

Bloggings

+ Back +
+
+
+

Oops! I did it again

+

2026-05-31

+

More blog improvements

+

+ This blog now + sits in my git repository, so I made a few changes. +

+

+ One is the + blogsend + bash script which now does a git commit on my local machine, and a git + pull on the remote web server. The other is the git repos itself which + now has the web site in one repos, and the bash scripts in another. +

+

+ All this is probably of little interest to anyone other than myself, + but if you want to run a smol blog you might find it useful, after all + that's why I'm posting it. +

+
+
+ + + diff --git a/articles/2026/This-blog-wasn't-so-bad-after-all.html b/articles/2026/This-blog-wasn't-so-bad-after-all.html new file mode 100644 index 0000000..f0392cd --- /dev/null +++ b/articles/2026/This-blog-wasn't-so-bad-after-all.html @@ -0,0 +1,46 @@ + + + + + + + This blog wasn't so bad after all + + +
+

Bloggings

+ Back +
+ +
+
+

This blog wasn't so bad after all

+

2026-05-05

+

Smolweb, Fediverse and blogs

+

+ For several months I've been using Gopher exclusively, I find it's + minimalist raw text files appealing. +

+

+ It does have several drawbacks however. Recently I've been self + hosting my own fediverse instance at + social.wittamore.fr. +

+

+ Looking around for a fediverse blog, I realised that this html blog + wasn't so bad after all. +

+

+ It's work in progress, as I would like to have it + smolweb validated, and needs a + clean up. +

+
+
+ + + diff --git a/articles/2026/Updated-blogthis-script.html b/articles/2026/Updated-blogthis-script.html new file mode 100644 index 0000000..6bb2ad7 --- /dev/null +++ b/articles/2026/Updated-blogthis-script.html @@ -0,0 +1,38 @@ + + + + + + + + Updated blogthis script + + +
+

Bloggings

+ Back +
+
+
+

Updated blogthis script

+

2026-06-16

+

now with markdown

+

+ I've updated my blogthis script so I can write articles in markdown. +

+

+ It saves me having to write html in the articles and avoids errors. +

+

+ I've uploaded it to + my git server +

+
+
+ + + diff --git a/articles/2026/Updated-poem.html b/articles/2026/Updated-poem.html new file mode 100644 index 0000000..ad2a02d --- /dev/null +++ b/articles/2026/Updated-poem.html @@ -0,0 +1,45 @@ + + + + + + + + Updated poem + + +
+

Bloggings

+ Back +
+
+
+

Updated poem

+

2026-06-01

+

Existance is the prize itself

+

+ Sadly a childhood friend of one of my nieces recently died at 36 of a + brain tumour. I gave this poem a re-write. +

+
+Life is but a long farewell
+While clawing at eternity's shell
+No rhyme nor reason, no justice in sight 
+A frantic push against entropy's might
+This fleeting hare 
+Brings grave dispair
+But pointless is a daily dread
+Of closing circuit in our bed
+Here then gone, a spark in the night?
+Our passing through is never trite
+Best keep your malaise on the shelf
+Existance is the prize itself
+
+
+
+ + + diff --git a/articles/2026/Web-sites-that-work-well-in-Dillo.html b/articles/2026/Web-sites-that-work-well-in-Dillo.html new file mode 100644 index 0000000..42caa89 --- /dev/null +++ b/articles/2026/Web-sites-that-work-well-in-Dillo.html @@ -0,0 +1,60 @@ + + + + + + + + Web sites that work well in Dillo + + +
+

Bloggings

+ Back +
+
+
+

Web sites that work well in Dillo

+

2026-06-19

+

and other minimal browsers

+

+ I'm always looking for sane web sites. By sane I mean websites that + are not bloated with useless eye candy, nor have any ketamin fired + javascript. Sites that just want to display data in the form of easily + readable text with or without some images. +

+

+ There is an increasing number of web sites that do this, as it's a + normal backlash to the slop infested, obfuscated tar pit that the + internet has sadly become. I haven't found a single compendium of + sites, but I'll list some here. +

+

+ One of the things I find realy interesting are web proxies that render + sites into a saner version, like + Textise or + Textdot. I fact I'm wondering if I + could roll my own simple version using php and rdrview. +

+

+ This list incomplete list will be updated as I find them, or maybe you + dear reader might have suggestions? +

+

+ Directory of useful sites that work reasonably well on Dillo +

+

Wiby search engine

+

+ Text only web sites +

+
+
+ + + diff --git a/articles/2026/displaying-MATE-on-a-1366x768-screen.html b/articles/2026/displaying-MATE-on-a-1366x768-screen.html new file mode 100644 index 0000000..da49491 --- /dev/null +++ b/articles/2026/displaying-MATE-on-a-1366x768-screen.html @@ -0,0 +1,73 @@ + + + + + + + + displaying MATE on a 1366x768 screen + + +
+

Bloggings

+ Back +
+
+
+

displaying MATE on a 1366x768 screen

+

2026-06-15

+

xrandr to the rescue

+

+ Unfortunately several Ghostbsd MATE configuration windows are taller + than my Thinkpad X220's 768 pixels. I made both the top and bottom + panels autohide, but a better solution is to use xrandr: +

+
sudo pkg install xrandr
+

+ type xrandr to get the name of your display, in my case LVDS-1 on my + Thinkpad X220. +

+

To change 16:9 screen resolution, you can type:

+
+xrandr --output LVDS-1 --panning 1600x900  --scale 1.171x1.172
+

or

+
+xrandr --output LVDS-1 --panning 1920x1080 --scale 1.406x1.406
+

To get back to the original resolution type:

+
xrandr --output LVDS-1 --panning 1366x768 --scale 1x1
+

+ Here's a simple bash script that will rotate the screen resolution on + each call. I've added a MATE keyboard shortcut (CTRL+RIGHT) to run + this script. It is intended for my 1366x768 screen, so it will need + adapting for other screens. +

+
+#!/usr/bin/env bash
+        
+RES="$(xdpyinfo | grep dimensions | sed -r 's/^[^0-9]*([0-9]+x[0-9]+).*$/\1/')"
+# xdpy results can vary by 1 or two pixels 
+# so take the first two characters
+RES="${RES:0:2}"
+        
+case "$RES" in
+  "13")
+  xrandr --output LVDS-1 --panning 1600x900  --scale 1.171x1.172
+  ;;
+  "16")
+  xrandr --output LVDS-1 --panning 1920x1080 --scale 1.406x1.406
+  ;;
+  *)
+  xrandr --output LVDS-1 --panning 1366x768 --scale 1x1
+  ;;
+esac
+        
+

I doubt you can do that in Wayland.

+
+
+ + + diff --git a/articles/2026/semware-tse-editor.html b/articles/2026/semware-tse-editor.html new file mode 100644 index 0000000..441df10 --- /dev/null +++ b/articles/2026/semware-tse-editor.html @@ -0,0 +1,44 @@ + + + + + + + + semware-tse-editor + + +
+

Bloggings

+ Back +
+
+
+

semware-tse-editor

+

2026-06-13

+

A very interesting editor for DOS, Linux and Windows

+

+ semware editor screenshot +

+

+ I stumbled upon the #Semware editor (TSE) for #Linux, Windows and #Dos + and thought I'd share it here. Originally named Qedit in 1985, the + latest version is dated May 2026. I don't know about Windows (as I + haven't used it in years), but the Linux version is a pleasant CLI app + with many features, including it's own application language. The more + I look at it, the more I realise that it is a profound piece of + software. +

+

+ www.semware.com
+ Knud Vaneeded YT videos +

+
+
+ + + diff --git a/articles/2026/smolweb-validation-in-Dillo.html b/articles/2026/smolweb-validation-in-Dillo.html new file mode 100644 index 0000000..38e543d --- /dev/null +++ b/articles/2026/smolweb-validation-in-Dillo.html @@ -0,0 +1,57 @@ + + + + + + + + smolweb validation in Dillo + + +
+

Bloggings

+ Back +
+
+
+

smolweb validation in Dillo

+

2026-06-22

+

a simple page_action script

+

+ I added smolweb-validator as a page action in Dillo. It's a bit of a + pain as the validator is a java application. The action script is + brutal so far but it works. +

+

.dillo/actions/smolcheck.sh

+
#!/bin/sh
+
+#smolweb validator location
+smp="$HOME/src/smolweb-validator"
+
+# validate
+cd $smp
+output="$(java -cp "jsoup-1.21.2.jar:." SmolwebValidator $url --verbose)"
+echo "$output" | dilloc rawload
+
+

+ The action is added to ~/.dillo/dillorc and appears in Dillo's + right-click menu +

+
+page_action="Smolcheck:~/.dillo/actions/smolcheck.sh"
+
+

+ see + https://codeberg.org/smolweb/smolweb-validator +

+

 

+
+
+ + + diff --git a/articles/2026/upgrade-to-dwm-6.8.html b/articles/2026/upgrade-to-dwm-6.8.html new file mode 100644 index 0000000..1637f7c --- /dev/null +++ b/articles/2026/upgrade-to-dwm-6.8.html @@ -0,0 +1,49 @@ + + + + + + + + upgrade to dwm 6.8 + + +
+

Bloggings

+ Back +
+
+
+

upgrade to dwm 6.8

+

2026-06-29

+

dwm with some patches

+

+ I've been using dwm 6.5 for a while now, so I thought it was about + time I upgraded to dwm 6.8. +

+

+ Ideally I'd settle for a horizontal scrolling wm as my Thinkpad screen + is small, but after flirting with nwm I'm staying with dwm for now. +

+

+ I've added a few patches and shortcuts that make it easy for me to + circulate between windows and workspaces. I haven't added the xrdb + patch this time, instead I added the colorbar patch which nicely + colours the dwmblocks status bar and keeps things simple. Many of the + tiling patches I added to 6.5 have been left out as I just don't use + them. On a small 1366x768 screen splitting it renders it useless so + instead I use the monocle layout by default and switch workspaces with + super+right and left, and switch windows with super+up and down. +

+

+ It's up on my git server if you are interested. +

+
+
+ + + diff --git a/articles/test.html b/articles/test.html new file mode 100755 index 0000000..461b48a --- /dev/null +++ b/articles/test.html @@ -0,0 +1,3 @@ + + +

 

§

diff --git a/deltachat.html b/deltachat.html new file mode 100644 index 0000000..4443a94 --- /dev/null +++ b/deltachat.html @@ -0,0 +1,22 @@ + + + + + + + + + Deltachat Contact + + +

+ Contact me via Deltachat +

+ + diff --git a/favicon-32x32.png b/favicon-32x32.png new file mode 100644 index 0000000..20f3fcb Binary files /dev/null and b/favicon-32x32.png differ diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..960b017 Binary files /dev/null and b/favicon.ico differ diff --git a/feeds/fediverse-philip/feed b/feeds/fediverse-philip/feed new file mode 100644 index 0000000..0c5f064 --- /dev/null +++ b/feeds/fediverse-philip/feed @@ -0,0 +1 @@ +https://social.wittamore.fr/philip.rss diff --git a/feeds/fediverse-philip/title b/feeds/fediverse-philip/title new file mode 100644 index 0000000..0ad535d --- /dev/null +++ b/feeds/fediverse-philip/title @@ -0,0 +1 @@ +fediverse - philip@social.wittamore.fr diff --git a/feeds/gemini-vigrey.com/feed b/feeds/gemini-vigrey.com/feed new file mode 100644 index 0000000..cb2806e --- /dev/null +++ b/feeds/gemini-vigrey.com/feed @@ -0,0 +1 @@ +gemini://vigrey.com/journal/feed.rss diff --git a/feeds/gemini-vigrey.com/index.php b/feeds/gemini-vigrey.com/index.php new file mode 100644 index 0000000..a3a7d91 --- /dev/null +++ b/feeds/gemini-vigrey.com/index.php @@ -0,0 +1,32 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : gemini://vigrey.com/journal/feed.rss (10 Items)

+ + + + + + + + + + + +
2025-12-31 2025 Comes to a Close
2025-11-19 My First Game for the Playdate - FlatCube
2025-10-06 Winter Nights 2025
2025-09-23 Happy Equinox!
2025-09-14 Sundial Created - Oaths Update 3
2025-09-10 I Ordered a Playdate Console
2025-08-09 The Magic That Scares Me
2025-07-13 Using FCEUX to Play this Proof-of-Concept NES Game is a User Input - Expanding on PoC||GTFO 18:4
2025-05-28 No Online June
2025-05-21 A Call for Help - ZIM File Expertise
+
+ + \ No newline at end of file diff --git a/feeds/gemini-vigrey.com/title b/feeds/gemini-vigrey.com/title new file mode 100644 index 0000000..00d6d69 --- /dev/null +++ b/feeds/gemini-vigrey.com/title @@ -0,0 +1 @@ +gemini - vigrey.com diff --git a/feeds/gopher-cosmic-voyage/feed b/feeds/gopher-cosmic-voyage/feed new file mode 100644 index 0000000..a51c1ef --- /dev/null +++ b/feeds/gopher-cosmic-voyage/feed @@ -0,0 +1 @@ +gopher://cosmic.voyage/0/rss.xml diff --git a/feeds/gopher-cosmic-voyage/index.php b/feeds/gopher-cosmic-voyage/index.php new file mode 100644 index 0000000..8f99cc1 --- /dev/null +++ b/feeds/gopher-cosmic-voyage/index.php @@ -0,0 +1,41 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : gopher://cosmic.voyage/0/rss.xml (19 Items)

+ + + + + + + + + + + + + + + + + + + + +
2025-12-17 APETLUNE ABYSS - Mission Log
2025-11-20 YZJ-ZX-17 - Sofia Petrova 1
2025-11-04 Marauder - SUPPLY_UPDATE_009
2025-10-25 DO-28631-S-89594 - Transmission 09732
2025-10-16 AUDIO FREIGHTER 1 - LOGDATE_3
2025-10-12 Marauder - RE: AUDIO FREIGHTER 1
2025-10-11 AUDIO FREIGHTER 1 - LOGDATE_2
2025-10-11 AUDIO FREIGHTER 1 - RE: APETLUNE CORPORATION
2025-10-11 Apetlune Corporation - RE: Audio Freighter One
2025-10-10 Apetlune Corporation - RE: Audio Freighter One
2025-10-10 AUDIO FREIGHTER 1 - RE: MARAUDER
2025-10-10 Marauder - RE: AUDIO FREIGHTER 1
2025-10-10 AUDIO FREIGHTER 1 - LOGDATE_1
2025-10-09 AUDIO FREIGHTER 1 - LOGDATE_0
2025-10-09 Sagan - 1760025340 - First Transmission
2025-10-07 HRVST Farming Unit-59 - Status update
2025-10-02 Aegis - Aegis - QEC847293652841 - 7.12.1123
2025-09-25 HRVST Farming Unit-59 - Unusual findings
2025-05-15 S-99 Tekniko - COMM_02.TXT
+
+ + \ No newline at end of file diff --git a/feeds/gopher-cosmic-voyage/title b/feeds/gopher-cosmic-voyage/title new file mode 100644 index 0000000..9984e8b --- /dev/null +++ b/feeds/gopher-cosmic-voyage/title @@ -0,0 +1 @@ +gopher - Cosmic Voyage diff --git a/feeds/gopher-lfa/feed b/feeds/gopher-lfa/feed new file mode 100644 index 0000000..d504924 --- /dev/null +++ b/feeds/gopher-lfa/feed @@ -0,0 +1 @@ +gopher://lucio.albenga.es/0/lfa/en/phlog/rss.xml diff --git a/feeds/gopher-lfa/index.php b/feeds/gopher-lfa/index.php new file mode 100644 index 0000000..d0d8cfd --- /dev/null +++ b/feeds/gopher-lfa/index.php @@ -0,0 +1,32 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : gopher://lucio.albenga.es/0/lfa/en/phlog/rss.xml (10 Items)

+ + + + + + + + + + + +
2026-01-17 Why I switched back from EMACS to VIM?
2025-12-14 Liferea Now Supports Gopher
2025-12-12 Gopher Archaeology: Interview With Mark P. McCahill
2025-12-11 Four Months on Christmas
2025-12-01 Setting Up a GCC Toolchain For Programming Arduino In Plain C
2025-11-27 Setting Up The Fn Keys of a Thinkpad T430 on FreeBSD
2025-11-26 Changes On My Email
2025-11-16 100% "Free as in Freedom" Versions of Devuan and Debian
2025-11-14 Gopher Archaeology
2025-11-13 Ben Collver's Coprolit For Gopher & Fossil SCM
+
+ + \ No newline at end of file diff --git a/feeds/gopher-lfa/title b/feeds/gopher-lfa/title new file mode 100644 index 0000000..b715583 --- /dev/null +++ b/feeds/gopher-lfa/title @@ -0,0 +1 @@ +gopher - LFA's phlog diff --git a/feeds/gopher-mattos/feed b/feeds/gopher-mattos/feed new file mode 100644 index 0000000..6e29926 --- /dev/null +++ b/feeds/gopher-mattos/feed @@ -0,0 +1 @@ +gopher://box.matto.nl/0/gopherindex.rss diff --git a/feeds/gopher-mattos/index.php b/feeds/gopher-mattos/index.php new file mode 100644 index 0000000..9ecad01 --- /dev/null +++ b/feeds/gopher-mattos/index.php @@ -0,0 +1,27 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : gopher://box.matto.nl/0/gopherindex.rss (5 Items)

+ + + + + + +
2026-01-23 Lisp is fun
2026-01-05 Digital ad spending USD 710 billion in 2026
2026-01-03 Text-only websites
2026-01-02 Books I enjoyed reading in 2025
2026-01-01 Januari Questions of Christina
+
+ + \ No newline at end of file diff --git a/feeds/gopher-mattos/title b/feeds/gopher-mattos/title new file mode 100644 index 0000000..767f557 --- /dev/null +++ b/feeds/gopher-mattos/title @@ -0,0 +1 @@ +gopher - Mattos Phlog diff --git a/feeds/gopher-wittamore/feed b/feeds/gopher-wittamore/feed new file mode 100644 index 0000000..2f381c1 --- /dev/null +++ b/feeds/gopher-wittamore/feed @@ -0,0 +1 @@ +gopher://wittamore.fr/0/phlog/rss.xml diff --git a/feeds/gopher-wittamore/index.html b/feeds/gopher-wittamore/index.html new file mode 100644 index 0000000..8d82d9e --- /dev/null +++ b/feeds/gopher-wittamore/index.html @@ -0,0 +1,14 @@ + +
+ + + + + + + Toothless +
+ + \ No newline at end of file diff --git a/feeds/gopher-wittamore/index.php b/feeds/gopher-wittamore/index.php new file mode 100644 index 0000000..410faf4 --- /dev/null +++ b/feeds/gopher-wittamore/index.php @@ -0,0 +1,27 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : gopher://spike.nagatha.fr/0/phlog/rss.xml (5 Items)

+ + + + + + +
2026-01-24 A gopher server in Bash
2026-01-23 Xdm simple theme for dinit
2026-01-23 Back to DWM
2026-01-19 Bash language server for Micro editor
2026-01-01 Using Xfce4 with Devilspie2
+
+ + \ No newline at end of file diff --git a/feeds/gopher-wittamore/title b/feeds/gopher-wittamore/title new file mode 100644 index 0000000..c8818b6 --- /dev/null +++ b/feeds/gopher-wittamore/title @@ -0,0 +1 @@ +gopher - Phil's Phlog diff --git a/feeds/http-20minutes/feed b/feeds/http-20minutes/feed new file mode 100644 index 0000000..d2c3ed8 --- /dev/null +++ b/feeds/http-20minutes/feed @@ -0,0 +1,2 @@ +https://www.20minutes.fr/feeds/rss-bretagne.xml + diff --git a/feeds/http-20minutes/index.php b/feeds/http-20minutes/index.php new file mode 100644 index 0000000..ed3d92d --- /dev/null +++ b/feeds/http-20minutes/index.php @@ -0,0 +1,42 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : https://www.20minutes.fr/feeds/rss-bretagne.xml (20 Items)

+ + + + + + + + + + + + + + + + + + + + + +
2026-01-25 Municipales 2026 : Responsabilités difficiles, peu d’habitants… Des villes vont-elles finir sans candidats ?
2026-01-25 Météo-France : Finistère, Morbihan et Ille-et-Vilaine restent en vigilance orange crues
2026-01-24 Bretagne : Elle appelle depuis le coffre au secours, quatre hommes incarcérés pour enlèvement
2026-01-24 Dépression Ingrid : La Bretagne reste en alerte vigilance aux crues
2026-01-23 En vigilance orange aux crues, la Bretagne redoute l’arrivée de la tempête Ingrid
2026-01-23 Bretagne : Caravanes, maisons de luxe, armes… Une fraude à la TVA d’un million d’euros démantelée
2026-01-23 Bretagne : Accro à la cocaïne, une femme frappe son mari qui l’avait surprise au lit avec son dealer
2026-01-23 Rennes : « Je n’avais pas envie de rentrer »… L’évadé du planétarium écope de deux ans de prison (de plus)
2026-01-23 En vigilance orange aux crues, la Bretagne redoute l’arrivée de la tempête Ingrid
2026-01-22 Bateau à la dérive en Méditerranée : Après une opération de trente heures, un seul naufragé a pu être récupéré
2026-01-22 Bretagne : Route effondrée, maisons inondées… Vigilance orange dans le Finistère, l'Ille-et-Vilaine et le Morbihan
2026-01-21 Mort d’Allan Lambin à Saint-Malo : « C’est compliqué la nuit en garde à vue », défendent les avocats des policiers
2026-01-21 Morbihan : Un propriétaire condamné après s’être introduit chez sa locataire en son absence
2026-01-21 Procès Le Scouarnec : Environ 180 victimes fixées sur leurs indemnisations (que l’ancien chirurgien ne pourra pas payer)
2026-01-21 Bretagne : Les deux religieuses tuées roulaient à 15 km/h quand elles ont été percutées
2026-01-21 Météo : Deux départements de Bretagne en alerte orange pluie-inondation
2026-01-20 Mort d’Allan Lambin en garde à vue : « Ils ont commis des fautes graves »… Du sursis requis contre les quatre policiers
2026-01-20 Affaire Le Scouarnec : La justice reconnaît pour la première fois un préjudice de crime sériel aux victimes
2026-01-20 Mort d’Allan Lambin au commissariat de Saint-Malo : Une intervention plus rapide aurait-elle permis de le sauver ?
2026-01-20 Intempéries : L’Aude et l’Hérault restent en alerte orange, le Morbihan et le Finistère le seront mercredi
+
+ + \ No newline at end of file diff --git a/feeds/http-20minutes/title b/feeds/http-20minutes/title new file mode 100644 index 0000000..c2622ac --- /dev/null +++ b/feeds/http-20minutes/title @@ -0,0 +1 @@ +http - 20minutes Bretagne diff --git a/feeds/http-adele/feed b/feeds/http-adele/feed new file mode 100644 index 0000000..7c3ce3b --- /dev/null +++ b/feeds/http-adele/feed @@ -0,0 +1 @@ +https://adele.pages.casa/md/blog/rss.xml diff --git a/feeds/http-adele/index.php b/feeds/http-adele/index.php new file mode 100644 index 0000000..e923cbd --- /dev/null +++ b/feeds/http-adele/index.php @@ -0,0 +1,28 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : https://adele.pages.casa/md/blog/rss.xml (6 Items)

+ + + + + + + +
2025-12-18 A mailbox for Christmas
2025-11-04 Preparing a new mail and xmpp service
2025-09-01 Excited to announce writes.casa
2025-08-12 The difference between "smolweb" and "small web"
2025-08-04 Why I prefer human-readable file formats
2025-07-19 The charm of ASCII smileys :-)
+
+ + \ No newline at end of file diff --git a/feeds/http-adele/title b/feeds/http-adele/title new file mode 100644 index 0000000..8185fd5 --- /dev/null +++ b/feeds/http-adele/title @@ -0,0 +1 @@ +http - Adele diff --git a/feeds/http-bloggings/feed b/feeds/http-bloggings/feed new file mode 100644 index 0000000..f13a8ee --- /dev/null +++ b/feeds/http-bloggings/feed @@ -0,0 +1 @@ +https://wittamore.fr/rss.xml diff --git a/feeds/http-bloggings/title b/feeds/http-bloggings/title new file mode 100644 index 0000000..f3719a4 --- /dev/null +++ b/feeds/http-bloggings/title @@ -0,0 +1 @@ +This blog diff --git a/feeds/http-eff/feed b/feeds/http-eff/feed new file mode 100644 index 0000000..9518f49 --- /dev/null +++ b/feeds/http-eff/feed @@ -0,0 +1 @@ +https://www.eff.org/rss/updates.xml diff --git a/feeds/http-eff/index.php b/feeds/http-eff/index.php new file mode 100644 index 0000000..703d15a --- /dev/null +++ b/feeds/http-eff/index.php @@ -0,0 +1,52 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : https://www.eff.org/rss/updates.xml (30 Items)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
2026-01-24 Search Engines, AI, And The Long Fight Over Fair Use
2026-01-23 Rent-Only Copyright Culture Makes Us All Worse Off
2026-01-21 Copyright Kills Competition
2026-01-21 Copyright Should Not Enable Monopoly
2026-01-20 Statutory Damages: The Fuel of Copyright-based Censorship
2026-01-20 💾 The Worst Data Breaches of 2025—And What You Can Do | EFFector 38.1
2026-01-20 EFF Joins Internet Advocates Calling on the Iranian Government to Restore Full Internet Connectivity
2026-01-16 EFF Condemns FBI Search of Washington Post Reporter’s Home
2026-01-16 EFF to California Appeals Court: First Amendment Protects Journalist from Tech Executive’s Meritless Lawsuit
2026-01-16 Baton Rouge Acquires a Straight-Up Military Surveillance Drone
2026-01-16 Congress Wants To Hand Your Parenting to Big Tech
2026-01-15 Report: ICE Using Palantir Tool That Feeds On Medicaid Data
2026-01-14 So, You’ve Hit an Age Gate. What Now?
2026-01-08 How Hackers Are Fighting Back Against ICE
2026-01-07 ICE Is Going on a Surveillance Shopping Spree
2026-01-06 EFFecting Change: The Human Cost of Online Age Verification
2026-01-06 The Homeland Security Spending Trail: How to Follow the Money Through U.S. Government Databases
2026-01-02 The Year States Chose Surveillance Over Safety: 2025 in Review
2026-01-02 Surveillance Self-Defense: 2025 Year in Review
2025-12-31 Congress's Crusade to Age Gate the Internet: 2025 in Review
2025-12-31 States Tried to Censor Kids Online. Courts, and EFF, Mostly Stopped Them: 2025 in Review
2025-12-30 Site Blocking Laws Will Always Be a Bad Idea: 2025 in Review
2025-12-30 EFF's Investigations Expose Flock Safety's Surveillance Abuses: 2025 in Review
2025-12-30 Fighting Renewed Attempts to Make ISPs Copyright Cops: 2025 in Review
2025-12-29 Operations Security (OPSEC) Trainings: 2025 in Review
2025-12-29 EFF in the Press: 2025 in Review
2025-12-29 Drone as First Responder Programs: 2025 in Review
2025-12-28 EFFector Audio Speaks Up for Our Rights: 2025 Year in Review
2025-12-28 Procurement Power—When Cities Realized They Can Just Say No: 2025 in Review
2025-12-28 Defending Encryption in the U.S. and Abroad: 2025 in Review
+
+ + \ No newline at end of file diff --git a/feeds/http-eff/title b/feeds/http-eff/title new file mode 100644 index 0000000..531a8bb --- /dev/null +++ b/feeds/http-eff/title @@ -0,0 +1 @@ +http - Electronic Frontier Foundation diff --git a/feeds/http-petergartner/feed b/feeds/http-petergartner/feed new file mode 100644 index 0000000..f4742b7 --- /dev/null +++ b/feeds/http-petergartner/feed @@ -0,0 +1 @@ +https://petergarner.net/feed.rss diff --git a/feeds/http-petergartner/index.php b/feeds/http-petergartner/index.php new file mode 100644 index 0000000..8e46123 --- /dev/null +++ b/feeds/http-petergartner/index.php @@ -0,0 +1,37 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : https://petergarner.net/feed.rss (15 Items)

+ + + + + + + + + + + + + + + + +
2025-12-28 The Derbyshire Mini-Bash
2025-10-26 Mission Northumberland - Epilogue
2025-10-08 Mission Northumberland - Wednesday
2025-10-07 Mission Northumberland - Tuesday
2025-10-06 Mission Northumberland - Monday
2025-10-05 Mission Northumberland - Sunday
2025-07-14 The Times They Are a-changing - Part 3
2025-05-27 The Times They Are a-changing - Part 2
2025-05-26 Walking the Walk
2025-05-21 The Times They Are a-changing - Part 1
2025-01-30 Q1 Update - better late than never
2024-10-17 We need to talk about bread
2024-09-26 Drifting Back to the Dark Side
2024-01-01 Sod Off 2023 and Welcome 2024
2023-07-27 Back on the road again
+
+ + \ No newline at end of file diff --git a/feeds/http-petergartner/title b/feeds/http-petergartner/title new file mode 100644 index 0000000..4e39fc2 --- /dev/null +++ b/feeds/http-petergartner/title @@ -0,0 +1,2 @@ +http - PeterGartner + diff --git a/feeds/http-pluralistic/feed b/feeds/http-pluralistic/feed new file mode 100644 index 0000000..e3813a0 --- /dev/null +++ b/feeds/http-pluralistic/feed @@ -0,0 +1 @@ +https://pluralistic.net/feed/ diff --git a/feeds/http-pluralistic/index.php b/feeds/http-pluralistic/index.php new file mode 100644 index 0000000..bba6701 --- /dev/null +++ b/feeds/http-pluralistic/index.php @@ -0,0 +1,32 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : https://pluralistic.net/feed/ (10 Items)

+ + + + + + + + + + + +
2026-01-22 Pluralistic: The petty (but undeniable) delights of cultivating unoptimizability as a habit (22 Jan 2026)
2026-01-21 Pluralistic: Google's AI pricing plan (21 Jan 2026)
2026-01-20 Pluralistic: AI is how bosses wage war on "professions" (20 Jan 2026)
2026-01-19 Pluralistic: Social media without socializing (19 Jan 2026)
2026-01-17 Pluralistic: The world needs an Ireland for disenshittification (17 Jan 2026)
2026-01-16 Pluralistic: Catch this! (16 Jan 2026)
2026-01-15 Pluralistic: How the Light Gets In (15 Jan 2026)
2026-01-14 Pluralistic: It's not normal (14 Jan 2026)
2026-01-13 Pluralistic: Sorry, eh (13 Jan 2026)
2026-01-12 Pluralistic: A winning trade war strategy for Canada (11 Jan 2026)
+
+ + \ No newline at end of file diff --git a/feeds/http-pluralistic/title b/feeds/http-pluralistic/title new file mode 100644 index 0000000..9fafaae --- /dev/null +++ b/feeds/http-pluralistic/title @@ -0,0 +1 @@ +http - Pluralistic diff --git a/feeds/http-scitechdaily/feed b/feeds/http-scitechdaily/feed new file mode 100644 index 0000000..8b66ffe --- /dev/null +++ b/feeds/http-scitechdaily/feed @@ -0,0 +1 @@ +https://scitechdaily.com/feed/ diff --git a/feeds/http-scitechdaily/title b/feeds/http-scitechdaily/title new file mode 100644 index 0000000..65dfc09 --- /dev/null +++ b/feeds/http-scitechdaily/title @@ -0,0 +1 @@ +http - SciTechDaily diff --git a/feeds/http-smolweb/feed b/feeds/http-smolweb/feed new file mode 100644 index 0000000..18a4820 --- /dev/null +++ b/feeds/http-smolweb/feed @@ -0,0 +1 @@ +https://smolweb.org/atom.xml diff --git a/feeds/http-smolweb/index.php b/feeds/http-smolweb/index.php new file mode 100644 index 0000000..ab0a49b --- /dev/null +++ b/feeds/http-smolweb/index.php @@ -0,0 +1,33 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : https://smolweb.org/atom.xml (11 Items)

+ + + + + + + + + + + + +
2025-11-06 Adding semantic table elements
2025-08-17 Smolweb-validator
2025-04-20 `w.` is the preferred subdomain for smolweb sites
2025-04-20 New smolweb symbol could be a lowercase w between angle brackets in monospaced font
2024-07-22 Simple.css: a minimalist css framework
2024-06-15 Allowing JavaScript in smolweb
2023-12-23 Adding figure and figcaption tags
2023-10-16 Wiby, a search engine for the smolweb
2023-10-11 A smolweb bash generator
2023-10-09 First post
2023-10-09 An other Small Web definition
+
+ + \ No newline at end of file diff --git a/feeds/http-smolweb/title b/feeds/http-smolweb/title new file mode 100644 index 0000000..b44d8c8 --- /dev/null +++ b/feeds/http-smolweb/title @@ -0,0 +1 @@ +http - Smolweb diff --git a/feeds/index.php b/feeds/index.php new file mode 100644 index 0000000..c524291 --- /dev/null +++ b/feeds/index.php @@ -0,0 +1,42 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

 last update: 2026-01-25 11:01:01

+ + + + + + + + + + + + + + + + + + + + + +
📁gemini - vigrey.com
📁gopher - Cosmic Voyage
📁gopher - LFA's phlog
📁gopher - Mattos Phlog
📁gopher - Phil's Phlog
📁http - Adele
📁http - Electronic Frontier Foundation
📁http - PeterGartner
📁http - Pluralistic
📁http - Smolweb
📁news - 20minutes Bretagne
📁news - Euronews
📁news - The Guardian Europe
📁news - New Scientist
📁youtube - Bread on Penguins
📁youtube - Mental Outlaw
📁youtube - Naomi Brockwell
📁youtube - Veronica explains
📁youtube - You suck at programming
+
+ + \ No newline at end of file diff --git a/feeds/news-euronews/feed b/feeds/news-euronews/feed new file mode 100644 index 0000000..917e1da --- /dev/null +++ b/feeds/news-euronews/feed @@ -0,0 +1 @@ +https://www.euronews.com/rss diff --git a/feeds/news-euronews/index.php b/feeds/news-euronews/index.php new file mode 100644 index 0000000..c1156a1 --- /dev/null +++ b/feeds/news-euronews/index.php @@ -0,0 +1,52 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : https://www.euronews.com/rss (30 Items)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
2026-01-25 Who uses AI in Europe - and where it’s still taboo?
2026-01-25 Women lose 75 million years of healthy life annually – yet receive only 6% of health funding
2026-01-25 This year could be a surprisingly affordable time to see Japan’s cherry blossoms
2026-01-25 Latest news bulletin | January 25th, 2026 – Morning
2026-01-25 Wolf pack forces Italian airport to close: How this region is struggling with coexistence
2026-01-24 Hundreds gather at memorial for Alex Pretti, the man killed by Border Patrol officer in Minneapolis
2026-01-24 Video shows confrontation between federal agents leading up to a fatal shooting in Minneapolis
2026-01-24 Trump threatens Canada with 100% tariff over its trade deal with China
2026-01-24 Latest news bulletin | January 24th, 2026 – Evening
2026-01-24 Federal officers shoot man dead in Minneapolis amid immigration crackdown
2026-01-24 Italy recalls Swiss ambassador after Crans-Montana bar fire suspect is released
2026-01-24 Thousands rally and hundreds of businesses close in protest against ICE presence in Minnesota
2026-01-24 United States: Empty shelves as shoppers race to stock up before winter storm
2026-01-24 Latest news bulletin | January 24th, 2026 – Midday
2026-01-24 Ukraine facing emergency power cuts after Russian strikes hit energy infrastructure
2026-01-24 Green electricity: Which EU countries are using the most?
2026-01-24 More talks expected next week after Ukraine, Russia and US conclude Abu Dhabi meeting
2026-01-24 Denmark PM Mette Frederiksen visits Greenland for 'show of support'
2026-01-24 Straight from the heart: 'Love Letters' exhibition examines 500 years of emotions
2026-01-24 AI sovereignty requires partnerships, not isolation, says World Economic Forum’s AI chief
2026-01-24 The major new travel hubs that are going to change the way we fly
2026-01-24 How Europeans closed ranks to defend Greenland against Trump
2026-01-24 Brazilian mosquitoes grow increasing appetite for human blood as Atlantic forest shrinks
2026-01-24 Where is Europe’s cheapest pizza? Prices rise as diners feel the squeeze
2026-01-24 Latest news bulletin | January 24th, 2026 – Morning
2026-01-23 Russia releases video showing Air Forces patrolling the Baltic Sea in 'planned flights'
2026-01-23 'Electrifying everything' is key to Europe's future, IEA chief tells Euronews at Davos
2026-01-23 Italy-Germany summit: Meloni and Merz push for an autonomous, competitive Europe
2026-01-23 Greenland 'very happy with the EU' for support in face of Trump takeover threats, politician says
2026-01-23 Starmer calls Trump's remarks about Nato troops in Afghanistan 'insulting', urges apology
+
+ + \ No newline at end of file diff --git a/feeds/news-euronews/title b/feeds/news-euronews/title new file mode 100644 index 0000000..4f273db --- /dev/null +++ b/feeds/news-euronews/title @@ -0,0 +1 @@ +news - Euronews diff --git a/feeds/news-guardian-europe/feed b/feeds/news-guardian-europe/feed new file mode 100644 index 0000000..fa9096d --- /dev/null +++ b/feeds/news-guardian-europe/feed @@ -0,0 +1 @@ +https://www.theguardian.com/europe/rss diff --git a/feeds/news-guardian-europe/index.php b/feeds/news-guardian-europe/index.php new file mode 100644 index 0000000..67687c1 --- /dev/null +++ b/feeds/news-guardian-europe/index.php @@ -0,0 +1,52 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : https://www.theguardian.com/europe/rss (30 Items)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
2026-01-25 Australian Open 2026: De Minaur v Bublik, plus Zverev and Medvedev in action – live
2026-01-25 Arsenal v Manchester United buildup, WSL action and Hearts v Celtic – matchday live
2026-01-25 Minneapolis shooting: parents of Alex Pretti say Trump officials are telling ‘sickening lies’
2026-01-25 Brooklyn Beckham and Prince Harry are the canaries in the coalmine. The children of Instagram will be next | Rhiannon Lucy Cosslett
2026-01-25 ‘We cut through the online ocean of advice’: the rise of adult sleep coaching
2026-01-25 Witnesses contradict Trump officials’ account of killing – as it happened
2026-01-25 Fitness fraud: gym goers warned over fake deals on memberships and personal trainers
2026-01-25 The best women’s boots for winter: 24 favourites, from knee-high to ankle
2026-01-25 10 of the best retreats in Europe to soothe mind, body and soul
2026-01-25 Jay Vine recovers from kangaroo crash to win Tour Down Under for second time
2026-01-25 Carlos Alcaraz overcomes spirited Tommy Paul to reach Australian Open quarter-finals without dropping a set
2026-01-25 Dingoes on Australia’s K’gari island to be euthanised after tragic death of Canadian tourist Piper James
2026-01-25 Trump’s wrecking ball pushes US allies closer to China
2026-01-25 AI-generated British schoolgirl becomes far-right social media meme
2026-01-25 In Somerset, I found glorious proof that the UK can build great council houses. So what is holding us back? | John Harris
2026-01-25 ‘I was probably just as lost as my callers’: my six months as a telephone psychic
2026-01-25 Tin Roof Cafe, Maldon, Essex: ‘Come for topsoil, stay for the shortbread’ – review
2026-01-25 Democratic congressman punched in racist attack at Sundance film festival
2026-01-25 Alex Pretti did not brandish gun, witnesses say in sworn testimony
2026-01-25 Trump news at a glance: groundswell of anger at second fatal shooting by federal agents in weeks
2026-01-25 Josephine review – Channing Tatum is a knockout in shattering drama of lost innocence
2026-01-25 Alex Honnold successfully free solos Taipei 101 in live Netflix climb
2026-01-25 Worst heatwave since Black Saturday forecast for Victoria as WA faces Tropical Cyclone Luana
2026-01-25 Large protests spread across US after Alex Pretti fatally shot by federal agents
2026-01-25 Video contradicts Trump’s claim man killed in Minneapolis was a ‘gunman’
2026-01-25 Ukraine war briefing: Kyiv and Moscow set to hold more face-to-face talks as US hails ‘big step’ forward
2026-01-25 Myanmar election enters final stage amid airstrikes and exclusions
2026-01-24 Protesters supporting Palestine Action hunger striker arrested outside prison
2026-01-24 European football: Augsburg fight back to end Bayern Munich’s unbeaten run
2026-01-24 Alex Pretti killing in Minneapolis by federal agents: what we know so far
+
+ + \ No newline at end of file diff --git a/feeds/news-guardian-europe/title b/feeds/news-guardian-europe/title new file mode 100644 index 0000000..c7568c1 --- /dev/null +++ b/feeds/news-guardian-europe/title @@ -0,0 +1,2 @@ +news - The Guardian Europe + diff --git a/feeds/youtube-mental-outlaw/feed b/feeds/youtube-mental-outlaw/feed new file mode 100644 index 0000000..b067cb2 --- /dev/null +++ b/feeds/youtube-mental-outlaw/feed @@ -0,0 +1 @@ +https://www.youtube.com/feeds/videos.xml?playlist_id=UULF7YOGHUfC1Tb6E4pudI9STA diff --git a/feeds/youtube-mental-outlaw/index.php b/feeds/youtube-mental-outlaw/index.php new file mode 100644 index 0000000..81f6c90 --- /dev/null +++ b/feeds/youtube-mental-outlaw/index.php @@ -0,0 +1,37 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : https://www.youtube.com/feeds/videos.xml?playlist_id=UULF7YOGHUfC1Tb6E4pudI9STA (15 Items)

+ + + + + + + + + + + + + + + + +
2026-01-24 Photoshop Finally Works on Linux
2026-01-17 TuxMate Makes Linux Desktop Setup a Breeze
2026-01-05 CachyOS Is Coming For Your Server
2025-12-25 When You Forget To Redact The Epstein Files
2025-12-16 Desktop Linux Just Keeps Winning
2025-12-07 Jolla Phone - A Modern Linux Smart Phone
2025-12-04 Linux Shell Tricks For Managing Files
2025-11-27 The Best Desktop Linux For Windows Users
2025-11-22 Cloudflare Breaks the Internet (again)
2025-11-18 Based Developer Makes AirPods Open Source
2025-11-14 Can Crowd Funded Tech Support Fix a Linux Sound Bug?
2025-11-08 China Is Forcing People To Watch Ads for Free Toilet Paper
2025-11-02 When the Surveillance State Falsely Accuses You
2025-10-31 Rust Core Utils Are Breaking Ubuntu
2025-10-23 Desktop Linux is Under Attack
+
+ + \ No newline at end of file diff --git a/feeds/youtube-mental-outlaw/title b/feeds/youtube-mental-outlaw/title new file mode 100644 index 0000000..67ffe3a --- /dev/null +++ b/feeds/youtube-mental-outlaw/title @@ -0,0 +1 @@ +youtube - Mental Outlaw diff --git a/feeds/youtube-naomi-brockwell/feed b/feeds/youtube-naomi-brockwell/feed new file mode 100644 index 0000000..47951e2 --- /dev/null +++ b/feeds/youtube-naomi-brockwell/feed @@ -0,0 +1 @@ +https://www.youtube.com/feeds/videos.xml?channel_id=UCSuHzQ3GrHSzoBbwrIq3LLA diff --git a/feeds/youtube-naomi-brockwell/index.php b/feeds/youtube-naomi-brockwell/index.php new file mode 100644 index 0000000..33cd595 --- /dev/null +++ b/feeds/youtube-naomi-brockwell/index.php @@ -0,0 +1,37 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : https://www.youtube.com/feeds/videos.xml?channel_id=UCSuHzQ3GrHSzoBbwrIq3LLA (15 Items)

+ + + + + + + + + + + + + + + + +
2026-01-16 Using PGP With Proton
2026-01-01 3 Privacy Fixes Most People Never Make
2025-12-18 He Built a Privacy Tool. Now He’s Going to Prison.
2025-12-05 They Asked For My Name. I Said No.
2025-11-14 Why You Need To Reboot More Often
2025-10-24 The Creepy Cameras On Every Corner
2025-10-03 How Surveillance Turns You Into a Puppet of the Internet
2025-09-12 Why I Don't Use the Mac App Store (and What I Use Instead)
2025-08-22 This is the moment the internet changed forever
2025-08-01 Setting up pseudonymous social media
2025-07-11 She Said "Don’t Write That Down." The Doctor Did Anyway.
2025-06-20 Your TV Is Spying On You and Taking Screenshots
2025-05-30 Think before you share
2025-05-09 How To Make Your Mac PRIVATE
2025-04-18 Browser Extensions are DANGEROUS
+
+ + \ No newline at end of file diff --git a/feeds/youtube-naomi-brockwell/title b/feeds/youtube-naomi-brockwell/title new file mode 100644 index 0000000..864dc4c --- /dev/null +++ b/feeds/youtube-naomi-brockwell/title @@ -0,0 +1 @@ +youtube - Naomi Brockwell diff --git a/feeds/youtube-veronica-explains/feed b/feeds/youtube-veronica-explains/feed new file mode 100644 index 0000000..fbf63c6 --- /dev/null +++ b/feeds/youtube-veronica-explains/feed @@ -0,0 +1 @@ +https://www.youtube.com/feeds/videos.xml?channel_id=UCMiyV_Ib77XLpzHPQH_q0qQ diff --git a/feeds/youtube-veronica-explains/index.php b/feeds/youtube-veronica-explains/index.php new file mode 100644 index 0000000..f12aef3 --- /dev/null +++ b/feeds/youtube-veronica-explains/index.php @@ -0,0 +1,37 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : https://www.youtube.com/feeds/videos.xml?channel_id=UCMiyV_Ib77XLpzHPQH_q0qQ (15 Items)

+ + + + + + + + + + + + + + + + +
2026-01-05 Linux on a Mac (it's not going back)
2025-11-01 I stream nothing, and I am happy.
2025-10-01 Using rsync for backups, because it's not shiny and new
2025-09-06 Linux Mom prepping for the LUG like it's 2005
2025-09-05 So Action Retro, Taylor and Amy, and I just released an album
2025-08-26 Livestream: installing a VM inside a VM inside a VM because chat told me to
2025-07-28 My Commodore 64 has an HDMI port now.
2025-06-17 The modern web sucks. My band's website doesn't.
2025-05-26 I make all my videos using Linux. Here's how.
2025-04-26 45Drives hasn't sent me any hardware and I still think they're neat
2025-03-26 VimWiki: I write my notes in the terminal, like a monster
2025-03-03 How to install Debian Testing... and why you might not want to
2025-01-31 Clonezilla: disk cloning, but it's easier than dd
2025-01-05 Have you ever used the "column" command in Linux?
2024-12-24 Linux Q/A: desktop environments (Ask Veronica)
+
+ + \ No newline at end of file diff --git a/feeds/youtube-veronica-explains/title b/feeds/youtube-veronica-explains/title new file mode 100644 index 0000000..31018b1 --- /dev/null +++ b/feeds/youtube-veronica-explains/title @@ -0,0 +1 @@ +youtube - Veronica explains diff --git a/feeds/youtube-yousuckatprogramming/feed b/feeds/youtube-yousuckatprogramming/feed new file mode 100644 index 0000000..f78eeb0 --- /dev/null +++ b/feeds/youtube-yousuckatprogramming/feed @@ -0,0 +1 @@ +https://www.youtube.com/feeds/videos.xml?channel_id=UCMN0a7GHQnC6H74SmCGSmdw diff --git a/feeds/youtube-yousuckatprogramming/index.php b/feeds/youtube-yousuckatprogramming/index.php new file mode 100644 index 0000000..2199d75 --- /dev/null +++ b/feeds/youtube-yousuckatprogramming/index.php @@ -0,0 +1,37 @@ + + + + + + + + + +Spike.nagatha.fr + + + +
+

RSS feed : https://www.youtube.com/feeds/videos.xml?channel_id=UCMN0a7GHQnC6H74SmCGSmdw (15 Items)

+ + + + + + + + + + + + + + + + +
2026-01-24 the best way to get started with linux in general
2026-01-24 my full head of hair and unix beard make me unstoppable at linux
2026-01-24 Time command quirks: when order matters
2026-01-22 Read Input Line by Line in Bash
2026-01-21 you can just add new syntax to bash
2026-01-20 Bash Range Syntax: From One to Many
2026-01-18 Grep Flags: i and o Examples
2026-01-17 please don’t steal my software - it’s already free 😂
2026-01-17 the bruce lee of bash scripting
2026-01-17 i like to bring some positivity to the tech content world if possible!
2026-01-16 Comparing Numbers in Bash: (( ... ))
2026-01-16 we’ve had a fun past couple of days between the verizon outage and github going down.
2026-01-15 Building a Linux CPU Monitor in Bash From Scratch! small htop-like clone using the /proc filesystem
2026-01-15 Alternative Bash Syntax: What's Wild and What's Not
2026-01-14 Bash Basics: Understanding True and False Exit Codes
+
+ + \ No newline at end of file diff --git a/feeds/youtube-yousuckatprogramming/title b/feeds/youtube-yousuckatprogramming/title new file mode 100644 index 0000000..fe4a485 --- /dev/null +++ b/feeds/youtube-yousuckatprogramming/title @@ -0,0 +1 @@ +youtube - You suck at programming diff --git a/files/bulletins/bulletin_032015_WEB.pdf b/files/bulletins/bulletin_032015_WEB.pdf new file mode 100755 index 0000000..9444584 Binary files /dev/null and b/files/bulletins/bulletin_032015_WEB.pdf differ diff --git a/files/bulletins/bulletin_052014_006_WEB.pdf b/files/bulletins/bulletin_052014_006_WEB.pdf new file mode 100755 index 0000000..dae8ece Binary files /dev/null and b/files/bulletins/bulletin_052014_006_WEB.pdf differ diff --git a/files/bulletins/bulletin_062015_web.pdf b/files/bulletins/bulletin_062015_web.pdf new file mode 100755 index 0000000..0fdf234 Binary files /dev/null and b/files/bulletins/bulletin_062015_web.pdf differ diff --git a/files/bulletins/bulletin_092014_01_WEB.pdf b/files/bulletins/bulletin_092014_01_WEB.pdf new file mode 100755 index 0000000..850e6bd Binary files /dev/null and b/files/bulletins/bulletin_092014_01_WEB.pdf differ diff --git a/files/bulletins/bulletin_122014_WEB.pdf b/files/bulletins/bulletin_122014_WEB.pdf new file mode 100755 index 0000000..c95e1a5 Binary files /dev/null and b/files/bulletins/bulletin_122014_WEB.pdf differ diff --git a/files/langourla.pdf b/files/langourla.pdf new file mode 100755 index 0000000..74c04c9 Binary files /dev/null and b/files/langourla.pdf differ diff --git a/gopher/de/index.html b/gopher/de/index.html new file mode 100755 index 0000000..d90caea --- /dev/null +++ b/gopher/de/index.html @@ -0,0 +1,124 @@ + + + + + + +Gopher + + +gopher
+

Was ist Gopher?

+

+Das Gopher-Protokoll ist ein Kommunikationsprotokoll, das für die Verteilung, +Suche und den Abruf von Dokumenten in Internetprotokoll-Netzwerken entwickelt wurde. +Das Design des Gopher-Protokolls und der Benutzeroberfläche ist menügesteuert +und bietet eine Alternative zum World Wide Web für diejenigen, die dessen +überladen sind, und für Regionen der Welt, die aufgrund der Werbung und der +Überlastung des Netzes keine Verbindung herstellen können. +

+

Warum Gopher?

+

+Gopher ist eines der Smolnet-Protokolle, +das aufgrund der Unterdrückung des +World Wide Web durch Unternehmensinteressen, KI-Schlamassel und übermäßige +Komplexität, wo Ihre persönlichen Daten Geld einbringen und Sie das Produkt sind, +wieder an Interesse gewinnt. Gopher ist eine bewährte, einfache und unkomplizierte +Textlösung, die Text, Bilder, Dokumente, Videos, Audio, Streaming usw. +bereitstellen kann und mit fast jedem anderen Protokoll verknüpft werden kann, +solange Ihr Client oder Browser dies unterstützt. +

+

+Gopher hat kein Javascript, keine Cookies, kein CSS und keine Formatierungstags, +die Ihnen den Tag verderben könnten, sondern nur Inhalte und klar definierte Links. +Seine Einfachheit ist beabsichtigt, und deshalb ist es das ideale Sprungbrett zu +anderen Smolnet-Protokollen wie gemini:// oder spartan:// und einer Vielzahl +anderer. Nein, Sie sind nicht dazu verdammt, sich ewig aus dem Trog der +Unternehmensinteressen zu ernähren, es gibt da draußen ein ganz anderes Universum. +

+

Wie funktioniert es?

+

+Heute gibt es über 300 Gopher-Server, und ihre Zahl steigt weiter. Viele davon +sind Aggregatoren, d. h. sie ermöglichen es Menschen, ihre eigenen Phlogs +(Gopher-Blogs) zu betreiben, ohne Programmierkenntnisse zu haben oder eigene +Gopher-Server betreiben zu müssen. +

+Man findet dort Gedichte, Blogs, Romane, technische Schriften, Politik, Humor usw., +alles im Klartext und ohne Werbung, Pop-ups oder sonstige grafische oder kommerzielle +Störungen. Bei Gopher ist der Inhalt König. Außerdem senden Gopher-Server keine Cookies, +verwenden kein Javascript und erstellen keine Nutzerprofile. +

+Wenn Sie technisch versiert sind und lieber Ihren eigenen Gopher-Server hosten möchten, +ist das ganz einfach, genau wie das Gopher-Protokoll selbst. Ein Gopher-Server kann mit +wenigen Zeilen Bash-Skript geschrieben werden. Ausgefeiltere Server wie +Gophernicus +bieten eine aktuelle und sichere Lösung und sind leicht verfügbar. +

+

Wie verwende ich es?

+

+Ein Gopher-Client oder Browser ist oft eine Frage des persönlichen Geschmacks, +ähnlich wie das World Wide Web früher, bevor ein bestimmtes globales Unternehmen +das Surfen im Internet zu seinem eigenen Vorteil übernommen hat. Gopher liefert reinen +Text, daher ist es Aufgabe des Clients, diesen ansprechend darzustellen. +

+Lagrange +ist ein beliebter grafischer Browser, während +Lynx in einem Terminal eine +klassische Lösung darstellt. Vielleicht gefällt Ihnen auch ein moderner +Multiprotokoll-Client wie Chawan, +mit dem Sie verschiedene Smolnet-Protokolle sowie +das Web ohne Unterbrechung durchsuchen können. Egal, ob Sie Windows, Linux, MacOS, +FreeBSD oder ein anderes Betriebssystem verwenden, es gibt eine Lösung für Sie. +

+Es gibt auch Web-Proxys, die es jemandem mit nur einem Webbrowser ermöglichen, +Gopher-Löcher zu besuchen. Sie sind jedoch nur eine vorübergehende Lösung und kein +Ersatz für einen dedizierten Gopher-Client. +

+

Wie geht es nun weiter?

+

+Probieren Sie es aus! Sie werden vielleicht überrascht sein, wie einfach die reinen +Textseiten sind, aber Sie werden schnell die Geschwindigkeit des Zugriffs aufgrund +der Übersichtlichkeit und die einfache Navigation zu schätzen wissen. Gopher ist nur +so gut wie sein Inhalt, es verlässt sich nicht auf ausgefallene Grafiken, um einen +Mangel an Substanz zu ersetzen. Zögern Sie also nicht, Ihre eigenen Texte hinzuzufügen! +Wir freuen uns darauf, sie zu lesen. +

+

Screenshots

+ +

Links

+

Client-Software

+ +

Webbrowser-Erweiterungen

+ +

Web-Proxys

+ +

Einige zufällige Gopher-Links

+ + +

 

+ + diff --git a/gopher/de/smolnet.html b/gopher/de/smolnet.html new file mode 100755 index 0000000..eb25d11 --- /dev/null +++ b/gopher/de/smolnet.html @@ -0,0 +1,43 @@ + + + + + + +Gopher:Smolnet + + +
+

Smolnet

+

The SmolNet consists of content available through alternative protocols outside the web such as +gemini:// +gopher:// +Gopher+ +gophers:// +finger:// +spartan:// +text:// +SuperText +nex:// +scorpion:// +mercury:// +titan:// +guppy:// +scroll:// +molerat:// +terse:// +fsp://. +There is a summary +of the main SmolNet protocols. Related protocols include +telnet://, +nntp:// or news:// and +dict:// +and more obscure things like QOTD +or TCPMUX. +

+

+source: wiki.archiveteam.org +

+ + + diff --git a/gopher/en/index.html b/gopher/en/index.html new file mode 100755 index 0000000..2c3b5e2 --- /dev/null +++ b/gopher/en/index.html @@ -0,0 +1,121 @@ + + + + + + +Gopher + + +gopher
+

What is Gopher?

+

+The Gopher protocol is a communication protocol designed for distributing, +searching, and retrieving documents in Internet Protocol networks. The +design of the Gopher protocol and user interface is menu-driven, and +presents an alternative to the World Wide Web for those who have become tired +of it, and where many parts of the world cannot even connect due to bloat. +

+

Why Gopher?

+

+Gopher is one of the smolnet +protocols that is having a regain of interest due to the +smothering of the world wide web by corporate interests, AI slop, and overcomplexity, +where your personal data makes them money and you are the product. +Gopher is a tried and trusted, simple, plain text solution that can serve text, +images, documents, videos, audio, streaming, etc. and can link to almost +any other protocol as long as your client, or browser, supports it. +

+

+Gopher has no javascript, cookies, css, or formatting tags to ruin your day, only content and +clearly defined links. It's simplicity is by design, and because of this, +it is the ideal trampoline towards other smolnet protocols, +like gemini:// or spartan://, and a host of others. +No, you are not condemmed to be eternally feeding from the trough of corporate interests, +there's a whole other universe out there. +

+ +

How does it work?

+

+Over 300 gopher servers exist today, and the number is increasing. Many of them are aggregators, +i.e. they allow people to run their own phlogs (gopher blogs) without any knowledge of programming +or need to run their own Gopher servers. +

+One can find poetry,blogs, novels, technical writings, politics, humour, etc. all in plain text and without +adverts, pop-ups, or any kind of graphical or commercial disturbance. Content is king on Gopher. +Also, gopher servers send no cookies, use javascript, or create user profiling. +

+

+If you are more technically inclined and would prefer to host your own gopher server, +it's quite simple, like the gopher protocol itself. A gopher server can be written +in a few lines of Bash script. More sophisticated servers like +gophernicus +allow for an up to date and secure solution and are readily available. +

+ +

How do I use it?

+

+A Gopher client, or browser is often a matter of personal taste, much like the world wide web of +old until a certain global corporation took over web browsing for their own benefit. +Gopher serves up plain text, so it's up to the client to render it nicely. +

+

+Lagrange is a popular graphical browser, +while Lynx in a terminal is a classic solution. +You might also like a modern multiprotocol client such as Chawan +that enables you to browse various smolnet protocols as well as the web without skipping a beat. +Whether you use Windows, Linux, MacOS, FreeBSD, or any other operating system, there's a solution for you. +

+

+There are also web proxies that allow someone with ony a web browser to visit gopher holes. However +they are a temporary solution, not a replacement for a dedicated Gopher client. +

+ +

Where do I go from here?

+

+Try it out! You might be surprised at the simplicity of the text only pages, but you'll +quickly appreciate the speed of access due to lack of bloat, and the ease of browsing. +Gopher only as good as it's content, it doesn't rely on fancy graphics to replace a lack of substance, +so don't hesitate to add your own writings! We are looking forward to reading them. +

+ +

Screenshots

+ + +

Links

+

Client software

+ +

Web browser extensions

+ +

Web proxies

+ +

Some random Gopher links

+ + +

 

+ + diff --git a/gopher/en/smolnet.html b/gopher/en/smolnet.html new file mode 100755 index 0000000..eb25d11 --- /dev/null +++ b/gopher/en/smolnet.html @@ -0,0 +1,43 @@ + + + + + + +Gopher:Smolnet + + +
+

Smolnet

+

The SmolNet consists of content available through alternative protocols outside the web such as +gemini:// +gopher:// +Gopher+ +gophers:// +finger:// +spartan:// +text:// +SuperText +nex:// +scorpion:// +mercury:// +titan:// +guppy:// +scroll:// +molerat:// +terse:// +fsp://. +There is a summary +of the main SmolNet protocols. Related protocols include +telnet://, +nntp:// or news:// and +dict:// +and more obscure things like QOTD +or TCPMUX. +

+

+source: wiki.archiveteam.org +

+ + + diff --git a/gopher/es/index.html b/gopher/es/index.html new file mode 100755 index 0000000..6117de3 --- /dev/null +++ b/gopher/es/index.html @@ -0,0 +1,133 @@ + + + + + + +Gopher + + +gopher
+

¿Qué es Gopher?

+

+ Gopher es un protocolo de comunicación diseñado para compartir, buscar + y recuperar documentos en redes basadas en el protocolo IP (Internet + Protocol). El diseño del protocolo y la interfaz de usario de Gopher se + basan en menús y es una alternativa para aquellas personas que se han cansado + de la World Wide Web, o para los que viven en zonas del planeta con conexiones + que no les permiten acceder debido al peso excesivo de la web actual. +

+

¿Por qué Gopher?

+

+ Gopher es uno de los protocolos de la smolnet + que está resurgiendo debido a todos los abusos en la web por parte de intereses corporativos, + contenido basura generado por IA y un exceso de complejidad, en el que se + busca convertirte en un producto y ganar dinero con tus datos personales. + Gopher es una solución fiable, probada, simple y en texto plano, con la que + se pueden compartir textos, imágenes, documentos, vídeos, audio, streaming, etc., + y que además puede enlazar con diferentes protocolos (siempre y cuando tu cliente + o navegador lo soporte). +

+

+ Gopher no tiene ni JavaScript, ni cookies, ni CSS, ni etiquetas de formato + que te compliquen la vida; solo contenido y enlaces claramente definidos. Su + simplicidad es deliberada y por eso es el trampolín ideal hacia otros protocolos + de la smolnet, como gemini:// o spartan:// (entre otros). No, no estás condenado + de por vida a pasar por el aro de las grandes corporaciones. Hay todo un universo + ahí fuera. +

+ +

¿Cómo funciona?

+

+ Hoy en día existen más de 300 servidores Gopher en línea, y la cifra va en + aumento. Muchos de ellos son espacios compartidos, es decir, permiten a + la gente gestionar sus propios phlogs (blogs en Gopher) sin la necesidad de + conocimientos técnicos. +

+

+ Puedes encontrar poesía, blogs, novelas, textos técnicos, política, humor, + y un largo etc., todo ello en texto plano y sin anuncios, sin ventanas + emergentes y sin ningún tipo de interrupción gráfica o comercial. En Gopher, + el contenido es el rey. Además los servidores Gopher no envían cookies, + no usan JavaScript y no crean perfiles de usuario. +

+

+ Si tienes intereses técnicos y prefieres alojar tu propio servidor Gopher + es bastante sencillo, igual que el propio protocolo. Un servidor Gopher puede + escribirse en unas pocas líneas de Bash. Servidores más sofisticados como + Gophernicus están al + alcance de cualquiera y ofrecen una solución actual y segura. +

+ + +

¿Cómo lo uso?

+

+ Elegir un cliente o navegador Gopher suele ser una cuestión de gustos personales, + igual que ocurría en la web primigenia hasta que cierta corporación global + se adueñó de la navegación web para su propio beneficio. Gopher devuelve texto plano + así que es el programa cliente quien decide cómo presentarlo. +

+

+ Lagrange es un navegador gráfico + muy popular, mientras que Lynx + es una solución clásica para la terminal. También puedes preferir un cliente + multiprotocolo moderno como Chawan, que + te permite navegar por varios protocolos de la smolnet y por la web de forma + fluida. Tanto si usas Windows como Linux, macOS, FreeBSD u otro sistema operativo, + hay un programa cliente disponible. +

+

+ También existen proxies via web que permiten a quien solamente tiene un navegador + web visitar gopher holes (el equivalente Gopher de una página web). Sin embargo estos deben + verse más bien como una solución temporal y no como un sustituto de un cliente Gopher dedicado. +

+ +

¿Y ahora qué?

+

+ ¡Pruébalo! Puede que al principio te sorprenda la sencillez de los contenidos + en texto plano, pero pronto apreciarás la velocidad de acceso gracias a la + ausencia de bloatware y la facilidad de navegación. Gopher es tan bueno + como su contenido, no hay gráficos llamativos para suplir la falta de sustancia, + así que ¡no dudes en añadir tus textos! Estamos deseando leerlos. +

+ +

Capturas de pantalla

+ + +

Enlaces

+

Clientes / Navegadores

+ +

Extensiones de navegadores web

+ +

Proxies web

+ +

Enlaces varios

+ + +

 

+ + diff --git a/gopher/es/smolnet.html b/gopher/es/smolnet.html new file mode 100755 index 0000000..7bf351b --- /dev/null +++ b/gopher/es/smolnet.html @@ -0,0 +1,43 @@ + + + + + + +Gopher:Smolnet + + +
+

Smolnet

+

La SmolNet consiste en contenido disponible a través de protocolos alternativos fuera de la web, como +gemini:// +gopher:// +Gopher+ +gophers:// +finger:// +spartan:// +text:// +SuperText +nex:// +scorpion:// +mercury:// +titan:// +guppy:// +scroll:// +molerat:// +terse:// +fsp://. +Hay un resumen +de los principales protocolos de la SmolNet. Otros protocolos relacionados son +telnet://, +nntp:// o news://, +dict:// +y cosas más recónditas como QOTD +o TCPMUX. +

+

+fuente: wiki.archiveteam.org +

+ + + diff --git a/gopher/fr/index.html b/gopher/fr/index.html new file mode 100755 index 0000000..03f60cd --- /dev/null +++ b/gopher/fr/index.html @@ -0,0 +1,123 @@ + + + + + + +Gopher + + +gopher
+

Qu'est-ce que Gopher ?

+

+Le protocole Gopher est un protocole de communication conçu pour distribuer, +rechercher et récupérer des documents dans les réseaux IP (Internet Protocol). +La conception du protocole Gopher et de son interface utilisateur est basée +sur des menus. Il offre une alternative au World Wide Web pour ceux qui en +ont assez et pour les régions du monde qui ne peuvent même pas s'y connecter +en raison de sa surcharge. +

+

Pourquoi Gopher ?

+

+Gopher est l'un des protocoles smolnet qui suscite un regain d'intérêt en raison +de l'étouffement du World Wide Web par les intérêts des entreprises, +le bouillie de l'IA et la complexité excessive, où vos données personnelles +leur rapportent de l'argent car vous êtes le produit. Gopher est une solution +éprouvée, simple et fiable, qui permet de diffuser du texte, des images, des +documents, des vidéos, de l'audio, du streaming, etc. et qui peut se connecter +à presque tous les autres protocoles, à condition que votre client ou votre +navigateur le prenne en charge. +

+

+Gopher n'utilise pas de javascript, de cookies, de css ou de balises de formatage +qui pourraient gâcher votre journée, mais uniquement du contenu et des liens +clairement définis. Sa simplicité est voulue, et c'est pourquoi il constitue +le tremplin idéal vers d'autres protocoles smolnet, tels que gemini:// ou +spartan://, et bien d'autres encore. Non, vous n'êtes pas condamné à vous nourrir +éternellement à l'auge des intérêts des entreprises, il existe tout un autre univers. +

+

Comment ça marche ?

+

+Il existe aujourd'hui plus de 300 serveurs Gopher, et leur nombre ne cesse d'augmenter. +Beaucoup d'entre eux sont des agrégateurs, c'est-à-dire qu'ils permettent aux utilisateurs de gérer leurs propres phlogs +(blogs Gopher) sans avoir besoin de connaissances en programmation ni de gérer leurs propres serveurs Gopher. +

+

+On peut y trouver de la poésie, des blogs, des romans, des écrits techniques, des articles politiques, de l'humour, etc. +Le tout en texte brut, sans publicité, sans pop-up ni aucune autre forme de perturbation graphique ou +commerciale. Sur Gopher, le contenu est roi. De plus, les serveurs Gopher n'envoient +pas de cookies, n'utilisent pas de javascript et ne créent pas de profil utilisateur. +

+Si vous êtes plutôt doué en informatique et que vous préférez héberger votre propre serveur Gopher, +c'est assez simple, tout comme le protocole Gopher lui-même. Un serveur Gopher peut être écrit +en quelques lignes de script Bash. Des serveurs plus sophistiqués comme +gophernicus +offrent une solution sécurisée et à jour, et sont facilement disponibles. +

+

Comment l'utiliser ?

+

+Le choix d'un client ou d'un navigateur Gopher relève souvent des goûts personnels, +tout comme c'était le cas pour le World Wide Web avant qu'une certaine multinationale +ne s'empare de la navigation web à son propre profit. Gopher fournit du texte brut, +c'est donc au client de le rendre agréable à lire. +

+Lagrange est un navigateur graphique populaire, +tandis que Lynx dans un terminal +est une solution classique. Vous aimerez peut-être aussi un client multiprotocole +moderne tel que Chawan, +qui vous permet de naviguer sur divers protocoles smolnet +ainsi que sur le web sans perdre une miette. Que vous utilisiez Windows, Linux, +MacOS, FreeBSD ou tout autre système d'exploitation, il existe une solution pour vous. +

+Il existe également des proxys web qui permettent à toute personne disposant d'un +navigateur web de visiter des trous Gopher. Cependant, il s'agit d'une solution +temporaire qui ne remplace pas un client Gopher dédié. +

+

Et maintenant, que faire ?

+

+Essayez-le ! Vous serez peut-être surpris par la simplicité des pages contenant +uniquement du texte, mais vous apprécierez rapidement la rapidité d'accès due à +l'absence de fioritures et la facilité de navigation. Gopher n'est bon que par son +contenu, il ne compte pas sur des graphismes sophistiqués pour compenser un manque +de substance, alors n'hésitez pas à ajouter vos propres écrits ! +Nous sommes impatients de les lire. +

+

Captures d'écran

+ + +

Liens

+

Logiciels clients

+ +

Extensions de navigateur

+ +

Proxys Web

+ +

Quelques liens Gopher aléatoires

+ +

 

+ + diff --git a/gopher/fr/smolnet.html b/gopher/fr/smolnet.html new file mode 100755 index 0000000..f7fd792 --- /dev/null +++ b/gopher/fr/smolnet.html @@ -0,0 +1,43 @@ + + + + + + +Gopher:Smolnet + + +
+

Smolnet

+

Le SmolNet comprend des contenus accessibles via des protocoles alternatifs en dehors du Web, tels que +gemini:// +gopher:// +Gopher+ +gophers:// +finger:// +spartan:// +text:// +SuperText +nex:// +scorpion:// +mercury:// +titan:// +guppy:// +scroll:// +molerat:// +terse:// +fsp://. +Voici un résumé +des principaux protocoles. Les protocoles associés comprennent +telnet://, +nntp:// ou news:// et +dict:// +ainsi que d'autres protocoles moins connus tels que QOTD +ou TCPMUX. +

+

+source: wiki.archiveteam.org +

+ + + diff --git a/gopher/gdpr.html b/gopher/gdpr.html new file mode 100755 index 0000000..72b81a3 --- /dev/null +++ b/gopher/gdpr.html @@ -0,0 +1,106 @@ + + + + + + + GDPR + + +

GDPR Notice

+

[English]

+

gopher.fr is a personal site published in a non-professional capacity. +It does not use cookies, does not track visitors and does not store their e-mails or information +with the exception of temporary server logs for debugging purposes. Its content is free of rights. +

+ +

Details of the site host: +
OVH SAS, RCS Lille Métropole 424 761 419 00045 +
Head office: 2 rue Kellermann, 59100 Roubaix, France.

+
+

While I endeavour to keep the information up to date and correct, +I make no representations or warranties of any kind, +express or implied, about the completeness, accuracy, reliability, suitability +or availability of the information contained on the website for any purpose. +

+

All claims should be addressed to gopher@nagatha.fr

+ +

[Français]

+

+gopher.fr est un site personel édité à titre non professionnel. +Il n'utilise pas de cookies, ne traque pas les visiteurs et ne stocke pas +leurs d'e-mails ni informations à l'exception des journaux temporaires du serveur à des fins de débogage. +Son contenu est libre des droits. +

+ + +

Coordonnées de l’hébergeur du site : +
OVH SAS, RCS Lille Métropole 424 761 419 00045 +
Siège social : 2 rue Kellermann, 59100 Roubaix, France.

+
+ +

+Bien que je m'efforce de maintenir les informations à jour et correctes, +je ne fais aucune déclaration ni garantie de quelque nature que ce soit, +expresse ou implicite, concernant l'exhaustivité, l'exactitude, la fiabilité, la pertinence +ou la disponibilité des informations contenus sur le site web à quelque fin que ce soit. +

+

Toute réclamation doit être adressée à gopher@nagatha.fr

+ +

[Español]

+

+ gopher.fr es un sitio personal publicado a título no profesional. No utiliza cookies, no rastrea a los visitantes y no + almacena sus correos electrónicos ni su información, exceptuando logs temporales del servidor con fines de + depuración. Su contenido está libre de derechos. +

+ +

+ Detalles del proveedor de alojamiento del sitio: +
OVH SAS, RCS Lille Métropole 424 761 419 00045 +
Sede central: 2 rue Kellermann, 59100 Roubaix, France. +

+
+

+ Aunque me esfuerzo por mantener la información al día y correcta, no me hago responsable ni ofrezco + garantía alguna, expresa o implícita, sobre la integridad, exactitud, fiabilidad, idoneidad o disponibilidad de la + información contenida en este sitio web para cualquier fin. +

+

Todas las reclamaciones deben dirigirse a gopher@nagatha.fr

+ +

[Italiano]

+

+ gopher.fr è un sito personale pubblicato a titolo non professionale. Non utilizza cookie, non traccia gli utenti e + non memorizza le loro e-mail o informazioni, ad eccezione dei log temporanei del server a fini di debug. Il suo +

+ +

Dettagli dell’host del sito: +
OVH SAS, RCS Lille Métropole 424 761 419 00045 +
Sede centrale: 2 rue Kellermann, 59100 Roubaix, France. +

+
+

+ Pur impegnandomi a mantenere le informazioni aggiornate e corrette, non mi assumo alcuna responsabilità né fornisco + garanzie di alcun tipo, espresse o implicite, riguardo alla completezza, accuratezza, affidabilità, idoneità o + disponibilità delle informazioni contenute in questo sito web per qualsiasi scopo. +

+

Tutti i reclami devono essere indirizzati a gopher@nagatha.fr

+ +

[Deutsch]

+

gopher.fr ist eine private Website, die nicht zu gewerblichen Zwecken betrieben wird. +Sie verwendet keine Cookies, verfolgt keine Besucher und speichert keine E-Mail-Adressen oder Informationen +mit Ausnahme von temporären Server-Logs zu Debugging-Zwecken. Der Inhalt ist frei von Rechten. +

+ +

Angaben zum Host der Website: +
OVH SAS, RCS Lille Métropole 424 761 419 00045 +
Hauptsitz: 2 rue Kellermann, 59100 Roubaix, Frankreich.

+
+

Ich bemühe mich, die Informationen aktuell und korrekt zu halten, +übernehme jedoch keinerlei ausdrückliche oder stillschweigende Gewährleistung oder Zusicherung hinsichtlich der Vollständigkeit, Genauigkeit, Zuverlässigkeit, Eignung +oder Verfügbarkeit der auf der Website enthaltenen Informationen für irgendeinen Zweck. +

+

Alle Ansprüche sind an gopher@nagatha.fr zu richten.

+ +

 

+ + diff --git a/gopher/gopherfr-translations.tar.gz b/gopher/gopherfr-translations.tar.gz new file mode 100755 index 0000000..bba8556 Binary files /dev/null and b/gopher/gopherfr-translations.tar.gz differ diff --git a/gopher/images/chawan_1.png b/gopher/images/chawan_1.png new file mode 100755 index 0000000..a76eb55 Binary files /dev/null and b/gopher/images/chawan_1.png differ diff --git a/gopher/images/chawan_2.png b/gopher/images/chawan_2.png new file mode 100755 index 0000000..22f0ba8 Binary files /dev/null and b/gopher/images/chawan_2.png differ diff --git a/gopher/images/gopher-dark.png b/gopher/images/gopher-dark.png new file mode 100755 index 0000000..e2d2e90 Binary files /dev/null and b/gopher/images/gopher-dark.png differ diff --git a/gopher/images/gopher.png b/gopher/images/gopher.png new file mode 100755 index 0000000..e5bbcf2 Binary files /dev/null and b/gopher/images/gopher.png differ diff --git a/gopher/images/gopher_IGICv1.12..png b/gopher/images/gopher_IGICv1.12..png new file mode 100755 index 0000000..8fda205 Binary files /dev/null and b/gopher/images/gopher_IGICv1.12..png differ diff --git a/gopher/images/gopher_elpher.png b/gopher/images/gopher_elpher.png new file mode 100755 index 0000000..c271e7d Binary files /dev/null and b/gopher/images/gopher_elpher.png differ diff --git a/gopher/images/gopher_gophercle_android.jpg b/gopher/images/gopher_gophercle_android.jpg new file mode 100755 index 0000000..6dd1f38 Binary files /dev/null and b/gopher/images/gopher_gophercle_android.jpg differ diff --git a/gopher/images/gopher_little_gopher_client_mac.png b/gopher/images/gopher_little_gopher_client_mac.png new file mode 100755 index 0000000..a790f07 Binary files /dev/null and b/gopher/images/gopher_little_gopher_client_mac.png differ diff --git a/gopher/images/gopherus.png b/gopher/images/gopherus.png new file mode 100755 index 0000000..4a7ea8e Binary files /dev/null and b/gopher/images/gopherus.png differ diff --git a/gopher/images/lagrange.png b/gopher/images/lagrange.png new file mode 100755 index 0000000..2dc5e0e Binary files /dev/null and b/gopher/images/lagrange.png differ diff --git a/gopher/images/lynx_1.png b/gopher/images/lynx_1.png new file mode 100755 index 0000000..12a7043 Binary files /dev/null and b/gopher/images/lynx_1.png differ diff --git a/gopher/index.html b/gopher/index.html new file mode 100755 index 0000000..af82e6b --- /dev/null +++ b/gopher/index.html @@ -0,0 +1,26 @@ + + + + + + +Gopher + + +

Gopher

+gopher
+ +

 

+

+English +Français +Español +Italiano +Deutsche +

+

+GDPR | <w> +

+

 

+ + diff --git a/gopher/it/index.html b/gopher/it/index.html new file mode 100755 index 0000000..e7c5cc5 --- /dev/null +++ b/gopher/it/index.html @@ -0,0 +1,135 @@ + + + + + + +Gopher + + +gopher
+

Che cos'è Gopher?

+

+ Gopher è un protocollo di comunicazione progettato per condividere, + cercare e recuperare documenti nelle reti basate su Internet Protocol. + La struttura del protocollo e dell'interfaccia utente di Gopher è basata + su menu e rappresenta un'alternativa al World Wide Web per chi si è stancato + di essa, o per chi vive in parti del mondo dove non è nemmeno possibile + collegarsi a causa del sovraccarico della web moderna. +

+

Perché Gopher?

+

+ Gopher è uno dei protocolli della smolnet + che sta vivendo una nuova ondata di interesse a causa dell'oppressione della web + da parte degli interessi corporativi, dalla spazzatura prodotta dall’IA e dell'eccessiva + complessità, dove i tuoi dati personali fanno guadagnare denaro ad altri e tu diventi il + prodotto. Gopher è una soluzione semplice, affidabile e collaudata, in testo semplice, che può + fornire testi, immagini, documenti, video, audio, streaming, ecc., e può collegarsi a quasi + qualsiasi altro protocollo, purché il tuo client o browser lo supporti. +

+

+ Gopher non ha JavaScript, cookie, CSS o tag di formattazione che + possano rovinarti la giornata; solo contenuto e link chiaramente + definiti. La sua semplicità è intenzionale, e proprio per questo è il + trampolino ideale verso altri protocolli dello smolnet come gemini://, + spartan:// e molti altri. Non sei obbligato a vivere per sempre al + servizio degli interessi delle grandi corporazioni. Là fuori c’è un + mondo tutto da scoprire! +

+ +

Come funziona?

+

+ Oggi esistono oltre 300 server Gopher, e il numero è in aumento. Molti + di essi sono spazi condivisi che permettono agli utenti di aprire i + propri phlog (blog Gopher) senza saper programmare né mettere in piedi + un server Gopher. +

+

+ Si può trovare poesia, blog, romanzi, scritti tecnici, politica, + umorismo, ecc., tutto in testo semplice e senza pubblicità, pop-up o + qualsiasi tipo di invadenza grafica o commerciale. Su Gopher il + contenuto è ciò che conta davvero. Inoltre, i server Gopher non inviano + cookie, non usano JavaScript e non fanno profilazioni agli utenti. +

+

+ Se ti interessano gli aspetti tecnici e preferisci ospitare il tuo server + Gopher, è abbastanza semplice, proprio come il protocollo stesso. Un + server Gopher può essere scritto in poche righe di Bash. Server più + sofisticati come Gophernicus + sono alla portata di tutti e offrono una soluzione aggiornata e sicura. +

+ + +

Come lo uso?

+

+ Scegliere un client o browser Gopher è spesso una questione di gusto + personale, proprio come nel vecchio web dei primi tempi (prima che una + certa corporazione globale prendesse il controllo della navigazione web + per i propri interessi). Gopher fornisce testo semplice, quindi è il client + che decide come visualizzarlo. +

+

+ Lagrange è un client grafico + molto diffuso, mentre Lynx + è una soluzione classica nel terminale. Potrebbe anche piacerti un client + multiprotocollo moderno come Chawan, + che ti consente di navigare tra vari protocolli dello smolnet e anche sul web + senza alcuna interruzione. Sia che tu usi Windows, Linux, macOS, FreeBSD o un + altro sistema operativo, esiste un client disponibile. +

+

+ Esistono anche proxy web che permettono a chi dispone solo di un browser web di + visitare i gopher holes (l’equivalente, in Gopher, di una pagina web). Tuttavia, + dovrebbero essere visti come una soluzione provvisoria, non come un vero sostituto + di un client Gopher dedicato. +

+ +

E adesso?

+

+ Provalo! Le pagine esclusivamente testuali potrebbero sembrarti semplici all’inizio, + ma apprezzerai subito la velocità di accesso grazie all’assenza di bloatware e la + facilità di navigazione. Gopher è valido solo quanto lo è il contenuto, non si + basa su grafica appariscente per mascherare la mancanza di sostanza, + quindi non esitare ad aggiungere i tuoi scritti! Non vediamo l’ora di + leggerli. +

+ +

Schermate

+ + +

Link

+

Client

+ +

Estensioni per browser

+ +

Proxy web

+ +

Selezione di link Gopher vari

+ +

 

+ + diff --git a/gopher/it/smolnet.html b/gopher/it/smolnet.html new file mode 100755 index 0000000..a79433f --- /dev/null +++ b/gopher/it/smolnet.html @@ -0,0 +1,43 @@ + + + + + + +Gopher:Smolnet + + +
+

Smolnet

+

La SmolNet consiste in contenuti disponibili tramite protocolli alternativi al web, come +gemini:// +gopher:// +Gopher+ +gophers:// +finger:// +spartan:// +text:// +SuperText +nex:// +scorpion:// +mercury:// +titan:// +guppy:// +scroll:// +molerat:// +terse:// +fsp://. +C’è un elenco +dei principali protocolli della SmolNet. Altri protocolli correlati sono +telnet://, +nntp:// o news://, +dict:// +e altre cose meno conosciute, come QOTD +o TCPMUX. +

+

+fonte: wiki.archiveteam.org +

+ + + diff --git a/gopher/style.css b/gopher/style.css new file mode 100755 index 0000000..86933c6 --- /dev/null +++ b/gopher/style.css @@ -0,0 +1,23 @@ +body,html { + background-color:#046786; + color: #E0E0E0; + font-family:monospace; + font-size:1.1em; + height:100%; +} +body { + margin: 0 auto; + max-width: 67ch; + min-height:100%; +} +h1,h2,h3,h4 { + color:#f1fa19; +} +h1 { + border-bottom: 1px solid #ffffff; + padding-bottom: 4px; +} +p { + text-align: justify; +} +a { color: #ffffff;} diff --git a/images/1.png b/images/1.png new file mode 100755 index 0000000..3f6f07c Binary files /dev/null and b/images/1.png differ diff --git a/images/Lenovo-ThinkPad-X220.JPEG b/images/Lenovo-ThinkPad-X220.JPEG new file mode 100755 index 0000000..79b7f9f Binary files /dev/null and b/images/Lenovo-ThinkPad-X220.JPEG differ diff --git a/images/Lenovo-ThinkPad-X220.webp b/images/Lenovo-ThinkPad-X220.webp new file mode 100755 index 0000000..1e23c3d Binary files /dev/null and b/images/Lenovo-ThinkPad-X220.webp differ diff --git a/images/ac-7620-spot.jpg b/images/ac-7620-spot.jpg new file mode 100755 index 0000000..5ccbbdd Binary files /dev/null and b/images/ac-7620-spot.jpg differ diff --git a/images/alacritty.JPEG b/images/alacritty.JPEG new file mode 100755 index 0000000..1323d38 Binary files /dev/null and b/images/alacritty.JPEG differ diff --git a/images/alacritty.webp b/images/alacritty.webp new file mode 100755 index 0000000..0856f60 Binary files /dev/null and b/images/alacritty.webp differ diff --git a/images/applixware-labelmaker_0.jpg b/images/applixware-labelmaker_0.jpg new file mode 100755 index 0000000..fb3a7d4 Binary files /dev/null and b/images/applixware-labelmaker_0.jpg differ diff --git a/images/applixware-labelmaker_1.jpg b/images/applixware-labelmaker_1.jpg new file mode 100755 index 0000000..405bee2 Binary files /dev/null and b/images/applixware-labelmaker_1.jpg differ diff --git a/images/arch-logo.JPEG b/images/arch-logo.JPEG new file mode 100755 index 0000000..43db338 Binary files /dev/null and b/images/arch-logo.JPEG differ diff --git a/images/arch-logo.webp b/images/arch-logo.webp new file mode 100755 index 0000000..004a481 Binary files /dev/null and b/images/arch-logo.webp differ diff --git a/images/balancoire.JPEG b/images/balancoire.JPEG new file mode 100755 index 0000000..a4f736a Binary files /dev/null and b/images/balancoire.JPEG differ diff --git a/images/balancoire.webp b/images/balancoire.webp new file mode 100755 index 0000000..fc396b2 Binary files /dev/null and b/images/balancoire.webp differ diff --git a/images/bandsaw-frame-pieces.JPEG b/images/bandsaw-frame-pieces.JPEG new file mode 100755 index 0000000..37c4d27 Binary files /dev/null and b/images/bandsaw-frame-pieces.JPEG differ diff --git a/images/bandsaw-frame-pieces.webp b/images/bandsaw-frame-pieces.webp new file mode 100755 index 0000000..d133e7c Binary files /dev/null and b/images/bandsaw-frame-pieces.webp differ diff --git a/images/bandsaw_1.JPEG b/images/bandsaw_1.JPEG new file mode 100755 index 0000000..529abd5 Binary files /dev/null and b/images/bandsaw_1.JPEG differ diff --git a/images/bandsaw_1.jpg b/images/bandsaw_1.jpg new file mode 100755 index 0000000..2d1ad7d Binary files /dev/null and b/images/bandsaw_1.jpg differ diff --git a/images/bandsaw_1.webp b/images/bandsaw_1.webp new file mode 100755 index 0000000..e88c831 Binary files /dev/null and b/images/bandsaw_1.webp differ diff --git a/images/blog/Lenovo-ThinkPad-X220.JPEG b/images/blog/Lenovo-ThinkPad-X220.JPEG new file mode 100755 index 0000000..98b4fb7 Binary files /dev/null and b/images/blog/Lenovo-ThinkPad-X220.JPEG differ diff --git a/images/blog/Lenovo-ThinkPad-X220.webp b/images/blog/Lenovo-ThinkPad-X220.webp new file mode 100755 index 0000000..2a597e5 Binary files /dev/null and b/images/blog/Lenovo-ThinkPad-X220.webp differ diff --git a/images/blog/Screenshot_2025-04-08_14-20-40.png b/images/blog/Screenshot_2025-04-08_14-20-40.png new file mode 100755 index 0000000..0043d02 Binary files /dev/null and b/images/blog/Screenshot_2025-04-08_14-20-40.png differ diff --git a/images/blog/alacritty.JPEG b/images/blog/alacritty.JPEG new file mode 100755 index 0000000..4828bba Binary files /dev/null and b/images/blog/alacritty.JPEG differ diff --git a/images/blog/alacritty.webp b/images/blog/alacritty.webp new file mode 100755 index 0000000..850920f Binary files /dev/null and b/images/blog/alacritty.webp differ diff --git a/images/blog/arch-logo.JPEG b/images/blog/arch-logo.JPEG new file mode 100755 index 0000000..342112a Binary files /dev/null and b/images/blog/arch-logo.JPEG differ diff --git a/images/blog/arch-logo.webp b/images/blog/arch-logo.webp new file mode 100755 index 0000000..a0592f8 Binary files /dev/null and b/images/blog/arch-logo.webp differ diff --git a/images/blog/balancoire.JPEG b/images/blog/balancoire.JPEG new file mode 100755 index 0000000..3f29d2f Binary files /dev/null and b/images/blog/balancoire.JPEG differ diff --git a/images/blog/balancoire.webp b/images/blog/balancoire.webp new file mode 100755 index 0000000..5e7f62a Binary files /dev/null and b/images/blog/balancoire.webp differ diff --git a/images/blog/bandsaw-frame-pieces.JPEG b/images/blog/bandsaw-frame-pieces.JPEG new file mode 100755 index 0000000..f329dbb Binary files /dev/null and b/images/blog/bandsaw-frame-pieces.JPEG differ diff --git a/images/blog/bandsaw-frame-pieces.webp b/images/blog/bandsaw-frame-pieces.webp new file mode 100755 index 0000000..367a040 Binary files /dev/null and b/images/blog/bandsaw-frame-pieces.webp differ diff --git a/images/blog/bandsaw_1.JPEG b/images/blog/bandsaw_1.JPEG new file mode 100755 index 0000000..07d2ae4 Binary files /dev/null and b/images/blog/bandsaw_1.JPEG differ diff --git a/images/blog/bandsaw_1.jpg b/images/blog/bandsaw_1.jpg new file mode 100755 index 0000000..ee882b4 Binary files /dev/null and b/images/blog/bandsaw_1.jpg differ diff --git a/images/blog/bandsaw_1.webp b/images/blog/bandsaw_1.webp new file mode 100755 index 0000000..a8c6411 Binary files /dev/null and b/images/blog/bandsaw_1.webp differ diff --git a/images/blog/bash-prompt-pywal.png b/images/blog/bash-prompt-pywal.png new file mode 100755 index 0000000..4b12018 Binary files /dev/null and b/images/blog/bash-prompt-pywal.png differ diff --git a/images/blog/battery.c.png b/images/blog/battery.c.png new file mode 100755 index 0000000..2d17652 Binary files /dev/null and b/images/blog/battery.c.png differ diff --git a/images/blog/bonneannee.gif b/images/blog/bonneannee.gif new file mode 100755 index 0000000..9eafaf0 Binary files /dev/null and b/images/blog/bonneannee.gif differ diff --git a/images/blog/box-chevaux.JPEG b/images/blog/box-chevaux.JPEG new file mode 100755 index 0000000..51b140b Binary files /dev/null and b/images/blog/box-chevaux.JPEG differ diff --git a/images/blog/box-chevaux.webp b/images/blog/box-chevaux.webp new file mode 100755 index 0000000..01d03b5 Binary files /dev/null and b/images/blog/box-chevaux.webp differ diff --git a/images/blog/carbide-tools.jpg b/images/blog/carbide-tools.jpg new file mode 100755 index 0000000..172a7ab Binary files /dev/null and b/images/blog/carbide-tools.jpg differ diff --git a/images/blog/cha-wittamore-1.png b/images/blog/cha-wittamore-1.png new file mode 100755 index 0000000..0207788 Binary files /dev/null and b/images/blog/cha-wittamore-1.png differ diff --git a/images/blog/chawan-ghostty.png b/images/blog/chawan-ghostty.png new file mode 100755 index 0000000..5b80d61 Binary files /dev/null and b/images/blog/chawan-ghostty.png differ diff --git a/images/blog/classic-keyboard.JPEG b/images/blog/classic-keyboard.JPEG new file mode 100755 index 0000000..6b313f2 Binary files /dev/null and b/images/blog/classic-keyboard.JPEG differ diff --git a/images/blog/classic-keyboard.webp b/images/blog/classic-keyboard.webp new file mode 100755 index 0000000..c206647 Binary files /dev/null and b/images/blog/classic-keyboard.webp differ diff --git a/images/blog/core-i5-cpu.JPEG b/images/blog/core-i5-cpu.JPEG new file mode 100755 index 0000000..4d46a64 Binary files /dev/null and b/images/blog/core-i5-cpu.JPEG differ diff --git a/images/blog/core-i5-cpu.png b/images/blog/core-i5-cpu.png new file mode 100755 index 0000000..ac3b076 Binary files /dev/null and b/images/blog/core-i5-cpu.png differ diff --git a/images/blog/core-i5-cpu.webp b/images/blog/core-i5-cpu.webp new file mode 100755 index 0000000..098946f Binary files /dev/null and b/images/blog/core-i5-cpu.webp differ diff --git a/images/blog/dane-success.png b/images/blog/dane-success.png new file mode 100755 index 0000000..d999593 Binary files /dev/null and b/images/blog/dane-success.png differ diff --git a/images/blog/dwm-btop.png b/images/blog/dwm-btop.png new file mode 100755 index 0000000..5f62865 Binary files /dev/null and b/images/blog/dwm-btop.png differ diff --git a/images/blog/dwm_bar.png b/images/blog/dwm_bar.png new file mode 100755 index 0000000..9f05063 Binary files /dev/null and b/images/blog/dwm_bar.png differ diff --git a/images/blog/dwmblocks-emoji.png b/images/blog/dwmblocks-emoji.png new file mode 100755 index 0000000..71bc0bf Binary files /dev/null and b/images/blog/dwmblocks-emoji.png differ diff --git a/images/blog/espion-dans-notre-poche.JPEG b/images/blog/espion-dans-notre-poche.JPEG new file mode 100755 index 0000000..3988f6e Binary files /dev/null and b/images/blog/espion-dans-notre-poche.JPEG differ diff --git a/images/blog/espion-dans-notre-poche.webp b/images/blog/espion-dans-notre-poche.webp new file mode 100755 index 0000000..aa156c7 Binary files /dev/null and b/images/blog/espion-dans-notre-poche.webp differ diff --git a/images/blog/etau.JPEG b/images/blog/etau.JPEG new file mode 100755 index 0000000..e816e64 Binary files /dev/null and b/images/blog/etau.JPEG differ diff --git a/images/blog/etau.webp b/images/blog/etau.webp new file mode 100755 index 0000000..b174490 Binary files /dev/null and b/images/blog/etau.webp differ diff --git a/images/blog/eu-flag.JPEG b/images/blog/eu-flag.JPEG new file mode 100755 index 0000000..9b22071 Binary files /dev/null and b/images/blog/eu-flag.JPEG differ diff --git a/images/blog/eu-flag.webp b/images/blog/eu-flag.webp new file mode 100755 index 0000000..27ac76e Binary files /dev/null and b/images/blog/eu-flag.webp differ diff --git a/images/blog/eza.png b/images/blog/eza.png new file mode 100755 index 0000000..96d7434 Binary files /dev/null and b/images/blog/eza.png differ diff --git a/images/blog/first-box.JPEG b/images/blog/first-box.JPEG new file mode 100755 index 0000000..3857478 Binary files /dev/null and b/images/blog/first-box.JPEG differ diff --git a/images/blog/first-box.webp b/images/blog/first-box.webp new file mode 100755 index 0000000..4395a6a Binary files /dev/null and b/images/blog/first-box.webp differ diff --git a/images/blog/fugitive-telemetry.JPEG b/images/blog/fugitive-telemetry.JPEG new file mode 100755 index 0000000..d62fd7a Binary files /dev/null and b/images/blog/fugitive-telemetry.JPEG differ diff --git a/images/blog/fugitive-telemetry.webp b/images/blog/fugitive-telemetry.webp new file mode 100755 index 0000000..6f6a619 Binary files /dev/null and b/images/blog/fugitive-telemetry.webp differ diff --git a/images/blog/ghostty.gif b/images/blog/ghostty.gif new file mode 100755 index 0000000..bcb4905 Binary files /dev/null and b/images/blog/ghostty.gif differ diff --git a/images/blog/grande-vase.JPEG b/images/blog/grande-vase.JPEG new file mode 100755 index 0000000..ee6901b Binary files /dev/null and b/images/blog/grande-vase.JPEG differ diff --git a/images/blog/grande-vase.webp b/images/blog/grande-vase.webp new file mode 100755 index 0000000..25122c3 Binary files /dev/null and b/images/blog/grande-vase.webp differ diff --git a/images/blog/gsimplecal.JPEG b/images/blog/gsimplecal.JPEG new file mode 100755 index 0000000..fb08053 Binary files /dev/null and b/images/blog/gsimplecal.JPEG differ diff --git a/images/blog/gsimplecal.webp b/images/blog/gsimplecal.webp new file mode 100755 index 0000000..a1eb487 Binary files /dev/null and b/images/blog/gsimplecal.webp differ diff --git a/images/blog/hail-mary-wier.JPEG b/images/blog/hail-mary-wier.JPEG new file mode 100755 index 0000000..880bb01 Binary files /dev/null and b/images/blog/hail-mary-wier.JPEG differ diff --git a/images/blog/hail-mary-wier.webp b/images/blog/hail-mary-wier.webp new file mode 100755 index 0000000..da6e5f0 Binary files /dev/null and b/images/blog/hail-mary-wier.webp differ diff --git a/images/blog/happy-new-year.gif b/images/blog/happy-new-year.gif new file mode 100755 index 0000000..6bc958e Binary files /dev/null and b/images/blog/happy-new-year.gif differ diff --git a/images/blog/i3-blocks.JPEG b/images/blog/i3-blocks.JPEG new file mode 100755 index 0000000..ae497bd Binary files /dev/null and b/images/blog/i3-blocks.JPEG differ diff --git a/images/blog/i3-blocks.webp b/images/blog/i3-blocks.webp new file mode 100755 index 0000000..2b3dd4d Binary files /dev/null and b/images/blog/i3-blocks.webp differ diff --git a/images/blog/kbca-jazz-radio.png b/images/blog/kbca-jazz-radio.png new file mode 100755 index 0000000..905e54e Binary files /dev/null and b/images/blog/kbca-jazz-radio.png differ diff --git a/images/blog/kobo-clara-colour.png b/images/blog/kobo-clara-colour.png new file mode 100755 index 0000000..703b7c3 Binary files /dev/null and b/images/blog/kobo-clara-colour.png differ diff --git a/images/blog/la-grande-illusion.JPEG b/images/blog/la-grande-illusion.JPEG new file mode 100755 index 0000000..9c1a793 Binary files /dev/null and b/images/blog/la-grande-illusion.JPEG differ diff --git a/images/blog/la-grande-illusion.webp b/images/blog/la-grande-illusion.webp new file mode 100755 index 0000000..06b921b Binary files /dev/null and b/images/blog/la-grande-illusion.webp differ diff --git a/images/blog/librefox-wittamore-1.png b/images/blog/librefox-wittamore-1.png new file mode 100755 index 0000000..8717d6e Binary files /dev/null and b/images/blog/librefox-wittamore-1.png differ diff --git a/images/blog/links-wittamore-1.png b/images/blog/links-wittamore-1.png new file mode 100755 index 0000000..79049f9 Binary files /dev/null and b/images/blog/links-wittamore-1.png differ diff --git a/images/blog/lnav.JPEG b/images/blog/lnav.JPEG new file mode 100755 index 0000000..e6c877e Binary files /dev/null and b/images/blog/lnav.JPEG differ diff --git a/images/blog/lnav.webp b/images/blog/lnav.webp new file mode 100755 index 0000000..17d2aac Binary files /dev/null and b/images/blog/lnav.webp differ diff --git a/images/blog/lower-blade-guide-1.JPEG b/images/blog/lower-blade-guide-1.JPEG new file mode 100755 index 0000000..e9cdf25 Binary files /dev/null and b/images/blog/lower-blade-guide-1.JPEG differ diff --git a/images/blog/lower-blade-guide-1.webp b/images/blog/lower-blade-guide-1.webp new file mode 100755 index 0000000..3bb7072 Binary files /dev/null and b/images/blog/lower-blade-guide-1.webp differ diff --git a/images/blog/maison-poupee.JPEG b/images/blog/maison-poupee.JPEG new file mode 100755 index 0000000..87d9ea9 Binary files /dev/null and b/images/blog/maison-poupee.JPEG differ diff --git a/images/blog/maison-poupee.webp b/images/blog/maison-poupee.webp new file mode 100755 index 0000000..5f20145 Binary files /dev/null and b/images/blog/maison-poupee.webp differ diff --git a/images/blog/mini-chariot.JPEG b/images/blog/mini-chariot.JPEG new file mode 100755 index 0000000..4ccae9f Binary files /dev/null and b/images/blog/mini-chariot.JPEG differ diff --git a/images/blog/mini-chariot.webp b/images/blog/mini-chariot.webp new file mode 100755 index 0000000..1a59726 Binary files /dev/null and b/images/blog/mini-chariot.webp differ diff --git a/images/blog/mygophersite.png b/images/blog/mygophersite.png new file mode 100755 index 0000000..3fed567 Binary files /dev/null and b/images/blog/mygophersite.png differ diff --git a/images/blog/neomutt-screenshot.png b/images/blog/neomutt-screenshot.png new file mode 100755 index 0000000..b9702cb Binary files /dev/null and b/images/blog/neomutt-screenshot.png differ diff --git a/images/blog/new-bench.JPEG b/images/blog/new-bench.JPEG new file mode 100755 index 0000000..4f6c2f9 Binary files /dev/null and b/images/blog/new-bench.JPEG differ diff --git a/images/blog/new-bench.webp b/images/blog/new-bench.webp new file mode 100755 index 0000000..e763620 Binary files /dev/null and b/images/blog/new-bench.webp differ diff --git a/images/blog/newsboat-cha-1.png b/images/blog/newsboat-cha-1.png new file mode 100755 index 0000000..439d4a5 Binary files /dev/null and b/images/blog/newsboat-cha-1.png differ diff --git a/images/blog/newsboat-rdrview-1.png b/images/blog/newsboat-rdrview-1.png new file mode 100755 index 0000000..851740a Binary files /dev/null and b/images/blog/newsboat-rdrview-1.png differ diff --git a/images/blog/oak-bowl-28cm.JPEG b/images/blog/oak-bowl-28cm.JPEG new file mode 100755 index 0000000..d6a5348 Binary files /dev/null and b/images/blog/oak-bowl-28cm.JPEG differ diff --git a/images/blog/oak-bowl-28cm.webp b/images/blog/oak-bowl-28cm.webp new file mode 100755 index 0000000..7a5beb3 Binary files /dev/null and b/images/blog/oak-bowl-28cm.webp differ diff --git a/images/blog/odysee.JPEG b/images/blog/odysee.JPEG new file mode 100755 index 0000000..0c9b59a Binary files /dev/null and b/images/blog/odysee.JPEG differ diff --git a/images/blog/odysee.webp b/images/blog/odysee.webp new file mode 100755 index 0000000..10a1eb1 Binary files /dev/null and b/images/blog/odysee.webp differ diff --git a/images/blog/pense-bete.JPEG b/images/blog/pense-bete.JPEG new file mode 100755 index 0000000..647005f Binary files /dev/null and b/images/blog/pense-bete.JPEG differ diff --git a/images/blog/pense-bete.webp b/images/blog/pense-bete.webp new file mode 100755 index 0000000..2e66add Binary files /dev/null and b/images/blog/pense-bete.webp differ diff --git "a/images/blog/planche-\303\240-d\303\251couper-jacqueline.JPEG" "b/images/blog/planche-\303\240-d\303\251couper-jacqueline.JPEG" new file mode 100755 index 0000000..5bc400d Binary files /dev/null and "b/images/blog/planche-\303\240-d\303\251couper-jacqueline.JPEG" differ diff --git "a/images/blog/planche-\303\240-d\303\251couper-jacqueline.webp" "b/images/blog/planche-\303\240-d\303\251couper-jacqueline.webp" new file mode 100755 index 0000000..62be8fe Binary files /dev/null and "b/images/blog/planche-\303\240-d\303\251couper-jacqueline.webp" differ diff --git a/images/blog/poster-oiseaux-des-jardins.png b/images/blog/poster-oiseaux-des-jardins.png new file mode 100755 index 0000000..76faebb Binary files /dev/null and b/images/blog/poster-oiseaux-des-jardins.png differ diff --git a/images/blog/powerline_bash.png b/images/blog/powerline_bash.png new file mode 100755 index 0000000..8f83f23 Binary files /dev/null and b/images/blog/powerline_bash.png differ diff --git a/images/blog/powermenu.png b/images/blog/powermenu.png new file mode 100755 index 0000000..f068ea2 Binary files /dev/null and b/images/blog/powermenu.png differ diff --git a/images/blog/pyradio-again.png b/images/blog/pyradio-again.png new file mode 100755 index 0000000..392f0ea Binary files /dev/null and b/images/blog/pyradio-again.png differ diff --git a/images/blog/pyradio.JPEG b/images/blog/pyradio.JPEG new file mode 100755 index 0000000..0010c93 Binary files /dev/null and b/images/blog/pyradio.JPEG differ diff --git a/images/blog/pyradio.webp b/images/blog/pyradio.webp new file mode 100755 index 0000000..bdafdf0 Binary files /dev/null and b/images/blog/pyradio.webp differ diff --git a/images/blog/pywal16-screenshot.png b/images/blog/pywal16-screenshot.png new file mode 100755 index 0000000..65a2a8b Binary files /dev/null and b/images/blog/pywal16-screenshot.png differ diff --git a/images/blog/resistance.jpg b/images/blog/resistance.jpg new file mode 100755 index 0000000..8eda020 Binary files /dev/null and b/images/blog/resistance.jpg differ diff --git a/images/blog/riddings-web-site.JPEG b/images/blog/riddings-web-site.JPEG new file mode 100755 index 0000000..43d3522 Binary files /dev/null and b/images/blog/riddings-web-site.JPEG differ diff --git a/images/blog/riddings-web-site.webp b/images/blog/riddings-web-site.webp new file mode 100755 index 0000000..5fce9be Binary files /dev/null and b/images/blog/riddings-web-site.webp differ diff --git a/images/blog/screw-jig.JPEG b/images/blog/screw-jig.JPEG new file mode 100755 index 0000000..fb98ed8 Binary files /dev/null and b/images/blog/screw-jig.JPEG differ diff --git a/images/blog/screw-jig.webp b/images/blog/screw-jig.webp new file mode 100755 index 0000000..05e47c6 Binary files /dev/null and b/images/blog/screw-jig.webp differ diff --git a/images/blog/semware.png b/images/blog/semware.png new file mode 100644 index 0000000..8d771af Binary files /dev/null and b/images/blog/semware.png differ diff --git a/images/blog/shadocks-pumping.gif b/images/blog/shadocks-pumping.gif new file mode 100755 index 0000000..32abdb6 Binary files /dev/null and b/images/blog/shadocks-pumping.gif differ diff --git a/images/blog/slim-theme.png b/images/blog/slim-theme.png new file mode 100755 index 0000000..6b732e9 Binary files /dev/null and b/images/blog/slim-theme.png differ diff --git a/images/blog/slstatus-french.png b/images/blog/slstatus-french.png new file mode 100755 index 0000000..a5ea132 Binary files /dev/null and b/images/blog/slstatus-french.png differ diff --git a/images/blog/slstatus.png b/images/blog/slstatus.png new file mode 100755 index 0000000..2af84f3 Binary files /dev/null and b/images/blog/slstatus.png differ diff --git a/images/blog/sous-plat-incrustation.JPEG b/images/blog/sous-plat-incrustation.JPEG new file mode 100755 index 0000000..a1936f5 Binary files /dev/null and b/images/blog/sous-plat-incrustation.JPEG differ diff --git a/images/blog/sous-plat-incrustation.webp b/images/blog/sous-plat-incrustation.webp new file mode 100755 index 0000000..2a38bd2 Binary files /dev/null and b/images/blog/sous-plat-incrustation.webp differ diff --git a/images/blog/starship-gruvbox-rainbow.png b/images/blog/starship-gruvbox-rainbow.png new file mode 100755 index 0000000..cd99e56 Binary files /dev/null and b/images/blog/starship-gruvbox-rainbow.png differ diff --git a/images/blog/stop-block.JPEG b/images/blog/stop-block.JPEG new file mode 100755 index 0000000..d4628f1 Binary files /dev/null and b/images/blog/stop-block.JPEG differ diff --git a/images/blog/stop-block.webp b/images/blog/stop-block.webp new file mode 100755 index 0000000..fd6f179 Binary files /dev/null and b/images/blog/stop-block.webp differ diff --git a/images/blog/support-roue-superior.JPEG b/images/blog/support-roue-superior.JPEG new file mode 100755 index 0000000..13db203 Binary files /dev/null and b/images/blog/support-roue-superior.JPEG differ diff --git a/images/blog/support-roue-superior.webp b/images/blog/support-roue-superior.webp new file mode 100755 index 0000000..f1d1180 Binary files /dev/null and b/images/blog/support-roue-superior.webp differ diff --git a/images/blog/support-trunnion.JPEG b/images/blog/support-trunnion.JPEG new file mode 100755 index 0000000..47ad421 Binary files /dev/null and b/images/blog/support-trunnion.JPEG differ diff --git a/images/blog/support-trunnion.webp b/images/blog/support-trunnion.webp new file mode 100755 index 0000000..3b00ca4 Binary files /dev/null and b/images/blog/support-trunnion.webp differ diff --git a/images/blog/table-support-trunions.JPEG b/images/blog/table-support-trunions.JPEG new file mode 100755 index 0000000..054024f Binary files /dev/null and b/images/blog/table-support-trunions.JPEG differ diff --git a/images/blog/table-support-trunions.webp b/images/blog/table-support-trunions.webp new file mode 100755 index 0000000..1c0b91c Binary files /dev/null and b/images/blog/table-support-trunions.webp differ diff --git a/images/blog/thinkpad-x220-mic-button.JPEG b/images/blog/thinkpad-x220-mic-button.JPEG new file mode 100755 index 0000000..8fee6e1 Binary files /dev/null and b/images/blog/thinkpad-x220-mic-button.JPEG differ diff --git a/images/blog/thinkpad-x220-mic-button.webp b/images/blog/thinkpad-x220-mic-button.webp new file mode 100755 index 0000000..d214907 Binary files /dev/null and b/images/blog/thinkpad-x220-mic-button.webp differ diff --git a/images/blog/thinkvantage.png b/images/blog/thinkvantage.png new file mode 100755 index 0000000..3eb194e Binary files /dev/null and b/images/blog/thinkvantage.png differ diff --git a/images/blog/tprg.JPEG b/images/blog/tprg.JPEG new file mode 100755 index 0000000..39e4928 Binary files /dev/null and b/images/blog/tprg.JPEG differ diff --git a/images/blog/tprg.webp b/images/blog/tprg.webp new file mode 100755 index 0000000..62988ab Binary files /dev/null and b/images/blog/tprg.webp differ diff --git a/images/blog/trunion-shim.JPEG b/images/blog/trunion-shim.JPEG new file mode 100755 index 0000000..000db96 Binary files /dev/null and b/images/blog/trunion-shim.JPEG differ diff --git a/images/blog/trunion-shim.webp b/images/blog/trunion-shim.webp new file mode 100755 index 0000000..07e0f60 Binary files /dev/null and b/images/blog/trunion-shim.webp differ diff --git a/images/blog/trunions-chevilles.JPEG b/images/blog/trunions-chevilles.JPEG new file mode 100755 index 0000000..f6df681 Binary files /dev/null and b/images/blog/trunions-chevilles.JPEG differ diff --git a/images/blog/trunions-chevilles.webp b/images/blog/trunions-chevilles.webp new file mode 100755 index 0000000..1ebd8a9 Binary files /dev/null and b/images/blog/trunions-chevilles.webp differ diff --git a/images/blog/upper-blade-guide-1.JPEG b/images/blog/upper-blade-guide-1.JPEG new file mode 100755 index 0000000..1f7ce98 Binary files /dev/null and b/images/blog/upper-blade-guide-1.JPEG differ diff --git a/images/blog/upper-blade-guide-1.webp b/images/blog/upper-blade-guide-1.webp new file mode 100755 index 0000000..408aa45 Binary files /dev/null and b/images/blog/upper-blade-guide-1.webp differ diff --git a/images/blog/w3m-wittamore-1.png b/images/blog/w3m-wittamore-1.png new file mode 100755 index 0000000..f710f82 Binary files /dev/null and b/images/blog/w3m-wittamore-1.png differ diff --git a/images/blog/workshop-update.JPEG b/images/blog/workshop-update.JPEG new file mode 100755 index 0000000..779928f Binary files /dev/null and b/images/blog/workshop-update.JPEG differ diff --git a/images/blog/workshop-update.webp b/images/blog/workshop-update.webp new file mode 100755 index 0000000..c1a30c3 Binary files /dev/null and b/images/blog/workshop-update.webp differ diff --git a/images/blog/wttr.JPEG b/images/blog/wttr.JPEG new file mode 100755 index 0000000..c5acd36 Binary files /dev/null and b/images/blog/wttr.JPEG differ diff --git a/images/blog/wttr.webp b/images/blog/wttr.webp new file mode 100755 index 0000000..60e57e7 Binary files /dev/null and b/images/blog/wttr.webp differ diff --git a/images/blog/x200.JPEG b/images/blog/x200.JPEG new file mode 100755 index 0000000..13c3d2b Binary files /dev/null and b/images/blog/x200.JPEG differ diff --git a/images/blog/x200.webp b/images/blog/x200.webp new file mode 100755 index 0000000..ae044ca Binary files /dev/null and b/images/blog/x200.webp differ diff --git a/images/blog/xfce4-panel-cpu-fan.png b/images/blog/xfce4-panel-cpu-fan.png new file mode 100755 index 0000000..a0c15ee Binary files /dev/null and b/images/blog/xfce4-panel-cpu-fan.png differ diff --git a/images/blog/zellij.JPEG b/images/blog/zellij.JPEG new file mode 100755 index 0000000..fa0a73a Binary files /dev/null and b/images/blog/zellij.JPEG differ diff --git a/images/blog/zellij.webp b/images/blog/zellij.webp new file mode 100755 index 0000000..2fdc50a Binary files /dev/null and b/images/blog/zellij.webp differ diff --git a/images/bloggings.jpg b/images/bloggings.jpg new file mode 100755 index 0000000..da03ed3 Binary files /dev/null and b/images/bloggings.jpg differ diff --git a/images/bonneannee.gif b/images/bonneannee.gif new file mode 100755 index 0000000..9eafaf0 Binary files /dev/null and b/images/bonneannee.gif differ diff --git a/images/bowl.jpg b/images/bowl.jpg new file mode 100755 index 0000000..c25934c Binary files /dev/null and b/images/bowl.jpg differ diff --git a/images/box-chevaux.JPEG b/images/box-chevaux.JPEG new file mode 100755 index 0000000..2298ef1 Binary files /dev/null and b/images/box-chevaux.JPEG differ diff --git a/images/box-chevaux.webp b/images/box-chevaux.webp new file mode 100755 index 0000000..855d5eb Binary files /dev/null and b/images/box-chevaux.webp differ diff --git a/images/browsers/dillo.jpg b/images/browsers/dillo.jpg new file mode 100755 index 0000000..144aa73 Binary files /dev/null and b/images/browsers/dillo.jpg differ diff --git a/images/browsers/firefox.jpg b/images/browsers/firefox.jpg new file mode 100755 index 0000000..144f55d Binary files /dev/null and b/images/browsers/firefox.jpg differ diff --git a/images/browsers/index.php b/images/browsers/index.php new file mode 100755 index 0000000..84c0b5c --- /dev/null +++ b/images/browsers/index.php @@ -0,0 +1,28 @@ + + + + + + + Bloggings + + +
+
+

Bloggings

+
+ Back +
+
+

+
%s
", $img, $caption); + } + ?> + + diff --git a/images/browsers/links.jpg b/images/browsers/links.jpg new file mode 100755 index 0000000..42c79a6 Binary files /dev/null and b/images/browsers/links.jpg differ diff --git a/images/browsers/lynx.jpg b/images/browsers/lynx.jpg new file mode 100755 index 0000000..3841b6f Binary files /dev/null and b/images/browsers/lynx.jpg differ diff --git a/images/browsers/w3m.jpg b/images/browsers/w3m.jpg new file mode 100755 index 0000000..d6886c9 Binary files /dev/null and b/images/browsers/w3m.jpg differ diff --git a/images/chantournage/animaux-ferme.webp b/images/chantournage/animaux-ferme.webp new file mode 100755 index 0000000..7ebf5e0 Binary files /dev/null and b/images/chantournage/animaux-ferme.webp differ diff --git a/images/chantournage/boite-coeur.webp b/images/chantournage/boite-coeur.webp new file mode 100755 index 0000000..083d34a Binary files /dev/null and b/images/chantournage/boite-coeur.webp differ diff --git a/images/chantournage/feuilles-plumes.webp b/images/chantournage/feuilles-plumes.webp new file mode 100755 index 0000000..1fb8e5c Binary files /dev/null and b/images/chantournage/feuilles-plumes.webp differ diff --git "a/images/chantournage/f\303\252te-des-grandparents.webp" "b/images/chantournage/f\303\252te-des-grandparents.webp" new file mode 100755 index 0000000..ead693b Binary files /dev/null and "b/images/chantournage/f\303\252te-des-grandparents.webp" differ diff --git a/images/chantournage/index.php b/images/chantournage/index.php new file mode 100755 index 0000000..2b9c6b5 --- /dev/null +++ b/images/chantournage/index.php @@ -0,0 +1,33 @@ + + + + + + + + Bloggings + + +
+

Bloggings

+ Back +
+
+
+

+
%s
", $img, $caption); + } + ?> +
+
+ + + diff --git "a/images/chantournage/moto-ch\303\252ne.webp" "b/images/chantournage/moto-ch\303\252ne.webp" new file mode 100755 index 0000000..4fe2f13 Binary files /dev/null and "b/images/chantournage/moto-ch\303\252ne.webp" differ diff --git "a/images/chantournage/sous-plat-ch\303\252ne-33cm-triskele.webp" "b/images/chantournage/sous-plat-ch\303\252ne-33cm-triskele.webp" new file mode 100755 index 0000000..e1c789e Binary files /dev/null and "b/images/chantournage/sous-plat-ch\303\252ne-33cm-triskele.webp" differ diff --git a/images/chantournage/sous-plat-hermine.webp b/images/chantournage/sous-plat-hermine.webp new file mode 100755 index 0000000..43026b2 Binary files /dev/null and b/images/chantournage/sous-plat-hermine.webp differ diff --git a/images/chantournage/triskele-cp-bouleau.webp b/images/chantournage/triskele-cp-bouleau.webp new file mode 100755 index 0000000..574201c Binary files /dev/null and b/images/chantournage/triskele-cp-bouleau.webp differ diff --git a/images/chimera.jpg b/images/chimera.jpg new file mode 100755 index 0000000..0462ea0 Binary files /dev/null and b/images/chimera.jpg differ diff --git a/images/classic-keyboard.JPEG b/images/classic-keyboard.JPEG new file mode 100755 index 0000000..ab45a6d Binary files /dev/null and b/images/classic-keyboard.JPEG differ diff --git a/images/classic-keyboard.webp b/images/classic-keyboard.webp new file mode 100755 index 0000000..4afa8e5 Binary files /dev/null and b/images/classic-keyboard.webp differ diff --git a/images/core-i5-cpu.JPEG b/images/core-i5-cpu.JPEG new file mode 100755 index 0000000..497e5aa Binary files /dev/null and b/images/core-i5-cpu.JPEG differ diff --git a/images/core-i5-cpu.png b/images/core-i5-cpu.png new file mode 100755 index 0000000..4d98615 Binary files /dev/null and b/images/core-i5-cpu.png differ diff --git a/images/core-i5-cpu.webp b/images/core-i5-cpu.webp new file mode 100755 index 0000000..62353d9 Binary files /dev/null and b/images/core-i5-cpu.webp differ diff --git a/images/coronet-herald.jpg b/images/coronet-herald.jpg new file mode 100755 index 0000000..c38786a Binary files /dev/null and b/images/coronet-herald.jpg differ diff --git a/images/corse-1975.jpg b/images/corse-1975.jpg new file mode 100755 index 0000000..35c41fb Binary files /dev/null and b/images/corse-1975.jpg differ diff --git a/images/dates-limites.jpg b/images/dates-limites.jpg new file mode 100755 index 0000000..b0c0b89 Binary files /dev/null and b/images/dates-limites.jpg differ diff --git a/images/debian-vs-arch.jpg b/images/debian-vs-arch.jpg new file mode 100755 index 0000000..c203e89 Binary files /dev/null and b/images/debian-vs-arch.jpg differ diff --git a/images/deltachatqr.jpg b/images/deltachatqr.jpg new file mode 100644 index 0000000..322b442 Binary files /dev/null and b/images/deltachatqr.jpg differ diff --git a/images/espion-dans-notre-poche.JPEG b/images/espion-dans-notre-poche.JPEG new file mode 100755 index 0000000..1fad281 Binary files /dev/null and b/images/espion-dans-notre-poche.JPEG differ diff --git a/images/espion-dans-notre-poche.webp b/images/espion-dans-notre-poche.webp new file mode 100755 index 0000000..33363f0 Binary files /dev/null and b/images/espion-dans-notre-poche.webp differ diff --git a/images/etau.JPEG b/images/etau.JPEG new file mode 100755 index 0000000..ca69d26 Binary files /dev/null and b/images/etau.JPEG differ diff --git a/images/etau.webp b/images/etau.webp new file mode 100755 index 0000000..b35400b Binary files /dev/null and b/images/etau.webp differ diff --git a/images/eu-flag.JPEG b/images/eu-flag.JPEG new file mode 100755 index 0000000..15285f2 Binary files /dev/null and b/images/eu-flag.JPEG differ diff --git a/images/eu-flag.webp b/images/eu-flag.webp new file mode 100755 index 0000000..487dfd4 Binary files /dev/null and b/images/eu-flag.webp differ diff --git a/images/evil-empire.jpg b/images/evil-empire.jpg new file mode 100755 index 0000000..78b3eb4 Binary files /dev/null and b/images/evil-empire.jpg differ diff --git a/images/firefox-drm.jpg b/images/firefox-drm.jpg new file mode 100755 index 0000000..45282ce Binary files /dev/null and b/images/firefox-drm.jpg differ diff --git a/images/first-box.JPEG b/images/first-box.JPEG new file mode 100755 index 0000000..3aa106d Binary files /dev/null and b/images/first-box.JPEG differ diff --git a/images/first-box.webp b/images/first-box.webp new file mode 100755 index 0000000..9f6f95c Binary files /dev/null and b/images/first-box.webp differ diff --git a/images/fugitive-telemetry.JPEG b/images/fugitive-telemetry.JPEG new file mode 100755 index 0000000..3a4c55b Binary files /dev/null and b/images/fugitive-telemetry.JPEG differ diff --git a/images/fugitive-telemetry.webp b/images/fugitive-telemetry.webp new file mode 100755 index 0000000..466185c Binary files /dev/null and b/images/fugitive-telemetry.webp differ diff --git a/images/grande-vase.JPEG b/images/grande-vase.JPEG new file mode 100755 index 0000000..2757b37 Binary files /dev/null and b/images/grande-vase.JPEG differ diff --git a/images/grande-vase.webp b/images/grande-vase.webp new file mode 100755 index 0000000..81f746c Binary files /dev/null and b/images/grande-vase.webp differ diff --git a/images/gsimplecal.JPEG b/images/gsimplecal.JPEG new file mode 100755 index 0000000..b642dac Binary files /dev/null and b/images/gsimplecal.JPEG differ diff --git a/images/gsimplecal.webp b/images/gsimplecal.webp new file mode 100755 index 0000000..8c7b997 Binary files /dev/null and b/images/gsimplecal.webp differ diff --git a/images/hail-mary-wier.JPEG b/images/hail-mary-wier.JPEG new file mode 100755 index 0000000..438e790 Binary files /dev/null and b/images/hail-mary-wier.JPEG differ diff --git a/images/hail-mary-wier.webp b/images/hail-mary-wier.webp new file mode 100755 index 0000000..2f7d8cc Binary files /dev/null and b/images/hail-mary-wier.webp differ diff --git a/images/happy-new-year.gif b/images/happy-new-year.gif new file mode 100755 index 0000000..6bc958e Binary files /dev/null and b/images/happy-new-year.gif differ diff --git a/images/histoire/1740.jpg b/images/histoire/1740.jpg new file mode 100755 index 0000000..d276f5c Binary files /dev/null and b/images/histoire/1740.jpg differ diff --git a/images/histoire/1866.jpg b/images/histoire/1866.jpg new file mode 100755 index 0000000..64a6a4f Binary files /dev/null and b/images/histoire/1866.jpg differ diff --git a/images/histoire/Allee-couverte-la-rocher.png b/images/histoire/Allee-couverte-la-rocher.png new file mode 100755 index 0000000..333d5b0 Binary files /dev/null and b/images/histoire/Allee-couverte-la-rocher.png differ diff --git "a/images/histoire/Ch\303\252ne-mill\303\251naire-saint-jacut.jpg" "b/images/histoire/Ch\303\252ne-mill\303\251naire-saint-jacut.jpg" new file mode 100755 index 0000000..c70fc94 Binary files /dev/null and "b/images/histoire/Ch\303\252ne-mill\303\251naire-saint-jacut.jpg" differ diff --git a/images/histoire/Menhir-de-la-Touche-article.png b/images/histoire/Menhir-de-la-Touche-article.png new file mode 100755 index 0000000..3796188 Binary files /dev/null and b/images/histoire/Menhir-de-la-Touche-article.png differ diff --git a/images/histoire/arrete-confitures-1942.webp b/images/histoire/arrete-confitures-1942.webp new file mode 100755 index 0000000..f6b931e Binary files /dev/null and b/images/histoire/arrete-confitures-1942.webp differ diff --git a/images/histoire/bas_fournaux.jpg b/images/histoire/bas_fournaux.jpg new file mode 100755 index 0000000..6db8a0d Binary files /dev/null and b/images/histoire/bas_fournaux.jpg differ diff --git a/images/histoire/bourg-1824.png b/images/histoire/bourg-1824.png new file mode 100755 index 0000000..1dbf36f Binary files /dev/null and b/images/histoire/bourg-1824.png differ diff --git a/images/histoire/chateau-de-la-couaille.jpg b/images/histoire/chateau-de-la-couaille.jpg new file mode 100755 index 0000000..7d7e057 Binary files /dev/null and b/images/histoire/chateau-de-la-couaille.jpg differ diff --git a/images/histoire/cocantine.png b/images/histoire/cocantine.png new file mode 100755 index 0000000..175a2d9 Binary files /dev/null and b/images/histoire/cocantine.png differ diff --git a/images/histoire/cour-de-queneleue.png b/images/histoire/cour-de-queneleue.png new file mode 100755 index 0000000..7d39d92 Binary files /dev/null and b/images/histoire/cour-de-queneleue.png differ diff --git a/images/histoire/delivrance-cartes-alimentaires-1941.webp b/images/histoire/delivrance-cartes-alimentaires-1941.webp new file mode 100755 index 0000000..3a10012 Binary files /dev/null and b/images/histoire/delivrance-cartes-alimentaires-1941.webp differ diff --git a/images/histoire/eglise_saint_eutrope.jpg b/images/histoire/eglise_saint_eutrope.jpg new file mode 100755 index 0000000..8ac5bd1 Binary files /dev/null and b/images/histoire/eglise_saint_eutrope.jpg differ diff --git a/images/histoire/haches-langourla.png b/images/histoire/haches-langourla.png new file mode 100755 index 0000000..fda8902 Binary files /dev/null and b/images/histoire/haches-langourla.png differ diff --git a/images/histoire/index.php b/images/histoire/index.php new file mode 100755 index 0000000..276724b --- /dev/null +++ b/images/histoire/index.php @@ -0,0 +1,33 @@ + + + + + + + + Bloggings + + +
+

Bloggings

+ Back +
+
+
+

+
%s
", $img, $caption); + } + ?> +
+
+ + + diff --git a/images/histoire/vieux-chatet-de-coelan.png b/images/histoire/vieux-chatet-de-coelan.png new file mode 100755 index 0000000..eb2f821 Binary files /dev/null and b/images/histoire/vieux-chatet-de-coelan.png differ diff --git a/images/i3-blocks.JPEG b/images/i3-blocks.JPEG new file mode 100755 index 0000000..9a35cf8 Binary files /dev/null and b/images/i3-blocks.JPEG differ diff --git a/images/i3-blocks.webp b/images/i3-blocks.webp new file mode 100755 index 0000000..d31373d Binary files /dev/null and b/images/i3-blocks.webp differ diff --git a/images/index.php b/images/index.php new file mode 100755 index 0000000..84c0b5c --- /dev/null +++ b/images/index.php @@ -0,0 +1,28 @@ + + + + + + + Bloggings + + +
+
+

Bloggings

+
+ Back +
+
+

+
%s
", $img, $caption); + } + ?> + + diff --git a/images/informatique/amstrad-cpc-464.jpg b/images/informatique/amstrad-cpc-464.jpg new file mode 100755 index 0000000..592e135 Binary files /dev/null and b/images/informatique/amstrad-cpc-464.jpg differ diff --git a/images/informatique/burroghs_tc500.jpg b/images/informatique/burroghs_tc500.jpg new file mode 100755 index 0000000..de5633a Binary files /dev/null and b/images/informatique/burroghs_tc500.jpg differ diff --git a/images/informatique/ibmpc.jpg b/images/informatique/ibmpc.jpg new file mode 100755 index 0000000..2091f9e Binary files /dev/null and b/images/informatique/ibmpc.jpg differ diff --git a/images/informatique/index.php b/images/informatique/index.php new file mode 100755 index 0000000..276724b --- /dev/null +++ b/images/informatique/index.php @@ -0,0 +1,33 @@ + + + + + + + + Bloggings + + +
+

Bloggings

+ Back +
+
+
+

+
%s
", $img, $caption); + } + ?> +
+
+ + + diff --git "a/images/informatique/no\303\253l-magasin.jpg" "b/images/informatique/no\303\253l-magasin.jpg" new file mode 100755 index 0000000..06bc5f9 Binary files /dev/null and "b/images/informatique/no\303\253l-magasin.jpg" differ diff --git a/images/informatique/shop.jpg b/images/informatique/shop.jpg new file mode 100755 index 0000000..579be7a Binary files /dev/null and b/images/informatique/shop.jpg differ diff --git a/images/informatique/tandy1000hx.jpg b/images/informatique/tandy1000hx.jpg new file mode 100755 index 0000000..1302013 Binary files /dev/null and b/images/informatique/tandy1000hx.jpg differ diff --git a/images/informatique/zx81.jpg b/images/informatique/zx81.jpg new file mode 100755 index 0000000..ba481f0 Binary files /dev/null and b/images/informatique/zx81.jpg differ diff --git a/images/jersey-before.jpg b/images/jersey-before.jpg new file mode 100755 index 0000000..aa02ce4 Binary files /dev/null and b/images/jersey-before.jpg differ diff --git a/images/kobo-clara-colour.png b/images/kobo-clara-colour.png new file mode 100755 index 0000000..703b7c3 Binary files /dev/null and b/images/kobo-clara-colour.png differ diff --git a/images/la-grande-illusion.JPEG b/images/la-grande-illusion.JPEG new file mode 100755 index 0000000..9250c1d Binary files /dev/null and b/images/la-grande-illusion.JPEG differ diff --git a/images/la-grande-illusion.webp b/images/la-grande-illusion.webp new file mode 100755 index 0000000..9f1fa17 Binary files /dev/null and b/images/la-grande-illusion.webp differ diff --git a/images/langourla_map.jpg b/images/langourla_map.jpg new file mode 100755 index 0000000..bdba94e Binary files /dev/null and b/images/langourla_map.jpg differ diff --git a/images/linux-servers-cloud.jpg b/images/linux-servers-cloud.jpg new file mode 100755 index 0000000..4147240 Binary files /dev/null and b/images/linux-servers-cloud.jpg differ diff --git a/images/lnav.JPEG b/images/lnav.JPEG new file mode 100755 index 0000000..f357cc0 Binary files /dev/null and b/images/lnav.JPEG differ diff --git a/images/lnav.webp b/images/lnav.webp new file mode 100755 index 0000000..f4fc3ed Binary files /dev/null and b/images/lnav.webp differ diff --git a/images/logos/aposit.jpg b/images/logos/aposit.jpg new file mode 100755 index 0000000..cb46aba Binary files /dev/null and b/images/logos/aposit.jpg differ diff --git a/images/logos/applixware.jpg b/images/logos/applixware.jpg new file mode 100755 index 0000000..0b7e3e5 Binary files /dev/null and b/images/logos/applixware.jpg differ diff --git a/images/logos/avenir.jpg b/images/logos/avenir.jpg new file mode 100755 index 0000000..bb47a3c Binary files /dev/null and b/images/logos/avenir.jpg differ diff --git a/images/logos/emess.jpg b/images/logos/emess.jpg new file mode 100755 index 0000000..cf9b00a Binary files /dev/null and b/images/logos/emess.jpg differ diff --git a/images/logos/greenhouse.jpg b/images/logos/greenhouse.jpg new file mode 100755 index 0000000..af4c420 Binary files /dev/null and b/images/logos/greenhouse.jpg differ diff --git a/images/logos/idinan.jpg b/images/logos/idinan.jpg new file mode 100755 index 0000000..aa0a1de Binary files /dev/null and b/images/logos/idinan.jpg differ diff --git a/images/logos/index.php b/images/logos/index.php new file mode 100755 index 0000000..bc46e7a --- /dev/null +++ b/images/logos/index.php @@ -0,0 +1,33 @@ + + + + + + + + Bloggings + + +
+

Bloggings

+ Back +
+
+
+

+
%s
", $img, $caption); + } + ?> +
+
+ + + diff --git a/images/logos/langourlavie.jpg b/images/logos/langourlavie.jpg new file mode 100755 index 0000000..bd399ca Binary files /dev/null and b/images/logos/langourlavie.jpg differ diff --git a/images/logos/quilting.jpg b/images/logos/quilting.jpg new file mode 100755 index 0000000..b5d5902 Binary files /dev/null and b/images/logos/quilting.jpg differ diff --git a/images/logos/vistasource.jpg b/images/logos/vistasource.jpg new file mode 100755 index 0000000..70593c8 Binary files /dev/null and b/images/logos/vistasource.jpg differ diff --git a/images/lower-blade-guide-1.JPEG b/images/lower-blade-guide-1.JPEG new file mode 100755 index 0000000..90d3bfd Binary files /dev/null and b/images/lower-blade-guide-1.JPEG differ diff --git a/images/lower-blade-guide-1.webp b/images/lower-blade-guide-1.webp new file mode 100755 index 0000000..31c61d1 Binary files /dev/null and b/images/lower-blade-guide-1.webp differ diff --git a/images/mailcow-logo.png b/images/mailcow-logo.png new file mode 100755 index 0000000..3669cdc Binary files /dev/null and b/images/mailcow-logo.png differ diff --git a/images/maison-poupee.JPEG b/images/maison-poupee.JPEG new file mode 100755 index 0000000..ae7217a Binary files /dev/null and b/images/maison-poupee.JPEG differ diff --git a/images/maison-poupee.webp b/images/maison-poupee.webp new file mode 100755 index 0000000..32005a7 Binary files /dev/null and b/images/maison-poupee.webp differ diff --git a/images/me.jpg b/images/me.jpg new file mode 100755 index 0000000..d987a19 Binary files /dev/null and b/images/me.jpg differ diff --git a/images/mene/langourla/cartes-postales/1937-ancien-mairie.webp b/images/mene/langourla/cartes-postales/1937-ancien-mairie.webp new file mode 100755 index 0000000..84f29de Binary files /dev/null and b/images/mene/langourla/cartes-postales/1937-ancien-mairie.webp differ diff --git a/images/mene/langourla/cartes-postales/avenues-saint-joseph-2.webp b/images/mene/langourla/cartes-postales/avenues-saint-joseph-2.webp new file mode 100755 index 0000000..0b69ec8 Binary files /dev/null and b/images/mene/langourla/cartes-postales/avenues-saint-joseph-2.webp differ diff --git a/images/mene/langourla/cartes-postales/avenues-saint-joseph.webp b/images/mene/langourla/cartes-postales/avenues-saint-joseph.webp new file mode 100755 index 0000000..244c8fa Binary files /dev/null and b/images/mene/langourla/cartes-postales/avenues-saint-joseph.webp differ diff --git a/images/mene/langourla/cartes-postales/bourg-et-monument.webp b/images/mene/langourla/cartes-postales/bourg-et-monument.webp new file mode 100755 index 0000000..6acde74 Binary files /dev/null and b/images/mene/langourla/cartes-postales/bourg-et-monument.webp differ diff --git a/images/mene/langourla/cartes-postales/bourg.webp b/images/mene/langourla/cartes-postales/bourg.webp new file mode 100755 index 0000000..bc67d48 Binary files /dev/null and b/images/mene/langourla/cartes-postales/bourg.webp differ diff --git "a/images/mene/langourla/cartes-postales/caf\303\251-du-centre.webp" "b/images/mene/langourla/cartes-postales/caf\303\251-du-centre.webp" new file mode 100755 index 0000000..c02e25b Binary files /dev/null and "b/images/mene/langourla/cartes-postales/caf\303\251-du-centre.webp" differ diff --git "a/images/mene/langourla/cartes-postales/champ-du-ch\303\252ne.webp" "b/images/mene/langourla/cartes-postales/champ-du-ch\303\252ne.webp" new file mode 100755 index 0000000..513bd22 Binary files /dev/null and "b/images/mene/langourla/cartes-postales/champ-du-ch\303\252ne.webp" differ diff --git "a/images/mene/langourla/cartes-postales/chapel-et-ch\303\252ne.webp" "b/images/mene/langourla/cartes-postales/chapel-et-ch\303\252ne.webp" new file mode 100755 index 0000000..2dc5b22 Binary files /dev/null and "b/images/mene/langourla/cartes-postales/chapel-et-ch\303\252ne.webp" differ diff --git a/images/mene/langourla/cartes-postales/chapelle-saint-joseph.webp b/images/mene/langourla/cartes-postales/chapelle-saint-joseph.webp new file mode 100755 index 0000000..6240a3f Binary files /dev/null and b/images/mene/langourla/cartes-postales/chapelle-saint-joseph.webp differ diff --git a/images/mene/langourla/cartes-postales/chateau-de-coelan.webp b/images/mene/langourla/cartes-postales/chateau-de-coelan.webp new file mode 100755 index 0000000..c395ead Binary files /dev/null and b/images/mene/langourla/cartes-postales/chateau-de-coelan.webp differ diff --git "a/images/mene/langourla/cartes-postales/ch\303\252ne-miraculeux.webp" "b/images/mene/langourla/cartes-postales/ch\303\252ne-miraculeux.webp" new file mode 100755 index 0000000..775e9ed Binary files /dev/null and "b/images/mene/langourla/cartes-postales/ch\303\252ne-miraculeux.webp" differ diff --git a/images/mene/langourla/cartes-postales/ecole.webp b/images/mene/langourla/cartes-postales/ecole.webp new file mode 100755 index 0000000..5259f0c Binary files /dev/null and b/images/mene/langourla/cartes-postales/ecole.webp differ diff --git a/images/mene/langourla/cartes-postales/eglise.webp b/images/mene/langourla/cartes-postales/eglise.webp new file mode 100755 index 0000000..5b6edd8 Binary files /dev/null and b/images/mene/langourla/cartes-postales/eglise.webp differ diff --git a/images/mene/langourla/cartes-postales/index.php b/images/mene/langourla/cartes-postales/index.php new file mode 100755 index 0000000..bc46e7a --- /dev/null +++ b/images/mene/langourla/cartes-postales/index.php @@ -0,0 +1,33 @@ + + + + + + + + Bloggings + + +
+

Bloggings

+ Back +
+
+
+

+
%s
", $img, $caption); + } + ?> +
+
+ + + diff --git a/images/mene/langourla/cartes-postales/multivues.webp b/images/mene/langourla/cartes-postales/multivues.webp new file mode 100755 index 0000000..185dec0 Binary files /dev/null and b/images/mene/langourla/cartes-postales/multivues.webp differ diff --git a/images/mene/langourla/cartes-postales/rocher-le-celte.webp b/images/mene/langourla/cartes-postales/rocher-le-celte.webp new file mode 100755 index 0000000..bd34a4f Binary files /dev/null and b/images/mene/langourla/cartes-postales/rocher-le-celte.webp differ diff --git "a/images/mene/langourla/cartes-postales/tour-saint-eutrop-et-cimiti\303\250re.webp" "b/images/mene/langourla/cartes-postales/tour-saint-eutrop-et-cimiti\303\250re.webp" new file mode 100755 index 0000000..1f359a3 Binary files /dev/null and "b/images/mene/langourla/cartes-postales/tour-saint-eutrop-et-cimiti\303\250re.webp" differ diff --git a/images/mene/langourla/cartes-postales/tour-saint-eutrop.webp b/images/mene/langourla/cartes-postales/tour-saint-eutrop.webp new file mode 100755 index 0000000..fdbae8b Binary files /dev/null and b/images/mene/langourla/cartes-postales/tour-saint-eutrop.webp differ diff --git a/images/mene/langourla/cartes-postales/vieux-moulin-de-cadeuc.webp b/images/mene/langourla/cartes-postales/vieux-moulin-de-cadeuc.webp new file mode 100755 index 0000000..1c0f5d5 Binary files /dev/null and b/images/mene/langourla/cartes-postales/vieux-moulin-de-cadeuc.webp differ diff --git a/images/mene/langourla/cartes-postales/villa-des-rosiers.webp b/images/mene/langourla/cartes-postales/villa-des-rosiers.webp new file mode 100755 index 0000000..2724fce Binary files /dev/null and b/images/mene/langourla/cartes-postales/villa-des-rosiers.webp differ diff --git a/images/mene/langourla/cartes-postales/vue-aerienne.webp b/images/mene/langourla/cartes-postales/vue-aerienne.webp new file mode 100755 index 0000000..4ed5052 Binary files /dev/null and b/images/mene/langourla/cartes-postales/vue-aerienne.webp differ diff --git a/images/mene/langourla/cartes-postales/vue-eglise.webp b/images/mene/langourla/cartes-postales/vue-eglise.webp new file mode 100755 index 0000000..419d5ac Binary files /dev/null and b/images/mene/langourla/cartes-postales/vue-eglise.webp differ diff --git "a/images/mene/langourla/photos/Chemin-de-saint-gilles-\303\240-Tangaie.webp" "b/images/mene/langourla/photos/Chemin-de-saint-gilles-\303\240-Tangaie.webp" new file mode 100755 index 0000000..e78d3c9 Binary files /dev/null and "b/images/mene/langourla/photos/Chemin-de-saint-gilles-\303\240-Tangaie.webp" differ diff --git a/images/mene/langourla/photos/La-roche-de-la-Jument.webp b/images/mene/langourla/photos/La-roche-de-la-Jument.webp new file mode 100755 index 0000000..3f13d8e Binary files /dev/null and b/images/mene/langourla/photos/La-roche-de-la-Jument.webp differ diff --git "a/images/mene/langourla/photos/Pied-de-boeuf-\303\240-Tangaie.webp" "b/images/mene/langourla/photos/Pied-de-boeuf-\303\240-Tangaie.webp" new file mode 100755 index 0000000..9e7dea4 Binary files /dev/null and "b/images/mene/langourla/photos/Pied-de-boeuf-\303\240-Tangaie.webp" differ diff --git a/images/mene/langourla/photos/index.php b/images/mene/langourla/photos/index.php new file mode 100755 index 0000000..bc46e7a --- /dev/null +++ b/images/mene/langourla/photos/index.php @@ -0,0 +1,33 @@ + + + + + + + + Bloggings + + +
+

Bloggings

+ Back +
+
+
+

+
%s
", $img, $caption); + } + ?> +
+
+ + + diff --git a/images/mene/logos/collinee.png b/images/mene/logos/collinee.png new file mode 100755 index 0000000..8f63ca2 Binary files /dev/null and b/images/mene/logos/collinee.png differ diff --git a/images/mene/logos/index.php b/images/mene/logos/index.php new file mode 100755 index 0000000..bc46e7a --- /dev/null +++ b/images/mene/logos/index.php @@ -0,0 +1,33 @@ + + + + + + + + Bloggings + + +
+

Bloggings

+ Back +
+
+
+

+
%s
", $img, $caption); + } + ?> +
+
+ + + diff --git a/images/mene/logos/langourla.png b/images/mene/logos/langourla.png new file mode 100755 index 0000000..387b070 Binary files /dev/null and b/images/mene/logos/langourla.png differ diff --git a/images/mene/logos/le gouray.png b/images/mene/logos/le gouray.png new file mode 100755 index 0000000..fba3b7b Binary files /dev/null and b/images/mene/logos/le gouray.png differ diff --git a/images/mene/logos/plessala.png b/images/mene/logos/plessala.png new file mode 100755 index 0000000..b76cbe3 Binary files /dev/null and b/images/mene/logos/plessala.png differ diff --git a/images/mene/logos/saint gilles.png b/images/mene/logos/saint gilles.png new file mode 100755 index 0000000..ff76a4e Binary files /dev/null and b/images/mene/logos/saint gilles.png differ diff --git a/images/mene/logos/saint goueno.png b/images/mene/logos/saint goueno.png new file mode 100755 index 0000000..2a55b22 Binary files /dev/null and b/images/mene/logos/saint goueno.png differ diff --git "a/images/mene/logos/saint jacut du men\303\251.png" "b/images/mene/logos/saint jacut du men\303\251.png" new file mode 100755 index 0000000..8b79a1b Binary files /dev/null and "b/images/mene/logos/saint jacut du men\303\251.png" differ diff --git a/images/mene/saint-jacut-du-mene/cartes-postale b/images/mene/saint-jacut-du-mene/cartes-postale new file mode 100755 index 0000000..665191a Binary files /dev/null and b/images/mene/saint-jacut-du-mene/cartes-postale differ diff --git "a/images/mene/saint-jacut-du-mene/cartes-postales/All\303\251e-couverte-du-Rocher.webp" "b/images/mene/saint-jacut-du-mene/cartes-postales/All\303\251e-couverte-du-Rocher.webp" new file mode 100755 index 0000000..456a0b5 Binary files /dev/null and "b/images/mene/saint-jacut-du-mene/cartes-postales/All\303\251e-couverte-du-Rocher.webp" differ diff --git "a/images/mene/saint-jacut-du-mene/cartes-postales/Ch\303\252ne-mill\303\251naire.webp" "b/images/mene/saint-jacut-du-mene/cartes-postales/Ch\303\252ne-mill\303\251naire.webp" new file mode 100755 index 0000000..e723a85 Binary files /dev/null and "b/images/mene/saint-jacut-du-mene/cartes-postales/Ch\303\252ne-mill\303\251naire.webp" differ diff --git a/images/mene/saint-jacut-du-mene/cartes-postales/Menhir-de-la-Touche.webp b/images/mene/saint-jacut-du-mene/cartes-postales/Menhir-de-la-Touche.webp new file mode 100755 index 0000000..2577538 Binary files /dev/null and b/images/mene/saint-jacut-du-mene/cartes-postales/Menhir-de-la-Touche.webp differ diff --git a/images/mene/saint-jacut-du-mene/cartes-postales/chapelle-et-monument.webp b/images/mene/saint-jacut-du-mene/cartes-postales/chapelle-et-monument.webp new file mode 100755 index 0000000..825579d Binary files /dev/null and b/images/mene/saint-jacut-du-mene/cartes-postales/chapelle-et-monument.webp differ diff --git a/images/mene/saint-jacut-du-mene/cartes-postales/chapelle-n.d-de-bon-reconfort.webp b/images/mene/saint-jacut-du-mene/cartes-postales/chapelle-n.d-de-bon-reconfort.webp new file mode 100755 index 0000000..3f89634 Binary files /dev/null and b/images/mene/saint-jacut-du-mene/cartes-postales/chapelle-n.d-de-bon-reconfort.webp differ diff --git a/images/mene/saint-jacut-du-mene/cartes-postales/chapelle-saint-jean-baptiste.webp b/images/mene/saint-jacut-du-mene/cartes-postales/chapelle-saint-jean-baptiste.webp new file mode 100755 index 0000000..665191a Binary files /dev/null and b/images/mene/saint-jacut-du-mene/cartes-postales/chapelle-saint-jean-baptiste.webp differ diff --git a/images/mene/saint-jacut-du-mene/cartes-postales/chateau-du-parc.webp b/images/mene/saint-jacut-du-mene/cartes-postales/chateau-du-parc.webp new file mode 100755 index 0000000..883674c Binary files /dev/null and b/images/mene/saint-jacut-du-mene/cartes-postales/chateau-du-parc.webp differ diff --git a/images/mene/saint-jacut-du-mene/cartes-postales/eglise.webp b/images/mene/saint-jacut-du-mene/cartes-postales/eglise.webp new file mode 100755 index 0000000..54405a1 Binary files /dev/null and b/images/mene/saint-jacut-du-mene/cartes-postales/eglise.webp differ diff --git a/images/mene/saint-jacut-du-mene/cartes-postales/index.php b/images/mene/saint-jacut-du-mene/cartes-postales/index.php new file mode 100755 index 0000000..bc46e7a --- /dev/null +++ b/images/mene/saint-jacut-du-mene/cartes-postales/index.php @@ -0,0 +1,33 @@ + + + + + + + + Bloggings + + +
+

Bloggings

+ Back +
+
+
+

+
%s
", $img, $caption); + } + ?> +
+
+ + + diff --git a/images/mene/saint-jacut-du-mene/cartes-postales/la-batterie.webp b/images/mene/saint-jacut-du-mene/cartes-postales/la-batterie.webp new file mode 100755 index 0000000..97cb955 Binary files /dev/null and b/images/mene/saint-jacut-du-mene/cartes-postales/la-batterie.webp differ diff --git a/images/mene/saint-jacut-du-mene/cartes-postales/recette-buraliste.webp b/images/mene/saint-jacut-du-mene/cartes-postales/recette-buraliste.webp new file mode 100755 index 0000000..e194abf Binary files /dev/null and b/images/mene/saint-jacut-du-mene/cartes-postales/recette-buraliste.webp differ diff --git a/images/mene/saint-jacut-du-mene/cartes-postales/une-batterie.webp b/images/mene/saint-jacut-du-mene/cartes-postales/une-batterie.webp new file mode 100755 index 0000000..f7806f3 Binary files /dev/null and b/images/mene/saint-jacut-du-mene/cartes-postales/une-batterie.webp differ diff --git a/images/mene/saint-jacut-du-mene/photos/bois-du-moulin.webp b/images/mene/saint-jacut-du-mene/photos/bois-du-moulin.webp new file mode 100755 index 0000000..cc8c674 Binary files /dev/null and b/images/mene/saint-jacut-du-mene/photos/bois-du-moulin.webp differ diff --git "a/images/mene/saint-jacut-du-mene/photos/ch\303\252ne-du-parc-1.webp" "b/images/mene/saint-jacut-du-mene/photos/ch\303\252ne-du-parc-1.webp" new file mode 100755 index 0000000..e7dc4af Binary files /dev/null and "b/images/mene/saint-jacut-du-mene/photos/ch\303\252ne-du-parc-1.webp" differ diff --git "a/images/mene/saint-jacut-du-mene/photos/ch\303\252ne-du-parc-2.webp" "b/images/mene/saint-jacut-du-mene/photos/ch\303\252ne-du-parc-2.webp" new file mode 100755 index 0000000..6f391d7 Binary files /dev/null and "b/images/mene/saint-jacut-du-mene/photos/ch\303\252ne-du-parc-2.webp" differ diff --git "a/images/mene/saint-jacut-du-mene/photos/ch\303\252ne-du-parc-3.webp" "b/images/mene/saint-jacut-du-mene/photos/ch\303\252ne-du-parc-3.webp" new file mode 100755 index 0000000..0171f0a Binary files /dev/null and "b/images/mene/saint-jacut-du-mene/photos/ch\303\252ne-du-parc-3.webp" differ diff --git a/images/mene/saint-jacut-du-mene/photos/eglise-vue-de-la-mairie.webp b/images/mene/saint-jacut-du-mene/photos/eglise-vue-de-la-mairie.webp new file mode 100755 index 0000000..528f392 Binary files /dev/null and b/images/mene/saint-jacut-du-mene/photos/eglise-vue-de-la-mairie.webp differ diff --git a/images/mene/saint-jacut-du-mene/photos/index.php b/images/mene/saint-jacut-du-mene/photos/index.php new file mode 100755 index 0000000..bc46e7a --- /dev/null +++ b/images/mene/saint-jacut-du-mene/photos/index.php @@ -0,0 +1,33 @@ + + + + + + + + Bloggings + + +
+

Bloggings

+ Back +
+
+
+

+
%s
", $img, $caption); + } + ?> +
+
+ + + diff --git a/images/menuiserie/abris jeux enfants.webp b/images/menuiserie/abris jeux enfants.webp new file mode 100755 index 0000000..0eb2269 Binary files /dev/null and b/images/menuiserie/abris jeux enfants.webp differ diff --git a/images/menuiserie/abris-jardin.webp b/images/menuiserie/abris-jardin.webp new file mode 100755 index 0000000..1f155b2 Binary files /dev/null and b/images/menuiserie/abris-jardin.webp differ diff --git a/images/menuiserie/abris-jeux-enfants.webp b/images/menuiserie/abris-jeux-enfants.webp new file mode 100755 index 0000000..0eb2269 Binary files /dev/null and b/images/menuiserie/abris-jeux-enfants.webp differ diff --git a/images/menuiserie/avion.webp b/images/menuiserie/avion.webp new file mode 100755 index 0000000..19dd53f Binary files /dev/null and b/images/menuiserie/avion.webp differ diff --git "a/images/menuiserie/berceau-poup\303\251e.webp" "b/images/menuiserie/berceau-poup\303\251e.webp" new file mode 100755 index 0000000..8264894 Binary files /dev/null and "b/images/menuiserie/berceau-poup\303\251e.webp" differ diff --git a/images/menuiserie/bonhomme-neige.webp b/images/menuiserie/bonhomme-neige.webp new file mode 100755 index 0000000..e3958e3 Binary files /dev/null and b/images/menuiserie/bonhomme-neige.webp differ diff --git a/images/menuiserie/chariot-scie-table.webp b/images/menuiserie/chariot-scie-table.webp new file mode 100755 index 0000000..91b1930 Binary files /dev/null and b/images/menuiserie/chariot-scie-table.webp differ diff --git a/images/menuiserie/compas-400mm.webp b/images/menuiserie/compas-400mm.webp new file mode 100755 index 0000000..5b425d6 Binary files /dev/null and b/images/menuiserie/compas-400mm.webp differ diff --git a/images/menuiserie/dominos-cumaru.jpg b/images/menuiserie/dominos-cumaru.jpg new file mode 100755 index 0000000..49e0d1f Binary files /dev/null and b/images/menuiserie/dominos-cumaru.jpg differ diff --git a/images/menuiserie/dominos.jpg b/images/menuiserie/dominos.jpg new file mode 100755 index 0000000..cf1fc57 Binary files /dev/null and b/images/menuiserie/dominos.jpg differ diff --git a/images/menuiserie/etau.webp b/images/menuiserie/etau.webp new file mode 100755 index 0000000..8c466d3 Binary files /dev/null and b/images/menuiserie/etau.webp differ diff --git a/images/menuiserie/index.php b/images/menuiserie/index.php new file mode 100755 index 0000000..2b9c6b5 --- /dev/null +++ b/images/menuiserie/index.php @@ -0,0 +1,33 @@ + + + + + + + + Bloggings + + +
+

Bloggings

+ Back +
+
+
+

+
%s
", $img, $caption); + } + ?> +
+
+ + + diff --git a/images/menuiserie/planche-en-bois-de-bout.webp b/images/menuiserie/planche-en-bois-de-bout.webp new file mode 100755 index 0000000..e6d2452 Binary files /dev/null and b/images/menuiserie/planche-en-bois-de-bout.webp differ diff --git a/images/menuiserie/support-roulettes.webp b/images/menuiserie/support-roulettes.webp new file mode 100755 index 0000000..41150e5 Binary files /dev/null and b/images/menuiserie/support-roulettes.webp differ diff --git a/images/menuiserie/table-atelier.webp b/images/menuiserie/table-atelier.webp new file mode 100755 index 0000000..05a6228 Binary files /dev/null and b/images/menuiserie/table-atelier.webp differ diff --git a/images/menuiserie/voiture-oliver.webp b/images/menuiserie/voiture-oliver.webp new file mode 100755 index 0000000..a88c51e Binary files /dev/null and b/images/menuiserie/voiture-oliver.webp differ diff --git a/images/mer-des-faluns.jpg b/images/mer-des-faluns.jpg new file mode 100755 index 0000000..0ad149d Binary files /dev/null and b/images/mer-des-faluns.jpg differ diff --git a/images/mini-chariot.JPEG b/images/mini-chariot.JPEG new file mode 100755 index 0000000..769522e Binary files /dev/null and b/images/mini-chariot.JPEG differ diff --git a/images/mini-chariot.webp b/images/mini-chariot.webp new file mode 100755 index 0000000..8096ef6 Binary files /dev/null and b/images/mini-chariot.webp differ diff --git a/images/mugshot-02-12-2023.png b/images/mugshot-02-12-2023.png new file mode 100755 index 0000000..3f6f07c Binary files /dev/null and b/images/mugshot-02-12-2023.png differ diff --git a/images/mugshot-09-11-2023.jpg b/images/mugshot-09-11-2023.jpg new file mode 100755 index 0000000..d9aeac2 Binary files /dev/null and b/images/mugshot-09-11-2023.jpg differ diff --git a/images/new-bench.JPEG b/images/new-bench.JPEG new file mode 100755 index 0000000..ac233bc Binary files /dev/null and b/images/new-bench.JPEG differ diff --git a/images/new-bench.webp b/images/new-bench.webp new file mode 100755 index 0000000..777a977 Binary files /dev/null and b/images/new-bench.webp differ diff --git a/images/new-oland-tool.jpg b/images/new-oland-tool.jpg new file mode 100755 index 0000000..899afc6 Binary files /dev/null and b/images/new-oland-tool.jpg differ diff --git a/images/oak-bowl-28cm.JPEG b/images/oak-bowl-28cm.JPEG new file mode 100755 index 0000000..bba23d3 Binary files /dev/null and b/images/oak-bowl-28cm.JPEG differ diff --git a/images/oak-bowl-28cm.webp b/images/oak-bowl-28cm.webp new file mode 100755 index 0000000..9f981c5 Binary files /dev/null and b/images/oak-bowl-28cm.webp differ diff --git a/images/obscurantism.jpg b/images/obscurantism.jpg new file mode 100755 index 0000000..d1f136b Binary files /dev/null and b/images/obscurantism.jpg differ diff --git a/images/odysee.JPEG b/images/odysee.JPEG new file mode 100755 index 0000000..49d9504 Binary files /dev/null and b/images/odysee.JPEG differ diff --git a/images/odysee.webp b/images/odysee.webp new file mode 100755 index 0000000..56c31a7 Binary files /dev/null and b/images/odysee.webp differ diff --git a/images/oland-tools.jpg b/images/oland-tools.jpg new file mode 100755 index 0000000..0112ba5 Binary files /dev/null and b/images/oland-tools.jpg differ diff --git a/images/pense-bete.JPEG b/images/pense-bete.JPEG new file mode 100755 index 0000000..a1d6249 Binary files /dev/null and b/images/pense-bete.JPEG differ diff --git a/images/pense-bete.webp b/images/pense-bete.webp new file mode 100755 index 0000000..d7e3623 Binary files /dev/null and b/images/pense-bete.webp differ diff --git a/images/phil_from_france.jpg b/images/phil_from_france.jpg new file mode 100755 index 0000000..27a2c57 Binary files /dev/null and b/images/phil_from_france.jpg differ diff --git "a/images/planche-\303\240-d\303\251couper-jacqueline.JPEG" "b/images/planche-\303\240-d\303\251couper-jacqueline.JPEG" new file mode 100755 index 0000000..4045295 Binary files /dev/null and "b/images/planche-\303\240-d\303\251couper-jacqueline.JPEG" differ diff --git "a/images/planche-\303\240-d\303\251couper-jacqueline.webp" "b/images/planche-\303\240-d\303\251couper-jacqueline.webp" new file mode 100755 index 0000000..24b5cf0 Binary files /dev/null and "b/images/planche-\303\240-d\303\251couper-jacqueline.webp" differ diff --git a/images/prettier.jpg b/images/prettier.jpg new file mode 100755 index 0000000..b8e46e6 Binary files /dev/null and b/images/prettier.jpg differ diff --git a/images/pyradio.JPEG b/images/pyradio.JPEG new file mode 100755 index 0000000..91b23af Binary files /dev/null and b/images/pyradio.JPEG differ diff --git a/images/pyradio.webp b/images/pyradio.webp new file mode 100755 index 0000000..0fbc35b Binary files /dev/null and b/images/pyradio.webp differ diff --git a/images/pyrogravure/chaise-clara.webp b/images/pyrogravure/chaise-clara.webp new file mode 100755 index 0000000..791068c Binary files /dev/null and b/images/pyrogravure/chaise-clara.webp differ diff --git a/images/pyrogravure/chestnut-bowl.webp b/images/pyrogravure/chestnut-bowl.webp new file mode 100755 index 0000000..6fbd5f2 Binary files /dev/null and b/images/pyrogravure/chestnut-bowl.webp differ diff --git "a/images/pyrogravure/graines-tonton-lo\303\257c.webp" "b/images/pyrogravure/graines-tonton-lo\303\257c.webp" new file mode 100755 index 0000000..437235f Binary files /dev/null and "b/images/pyrogravure/graines-tonton-lo\303\257c.webp" differ diff --git a/images/pyrogravure/guitar-ian.webp b/images/pyrogravure/guitar-ian.webp new file mode 100755 index 0000000..b0dbb38 Binary files /dev/null and b/images/pyrogravure/guitar-ian.webp differ diff --git a/images/pyrogravure/index.php b/images/pyrogravure/index.php new file mode 100755 index 0000000..2b9c6b5 --- /dev/null +++ b/images/pyrogravure/index.php @@ -0,0 +1,33 @@ + + + + + + + + Bloggings + + +
+

Bloggings

+ Back +
+
+
+

+
%s
", $img, $caption); + } + ?> +
+
+ + + diff --git "a/images/pyrogravure/logo-fer-\303\240-marquer.webp" "b/images/pyrogravure/logo-fer-\303\240-marquer.webp" new file mode 100755 index 0000000..321ee8e Binary files /dev/null and "b/images/pyrogravure/logo-fer-\303\240-marquer.webp" differ diff --git "a/images/pyrogravure/triskel-sous-plat-sign\303\251.webp" "b/images/pyrogravure/triskel-sous-plat-sign\303\251.webp" new file mode 100755 index 0000000..6a1d2e5 Binary files /dev/null and "b/images/pyrogravure/triskel-sous-plat-sign\303\251.webp" differ diff --git "a/images/pyrogravure/vase-ch\303\252ne-d\303\251cor\303\251.webp" "b/images/pyrogravure/vase-ch\303\252ne-d\303\251cor\303\251.webp" new file mode 100755 index 0000000..98e58a8 Binary files /dev/null and "b/images/pyrogravure/vase-ch\303\252ne-d\303\251cor\303\251.webp" differ diff --git a/images/recordpower_AC400.jpg b/images/recordpower_AC400.jpg new file mode 100755 index 0000000..216f186 Binary files /dev/null and b/images/recordpower_AC400.jpg differ diff --git a/images/riddings-web-site.JPEG b/images/riddings-web-site.JPEG new file mode 100755 index 0000000..aa49850 Binary files /dev/null and b/images/riddings-web-site.JPEG differ diff --git a/images/riddings-web-site.webp b/images/riddings-web-site.webp new file mode 100755 index 0000000..4fbad6e Binary files /dev/null and b/images/riddings-web-site.webp differ diff --git a/images/rss.png b/images/rss.png new file mode 100755 index 0000000..60ac9e4 Binary files /dev/null and b/images/rss.png differ diff --git a/images/saint-catherine.jpg b/images/saint-catherine.jpg new file mode 100755 index 0000000..8ce7145 Binary files /dev/null and b/images/saint-catherine.jpg differ diff --git a/images/scie-alignment-1.jpg b/images/scie-alignment-1.jpg new file mode 100755 index 0000000..28027d5 Binary files /dev/null and b/images/scie-alignment-1.jpg differ diff --git a/images/scie-alignment-2.jpg b/images/scie-alignment-2.jpg new file mode 100755 index 0000000..03e8573 Binary files /dev/null and b/images/scie-alignment-2.jpg differ diff --git a/images/scie-completed-1.jpg b/images/scie-completed-1.jpg new file mode 100755 index 0000000..d345f57 Binary files /dev/null and b/images/scie-completed-1.jpg differ diff --git a/images/scie-completed-2.jpg b/images/scie-completed-2.jpg new file mode 100755 index 0000000..b7d1d67 Binary files /dev/null and b/images/scie-completed-2.jpg differ diff --git a/images/scie-completed-3.jpg b/images/scie-completed-3.jpg new file mode 100755 index 0000000..b7694a8 Binary files /dev/null and b/images/scie-completed-3.jpg differ diff --git a/images/scie-completed-4.jpg b/images/scie-completed-4.jpg new file mode 100755 index 0000000..8aeea49 Binary files /dev/null and b/images/scie-completed-4.jpg differ diff --git a/images/scie-completed-5.jpg b/images/scie-completed-5.jpg new file mode 100755 index 0000000..a63b10f Binary files /dev/null and b/images/scie-completed-5.jpg differ diff --git a/images/scie-frame-1.jpg b/images/scie-frame-1.jpg new file mode 100755 index 0000000..ee97aa9 Binary files /dev/null and b/images/scie-frame-1.jpg differ diff --git a/images/scie-frame-2.jpg b/images/scie-frame-2.jpg new file mode 100755 index 0000000..b3213e7 Binary files /dev/null and b/images/scie-frame-2.jpg differ diff --git a/images/scie-frame-3.jpg b/images/scie-frame-3.jpg new file mode 100755 index 0000000..2e98592 Binary files /dev/null and b/images/scie-frame-3.jpg differ diff --git a/images/scie-frame-4.jpg b/images/scie-frame-4.jpg new file mode 100755 index 0000000..06b0856 Binary files /dev/null and b/images/scie-frame-4.jpg differ diff --git a/images/scie-improvement-1.jpg b/images/scie-improvement-1.jpg new file mode 100755 index 0000000..03e8573 Binary files /dev/null and b/images/scie-improvement-1.jpg differ diff --git a/images/scie-parts-1.jpg b/images/scie-parts-1.jpg new file mode 100755 index 0000000..bfc828c Binary files /dev/null and b/images/scie-parts-1.jpg differ diff --git a/images/scie-wheel-1.jpg b/images/scie-wheel-1.jpg new file mode 100755 index 0000000..f69d073 Binary files /dev/null and b/images/scie-wheel-1.jpg differ diff --git a/images/screw-jig.JPEG b/images/screw-jig.JPEG new file mode 100755 index 0000000..4254dcf Binary files /dev/null and b/images/screw-jig.JPEG differ diff --git a/images/screw-jig.webp b/images/screw-jig.webp new file mode 100755 index 0000000..3713f68 Binary files /dev/null and b/images/screw-jig.webp differ diff --git a/images/searxng.jpg b/images/searxng.jpg new file mode 100755 index 0000000..514de4a Binary files /dev/null and b/images/searxng.jpg differ diff --git a/images/shadocks-pumping-300.gif b/images/shadocks-pumping-300.gif new file mode 100755 index 0000000..8c5e42b Binary files /dev/null and b/images/shadocks-pumping-300.gif differ diff --git a/images/shadocks-pumping.gif b/images/shadocks-pumping.gif new file mode 100755 index 0000000..922bf5a Binary files /dev/null and b/images/shadocks-pumping.gif differ diff --git a/images/shadocks.gif b/images/shadocks.gif new file mode 100644 index 0000000..a055ad0 Binary files /dev/null and b/images/shadocks.gif differ diff --git a/images/shop.jpg b/images/shop.jpg new file mode 100755 index 0000000..f23209b Binary files /dev/null and b/images/shop.jpg differ diff --git a/images/sous-plat-incrustation.JPEG b/images/sous-plat-incrustation.JPEG new file mode 100755 index 0000000..a1524ae Binary files /dev/null and b/images/sous-plat-incrustation.JPEG differ diff --git a/images/sous-plat-incrustation.webp b/images/sous-plat-incrustation.webp new file mode 100755 index 0000000..2c56c29 Binary files /dev/null and b/images/sous-plat-incrustation.webp differ diff --git a/images/stop-block.JPEG b/images/stop-block.JPEG new file mode 100755 index 0000000..812861e Binary files /dev/null and b/images/stop-block.JPEG differ diff --git a/images/stop-block.webp b/images/stop-block.webp new file mode 100755 index 0000000..71fdbc6 Binary files /dev/null and b/images/stop-block.webp differ diff --git a/images/support-roue-superior.JPEG b/images/support-roue-superior.JPEG new file mode 100755 index 0000000..93105bb Binary files /dev/null and b/images/support-roue-superior.JPEG differ diff --git a/images/support-roue-superior.webp b/images/support-roue-superior.webp new file mode 100755 index 0000000..d93aac2 Binary files /dev/null and b/images/support-roue-superior.webp differ diff --git a/images/support-trunnion.JPEG b/images/support-trunnion.JPEG new file mode 100755 index 0000000..844b449 Binary files /dev/null and b/images/support-trunnion.JPEG differ diff --git a/images/support-trunnion.webp b/images/support-trunnion.webp new file mode 100755 index 0000000..3f28a08 Binary files /dev/null and b/images/support-trunnion.webp differ diff --git a/images/systemcollapse.jpg b/images/systemcollapse.jpg new file mode 100755 index 0000000..f37165d Binary files /dev/null and b/images/systemcollapse.jpg differ diff --git a/images/t430-cpu-upgrade.jpg b/images/t430-cpu-upgrade.jpg new file mode 100755 index 0000000..613d1d5 Binary files /dev/null and b/images/t430-cpu-upgrade.jpg differ diff --git a/images/table-support-trunions.JPEG b/images/table-support-trunions.JPEG new file mode 100755 index 0000000..7f3da60 Binary files /dev/null and b/images/table-support-trunions.JPEG differ diff --git a/images/table-support-trunions.webp b/images/table-support-trunions.webp new file mode 100755 index 0000000..086723b Binary files /dev/null and b/images/table-support-trunions.webp differ diff --git a/images/tcl.webp b/images/tcl.webp new file mode 100755 index 0000000..23c6958 Binary files /dev/null and b/images/tcl.webp differ diff --git a/images/thinkpad-x220-mic-button.JPEG b/images/thinkpad-x220-mic-button.JPEG new file mode 100755 index 0000000..d9179b9 Binary files /dev/null and b/images/thinkpad-x220-mic-button.JPEG differ diff --git a/images/thinkpad-x220-mic-button.webp b/images/thinkpad-x220-mic-button.webp new file mode 100755 index 0000000..db15d4b Binary files /dev/null and b/images/thinkpad-x220-mic-button.webp differ diff --git "a/images/tour/1er-id\303\251e.webp" "b/images/tour/1er-id\303\251e.webp" new file mode 100755 index 0000000..30af204 Binary files /dev/null and "b/images/tour/1er-id\303\251e.webp" differ diff --git "a/images/tour/2i\303\250me-id\303\251e.webp" "b/images/tour/2i\303\250me-id\303\251e.webp" new file mode 100755 index 0000000..ff63eab Binary files /dev/null and "b/images/tour/2i\303\250me-id\303\251e.webp" differ diff --git "a/images/tour/3i\303\250me-id\303\251e.webp" "b/images/tour/3i\303\250me-id\303\251e.webp" new file mode 100755 index 0000000..ec914f3 Binary files /dev/null and "b/images/tour/3i\303\250me-id\303\251e.webp" differ diff --git "a/images/tour/4-poup\303\251e-fixe.webp" "b/images/tour/4-poup\303\251e-fixe.webp" new file mode 100755 index 0000000..f042985 Binary files /dev/null and "b/images/tour/4-poup\303\251e-fixe.webp" differ diff --git "a/images/tour/5-porte-outil-poup\303\251e-mobile.webp" "b/images/tour/5-porte-outil-poup\303\251e-mobile.webp" new file mode 100755 index 0000000..ad2859f Binary files /dev/null and "b/images/tour/5-porte-outil-poup\303\251e-mobile.webp" differ diff --git "a/images/tour/6-id\303\251e-poup\303\251e-mobile.webp" "b/images/tour/6-id\303\251e-poup\303\251e-mobile.webp" new file mode 100755 index 0000000..159131e Binary files /dev/null and "b/images/tour/6-id\303\251e-poup\303\251e-mobile.webp" differ diff --git "a/images/tour/7-poup\303\251e-mobile.webp" "b/images/tour/7-poup\303\251e-mobile.webp" new file mode 100755 index 0000000..13eb5f2 Binary files /dev/null and "b/images/tour/7-poup\303\251e-mobile.webp" differ diff --git a/images/tour/8-contre-pointe-tournante.webp b/images/tour/8-contre-pointe-tournante.webp new file mode 100755 index 0000000..2456156 Binary files /dev/null and b/images/tour/8-contre-pointe-tournante.webp differ diff --git a/images/tour/9-ecrou-de-blocage.webp b/images/tour/9-ecrou-de-blocage.webp new file mode 100755 index 0000000..884ea52 Binary files /dev/null and b/images/tour/9-ecrou-de-blocage.webp differ diff --git "a/images/tour/91-cliquet-blocage-poup\303\251e-mobile.webp" "b/images/tour/91-cliquet-blocage-poup\303\251e-mobile.webp" new file mode 100755 index 0000000..5e7591e Binary files /dev/null and "b/images/tour/91-cliquet-blocage-poup\303\251e-mobile.webp" differ diff --git a/images/tour/92-broche.webp b/images/tour/92-broche.webp new file mode 100755 index 0000000..d5f8792 Binary files /dev/null and b/images/tour/92-broche.webp differ diff --git a/images/tour/93-assemblage.webp b/images/tour/93-assemblage.webp new file mode 100755 index 0000000..fd630cb Binary files /dev/null and b/images/tour/93-assemblage.webp differ diff --git a/images/tour/931-serrage--porte-outils.webp b/images/tour/931-serrage--porte-outils.webp new file mode 100755 index 0000000..a7ec966 Binary files /dev/null and b/images/tour/931-serrage--porte-outils.webp differ diff --git a/images/tour/932-blocage-broche.webp b/images/tour/932-blocage-broche.webp new file mode 100755 index 0000000..4e20576 Binary files /dev/null and b/images/tour/932-blocage-broche.webp differ diff --git a/images/tour/94-300-tpm.webp b/images/tour/94-300-tpm.webp new file mode 100755 index 0000000..d8ce360 Binary files /dev/null and b/images/tour/94-300-tpm.webp differ diff --git a/images/tour/95-600-tpm.webp b/images/tour/95-600-tpm.webp new file mode 100755 index 0000000..cba4560 Binary files /dev/null and b/images/tour/95-600-tpm.webp differ diff --git a/images/tour/96-1400-tpm.webp b/images/tour/96-1400-tpm.webp new file mode 100755 index 0000000..8bb0c9a Binary files /dev/null and b/images/tour/96-1400-tpm.webp differ diff --git a/images/tour/97-finale.webp b/images/tour/97-finale.webp new file mode 100755 index 0000000..3bb4949 Binary files /dev/null and b/images/tour/97-finale.webp differ diff --git a/images/tour/index.php b/images/tour/index.php new file mode 100755 index 0000000..276724b --- /dev/null +++ b/images/tour/index.php @@ -0,0 +1,33 @@ + + + + + + + + Bloggings + + +
+

Bloggings

+ Back +
+
+
+

+
%s
", $img, $caption); + } + ?> +
+
+ + + diff --git a/images/tournages/3-bols-chataigner.webp b/images/tournages/3-bols-chataigner.webp new file mode 100755 index 0000000..58443b5 Binary files /dev/null and b/images/tournages/3-bols-chataigner.webp differ diff --git a/images/tournages/4-bols-cerisier.webp b/images/tournages/4-bols-cerisier.webp new file mode 100755 index 0000000..3157088 Binary files /dev/null and b/images/tournages/4-bols-cerisier.webp differ diff --git a/images/tournages/Pot-en-charme.webp b/images/tournages/Pot-en-charme.webp new file mode 100755 index 0000000..6789136 Binary files /dev/null and b/images/tournages/Pot-en-charme.webp differ diff --git a/images/tournages/bol-cerisier.webp b/images/tournages/bol-cerisier.webp new file mode 100755 index 0000000..93290af Binary files /dev/null and b/images/tournages/bol-cerisier.webp differ diff --git "a/images/tournages/bol-ch\303\242taigner.webp" "b/images/tournages/bol-ch\303\242taigner.webp" new file mode 100755 index 0000000..4191f1d Binary files /dev/null and "b/images/tournages/bol-ch\303\242taigner.webp" differ diff --git "a/images/tournages/bol-h\303\252tre-12-02-20.webp" "b/images/tournages/bol-h\303\252tre-12-02-20.webp" new file mode 100755 index 0000000..ba95906 Binary files /dev/null and "b/images/tournages/bol-h\303\252tre-12-02-20.webp" differ diff --git "a/images/tournages/bol-h\303\252tre-15-02-20.webp" "b/images/tournages/bol-h\303\252tre-15-02-20.webp" new file mode 100755 index 0000000..a3425f9 Binary files /dev/null and "b/images/tournages/bol-h\303\252tre-15-02-20.webp" differ diff --git "a/images/tournages/bol-h\303\252tre-21-02-20.webp" "b/images/tournages/bol-h\303\252tre-21-02-20.webp" new file mode 100755 index 0000000..b68890f Binary files /dev/null and "b/images/tournages/bol-h\303\252tre-21-02-20.webp" differ diff --git "a/images/tournages/bol-h\303\252tre-epoxy.webp" "b/images/tournages/bol-h\303\252tre-epoxy.webp" new file mode 100755 index 0000000..b314e15 Binary files /dev/null and "b/images/tournages/bol-h\303\252tre-epoxy.webp" differ diff --git a/images/tournages/coquetiers-modernes.webp b/images/tournages/coquetiers-modernes.webp new file mode 100755 index 0000000..e2becf0 Binary files /dev/null and b/images/tournages/coquetiers-modernes.webp differ diff --git a/images/tournages/dauphin.webp b/images/tournages/dauphin.webp new file mode 100755 index 0000000..f24816c Binary files /dev/null and b/images/tournages/dauphin.webp differ diff --git a/images/tournages/deux-vases-thuya.webp b/images/tournages/deux-vases-thuya.webp new file mode 100755 index 0000000..9962443 Binary files /dev/null and b/images/tournages/deux-vases-thuya.webp differ diff --git a/images/tournages/ebauche-bol-30cm.webp b/images/tournages/ebauche-bol-30cm.webp new file mode 100755 index 0000000..85bb874 Binary files /dev/null and b/images/tournages/ebauche-bol-30cm.webp differ diff --git "a/images/tournages/exp\303\251rimentation.webp" "b/images/tournages/exp\303\251rimentation.webp" new file mode 100755 index 0000000..28d197b Binary files /dev/null and "b/images/tournages/exp\303\251rimentation.webp" differ diff --git a/images/tournages/fleurs.webp b/images/tournages/fleurs.webp new file mode 100755 index 0000000..a135cd5 Binary files /dev/null and b/images/tournages/fleurs.webp differ diff --git a/images/tournages/flowers.webp b/images/tournages/flowers.webp new file mode 100755 index 0000000..22f0351 Binary files /dev/null and b/images/tournages/flowers.webp differ diff --git a/images/tournages/grande-vase.webp b/images/tournages/grande-vase.webp new file mode 100755 index 0000000..25122c3 Binary files /dev/null and b/images/tournages/grande-vase.webp differ diff --git a/images/tournages/index.php b/images/tournages/index.php new file mode 100755 index 0000000..2b9c6b5 --- /dev/null +++ b/images/tournages/index.php @@ -0,0 +1,33 @@ + + + + + + + + Bloggings + + +
+

Bloggings

+ Back +
+
+
+

+
%s
", $img, $caption); + } + ?> +
+
+ + + diff --git a/images/tournages/mortier-cerisier.webp b/images/tournages/mortier-cerisier.webp new file mode 100755 index 0000000..2ad675b Binary files /dev/null and b/images/tournages/mortier-cerisier.webp differ diff --git a/images/tournages/mortier-pilon-cerisier.webp b/images/tournages/mortier-pilon-cerisier.webp new file mode 100755 index 0000000..a407fa0 Binary files /dev/null and b/images/tournages/mortier-pilon-cerisier.webp differ diff --git a/images/tournages/oak-bowl-28cm.webp b/images/tournages/oak-bowl-28cm.webp new file mode 100755 index 0000000..7a5beb3 Binary files /dev/null and b/images/tournages/oak-bowl-28cm.webp differ diff --git a/images/tournages/oak_bowl_experiment_1.webp b/images/tournages/oak_bowl_experiment_1.webp new file mode 100755 index 0000000..c17bf72 Binary files /dev/null and b/images/tournages/oak_bowl_experiment_1.webp differ diff --git a/images/tournages/petit-vase-thuya.webp b/images/tournages/petit-vase-thuya.webp new file mode 100755 index 0000000..065b53a Binary files /dev/null and b/images/tournages/petit-vase-thuya.webp differ diff --git a/images/tournages/plat-ceriser.webp b/images/tournages/plat-ceriser.webp new file mode 100755 index 0000000..d161de4 Binary files /dev/null and b/images/tournages/plat-ceriser.webp differ diff --git a/images/tournages/porte-bougie-1.jpg b/images/tournages/porte-bougie-1.jpg new file mode 100755 index 0000000..f3e5d7e Binary files /dev/null and b/images/tournages/porte-bougie-1.jpg differ diff --git a/images/tournages/pot-charme-pyro.webp b/images/tournages/pot-charme-pyro.webp new file mode 100755 index 0000000..873c775 Binary files /dev/null and b/images/tournages/pot-charme-pyro.webp differ diff --git a/images/tournages/pot-open.webp b/images/tournages/pot-open.webp new file mode 100755 index 0000000..091c0f3 Binary files /dev/null and b/images/tournages/pot-open.webp differ diff --git a/images/tournages/pot.webp b/images/tournages/pot.webp new file mode 100755 index 0000000..113e6bf Binary files /dev/null and b/images/tournages/pot.webp differ diff --git a/images/tournages/tall-vase.webp b/images/tournages/tall-vase.webp new file mode 100755 index 0000000..9041649 Binary files /dev/null and b/images/tournages/tall-vase.webp differ diff --git a/images/tournages/two-vases.webp b/images/tournages/two-vases.webp new file mode 100755 index 0000000..07bb549 Binary files /dev/null and b/images/tournages/two-vases.webp differ diff --git "a/images/tournages/vase-ch\303\252ne.webp" "b/images/tournages/vase-ch\303\252ne.webp" new file mode 100755 index 0000000..db5fb67 Binary files /dev/null and "b/images/tournages/vase-ch\303\252ne.webp" differ diff --git a/images/tournages/vase-moabi.webp b/images/tournages/vase-moabi.webp new file mode 100755 index 0000000..0ff0850 Binary files /dev/null and b/images/tournages/vase-moabi.webp differ diff --git a/images/tournages/vase-thuya.webp b/images/tournages/vase-thuya.webp new file mode 100755 index 0000000..a134e3e Binary files /dev/null and b/images/tournages/vase-thuya.webp differ diff --git a/images/tprg.JPEG b/images/tprg.JPEG new file mode 100755 index 0000000..d9fe62c Binary files /dev/null and b/images/tprg.JPEG differ diff --git a/images/tprg.webp b/images/tprg.webp new file mode 100755 index 0000000..7b974c1 Binary files /dev/null and b/images/tprg.webp differ diff --git a/images/trunion-shim.JPEG b/images/trunion-shim.JPEG new file mode 100755 index 0000000..abc3bb3 Binary files /dev/null and b/images/trunion-shim.JPEG differ diff --git a/images/trunion-shim.webp b/images/trunion-shim.webp new file mode 100755 index 0000000..6dee626 Binary files /dev/null and b/images/trunion-shim.webp differ diff --git a/images/trunions-chevilles.JPEG b/images/trunions-chevilles.JPEG new file mode 100755 index 0000000..b33f335 Binary files /dev/null and b/images/trunions-chevilles.JPEG differ diff --git a/images/trunions-chevilles.webp b/images/trunions-chevilles.webp new file mode 100755 index 0000000..7fbf818 Binary files /dev/null and b/images/trunions-chevilles.webp differ diff --git a/images/upper-blade-guide-1.JPEG b/images/upper-blade-guide-1.JPEG new file mode 100755 index 0000000..748f248 Binary files /dev/null and b/images/upper-blade-guide-1.JPEG differ diff --git a/images/upper-blade-guide-1.webp b/images/upper-blade-guide-1.webp new file mode 100755 index 0000000..4cd4f9b Binary files /dev/null and b/images/upper-blade-guide-1.webp differ diff --git a/images/w3m-display-options.png b/images/w3m-display-options.png new file mode 100755 index 0000000..0f1b7cb Binary files /dev/null and b/images/w3m-display-options.png differ diff --git a/images/windowmaker096.png b/images/windowmaker096.png new file mode 100755 index 0000000..f7832bc Binary files /dev/null and b/images/windowmaker096.png differ diff --git a/images/workshop-update.JPEG b/images/workshop-update.JPEG new file mode 100755 index 0000000..4c1385d Binary files /dev/null and b/images/workshop-update.JPEG differ diff --git a/images/workshop-update.webp b/images/workshop-update.webp new file mode 100755 index 0000000..8a68501 Binary files /dev/null and b/images/workshop-update.webp differ diff --git a/images/wttr.JPEG b/images/wttr.JPEG new file mode 100755 index 0000000..b5023f3 Binary files /dev/null and b/images/wttr.JPEG differ diff --git a/images/wttr.webp b/images/wttr.webp new file mode 100755 index 0000000..93bf3d2 Binary files /dev/null and b/images/wttr.webp differ diff --git a/images/x200.JPEG b/images/x200.JPEG new file mode 100755 index 0000000..401db0f Binary files /dev/null and b/images/x200.JPEG differ diff --git a/images/x200.webp b/images/x200.webp new file mode 100755 index 0000000..98863ac Binary files /dev/null and b/images/x200.webp differ diff --git a/images/xterm-images-400.jpg b/images/xterm-images-400.jpg new file mode 100755 index 0000000..238017a Binary files /dev/null and b/images/xterm-images-400.jpg differ diff --git a/images/zellij.JPEG b/images/zellij.JPEG new file mode 100755 index 0000000..e58c0d3 Binary files /dev/null and b/images/zellij.JPEG differ diff --git a/images/zellij.webp b/images/zellij.webp new file mode 100755 index 0000000..444f32f Binary files /dev/null and b/images/zellij.webp differ diff --git a/index.php b/index.php new file mode 100755 index 0000000..433c0ee --- /dev/null +++ b/index.php @@ -0,0 +1,149 @@ + + + + + + + + + + + + Bloggings + + +
+

Bloggings

+ + +
+
+ +isDir() && !$dirinfo->isDot()) { + $years[] = $dirinfo->getFilename(); + } +} +rsort($years); +// iterate through years +if (is_array($years)) { + foreach ($years as $key => $year) { + $items = []; + $thisdir = new DirectoryIterator("./articles/" . $year); + // iterate through files + foreach ($thisdir as $file) { + if (!$file->isDot()) { + $flag = ""; + $articledate = date("Y-m-d"); + $filemodified = date( + "Y-m-d", + filemtime("./articles/" . $year . "/" . $file), + ); + $content = file_get_contents($file->getPathname()); + # get first h3 + preg_match("/

(.*?)<\/h3>/s", $content, $date); + if (isset($date[1]) && strtotime($date[1])) { + $articledate = date("Y-m-d", strtotime($date[1])); + } + if ( + $articledate >= + date("Y-m-d", strtotime(date("Y-m-d") . " - 2 days")) + ) { + $flag = " New"; + } + preg_match("/

(.*?)<\/h2>/s", $content, $titre); + $title = "?"; + if (isset($titre[1])) { + $title = ucfirst($titre[1]); + } + + $items[] = + '\n"; + } + } + + rsort($items); + + echo "
\n"; + echo "

" . $year . "

\n"; + + foreach ($items as $key => $val) { + if ($key == 0) { + if ($year == date("Y")) { + preg_match("'href=\"(.*?)\"'si", $val, $match); + if ($match) { + $content = file_get_contents($match[1], false, null, 1); + preg_match( + "'\(.*?)<\/article\>'si", + $content, + $article, + ); + if ($article) { + # print latest article contents + echo $article[1] . "\n

\n"; + } + } + } + echo "
    \n"; + } + # print link to article + echo $val; + } + echo "
\n"; + echo "
\n"; + } +} else { + echo "Not an array"; +} +?> + +

+ + + + + + + + + + + + + + + + + + + + diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..3aee8e7 Binary files /dev/null and b/logo.png differ diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..6f538e6 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,70 @@ +user www-data; +worker_processes auto; +pid /run/nginx.pid; +error_log /var/log/nginx/error.log; +include /etc/nginx/modules-enabled/*.conf; +worker_rlimit_nofile 2048; + + +events { + worker_connections 768; + # multi_accept on; +} + +http { + + ## + # Basic Settings + ## + + # Caching configuration for snac + proxy_cache_path /var/cache/nginx/snac_cache levels=1:2 keys_zone=snac:10m max_size=1g inactive=1440m use_temp_path=off; + # request limiting + limit_req_zone $binary_remote_addr zone=mylimit:10m rate=50r/s; + + sendfile on; + tcp_nopush on; + types_hash_max_size 2048; + # server_tokens off; + + # server_names_hash_bucket_size 64; + # server_name_in_redirect off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ## + # SSL Settings + ## + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE + ssl_prefer_server_ciphers on; + + ## + # Logging Settings + ## + + access_log /var/log/nginx/access.log; + + ## + # Gzip Settings + ## + + gzip on; + + # gzip_vary on; + # gzip_proxied any; + # gzip_comp_level 6; + # gzip_buffers 16 8k; + # gzip_http_version 1.1; + # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + ## + # Virtual Host Configs + ## + + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; +} + + diff --git a/robots.txt b/robots.txt new file mode 100755 index 0000000..8f6cf3b --- /dev/null +++ b/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Disallow: +Sitemap: https://wittamore.fr/sitemap.xml diff --git a/rss.xml b/rss.xml new file mode 100644 index 0000000..07097a9 --- /dev/null +++ b/rss.xml @@ -0,0 +1,850 @@ + + + +Bloggings +https://wittamore.fr +Ramblings from Brittany +2026-07-02T10:22:43,069276386+02:00 + + + + upgrade to dwm 6.8 + 2026-06-29 + Philip A. Wittamore + https://wittamore.fr/articles/2026/upgrade-to-dwm-6.8.html + https://wittamore.fr/articles/2026/upgrade-to-dwm-6.8.html + + dwm with some patches +

I've been using dwm 6.5 for a while now, so I thought it + was time to upgrade to dwm 6.8.

+

Ideally I'd settle for a horizontal scrolling wm as my + Thinkpad screen is small, but after flirting with nwm I'm + staying with dwm for now.

+

I've added a few patches and shortcuts that make it easy + for me to circulate between windows and workspaces. I haven't + added the xrdb patch this time, instead I added the colorbar + patch which nicely colours the dwmblocks status bar and keeps + things simple. Many of the tiling patches I added to 6.5 have + been left out as I just don't use them. On a small 1366x768 + screen splitting it renders it useless so instead I use the + monocle layout by default and switch workspaces with + super+right and left, and switch windows with super+up and + down.

+

It's up on my + git server if you are interested.

]]>
+
+ + Freedos thoughts + 2026-06-11 + Philip A. Wittamore + https://wittamore.fr/articles/2026/Freedos-thoughts.html + https://wittamore.fr/articles/2026/Freedos-thoughts.html + + Maybe I have the best system already +

+ I've been looking at Freedos for the retrocomputing nostalgia value + and the text-only goodness. +

+

+ I wound up using Qemu on my X270 as Freedos on bare metal means my 14 + year old Thinkpad X220 is way too modern. +

+

+ Yes there's some crazy good software, and it's fun. But using + fullscreen in Qemu means a fuzzy display, and I came to measure how + the terminal on Linux is truly light years ahead. +

+

+ In fact it pushes home that my Artix / DWM / Kitty setup is the + veritable retrocomputing bees knees +

]]>
+
+ + semware-tse-editor + 2026-06-13 + Philip A. Wittamore + https://wittamore.fr/articles/2026/semware-tse-editor.html + https://wittamore.fr/articles/2026/semware-tse-editor.html + + A very interesting editor for DOS, Linux and Windows +

+ semware editor screenshot +

+

+ I stumbled upon the #Semware editor (TSE) for #Linux, Windows and #Dos + and thought I'd share it here. Originally named Qedit in 1985, the + latest version is dated May 2026. I don't know about Windows (as I + haven't used it in years), but the Linux version is a pleasant CLI app + with many features, including it's own application language. The more + I look at it, the more I realise that it is a profound piece of + software. +

+

+ www.semware.com
+ Knud Vaneeded YT videos +

]]>
+
+ + Living on Snac's bleeding edge + 2026-05-25 + Philip A. Wittamore + https://wittamore.fr/articles/2026/Living-on-Snac's-bleeding-edge.html + https://wittamore.fr/articles/2026/Living-on-Snac's-bleeding-edge.html + + Autoupdate script +

+ I like living on the bleeding edge of + snac2, so I added + this (banal) script to /etc/cron.daily. +

+
+#!/usr/bin/env bash
+# snaceto
+# Social Networks Are Crap Except This One
+
+cd /home/snac/src/snac2 || exit
+
+sudo -u snac git remote update
+UPDATED="$(git status -uno | grep -c 'up to date')"
+
+if [ "$UPDATED" -ne "1" ] ; then
+    echo "Updating..."
+    systemctl stop snac.service
+	# backup data before update
+	TODAY=$(date "+%Y-%m-%d-%H-%m")
+	BACKUPDIR="/opt/backups"
+	mkdir -p "$BACKUPDIR"
+	FILENAME="$BACKUPDIR/pre-update-snac-$TODAY.tar.gz"
+	tar -zcf "$FILENAME" /home/snac/data
+	# pull latest
+	sudo -u snac git pull
+	sudo -u snac make
+	make install
+	sudo -u snac snac upgrade /home/snac/data
+	systemctl start snac.service
+	VERSION="$(snac | head -n2)"
+	toot post "My bleeding edge #snac2 server has been updated
$VERSION" +fi +
]]>
+
+ + Web sites that work well in Dillo + 2026-06-19 + Philip A. Wittamore + https://wittamore.fr/articles/2026/Web-sites-that-work-well-in-Dillo.html + https://wittamore.fr/articles/2026/Web-sites-that-work-well-in-Dillo.html + + and other minimal browsers +

+ I'm always looking for sane web sites. By sane I mean websites that + are not bloated with useless eye candy, nor have any ketamin fired + javascript. Sites that just want to display data in the form of easily + readable text with or without some images. +

+

+ There is an increasing number of web sites that do this, as it's a + normal backlash to the slop infested, obfuscated tar pit that the + internet has sadly become. I haven't found a single compendium of + sites, but I'll list some here. +

+

+ One of the things I find realy interesting are web proxies that render + sites into a saner version, like + Textise or + Textdot. I fact I'm wondering if I + could roll my own simple version using php and rdrview. +

+

+ This list incomplete list will be updated as I find them, or maybe you + dear reader might have suggestions? +

+

+ Directory of useful sites that work reasonably well on Dillo +

+

Wiby search engine

+

+ Text only web sites +

]]>
+
+ + Ghostbsd installation + 2026-06-14 + Philip A. Wittamore + https://wittamore.fr/articles/2026/Ghostbsd-installation.html + https://wittamore.fr/articles/2026/Ghostbsd-installation.html + + messing around on a sunday +

+ So now we have Microsoft delivering Linux, and the Arch based AUR + getting wrecked by people who should be locked into a public pillory, + and I have some free time. Well in fact a lot of free time now that + I'm retired. +

+

+ So. I downloaded the latest #Ghostbsd Mate and cut a usb key, then + plugged it into my Thinkpad X220. +

+

+ The installation was simple and uneventful, and it booted without + problem. Everything worked out of the box. Mate seems pretty and + unobfuscated, just like me. +

+

So far I've done just two things:

+

+ (1) to get some thinkpad function keys to work I've added in + /boot/loader.conf +

+
+acpi_ibm_load="YES"
+
+

+ Printscreen works, brightness and volume controls work, vol mute + works, lid led works, but the MICMUTE button doesn't work.
+ For the X220 MICMUTE key, which is F20 or 0xBE (190), unfortunately + Ghostbsd's acpi_ibm doesn't pass on the event, so any script for it in + /etc/devd won't be fired. Checking the source code, acpi_ibm only accepts + a subset of events accepted by Linux's thinkpad_acpi, which appears to + cover them all. It's unfortunate as I'm a bit pedantic about keys but I + guess I'll have to live with it as the code is 20 years old and I + doubt it will change anytime soon. +

+ +

FYI, acpi_ibm accepts only these keyboard events:

+
+        LCD_BACKLIGHT
+        SUSPEND_TO_RAM
+        BLUETOOTH
+        SCREEN_EXPAND
+        SUSPEND_TO_DISK
+        BRIGHTNESS_UP
+        BRIGHTNESS_DOWN
+        THINKLIGHT
+        ZOOM
+        VOLUME_UP
+        VOLUME_DOWN
+        MUTE
+        ACCESS_IBM_BUTTON
+        
+

(2) to enable the automounting of usb storage media:

+
+sudo pkg install automount
+
+

+ I've replaced the default Firefox with Librewolf, and I'm not too sure + about zsh instead of bash, and why not doas instead of sudo? +

+

+ Ghostbsd is a very nice BSD distribution that's been around since + 2010. +

+

https://www.ghostbsd.org

]]>
+
+ + displaying MATE on a 1366x768 screen + 2026-06-15 + Philip A. Wittamore + https://wittamore.fr/articles/2026/displaying-MATE-on-a-1366x768-screen.html + https://wittamore.fr/articles/2026/displaying-MATE-on-a-1366x768-screen.html + + xrandr to the rescue +

+ Unfortunately several Ghostbsd MATE configuration windows are taller + than my Thinkpad X220's 768 pixels. I made both the top and bottom + panels autohide, but a better solution is to use xrandr: +

+
sudo pkg install xrandr
+

+ type xrandr to get the name of your display, in my case LVDS-1 on my + Thinkpad X220. +

+

To change 16:9 screen resolution, you can type:

+
+xrandr --output LVDS-1 --panning 1600x900  --scale 1.171x1.172
+

or

+
+xrandr --output LVDS-1 --panning 1920x1080 --scale 1.406x1.406
+

To get back to the original resolution type:

+
xrandr --output LVDS-1 --panning 1366x768 --scale 1x1
+

+ Here's a simple bash script that will rotate the screen resolution on + each call. I've added a MATE keyboard shortcut (CTRL+RIGHT) to run + this script. It is intended for my 1366x768 screen, so it will need + adapting for other screens. +

+
+#!/usr/bin/env bash
+        
+RES="$(xdpyinfo | grep dimensions | sed -r 's/^[^0-9]*([0-9]+x[0-9]+).*$/\1/')"
+# xdpy results can vary by 1 or two pixels 
+# so take the first two characters
+RES="${RES:0:2}"
+        
+case "$RES" in
+  "13")
+  xrandr --output LVDS-1 --panning 1600x900  --scale 1.171x1.172
+  ;;
+  "16")
+  xrandr --output LVDS-1 --panning 1920x1080 --scale 1.406x1.406
+  ;;
+  *)
+  xrandr --output LVDS-1 --panning 1366x768 --scale 1x1
+  ;;
+esac
+        
+

I doubt you can do that in Wayland.

]]>
+
+ + Oops! I did it again + 2026-05-31 + Philip A. Wittamore + https://wittamore.fr/articles/2026/Oops!-I-did-it-again.html + https://wittamore.fr/articles/2026/Oops!-I-did-it-again.html + + More blog improvements +

+ This blog now + sits in my git repository, so I made a few changes. +

+

+ One is the + blogsend + bash script which now does a git commit on my local machine, and a git + pull on the remote web server. The other is the git repos itself which + now has the web site in one repos, and the bash scripts in another. +

+

+ All this is probably of little interest to anyone other than myself, + but if you want to run a smol blog you might find it useful, after all + that's why I'm posting it. +

]]>
+
+ + smolweb validation in Dillo + 2026-06-22 +.dillo/actions/smolcheck.sh + Philip A. Wittamore + https://wittamore.fr/articles/2026/smolweb-validation-in-Dillo.html + https://wittamore.fr/articles/2026/smolweb-validation-in-Dillo.html + + a simple page_action script +

+ I added smolweb-validator as a page action in Dillo. It's a bit of a + pain as the validator is a java application. The action script is + brutal so far but it works. +

+
#!/bin/sh
+
+#smolweb validator location
+smp="$HOME/src/smolweb-validator"
+
+# validate
+cd $smp
+output="$(java -cp "jsoup-1.21.2.jar:." SmolwebValidator $url --verbose)"
+echo "$output" | dilloc rawload
+
+

+ The action is added to ~/.dillo/dillorc and appears in Dillo's + right-click menu +

+
+page_action="Smolcheck:~/.dillo/actions/smolcheck.sh"
+
+

+ see + https://codeberg.org/smolweb/smolweb-validator +

+

 

]]>
+
+ + Updated blogthis script + 2026-06-16 + Philip A. Wittamore + https://wittamore.fr/articles/2026/Updated-blogthis-script.html + https://wittamore.fr/articles/2026/Updated-blogthis-script.html + + now with markdown +

+ I've updated my blogthis script so I can write articles in markdown. +

+

+ It saves me having to write html in the articles and avoids errors. +

+

+ I've uploaded it to + my git server +

]]>
+
+ + Hebdogiciel + 2026-06-07 + Philip A. Wittamore + https://wittamore.fr/articles/2026/Hebdogiciel.html + https://wittamore.fr/articles/2026/Hebdogiciel.html + + Flashback to the 80's +

+ After my ZX81 I purchased an Amstrad CPC464, and oh joy I discovered + Hebdogiciel. Before the internet this magazine printed the code for + games and programs that you could painfully copy out. Today I + discovered that there's an online archive: +

+ +

+ abandonware-magazines +

+ +

+ 1983–1987: Imagine a weekly magazine that, in addition to + providing us with a weekly program to type out and delivering + uncompromising hardware and software reviews (and that’s saying + something), also featured TV listings, movie, music, and comic book + releases, assembly language tutorials, a comic strip, a discount club, + walkthroughs... A tone that blew you away from the first page to the + last, Carali’s drawings filling in the gaps, lawsuits galore… Simply + extraordinary, unmatched. +

]]>
+
+ + More on Dillo + 2026-06-21 + Philip A. Wittamore + https://wittamore.fr/articles/2026/More-on-Dillo.html + https://wittamore.fr/articles/2026/More-on-Dillo.html + + update on yesterday's post +

The git + version of dillo has a new option: page_action, which + enables running rdrview on the current page if the + setting:

+
page_action="Reader mode:dilloc dump | rdrview -H | dilloc load"
+
+

is set in dillorc.

+

When you right click on a page the menu will now include + the option "Reader Mode".

]]>
+
+ + Installation of Snac2 on Artix Linux + 2026-05-20 +add user + In /home/snac/data/server.json edit these settings: + As root, create the dinit script: /etc/dinit.d/snac2 +enable snac2 +NGINX Media caching +in /etc/nginx/nginx.conf http section add: +make cache folder +/etc/nginx/sites-available/example.com +add pleroma relay + Philip A. Wittamore + https://wittamore.fr/articles/2026/Installation-of-Snac2-on-Artix-Linux.html + https://wittamore.fr/articles/2026/Installation-of-Snac2-on-Artix-Linux.html + + Installation of snac2 on Artix Linux +Nginx +Dinit +
+useradd -m -s /bin/bash snac
+passwd snac
+usermod -aG wheel snac
+su - snac
+mkdir src
+cd src
+git clone https://codeberg.org/grunfink/snac2.git
+cd snac2
+make
+sudo make install
+snac init ~/data
+
+
+Host name: your.server.domain
+Path prefix: leave empty
+Network address: 127.0.0.1
+Port: 8001
+
+
+snac adduser ~/data
+
+
+admin_email
+admin_account
+title
+short_description
+
+and add this line:
+
+"proxy_media: true
+
+
+type = process
+run-as = snac
+command = /usr/local/bin/snac httpd /home/snac/data
+depends-on = NetworkManager
+restart = true
+smooth-recovery = yes
+log-type = file
+logfile = /var/log/dinit/snac2.log
+
+
+dinitcl enable snac2
+
+

+see it-notes.fragas.net +

+

+see comam.es.
+Use $host not $http_host (gixy) +

+
+proxy_cache_path /var/cache/nginx/snac_cache levels=1:2 keys_zone=snac:10m max_size=1g inactive=1440m use_temp_path=off;
+
+
+mkdir -p /var/cache/nginx/snac_cache
+
+
+server {
+    listen 443 ssl;
+    listen [::]:443 ssl;
+    server_name example.com;
+
+    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/example.com/privkey.pem;
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_ciphers HIGH:!aNULL:!MD5;
+    # openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
+    ssl_dhparam /etc/ssl/certs/dhparam.pem;
+
+    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+
+location / {
+    proxy_pass http://127.0.0.1:8001;
+    proxy_set_header Host $host;
+    proxy_set_header X-Forwarded-For $remote_addr;
+}
+
+location ~ ^/.+/s/ {
+    proxy_cache snac;
+    proxy_pass http://127.0.0.1:8001;
+    proxy_set_header Host $host;
+    proxy_cache_valid 200 1d;
+    proxy_cache_valid 404 1h;
+    proxy_ignore_headers "Cache-Control" "Expires";
+    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
+    proxy_cache_lock on;
+    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+    add_header X-Proxy-Cache $upstream_cache_status;
+}
+}
+
+server {
+    listen 80;
+    listen [::]:80;
+    server_name example.com;
+    return 301 https://$host$request_uri;
+}
+
+ +

see relaylist.com

+
+su - snac
+snac adduser ~/data relay
+snac follow ~/data relay https://relay.toot.io/actor
+
]]>
+
+ + Funeral blues + 2026-05-07 + Philip A. Wittamore + https://wittamore.fr/articles/2026/Funeral-blues.html + https://wittamore.fr/articles/2026/Funeral-blues.html + + A poem by W.H.Auden (1907-1973) +

This poem was read in the film "Four marriages and a funeral"

+ +

I just stumbled upon it and wanted to write it down.

+ + +Funeral Blues + +Stop all the clocks, cut off the telephone. +Prevent the dog from barking with a juicy bone, + +Silence the pianos and with muffled drum +Bring out the coffin, let the mourners come. + +Let aeroplanes circle moaning overhead +Scribbling in the sky the message He is Dead, + +Put crêpe bows round the white necks of the public doves, +Let the traffic policemen wear black cotton gloves. + +He was my North, my South, my East and West, +My working week and my Sunday rest + +My noon, my midnight, my talk, my song; +I thought that love would last forever, I was wrong. + +The stars are not wanted now; put out every one, +Pack up the moon and dismantle the sun. + +Pour away the ocean and sweep up the wood; +For nothing now can ever come to any good. + ]]>
+
+ + From minimalist to smolweb + 2026-05-06 + Philip A. Wittamore + https://wittamore.fr/articles/2026/From-minimalist-to-smolweb.html + https://wittamore.fr/articles/2026/From-minimalist-to-smolweb.html + + When smarter people get things done +

+ In 2023 + about what I was feeling at the time on the sorry state of the web. +

+

+ I'm happy to say that the people at + smolweb.org have made a far better + statement than mine. +

+

+ As this blog is now a few years old, I'll be working on making it pass + the + smolweb validation test. +

]]>
+
+ + De Gaulle - Two films + 2026-06-04 + Philip A. Wittamore + https://wittamore.fr/articles/2026/De-Gaulle---Two-films.html + https://wittamore.fr/articles/2026/De-Gaulle---Two-films.html + + De Gaulle is more popular than ever +

+ People who choose a country to be their home are often patriotic as + it's their choice, not mere circumstance. +

+

Personally I'm patriotic but certainly not nationalist.

+
+ “The difference between patriotism and nationalism is that the + patriot is proud of his country for what it does, and the + nationalist is proud of his country no matter what it does; the + first attitude creates a feeling of responsibility, but the second a + feeling of blind arrogance that leads to war.” + ― + Sydney J. Harris +
+

+
+ +
]]>
+
+ + Ladybird turns into a squashed bug + 2026-06-06 + Philip A. Wittamore + https://wittamore.fr/articles/2026/Ladybird-turns-into-a-squashed-bug.html + https://wittamore.fr/articles/2026/Ladybird-turns-into-a-squashed-bug.html + + Sponsored software hype +

+ Last year + I had high hopes for Ladybird, a web browser with a brand new engine written, from scratch and + open source. +

+

+ Unfortunately + the code has now been locked, and the code was moved from Swift to Rust because ... AI code. +

+

+ --> + The fash browser developed by "AI" hypeboys +

]]>
+
+ + My own private search engine + 2026-06-02 + Philip A. Wittamore + https://wittamore.fr/articles/2026/My-own-private-search-engine.html + https://wittamore.fr/articles/2026/My-own-private-search-engine.html + + Messing around with a bad back in stormy weather +

+ I made a bad movement in the shower this morning and made my back + scream again, so no woodworking today. One of the benefits of + retirement is that I get to choose the days activities. +

+

+ I had installed + SearxNG previously + more than two years ago, but as I'm constantly fiddling it wound up in + the bin. +

+

+ Convalescing in the armchair I saw that my current vps is mostly idle + running just snac2, my blog, xmpp, gophernicus and other bits, so I + reinstalled SearxNG. +

+

+ It's fast, responsive, and private. I have it behind an Nginx reverse + proxy, and only accepting my own IP address for now. +

+

+ Another thing I did was to play around with the + Fresh editor. Like Micro, it can + be configured to use prettier, the code formatter. Ok, it's written in + Rust, but it is a nice editor with a retro computing vibe. It won't + replace Micro as my daily editor, but if I ever need a TUI IDE it + could be a good choice. +

]]>
+
+ + Filtering the web + 2026-06-20 + Philip A. Wittamore + https://wittamore.fr/articles/2026/Filtering-the-web.html + https://wittamore.fr/articles/2026/Filtering-the-web.html + + A browser that says no +

+ To me the problem with the smolweb is that there's isn't a browser + that applies these rules. Dillo comes close, even though it needs a + makeover. +

+

+ Dillo also has an rdrview plugin, but you have to manually write + rdrview: in front of the url in the url bar of every page to see the + results. +

+

+ A better solution would be a smolweb plugin that filters the entire + html page rejecting anything that doesn't + validate or pass to + rdrview . Degrees of acceptance could be set in settings. +

+

+ A dedicated browser wouldn't require searching for smolweb / smallweb + / indieweb blogs, sites, or articles. (There seems to be no such thing + as a smolweb search engine, although points for wiby, marginalia, kagi + and mojeek). +

+

+ Just point the browser to any web site and if it borks, display the + "errors" or ignore it with a + + 418 I'm a teapot error: after all it's the web site author's responsibility to provide + clean, readable web sites. +

+

+ Imagine a world where this plugin would be as ubiquitous as + ublock-origin, it could even change web site development. +

]]>
+
+ + Updated poem + 2026-06-01 + Philip A. Wittamore + https://wittamore.fr/articles/2026/Updated-poem.html + https://wittamore.fr/articles/2026/Updated-poem.html + + Existance is the prize itself +

+ Sadly a childhood friend of one of my nieces recently died at 36 of a + brain tumour. I gave this poem a re-write. +

+
+Life is but a long farewell
+While clawing at eternity's shell
+No rhyme nor reason, no justice in sight 
+A frantic push against entropy's might
+This fleeting hare 
+Brings grave dispair
+But pointless is a daily dread
+Of closing circuit in our bed
+Here then gone, a spark in the night?
+Our passing through is never trite
+Best keep your malaise on the shelf
+Existance is the prize itself
+
]]>
+
+ + This blog wasn't so bad after all + 2026-05-05 + Philip A. Wittamore + https://wittamore.fr/articles/2026/This-blog-wasn't-so-bad-after-all.html + https://wittamore.fr/articles/2026/This-blog-wasn't-so-bad-after-all.html + + Smolweb, Fediverse and blogs +

+ For several months I've been using Gopher exclusively, I find it's + minimalist raw text files appealing. +

+

+ It does have several drawbacks however. Recently I've been self + hosting my own fediverse instance at + social.wittamore.fr. +

+

+ Looking around for a fediverse blog, I realised that this html blog + wasn't so bad after all. +

+

+ It's work in progress, as I would like to have it + smolweb validated, and needs a + clean up. +

]]>
+
+ +
+
+ diff --git a/sandbox/webproxy.php b/sandbox/webproxy.php new file mode 100644 index 0000000..977a890 --- /dev/null +++ b/sandbox/webproxy.php @@ -0,0 +1,7 @@ + diff --git a/scripts/SimplePie.compiled.php b/scripts/SimplePie.compiled.php new file mode 100644 index 0000000..1eaddc1 --- /dev/null +++ b/scripts/SimplePie.compiled.php @@ -0,0 +1,20130 @@ +' . self::NAME . ''; + + /** + * No Autodiscovery + * @see SimplePie::set_autodiscovery_level() + */ + public const LOCATOR_NONE = 0; + + /** + * Feed Link Element Autodiscovery + * @see SimplePie::set_autodiscovery_level() + */ + public const LOCATOR_AUTODISCOVERY = 1; + + /** + * Local Feed Extension Autodiscovery + * @see SimplePie::set_autodiscovery_level() + */ + public const LOCATOR_LOCAL_EXTENSION = 2; + + /** + * Local Feed Body Autodiscovery + * @see SimplePie::set_autodiscovery_level() + */ + public const LOCATOR_LOCAL_BODY = 4; + + /** + * Remote Feed Extension Autodiscovery + * @see SimplePie::set_autodiscovery_level() + */ + public const LOCATOR_REMOTE_EXTENSION = 8; + + /** + * Remote Feed Body Autodiscovery + * @see SimplePie::set_autodiscovery_level() + */ + public const LOCATOR_REMOTE_BODY = 16; + + /** + * All Feed Autodiscovery + * @see SimplePie::set_autodiscovery_level() + */ + public const LOCATOR_ALL = 31; + + /** + * No known feed type + */ + public const TYPE_NONE = 0; + + /** + * RSS 0.90 + */ + public const TYPE_RSS_090 = 1; + + /** + * RSS 0.91 (Netscape) + */ + public const TYPE_RSS_091_NETSCAPE = 2; + + /** + * RSS 0.91 (Userland) + */ + public const TYPE_RSS_091_USERLAND = 4; + + /** + * RSS 0.91 (both Netscape and Userland) + */ + public const TYPE_RSS_091 = 6; + + /** + * RSS 0.92 + */ + public const TYPE_RSS_092 = 8; + + /** + * RSS 0.93 + */ + public const TYPE_RSS_093 = 16; + + /** + * RSS 0.94 + */ + public const TYPE_RSS_094 = 32; + + /** + * RSS 1.0 + */ + public const TYPE_RSS_10 = 64; + + /** + * RSS 2.0 + */ + public const TYPE_RSS_20 = 128; + + /** + * RDF-based RSS + */ + public const TYPE_RSS_RDF = 65; + + /** + * Non-RDF-based RSS (truly intended as syndication format) + */ + public const TYPE_RSS_SYNDICATION = 190; + + /** + * All RSS + */ + public const TYPE_RSS_ALL = 255; + + /** + * Atom 0.3 + */ + public const TYPE_ATOM_03 = 256; + + /** + * Atom 1.0 + */ + public const TYPE_ATOM_10 = 512; + + /** + * All Atom + */ + public const TYPE_ATOM_ALL = 768; + + /** + * All feed types + */ + public const TYPE_ALL = 1023; + + /** + * No construct + */ + public const CONSTRUCT_NONE = 0; + + /** + * Text construct + */ + public const CONSTRUCT_TEXT = 1; + + /** + * HTML construct + */ + public const CONSTRUCT_HTML = 2; + + /** + * XHTML construct + */ + public const CONSTRUCT_XHTML = 4; + + /** + * base64-encoded construct + */ + public const CONSTRUCT_BASE64 = 8; + + /** + * IRI construct + */ + public const CONSTRUCT_IRI = 16; + + /** + * A construct that might be HTML + */ + public const CONSTRUCT_MAYBE_HTML = 32; + + /** + * All constructs + */ + public const CONSTRUCT_ALL = 63; + + /** + * Don't change case + */ + public const SAME_CASE = 1; + + /** + * Change to lowercase + */ + public const LOWERCASE = 2; + + /** + * Change to uppercase + */ + public const UPPERCASE = 4; + + /** + * PCRE for HTML attributes + */ + public const PCRE_HTML_ATTRIBUTE = '((?:[\x09\x0A\x0B\x0C\x0D\x20]+[^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3D\x3E]*(?:[\x09\x0A\x0B\x0C\x0D\x20]*=[\x09\x0A\x0B\x0C\x0D\x20]*(?:"(?:[^"]*)"|\'(?:[^\']*)\'|(?:[^\x09\x0A\x0B\x0C\x0D\x20\x22\x27\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x3E]*)?))?)*)[\x09\x0A\x0B\x0C\x0D\x20]*'; + + /** + * PCRE for XML attributes + */ + public const PCRE_XML_ATTRIBUTE = '((?:\s+(?:(?:[^\s:]+:)?[^\s:]+)\s*=\s*(?:"(?:[^"]*)"|\'(?:[^\']*)\'))*)\s*'; + + /** + * XML Namespace + */ + public const NAMESPACE_XML = 'http://www.w3.org/XML/1998/namespace'; + + /** + * Atom 1.0 Namespace + */ + public const NAMESPACE_ATOM_10 = 'http://www.w3.org/2005/Atom'; + + /** + * Atom 0.3 Namespace + */ + public const NAMESPACE_ATOM_03 = 'http://purl.org/atom/ns#'; + + /** + * RDF Namespace + */ + public const NAMESPACE_RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; + + /** + * RSS 0.90 Namespace + */ + public const NAMESPACE_RSS_090 = 'http://my.netscape.com/rdf/simple/0.9/'; + + /** + * RSS 1.0 Namespace + */ + public const NAMESPACE_RSS_10 = 'http://purl.org/rss/1.0/'; + + /** + * RSS 1.0 Content Module Namespace + */ + public const NAMESPACE_RSS_10_MODULES_CONTENT = 'http://purl.org/rss/1.0/modules/content/'; + + /** + * RSS 2.0 Namespace + * (Stupid, I know, but I'm certain it will confuse people less with support.) + */ + public const NAMESPACE_RSS_20 = ''; + + /** + * DC 1.0 Namespace + */ + public const NAMESPACE_DC_10 = 'http://purl.org/dc/elements/1.0/'; + + /** + * DC 1.1 Namespace + */ + public const NAMESPACE_DC_11 = 'http://purl.org/dc/elements/1.1/'; + + /** + * W3C Basic Geo (WGS84 lat/long) Vocabulary Namespace + */ + public const NAMESPACE_W3C_BASIC_GEO = 'http://www.w3.org/2003/01/geo/wgs84_pos#'; + + /** + * GeoRSS Namespace + */ + public const NAMESPACE_GEORSS = 'http://www.georss.org/georss'; + + /** + * Media RSS Namespace + */ + public const NAMESPACE_MEDIARSS = 'http://search.yahoo.com/mrss/'; + + /** + * Wrong Media RSS Namespace. Caused by a long-standing typo in the spec. + */ + public const NAMESPACE_MEDIARSS_WRONG = 'http://search.yahoo.com/mrss'; + + /** + * Wrong Media RSS Namespace #2. New namespace introduced in Media RSS 1.5. + */ + public const NAMESPACE_MEDIARSS_WRONG2 = 'http://video.search.yahoo.com/mrss'; + + /** + * Wrong Media RSS Namespace #3. A possible typo of the Media RSS 1.5 namespace. + */ + public const NAMESPACE_MEDIARSS_WRONG3 = 'http://video.search.yahoo.com/mrss/'; + + /** + * Wrong Media RSS Namespace #4. New spec location after the RSS Advisory Board takes it over, but not a valid namespace. + */ + public const NAMESPACE_MEDIARSS_WRONG4 = 'http://www.rssboard.org/media-rss'; + + /** + * Wrong Media RSS Namespace #5. A possible typo of the RSS Advisory Board URL. + */ + public const NAMESPACE_MEDIARSS_WRONG5 = 'http://www.rssboard.org/media-rss/'; + + /** + * iTunes RSS Namespace + */ + public const NAMESPACE_ITUNES = 'http://www.itunes.com/dtds/podcast-1.0.dtd'; + + /** + * XHTML Namespace + */ + public const NAMESPACE_XHTML = 'http://www.w3.org/1999/xhtml'; + + /** + * IANA Link Relations Registry + */ + public const IANA_LINK_RELATIONS_REGISTRY = 'http://www.iana.org/assignments/relation/'; + + /** + * No file source + */ + public const FILE_SOURCE_NONE = 0; + + /** + * Remote file source + */ + public const FILE_SOURCE_REMOTE = 1; + + /** + * Local file source + */ + public const FILE_SOURCE_LOCAL = 2; + + /** + * fsockopen() file source + */ + public const FILE_SOURCE_FSOCKOPEN = 4; + + /** + * cURL file source + */ + public const FILE_SOURCE_CURL = 8; + + /** + * file_get_contents() file source + */ + public const FILE_SOURCE_FILE_GET_CONTENTS = 16; + + /** + * @internal Default value of the HTTP Accept header when fetching/locating feeds + */ + public const DEFAULT_HTTP_ACCEPT_HEADER = 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1'; + + /** + * @var array Raw data + * @access private + */ + public $data = []; + + /** + * @var string|string[]|null Error string (or array when multiple feeds are initialized) + * @access private + */ + public $error = null; + + /** + * @var int HTTP status code + * @see SimplePie::status_code() + * @access private + */ + public $status_code = 0; + + /** + * @var Sanitize instance of Sanitize class + * @see SimplePie::set_sanitize_class() + * @access private + */ + public $sanitize; + + /** + * @var string SimplePie Useragent + * @see SimplePie::set_useragent() + * @access private + */ + public $useragent = ''; + + /** + * @var string Feed URL + * @see SimplePie::set_feed_url() + * @access private + */ + public $feed_url; + + /** + * @var ?string Original feed URL, or new feed URL iff HTTP 301 Moved Permanently + * @see SimplePie::subscribe_url() + * @access private + */ + public $permanent_url = null; + + /** + * @var File Instance of File class to use as a feed + * @see SimplePie::set_file() + */ + private $file; + + /** + * @var string|false Raw feed data + * @see SimplePie::set_raw_data() + * @access private + */ + public $raw_data; + + /** + * @var int Timeout for fetching remote files + * @see SimplePie::set_timeout() + * @access private + */ + public $timeout = 10; + + /** + * @var array Custom curl options + * @see SimplePie::set_curl_options() + * @access private + */ + public $curl_options = []; + + /** + * @var bool Forces fsockopen() to be used for remote files instead + * of cURL, even if a new enough version is installed + * @see SimplePie::force_fsockopen() + * @access private + */ + public $force_fsockopen = false; + + /** + * @var bool Force the given data/URL to be treated as a feed no matter what + * it appears like + * @see SimplePie::force_feed() + * @access private + */ + public $force_feed = false; + + /** + * @var bool Enable/Disable Caching + * @see SimplePie::enable_cache() + * @access private + */ + private $enable_cache = true; + + /** + * @var DataCache|null + * @see SimplePie::set_cache() + */ + private $cache = null; + + /** + * @var NameFilter + * @see SimplePie::set_cache_namefilter() + */ + private $cache_namefilter; + + /** + * @var bool Force SimplePie to fallback to expired cache, if enabled, + * when feed is unavailable. + * @see SimplePie::force_cache_fallback() + * @access private + */ + public $force_cache_fallback = false; + + /** + * @var int Cache duration (in seconds) + * @see SimplePie::set_cache_duration() + * @access private + */ + public $cache_duration = 3600; + + /** + * @var int Auto-discovery cache duration (in seconds) + * @see SimplePie::set_autodiscovery_cache_duration() + * @access private + */ + public $autodiscovery_cache_duration = 604800; // 7 Days. + + /** + * @var string Cache location (relative to executing script) + * @see SimplePie::set_cache_location() + * @access private + */ + public $cache_location = './cache'; + + /** + * @var string&(callable(string): string) Function that creates the cache filename + * @see SimplePie::set_cache_name_function() + * @access private + */ + public $cache_name_function = 'md5'; + + /** + * @var bool Reorder feed by date descending + * @see SimplePie::enable_order_by_date() + * @access private + */ + public $order_by_date = true; + + /** + * @var mixed Force input encoding to be set to the follow value + * (false, or anything type-cast to false, disables this feature) + * @see SimplePie::set_input_encoding() + * @access private + */ + public $input_encoding = false; + + /** + * @var int-mask-of Feed Autodiscovery Level + * @see SimplePie::set_autodiscovery_level() + * @access private + */ + public $autodiscovery = self::LOCATOR_ALL; + + /** + * Class registry object + * + * @var Registry + */ + public $registry; + + /** + * @var int Maximum number of feeds to check with autodiscovery + * @see SimplePie::set_max_checked_feeds() + * @access private + */ + public $max_checked_feeds = 10; + + /** + * @var array|null All the feeds found during the autodiscovery process + * @see SimplePie::get_all_discovered_feeds() + * @access private + */ + public $all_discovered_feeds = []; + + /** + * @var string Web-accessible path to the handler_image.php file. + * @see SimplePie::set_image_handler() + * @access private + */ + public $image_handler = ''; + + /** + * @var array Stores the URLs when multiple feeds are being initialized. + * @see SimplePie::set_feed_url() + * @access private + */ + public $multifeed_url = []; + + /** + * @var array Stores SimplePie objects when multiple feeds initialized. + * @access private + */ + public $multifeed_objects = []; + + /** + * @var array Stores the get_object_vars() array for use with multifeeds. + * @see SimplePie::set_feed_url() + * @access private + */ + public $config_settings = null; + + /** + * @var int Stores the number of items to return per-feed with multifeeds. + * @see SimplePie::set_item_limit() + * @access private + */ + public $item_limit = 0; + + /** + * @var bool Stores if last-modified and/or etag headers were sent with the + * request when checking a feed. + */ + public $check_modified = false; + + /** + * @var array Stores the default attributes to be stripped by strip_attributes(). + * @see SimplePie::strip_attributes() + * @access private + */ + public $strip_attributes = ['bgsound', 'class', 'expr', 'id', 'style', 'onclick', 'onerror', 'onfinish', 'onmouseover', 'onmouseout', 'onfocus', 'onblur', 'lowsrc', 'dynsrc']; + + /** + * @var array> Stores the default attributes to add to different tags by add_attributes(). + * @see SimplePie::add_attributes() + * @access private + */ + public $add_attributes = ['audio' => ['preload' => 'none'], 'iframe' => ['sandbox' => 'allow-scripts allow-same-origin'], 'video' => ['preload' => 'none']]; + + /** + * @var array Stores the default tags to be stripped by strip_htmltags(). + * @see SimplePie::strip_htmltags() + * @access private + */ + public $strip_htmltags = ['base', 'blink', 'body', 'doctype', 'embed', 'font', 'form', 'frame', 'frameset', 'html', 'iframe', 'input', 'marquee', 'meta', 'noscript', 'object', 'param', 'script', 'style']; + + /** + * @var string[]|string Stores the default attributes to be renamed by rename_attributes(). + * @see SimplePie::rename_attributes() + * @access private + */ + public $rename_attributes = []; + + /** + * @var bool Should we throw exceptions, or use the old-style error property? + * @access private + */ + public $enable_exceptions = false; + + /** + * @var Client|null + */ + private $http_client = null; + + /** @var bool Whether HTTP client has been injected */ + private $http_client_injected = false; + + /** + * The SimplePie class contains feed level data and options + * + * To use SimplePie, create the SimplePie object with no parameters. You can + * then set configuration options using the provided methods. After setting + * them, you must initialise the feed using $feed->init(). At that point the + * object's methods and properties will be available to you. + * + * Previously, it was possible to pass in the feed URL along with cache + * options directly into the constructor. This has been removed as of 1.3 as + * it caused a lot of confusion. + * + * @since 1.0 Preview Release + */ + public function __construct() + { + if (version_compare(PHP_VERSION, '7.2', '<')) { + exit('Please upgrade to PHP 7.2 or newer.'); + } + + $this->set_useragent(); + + $this->set_cache_namefilter(new CallableNameFilter($this->cache_name_function)); + + // Other objects, instances created here so we can set options on them + $this->sanitize = new Sanitize(); + $this->registry = new Registry(); + + if (func_num_args() > 0) { + trigger_error('Passing parameters to the constructor is no longer supported. Please use set_feed_url(), set_cache_location(), and set_cache_duration() directly.', \E_USER_DEPRECATED); + + $args = func_get_args(); + switch (count($args)) { + case 3: + $this->set_cache_duration($args[2]); + // no break + case 2: + $this->set_cache_location($args[1]); + // no break + case 1: + $this->set_feed_url($args[0]); + $this->init(); + } + } + } + + /** + * Used for converting object to a string + * @return string + */ + public function __toString() + { + return md5(serialize($this->data)); + } + + /** + * Remove items that link back to this before destroying this object + * @return void + */ + public function __destruct() + { + if (!gc_enabled()) { + if (!empty($this->data['items'])) { + foreach ($this->data['items'] as $item) { + $item->__destruct(); + } + unset($item, $this->data['items']); + } + if (!empty($this->data['ordered_items'])) { + foreach ($this->data['ordered_items'] as $item) { + $item->__destruct(); + } + unset($item, $this->data['ordered_items']); + } + } + } + + /** + * Force the given data/URL to be treated as a feed + * + * This tells SimplePie to ignore the content-type provided by the server. + * Be careful when using this option, as it will also disable autodiscovery. + * + * @since 1.1 + * @param bool $enable Force the given data/URL to be treated as a feed + * @return void + */ + public function force_feed(bool $enable = false) + { + $this->force_feed = $enable; + } + + /** + * Set the URL of the feed you want to parse + * + * This allows you to enter the URL of the feed you want to parse, or the + * website you want to try to use auto-discovery on. This takes priority + * over any set raw data. + * + * Deprecated since 1.9.0: You can set multiple feeds to mash together by passing an array instead + * of a string for the $url. Remember that with each additional feed comes + * additional processing and resources. + * + * @since 1.0 Preview Release + * @see set_raw_data() + * @param string|string[] $url This is the URL (or (deprecated) array of URLs) that you want to parse. + * @return void + */ + public function set_feed_url($url) + { + $this->multifeed_url = []; + if (is_array($url)) { + trigger_error('Fetching multiple feeds with single SimplePie instance is deprecated since SimplePie 1.9.0, create one SimplePie instance per feed and use SimplePie::merge_items to get a single list of items.', \E_USER_DEPRECATED); + foreach ($url as $value) { + $this->multifeed_url[] = $this->registry->call(Misc::class, 'fix_protocol', [$value, 1]); + } + } else { + $this->feed_url = $this->registry->call(Misc::class, 'fix_protocol', [$url, 1]); + $this->permanent_url = $this->feed_url; + } + } + + /** + * Set an instance of {@see File} to use as a feed + * + * @deprecated since SimplePie 1.9.0, use \SimplePie\SimplePie::set_http_client() or \SimplePie\SimplePie::set_raw_data() instead. + * + * @param File &$file + * @return bool True on success, false on failure + */ + public function set_file(File &$file) + { + // trigger_error(sprintf('SimplePie\SimplePie::set_file() is deprecated since SimplePie 1.9.0, please use "SimplePie\SimplePie::set_http_client()" or "SimplePie\SimplePie::set_raw_data()" instead.'), \E_USER_DEPRECATED); + + $this->feed_url = $file->get_final_requested_uri(); + $this->permanent_url = $this->feed_url; + $this->file = &$file; + + return true; + } + + /** + * Set the raw XML data to parse + * + * Allows you to use a string of RSS/Atom data instead of a remote feed. + * + * If you have a feed available as a string in PHP, you can tell SimplePie + * to parse that data string instead of a remote feed. Any set feed URL + * takes precedence. + * + * @since 1.0 Beta 3 + * @param string $data RSS or Atom data as a string. + * @see set_feed_url() + * @return void + */ + public function set_raw_data(string $data) + { + $this->raw_data = $data; + } + + /** + * Set a PSR-18 client and PSR-17 factories + * + * Allows you to use your own HTTP client implementations. + * This will become required with SimplePie 2.0.0. + */ + final public function set_http_client( + ClientInterface $http_client, + RequestFactoryInterface $request_factory, + UriFactoryInterface $uri_factory + ): void { + $this->http_client = new Psr18Client($http_client, $request_factory, $uri_factory); + } + + /** + * Set the default timeout for fetching remote feeds + * + * This allows you to change the maximum time the feed's server to respond + * and send the feed back. + * + * @since 1.0 Beta 3 + * @param int $timeout The maximum number of seconds to spend waiting to retrieve a feed. + * @return void + */ + public function set_timeout(int $timeout = 10) + { + if ($this->http_client_injected) { + throw new SimplePieException(sprintf( + 'Using "%s()" has no effect, because you already provided a HTTP client with "%s::set_http_client()". Configure timeout in your HTTP client instead.', + __METHOD__, + self::class + )); + } + + $this->timeout = (int) $timeout; + + // Reset a possible existing FileClient, + // so a new client with the changed value will be created + if (is_object($this->http_client) && $this->http_client instanceof FileClient) { + $this->http_client = null; + } elseif (is_object($this->http_client)) { + // Trigger notice if a PSR-18 client was set + trigger_error(sprintf( + 'Using "%s()" has no effect, because you already provided a HTTP client with "%s::set_http_client()". Configure the timeout in your HTTP client instead.', + __METHOD__, + get_class($this) + ), \E_USER_NOTICE); + } + } + + /** + * Set custom curl options + * + * This allows you to change default curl options + * + * @since 1.0 Beta 3 + * @param array $curl_options Curl options to add to default settings + * @return void + */ + public function set_curl_options(array $curl_options = []) + { + if ($this->http_client_injected) { + throw new SimplePieException(sprintf( + 'Using "%s()" has no effect, because you already provided a HTTP client with "%s::set_http_client()". Configure custom curl options in your HTTP client instead.', + __METHOD__, + self::class + )); + } + + $this->curl_options = $curl_options; + + // Reset a possible existing FileClient, + // so a new client with the changed value will be created + if (is_object($this->http_client) && $this->http_client instanceof FileClient) { + $this->http_client = null; + } elseif (is_object($this->http_client)) { + // Trigger notice if a PSR-18 client was set + trigger_error(sprintf( + 'Using "%s()" has no effect, because you already provided a HTTP client with "%s::set_http_client()". Configure the curl options in your HTTP client instead.', + __METHOD__, + get_class($this) + ), \E_USER_NOTICE); + } + } + + /** + * Force SimplePie to use fsockopen() instead of cURL + * + * @since 1.0 Beta 3 + * @param bool $enable Force fsockopen() to be used + * @return void + */ + public function force_fsockopen(bool $enable = false) + { + if ($this->http_client_injected) { + throw new SimplePieException(sprintf( + 'Using "%s()" has no effect, because you already provided a HTTP client with "%s::set_http_client()". Configure fsockopen in your HTTP client instead.', + __METHOD__, + self::class + )); + } + + $this->force_fsockopen = $enable; + + // Reset a possible existing FileClient, + // so a new client with the changed value will be created + if (is_object($this->http_client) && $this->http_client instanceof FileClient) { + $this->http_client = null; + } elseif (is_object($this->http_client)) { + // Trigger notice if a PSR-18 client was set + trigger_error(sprintf( + 'Using "%s()" has no effect, because you already provided a HTTP client with "%s::set_http_client()". Configure fsockopen in your HTTP client instead.', + __METHOD__, + get_class($this) + ), \E_USER_NOTICE); + } + } + + /** + * Enable/disable caching in SimplePie. + * + * This option allows you to disable caching all-together in SimplePie. + * However, disabling the cache can lead to longer load times. + * + * @since 1.0 Preview Release + * @param bool $enable Enable caching + * @return void + */ + public function enable_cache(bool $enable = true) + { + $this->enable_cache = $enable; + } + + /** + * Set a PSR-16 implementation as cache + * + * @param CacheInterface $cache The PSR-16 cache implementation + * + * @return void + */ + public function set_cache(CacheInterface $cache) + { + $this->cache = new Psr16($cache); + } + + /** + * SimplePie to continue to fall back to expired cache, if enabled, when + * feed is unavailable. + * + * This tells SimplePie to ignore any file errors and fall back to cache + * instead. This only works if caching is enabled and cached content + * still exists. + * + * @deprecated since SimplePie 1.8.0, expired cache will not be used anymore. + * + * @param bool $enable Force use of cache on fail. + * @return void + */ + public function force_cache_fallback(bool $enable = false) + { + // @trigger_error(sprintf('SimplePie\SimplePie::force_cache_fallback() is deprecated since SimplePie 1.8.0, expired cache will not be used anymore.'), \E_USER_DEPRECATED); + $this->force_cache_fallback = $enable; + } + + /** + * Set the length of time (in seconds) that the contents of a feed will be + * cached + * + * @param int $seconds The feed content cache duration + * @return void + */ + public function set_cache_duration(int $seconds = 3600) + { + $this->cache_duration = $seconds; + } + + /** + * Set the length of time (in seconds) that the autodiscovered feed URL will + * be cached + * + * @param int $seconds The autodiscovered feed URL cache duration. + * @return void + */ + public function set_autodiscovery_cache_duration(int $seconds = 604800) + { + $this->autodiscovery_cache_duration = $seconds; + } + + /** + * Set the file system location where the cached files should be stored + * + * @deprecated since SimplePie 1.8.0, use SimplePie::set_cache() instead. + * + * @param string $location The file system location. + * @return void + */ + public function set_cache_location(string $location = './cache') + { + // @trigger_error(sprintf('SimplePie\SimplePie::set_cache_location() is deprecated since SimplePie 1.8.0, please use "SimplePie\SimplePie::set_cache()" instead.'), \E_USER_DEPRECATED); + $this->cache_location = $location; + } + + /** + * Return the filename (i.e. hash, without path and without extension) of the file to cache a given URL. + * + * @param string $url The URL of the feed to be cached. + * @return string A filename (i.e. hash, without path and without extension). + */ + public function get_cache_filename(string $url) + { + // Append custom parameters to the URL to avoid cache pollution in case of multiple calls with different parameters. + $url .= $this->force_feed ? '#force_feed' : ''; + $options = []; + if ($this->timeout != 10) { + $options[CURLOPT_TIMEOUT] = $this->timeout; + } + if ($this->useragent !== Misc::get_default_useragent()) { + $options[CURLOPT_USERAGENT] = $this->useragent; + } + if (!empty($this->curl_options)) { + foreach ($this->curl_options as $k => $v) { + $options[$k] = $v; + } + } + if (!empty($options)) { + ksort($options); + $url .= '#' . urlencode(var_export($options, true)); + } + + return $this->cache_namefilter->filter($url); + } + + /** + * Set whether feed items should be sorted into reverse chronological order + * + * @param bool $enable Sort as reverse chronological order. + * @return void + */ + public function enable_order_by_date(bool $enable = true) + { + $this->order_by_date = $enable; + } + + /** + * Set the character encoding used to parse the feed + * + * This overrides the encoding reported by the feed, however it will fall + * back to the normal encoding detection if the override fails + * + * @param string|false $encoding Character encoding + * @return void + */ + public function set_input_encoding($encoding = false) + { + if ($encoding) { + $this->input_encoding = (string) $encoding; + } else { + $this->input_encoding = false; + } + } + + /** + * Set how much feed autodiscovery to do + * + * @see self::LOCATOR_NONE + * @see self::LOCATOR_AUTODISCOVERY + * @see self::LOCATOR_LOCAL_EXTENSION + * @see self::LOCATOR_LOCAL_BODY + * @see self::LOCATOR_REMOTE_EXTENSION + * @see self::LOCATOR_REMOTE_BODY + * @see self::LOCATOR_ALL + * @param int-mask-of $level Feed Autodiscovery Level (level can be a combination of the above constants, see bitwise OR operator) + * @return void + */ + public function set_autodiscovery_level(int $level = self::LOCATOR_ALL) + { + $this->autodiscovery = $level; + } + + /** + * Get the class registry + * + * Use this to override SimplePie's default classes + * + * @return Registry + */ + public function &get_registry() + { + return $this->registry; + } + + /** + * Set which class SimplePie uses for caching + * + * @deprecated since SimplePie 1.3, use {@see set_cache()} instead + * + * @param class-string $class Name of custom class + * + * @return bool True on success, false otherwise + */ + public function set_cache_class(string $class = Cache::class) + { + trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::set_cache()" instead.', __METHOD__), \E_USER_DEPRECATED); + + return $this->registry->register(Cache::class, $class, true); + } + + /** + * Set which class SimplePie uses for auto-discovery + * + * @deprecated since SimplePie 1.3, use {@see get_registry()} instead + * + * @param class-string $class Name of custom class + * + * @return bool True on success, false otherwise + */ + public function set_locator_class(string $class = Locator::class) + { + trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); + + return $this->registry->register(Locator::class, $class, true); + } + + /** + * Set which class SimplePie uses for XML parsing + * + * @deprecated since SimplePie 1.3, use {@see get_registry()} instead + * + * @param class-string $class Name of custom class + * + * @return bool True on success, false otherwise + */ + public function set_parser_class(string $class = Parser::class) + { + trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); + + return $this->registry->register(Parser::class, $class, true); + } + + /** + * Set which class SimplePie uses for remote file fetching + * + * @deprecated since SimplePie 1.3, use {@see get_registry()} instead + * + * @param class-string $class Name of custom class + * + * @return bool True on success, false otherwise + */ + public function set_file_class(string $class = File::class) + { + trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); + + return $this->registry->register(File::class, $class, true); + } + + /** + * Set which class SimplePie uses for data sanitization + * + * @deprecated since SimplePie 1.3, use {@see get_registry()} instead + * + * @param class-string $class Name of custom class + * + * @return bool True on success, false otherwise + */ + public function set_sanitize_class(string $class = Sanitize::class) + { + trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); + + return $this->registry->register(Sanitize::class, $class, true); + } + + /** + * Set which class SimplePie uses for handling feed items + * + * @deprecated since SimplePie 1.3, use {@see get_registry()} instead + * + * @param class-string $class Name of custom class + * + * @return bool True on success, false otherwise + */ + public function set_item_class(string $class = Item::class) + { + trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); + + return $this->registry->register(Item::class, $class, true); + } + + /** + * Set which class SimplePie uses for handling author data + * + * @deprecated since SimplePie 1.3, use {@see get_registry()} instead + * + * @param class-string $class Name of custom class + * + * @return bool True on success, false otherwise + */ + public function set_author_class(string $class = Author::class) + { + trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); + + return $this->registry->register(Author::class, $class, true); + } + + /** + * Set which class SimplePie uses for handling category data + * + * @deprecated since SimplePie 1.3, use {@see get_registry()} instead + * + * @param class-string $class Name of custom class + * + * @return bool True on success, false otherwise + */ + public function set_category_class(string $class = Category::class) + { + trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); + + return $this->registry->register(Category::class, $class, true); + } + + /** + * Set which class SimplePie uses for feed enclosures + * + * @deprecated since SimplePie 1.3, use {@see get_registry()} instead + * + * @param class-string $class Name of custom class + * + * @return bool True on success, false otherwise + */ + public function set_enclosure_class(string $class = Enclosure::class) + { + trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); + + return $this->registry->register(Enclosure::class, $class, true); + } + + /** + * Set which class SimplePie uses for `` captions + * + * @deprecated since SimplePie 1.3, use {@see get_registry()} instead + * + * @param class-string $class Name of custom class + * + * @return bool True on success, false otherwise + */ + public function set_caption_class(string $class = Caption::class) + { + trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); + + return $this->registry->register(Caption::class, $class, true); + } + + /** + * Set which class SimplePie uses for `` + * + * @deprecated since SimplePie 1.3, use {@see get_registry()} instead + * + * @param class-string $class Name of custom class + * + * @return bool True on success, false otherwise + */ + public function set_copyright_class(string $class = Copyright::class) + { + trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); + + return $this->registry->register(Copyright::class, $class, true); + } + + /** + * Set which class SimplePie uses for `` + * + * @deprecated since SimplePie 1.3, use {@see get_registry()} instead + * + * @param class-string $class Name of custom class + * + * @return bool True on success, false otherwise + */ + public function set_credit_class(string $class = Credit::class) + { + trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); + + return $this->registry->register(Credit::class, $class, true); + } + + /** + * Set which class SimplePie uses for `` + * + * @deprecated since SimplePie 1.3, use {@see get_registry()} instead + * + * @param class-string $class Name of custom class + * + * @return bool True on success, false otherwise + */ + public function set_rating_class(string $class = Rating::class) + { + trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); + + return $this->registry->register(Rating::class, $class, true); + } + + /** + * Set which class SimplePie uses for `` + * + * @deprecated since SimplePie 1.3, use {@see get_registry()} instead + * + * @param class-string $class Name of custom class + * + * @return bool True on success, false otherwise + */ + public function set_restriction_class(string $class = Restriction::class) + { + trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); + + return $this->registry->register(Restriction::class, $class, true); + } + + /** + * Set which class SimplePie uses for content-type sniffing + * + * @deprecated since SimplePie 1.3, use {@see get_registry()} instead + * + * @param class-string $class Name of custom class + * + * @return bool True on success, false otherwise + */ + public function set_content_type_sniffer_class(string $class = Sniffer::class) + { + trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); + + return $this->registry->register(Sniffer::class, $class, true); + } + + /** + * Set which class SimplePie uses item sources + * + * @deprecated since SimplePie 1.3, use {@see get_registry()} instead + * + * @param class-string $class Name of custom class + * + * @return bool True on success, false otherwise + */ + public function set_source_class(string $class = Source::class) + { + trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); + + return $this->registry->register(Source::class, $class, true); + } + + /** + * Set the user agent string + * + * @param string $ua New user agent string. + * @return void + */ + public function set_useragent(?string $ua = null) + { + if ($this->http_client_injected) { + throw new SimplePieException(sprintf( + 'Using "%s()" has no effect, because you already provided a HTTP client with "%s::set_http_client()". Configure user agent string in your HTTP client instead.', + __METHOD__, + self::class + )); + } + + if ($ua === null) { + $ua = Misc::get_default_useragent(); + } + + $this->useragent = (string) $ua; + + // Reset a possible existing FileClient, + // so a new client with the changed value will be created + if (is_object($this->http_client) && $this->http_client instanceof FileClient) { + $this->http_client = null; + } elseif (is_object($this->http_client)) { + // Trigger notice if a PSR-18 client was set + trigger_error(sprintf( + 'Using "%s()" has no effect, because you already provided a HTTP client with "%s::set_http_client()". Configure the useragent in your HTTP client instead.', + __METHOD__, + get_class($this) + ), \E_USER_NOTICE); + } + } + + /** + * Set a namefilter to modify the cache filename with + * + * @param NameFilter $filter + * + * @return void + */ + public function set_cache_namefilter(NameFilter $filter): void + { + $this->cache_namefilter = $filter; + } + + /** + * Set callback function to create cache filename with + * + * @deprecated since SimplePie 1.8.0, use {@see set_cache_namefilter()} instead + * + * @param (string&(callable(string): string))|null $function Callback function + * @return void + */ + public function set_cache_name_function(?string $function = null) + { + // trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.8.0, please use "SimplePie\SimplePie::set_cache_namefilter()" instead.', __METHOD__), \E_USER_DEPRECATED); + + if ($function === null) { + $function = 'md5'; + } + + $this->cache_name_function = $function; + + $this->set_cache_namefilter(new CallableNameFilter($this->cache_name_function)); + } + + /** + * Set options to make SP as fast as possible + * + * Forgoes a substantial amount of data sanitization in favor of speed. This + * turns SimplePie into a dumb parser of feeds. + * + * @param bool $set Whether to set them or not + * @return void + */ + public function set_stupidly_fast(bool $set = false) + { + if ($set) { + $this->enable_order_by_date(false); + $this->remove_div(false); + $this->strip_comments(false); + $this->strip_htmltags([]); + $this->strip_attributes([]); + $this->add_attributes([]); + $this->set_image_handler(false); + $this->set_https_domains([]); + } + } + + /** + * Set maximum number of feeds to check with autodiscovery + * + * @param int $max Maximum number of feeds to check + * @return void + */ + public function set_max_checked_feeds(int $max = 10) + { + $this->max_checked_feeds = $max; + } + + /** + * @return void + */ + public function remove_div(bool $enable = true) + { + $this->sanitize->remove_div($enable); + } + + /** + * @param string[]|string|false $tags Set a list of tags to strip, or set empty string to use default tags, or false to strip nothing. + * @return void + */ + public function strip_htmltags($tags = '', ?bool $encode = null) + { + if ($tags === '') { + $tags = $this->strip_htmltags; + } + $this->sanitize->strip_htmltags($tags); + if ($encode !== null) { + $this->sanitize->encode_instead_of_strip($encode); + } + } + + /** + * @return void + */ + public function encode_instead_of_strip(bool $enable = true) + { + $this->sanitize->encode_instead_of_strip($enable); + } + + /** + * @param string[]|string $attribs + * @return void + */ + public function rename_attributes($attribs = '') + { + if ($attribs === '') { + $attribs = $this->rename_attributes; + } + $this->sanitize->rename_attributes($attribs); + } + + /** + * @param string[]|string $attribs + * @return void + */ + public function strip_attributes($attribs = '') + { + if ($attribs === '') { + $attribs = $this->strip_attributes; + } + $this->sanitize->strip_attributes($attribs); + } + + /** + * @param array>|'' $attribs + * @return void + */ + public function add_attributes($attribs = '') + { + if ($attribs === '') { + $attribs = $this->add_attributes; + } + $this->sanitize->add_attributes($attribs); + } + + /** + * Set the output encoding + * + * Allows you to override SimplePie's output to match that of your webpage. + * This is useful for times when your webpages are not being served as + * UTF-8. This setting will be obeyed by {@see handle_content_type()}, and + * is similar to {@see set_input_encoding()}. + * + * It should be noted, however, that not all character encodings can support + * all characters. If your page is being served as ISO-8859-1 and you try + * to display a Japanese feed, you'll likely see garbled characters. + * Because of this, it is highly recommended to ensure that your webpages + * are served as UTF-8. + * + * The number of supported character encodings depends on whether your web + * host supports {@link http://php.net/mbstring mbstring}, + * {@link http://php.net/iconv iconv}, or both. See + * {@link http://simplepie.org/wiki/faq/Supported_Character_Encodings} for + * more information. + * + * @param string $encoding + * @return void + */ + public function set_output_encoding(string $encoding = 'UTF-8') + { + $this->sanitize->set_output_encoding($encoding); + } + + /** + * @return void + */ + public function strip_comments(bool $strip = false) + { + $this->sanitize->strip_comments($strip); + } + + /** + * Set element/attribute key/value pairs of HTML attributes + * containing URLs that need to be resolved relative to the feed + * + * Defaults to |a|@href, |area|@href, |blockquote|@cite, |del|@cite, + * |form|@action, |img|@longdesc, |img|@src, |input|@src, |ins|@cite, + * |q|@cite + * + * @since 1.0 + * @param array|null $element_attribute Element/attribute key/value pairs, null for default + * @return void + */ + public function set_url_replacements(?array $element_attribute = null) + { + $this->sanitize->set_url_replacements($element_attribute); + } + + /** + * Set the list of domains for which to force HTTPS. + * @see Sanitize::set_https_domains() + * @param array $domains List of HTTPS domains. Example array('biz', 'example.com', 'example.org', 'www.example.net'). + * @return void + */ + public function set_https_domains(array $domains = []) + { + $this->sanitize->set_https_domains($domains); + } + + /** + * Set the handler to enable the display of cached images. + * + * @param string|false $page Web-accessible path to the handler_image.php file. + * @param string $qs The query string that the value should be passed to. + * @return void + */ + public function set_image_handler($page = false, string $qs = 'i') + { + if ($page !== false) { + $this->sanitize->set_image_handler($page . '?' . $qs . '='); + } else { + $this->image_handler = ''; + } + } + + /** + * Set the limit for items returned per-feed with multifeeds + * + * @param int $limit The maximum number of items to return. + * @return void + */ + public function set_item_limit(int $limit = 0) + { + $this->item_limit = $limit; + } + + /** + * Enable throwing exceptions + * + * @param bool $enable Should we throw exceptions, or use the old-style error property? + * @return void + */ + public function enable_exceptions(bool $enable = true) + { + $this->enable_exceptions = $enable; + } + + /** + * Initialize the feed object + * + * This is what makes everything happen. Period. This is where all of the + * configuration options get processed, feeds are fetched, cached, and + * parsed, and all of that other good stuff. + * + * @return bool True if successful, false otherwise + */ + public function init() + { + // Check absolute bare minimum requirements. + if (!extension_loaded('xml') || !extension_loaded('pcre')) { + $this->error = 'XML or PCRE extensions not loaded!'; + return false; + } + // Then check the xml extension is sane (i.e., libxml 2.7.x issue on PHP < 5.2.9 and libxml 2.7.0 to 2.7.2 on any version) if we don't have xmlreader. + elseif (!extension_loaded('xmlreader')) { + static $xml_is_sane = null; + if ($xml_is_sane === null) { + $parser_check = xml_parser_create(); + xml_parse_into_struct($parser_check, '&', $values); + if (\PHP_VERSION_ID < 80000) { + xml_parser_free($parser_check); + } + $xml_is_sane = isset($values[0]['value']); + } + if (!$xml_is_sane) { + return false; + } + } + + // The default sanitize class gets set in the constructor, check if it has + // changed. + if ($this->registry->get_class(Sanitize::class) !== Sanitize::class) { + $this->sanitize = $this->registry->create(Sanitize::class); + } + if (method_exists($this->sanitize, 'set_registry')) { + $this->sanitize->set_registry($this->registry); + } + + // Pass whatever was set with config options over to the sanitizer. + // Pass the classes in for legacy support; new classes should use the registry instead + $cache = $this->registry->get_class(Cache::class); + \assert($cache !== null, 'Cache must be defined'); + $this->sanitize->pass_cache_data( + $this->enable_cache, + $this->cache_location, + $this->cache_namefilter, + $cache, + $this->cache + ); + + $http_client = $this->get_http_client(); + + if ($http_client instanceof Psr18Client) { + $this->sanitize->set_http_client( + $http_client->getHttpClient(), + $http_client->getRequestFactory(), + $http_client->getUriFactory() + ); + } + + if (!empty($this->multifeed_url)) { + $i = 0; + $success = 0; + $this->multifeed_objects = []; + $this->error = []; + foreach ($this->multifeed_url as $url) { + $this->multifeed_objects[$i] = clone $this; + $this->multifeed_objects[$i]->set_feed_url($url); + $single_success = $this->multifeed_objects[$i]->init(); + $success |= $single_success; + if (!$single_success) { + $this->error[$i] = $this->multifeed_objects[$i]->error(); + } + $i++; + } + return (bool) $success; + } elseif ($this->feed_url === null && $this->raw_data === null) { + return false; + } + + $this->error = null; + $this->data = []; + $this->check_modified = false; + $this->multifeed_objects = []; + $cache = false; + + if ($this->feed_url !== null) { + $parsed_feed_url = $this->registry->call(Misc::class, 'parse_url', [$this->feed_url]); + + // Decide whether to enable caching + if ($this->enable_cache && $parsed_feed_url['scheme'] !== '') { + $cache = $this->get_cache($this->feed_url); + } + + // Fetch the data into $this->raw_data + if (($fetched = $this->fetch_data($cache)) === true) { + return true; + } elseif ($fetched === false) { + return false; + } + + [$headers, $sniffed] = $fetched; + } + + // Empty response check + if (empty($this->raw_data)) { + $this->error = "A feed could not be found at `$this->feed_url`. Empty body."; + $this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, __FILE__, __LINE__]); + return false; + } + + // Set up array of possible encodings + $encodings = []; + + // First check to see if input has been overridden. + if ($this->input_encoding !== false) { + $encodings[] = strtoupper($this->input_encoding); + } + + $application_types = ['application/xml', 'application/xml-dtd', 'application/xml-external-parsed-entity']; + $text_types = ['text/xml', 'text/xml-external-parsed-entity']; + + // RFC 3023 (only applies to sniffed content) + if (isset($sniffed)) { + if (in_array($sniffed, $application_types) || substr($sniffed, 0, 12) === 'application/' && substr($sniffed, -4) === '+xml') { + if (isset($headers['content-type']) && preg_match('/;\x20?charset=([^;]*)/i', $headers['content-type'], $charset)) { + $encodings[] = strtoupper($charset[1]); + } + $encodings = array_merge($encodings, $this->registry->call(Misc::class, 'xml_encoding', [$this->raw_data, &$this->registry])); + $encodings[] = 'UTF-8'; + } elseif (in_array($sniffed, $text_types) || substr($sniffed, 0, 5) === 'text/' && substr($sniffed, -4) === '+xml') { + if (isset($headers['content-type']) && preg_match('/;\x20?charset=([^;]*)/i', $headers['content-type'], $charset)) { + $encodings[] = strtoupper($charset[1]); + } + $encodings[] = 'US-ASCII'; + } + // Text MIME-type default + elseif (substr($sniffed, 0, 5) === 'text/') { + $encodings[] = 'UTF-8'; + } + } + + // Fallback to XML 1.0 Appendix F.1/UTF-8/ISO-8859-1 + $encodings = array_merge($encodings, $this->registry->call(Misc::class, 'xml_encoding', [$this->raw_data, &$this->registry])); + $encodings[] = 'UTF-8'; + $encodings[] = 'ISO-8859-1'; + + // There's no point in trying an encoding twice + $encodings = array_unique($encodings); + + // Loop through each possible encoding, till we return something, or run out of possibilities + foreach ($encodings as $encoding) { + // Change the encoding to UTF-8 (as we always use UTF-8 internally) + if ($utf8_data = $this->registry->call(Misc::class, 'change_encoding', [$this->raw_data, $encoding, 'UTF-8'])) { + // Create new parser + $parser = $this->registry->create(Parser::class); + + // If it's parsed fine + if ($parser->parse($utf8_data, 'UTF-8', $this->permanent_url ?? '')) { + $this->data = $parser->get_data(); + if (!($this->get_type() & ~self::TYPE_NONE)) { + $this->error = "A feed could not be found at `$this->feed_url`. This does not appear to be a valid RSS or Atom feed."; + $this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, __FILE__, __LINE__]); + return false; + } + + if (isset($headers)) { + $this->data['headers'] = $headers; + } + $this->data['build'] = Misc::get_build(); + + // Cache the file if caching is enabled + $this->data['cache_expiration_time'] = $this->cache_duration + time(); + + if ($cache && !$cache->set_data($this->get_cache_filename($this->feed_url), $this->data, $this->cache_duration)) { + trigger_error("$this->cache_location is not writable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING); + } + return true; + } + } + } + + if (isset($parser)) { + // We have an error, just set Misc::error to it and quit + $this->error = $this->feed_url; + $this->error .= sprintf(' is invalid XML, likely due to invalid characters. XML error: %s at line %d, column %d', $parser->get_error_string(), $parser->get_current_line(), $parser->get_current_column()); + } else { + $this->error = 'The data could not be converted to UTF-8.'; + if (!extension_loaded('mbstring') && !extension_loaded('iconv') && !class_exists('\UConverter')) { + $this->error .= ' You MUST have either the iconv, mbstring or intl (PHP 5.5+) extension installed and enabled.'; + } else { + $missingExtensions = []; + if (!extension_loaded('iconv')) { + $missingExtensions[] = 'iconv'; + } + if (!extension_loaded('mbstring')) { + $missingExtensions[] = 'mbstring'; + } + if (!class_exists('\UConverter')) { + $missingExtensions[] = 'intl (PHP 5.5+)'; + } + $this->error .= ' Try installing/enabling the ' . implode(' or ', $missingExtensions) . ' extension.'; + } + } + + $this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, __FILE__, __LINE__]); + + return false; + } + + /** + * Fetch the data + * + * If the data is already cached, attempt to fetch it from there instead + * + * @param Base|DataCache|false $cache Cache handler, or false to not load from the cache + * @return array{array, string}|bool Returns true if the data was loaded from the cache, or an array of HTTP headers and sniffed type + */ + protected function fetch_data(&$cache) + { + if ($cache instanceof Base) { + // @trigger_error(sprintf('Providing $cache as "\SimplePie\Cache\Base" in %s() is deprecated since SimplePie 1.8.0, please provide "\SimplePie\Cache\DataCache" implementation instead.', __METHOD__), \E_USER_DEPRECATED); + $cache = new BaseDataCache($cache); + } + + // @phpstan-ignore-next-line Enforce PHPDoc type. + if ($cache !== false && !$cache instanceof DataCache) { + throw new InvalidArgumentException(sprintf( + '%s(): Argument #1 ($cache) must be of type %s|false', + __METHOD__, + DataCache::class + ), 1); + } + + $cacheKey = $this->get_cache_filename($this->feed_url); + + // If it's enabled, use the cache + if ($cache) { + // Load the Cache + $this->data = $cache->get_data($cacheKey, []); + + if (!empty($this->data)) { + // If the cache is for an outdated build of SimplePie + if (!isset($this->data['build']) || $this->data['build'] !== Misc::get_build()) { + $cache->delete_data($cacheKey); + $this->data = []; + } + // If we've hit a collision just rerun it with caching disabled + elseif (isset($this->data['url']) && $this->data['url'] !== $this->feed_url) { + $cache = false; + $this->data = []; + } + // If we've got a non feed_url stored (if the page isn't actually a feed, or is a redirect) use that URL. + elseif (isset($this->data['feed_url'])) { + // Do not need to do feed autodiscovery yet. + if ($this->data['feed_url'] !== $this->data['url']) { + $this->set_feed_url($this->data['feed_url']); + $this->data['url'] = $this->data['feed_url']; + + $cache->set_data($this->get_cache_filename($this->feed_url), $this->data, $this->autodiscovery_cache_duration); + + return $this->init(); + } + + $cache->delete_data($this->get_cache_filename($this->feed_url)); + $this->data = []; + } + // Check if the cache has been updated + elseif (!isset($this->data['cache_expiration_time']) || $this->data['cache_expiration_time'] < time()) { + // Want to know if we tried to send last-modified and/or etag headers + // when requesting this file. (Note that it's up to the file to + // support this, but we don't always send the headers either.) + $this->check_modified = true; + if (isset($this->data['headers']['last-modified']) || isset($this->data['headers']['etag'])) { + $headers = [ + 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER, + ]; + if (isset($this->data['headers']['last-modified'])) { + $headers['if-modified-since'] = $this->data['headers']['last-modified']; + } + if (isset($this->data['headers']['etag'])) { + $headers['if-none-match'] = $this->data['headers']['etag']; + } + + try { + $file = $this->get_http_client()->request(Client::METHOD_GET, $this->feed_url, $headers); + $this->status_code = $file->get_status_code(); + } catch (ClientException $th) { + $this->check_modified = false; + $this->status_code = 0; + + if ($this->force_cache_fallback) { + $this->data['cache_expiration_time'] = $this->cache_duration + time(); + $cache->set_data($cacheKey, $this->data, $this->cache_duration); + + return true; + } + + $failedFileReason = $th->getMessage(); + } + + if ($this->status_code === 304) { + // Set raw_data to false here too, to signify that the cache + // is still valid. + $this->raw_data = false; + $this->data['cache_expiration_time'] = $this->cache_duration + time(); + $cache->set_data($cacheKey, $this->data, $this->cache_duration); + + return true; + } + } + } + // If the cache is still valid, just return true + else { + $this->raw_data = false; + return true; + } + } + // If the cache is empty + else { + $this->data = []; + } + } + + // If we don't already have the file (it'll only exist if we've opened it to check if the cache has been modified), open it. + if (!isset($file)) { + if ($this->file instanceof File && $this->file->get_final_requested_uri() === $this->feed_url) { + $file = &$this->file; + } elseif (isset($failedFileReason)) { + // Do not try to fetch again if we already failed once. + // If the file connection had an error, set SimplePie::error to that and quit + $this->error = $failedFileReason; + + return !empty($this->data); + } else { + $headers = [ + 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER, + ]; + try { + $file = $this->get_http_client()->request(Client::METHOD_GET, $this->feed_url, $headers); + } catch (ClientException $th) { + // If the file connection has an error, set SimplePie::error to that and quit + $this->error = $th->getMessage(); + + return !empty($this->data); + } + } + } + $this->status_code = $file->get_status_code(); + + // If the file connection has an error, set SimplePie::error to that and quit + if (!(!Misc::is_remote_uri($file->get_final_requested_uri()) || ($file->get_status_code() === 200 || $file->get_status_code() > 206 && $file->get_status_code() < 300))) { + $this->error = 'Retrieved unsupported status code "' . $this->status_code . '"'; + return !empty($this->data); + } + + if (!$this->force_feed) { + // Check if the supplied URL is a feed, if it isn't, look for it. + $locate = $this->registry->create(Locator::class, [ + (!$file instanceof File) ? File::fromResponse($file) : $file, + $this->timeout, + $this->useragent, + $this->max_checked_feeds, + $this->force_fsockopen, + $this->curl_options + ]); + + $http_client = $this->get_http_client(); + + if ($http_client instanceof Psr18Client) { + $locate->set_http_client( + $http_client->getHttpClient(), + $http_client->getRequestFactory(), + $http_client->getUriFactory() + ); + } + + if (!$locate->is_feed($file)) { + $copyStatusCode = $file->get_status_code(); + $copyContentType = $file->get_header_line('content-type'); + try { + $microformats = false; + if (class_exists('DOMXpath') && function_exists('Mf2\parse')) { + $doc = new \DOMDocument(); + @$doc->loadHTML($file->get_body_content()); + $xpath = new \DOMXpath($doc); + // Check for both h-feed and h-entry, as both a feed with no entries + // and a list of entries without an h-feed wrapper are both valid. + $query = '//*[contains(concat(" ", @class, " "), " h-feed ") or '. + 'contains(concat(" ", @class, " "), " h-entry ")]'; + + /** @var \DOMNodeList<\DOMElement> $result */ + $result = $xpath->query($query); + $microformats = $result->length !== 0; + } + // Now also do feed discovery, but if microformats were found don't + // overwrite the current value of file. + $discovered = $locate->find( + $this->autodiscovery, + $this->all_discovered_feeds + ); + if ($microformats) { + $hub = $locate->get_rel_link('hub'); + $self = $locate->get_rel_link('self'); + if ($hub || $self) { + $file = $this->store_links($file, $hub, $self); + } + // Push the current file onto all_discovered feeds so the user can + // be shown this as one of the options. + if ($this->all_discovered_feeds !== null) { + $this->all_discovered_feeds[] = $file; + } + } else { + if ($discovered) { + $file = $discovered; + } else { + // We need to unset this so that if SimplePie::set_file() has + // been called that object is untouched + unset($file); + $this->error = "A feed could not be found at `$this->feed_url`; the status code is `$copyStatusCode` and content-type is `$copyContentType`"; + $this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, __FILE__, __LINE__]); + return false; + } + } + } catch (SimplePieException $e) { + // We need to unset this so that if SimplePie::set_file() has been called that object is untouched + unset($file); + // This is usually because DOMDocument doesn't exist + $this->error = $e->getMessage(); + $this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, $e->getFile(), $e->getLine()]); + return false; + } + + if ($cache) { + $this->data = [ + 'url' => $this->feed_url, + 'feed_url' => $file->get_final_requested_uri(), + 'build' => Misc::get_build(), + 'cache_expiration_time' => $this->cache_duration + time(), + ]; + + if (!$cache->set_data($cacheKey, $this->data, $this->cache_duration)) { + trigger_error("$this->cache_location is not writable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING); + } + } + } + $this->feed_url = $file->get_final_requested_uri(); + $locate = null; + } + + $this->raw_data = $file->get_body_content(); + $this->permanent_url = $file->get_permanent_uri(); + + $headers = []; + foreach ($file->get_headers() as $key => $values) { + $headers[$key] = implode(', ', $values); + } + + $sniffer = $this->registry->create(Sniffer::class, [&$file]); + $sniffed = $sniffer->get_type(); + + return [$headers, $sniffed]; + } + + /** + * Get the error message for the occurred error + * + * @return string|string[]|null Error message, or array of messages for multifeeds + */ + public function error() + { + return $this->error; + } + + /** + * Get the last HTTP status code + * + * @return int Status code + */ + public function status_code() + { + return $this->status_code; + } + + /** + * Get the raw XML + * + * This is the same as the old `$feed->enable_xml_dump(true)`, but returns + * the data instead of printing it. + * + * @return string|false Raw XML data, false if the cache is used + */ + public function get_raw_data() + { + return $this->raw_data; + } + + /** + * Get the character encoding used for output + * + * @since Preview Release + * @return string + */ + public function get_encoding() + { + return $this->sanitize->output_encoding; + } + + /** + * Send the content-type header with correct encoding + * + * This method ensures that the SimplePie-enabled page is being served with + * the correct {@link http://www.iana.org/assignments/media-types/ mime-type} + * and character encoding HTTP headers (character encoding determined by the + * {@see set_output_encoding} config option). + * + * This won't work properly if any content or whitespace has already been + * sent to the browser, because it relies on PHP's + * {@link http://php.net/header header()} function, and these are the + * circumstances under which the function works. + * + * Because it's setting these settings for the entire page (as is the nature + * of HTTP headers), this should only be used once per page (again, at the + * top). + * + * @param string $mime MIME type to serve the page as + * @return void + */ + public function handle_content_type(string $mime = 'text/html') + { + if (!headers_sent()) { + $header = "Content-type: $mime;"; + if ($this->get_encoding()) { + $header .= ' charset=' . $this->get_encoding(); + } else { + $header .= ' charset=UTF-8'; + } + header($header); + } + } + + /** + * Get the type of the feed + * + * This returns a self::TYPE_* constant, which can be tested against + * using {@link http://php.net/language.operators.bitwise bitwise operators} + * + * @since 0.8 (usage changed to using constants in 1.0) + * @see self::TYPE_NONE Unknown. + * @see self::TYPE_RSS_090 RSS 0.90. + * @see self::TYPE_RSS_091_NETSCAPE RSS 0.91 (Netscape). + * @see self::TYPE_RSS_091_USERLAND RSS 0.91 (Userland). + * @see self::TYPE_RSS_091 RSS 0.91. + * @see self::TYPE_RSS_092 RSS 0.92. + * @see self::TYPE_RSS_093 RSS 0.93. + * @see self::TYPE_RSS_094 RSS 0.94. + * @see self::TYPE_RSS_10 RSS 1.0. + * @see self::TYPE_RSS_20 RSS 2.0.x. + * @see self::TYPE_RSS_RDF RDF-based RSS. + * @see self::TYPE_RSS_SYNDICATION Non-RDF-based RSS (truly intended as syndication format). + * @see self::TYPE_RSS_ALL Any version of RSS. + * @see self::TYPE_ATOM_03 Atom 0.3. + * @see self::TYPE_ATOM_10 Atom 1.0. + * @see self::TYPE_ATOM_ALL Any version of Atom. + * @see self::TYPE_ALL Any known/supported feed type. + * @return int-mask-of constant + */ + public function get_type() + { + if (!isset($this->data['type'])) { + $this->data['type'] = self::TYPE_ALL; + if (isset($this->data['child'][self::NAMESPACE_ATOM_10]['feed'])) { + $this->data['type'] &= self::TYPE_ATOM_10; + } elseif (isset($this->data['child'][self::NAMESPACE_ATOM_03]['feed'])) { + $this->data['type'] &= self::TYPE_ATOM_03; + } elseif (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'])) { + if (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_10]['channel']) + || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_10]['image']) + || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_10]['item']) + || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_10]['textinput'])) { + $this->data['type'] &= self::TYPE_RSS_10; + } + if (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_090]['channel']) + || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_090]['image']) + || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_090]['item']) + || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_090]['textinput'])) { + $this->data['type'] &= self::TYPE_RSS_090; + } + } elseif (isset($this->data['child'][self::NAMESPACE_RSS_20]['rss'])) { + $this->data['type'] &= self::TYPE_RSS_ALL; + if (isset($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['attribs']['']['version'])) { + switch (trim($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['attribs']['']['version'])) { + case '0.91': + $this->data['type'] &= self::TYPE_RSS_091; + if (isset($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['child'][self::NAMESPACE_RSS_20]['skiphours']['hour'][0]['data'])) { + switch (trim($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['child'][self::NAMESPACE_RSS_20]['skiphours']['hour'][0]['data'])) { + case '0': + $this->data['type'] &= self::TYPE_RSS_091_NETSCAPE; + break; + + case '24': + $this->data['type'] &= self::TYPE_RSS_091_USERLAND; + break; + } + } + break; + + case '0.92': + $this->data['type'] &= self::TYPE_RSS_092; + break; + + case '0.93': + $this->data['type'] &= self::TYPE_RSS_093; + break; + + case '0.94': + $this->data['type'] &= self::TYPE_RSS_094; + break; + + case '2.0': + $this->data['type'] &= self::TYPE_RSS_20; + break; + } + } + } else { + $this->data['type'] = self::TYPE_NONE; + } + } + return $this->data['type']; + } + + /** + * Get the URL for the feed + * + * When the 'permanent' mode is enabled, returns the original feed URL, + * except in the case of an `HTTP 301 Moved Permanently` status response, + * in which case the location of the first redirection is returned. + * + * When the 'permanent' mode is disabled (default), + * may or may not be different from the URL passed to {@see set_feed_url()}, + * depending on whether auto-discovery was used, and whether there were + * any redirects along the way. + * + * @since Preview Release (previously called `get_feed_url()` since SimplePie 0.8.) + * @todo Support + * @todo Also, |atom:link|@rel=self + * @param bool $permanent Permanent mode to return only the original URL or the first redirection + * iff it is a 301 redirection + * @return string|null + */ + public function subscribe_url(bool $permanent = false) + { + if ($permanent) { + if ($this->permanent_url !== null) { + // sanitize encodes ampersands which are required when used in a url. + return str_replace( + '&', + '&', + $this->sanitize( + $this->permanent_url, + self::CONSTRUCT_IRI + ) + ); + } + } else { + if ($this->feed_url !== null) { + return str_replace( + '&', + '&', + $this->sanitize( + $this->feed_url, + self::CONSTRUCT_IRI + ) + ); + } + } + return null; + } + + /** + * Get data for an feed-level element + * + * This method allows you to get access to ANY element/attribute that is a + * sub-element of the opening feed tag. + * + * The return value is an indexed array of elements matching the given + * namespace and tag name. Each element has `attribs`, `data` and `child` + * subkeys. For `attribs` and `child`, these contain namespace subkeys. + * `attribs` then has one level of associative name => value data (where + * `value` is a string) after the namespace. `child` has tag-indexed keys + * after the namespace, each member of which is an indexed array matching + * this same format. + * + * For example: + *
+     * // This is probably a bad example because we already support
+     * //  natively, but it shows you how to parse through
+     * // the nodes.
+     * $group = $item->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'group');
+     * $content = $group[0]['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['content'];
+     * $file = $content[0]['attribs']['']['url'];
+     * echo $file;
+     * 
+ * + * @since 1.0 + * @see http://simplepie.org/wiki/faq/supported_xml_namespaces + * @param string $namespace The URL of the XML namespace of the elements you're trying to access + * @param string $tag Tag name + * @return array>|null + */ + public function get_feed_tags(string $namespace, string $tag) + { + $type = $this->get_type(); + if ($type & self::TYPE_ATOM_10) { + if (isset($this->data['child'][self::NAMESPACE_ATOM_10]['feed'][0]['child'][$namespace][$tag])) { + return $this->data['child'][self::NAMESPACE_ATOM_10]['feed'][0]['child'][$namespace][$tag]; + } + } + if ($type & self::TYPE_ATOM_03) { + if (isset($this->data['child'][self::NAMESPACE_ATOM_03]['feed'][0]['child'][$namespace][$tag])) { + return $this->data['child'][self::NAMESPACE_ATOM_03]['feed'][0]['child'][$namespace][$tag]; + } + } + if ($type & self::TYPE_RSS_RDF) { + if (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][$namespace][$tag])) { + return $this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][$namespace][$tag]; + } + } + if ($type & self::TYPE_RSS_SYNDICATION) { + if (isset($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['child'][$namespace][$tag])) { + return $this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['child'][$namespace][$tag]; + } + } + return null; + } + + /** + * Get data for an channel-level element + * + * This method allows you to get access to ANY element/attribute in the + * channel/header section of the feed. + * + * See {@see SimplePie::get_feed_tags()} for a description of the return value + * + * @since 1.0 + * @see http://simplepie.org/wiki/faq/supported_xml_namespaces + * @param string $namespace The URL of the XML namespace of the elements you're trying to access + * @param string $tag Tag name + * @return array>|null + */ + public function get_channel_tags(string $namespace, string $tag) + { + $type = $this->get_type(); + if ($type & self::TYPE_ATOM_ALL) { + if ($return = $this->get_feed_tags($namespace, $tag)) { + return $return; + } + } + if ($type & self::TYPE_RSS_10) { + if ($channel = $this->get_feed_tags(self::NAMESPACE_RSS_10, 'channel')) { + if (isset($channel[0]['child'][$namespace][$tag])) { + return $channel[0]['child'][$namespace][$tag]; + } + } + } + if ($type & self::TYPE_RSS_090) { + if ($channel = $this->get_feed_tags(self::NAMESPACE_RSS_090, 'channel')) { + if (isset($channel[0]['child'][$namespace][$tag])) { + return $channel[0]['child'][$namespace][$tag]; + } + } + } + if ($type & self::TYPE_RSS_SYNDICATION) { + if ($channel = $this->get_feed_tags(self::NAMESPACE_RSS_20, 'channel')) { + if (isset($channel[0]['child'][$namespace][$tag])) { + return $channel[0]['child'][$namespace][$tag]; + } + } + } + return null; + } + + /** + * Get data for an channel-level element + * + * This method allows you to get access to ANY element/attribute in the + * image/logo section of the feed. + * + * See {@see SimplePie::get_feed_tags()} for a description of the return value + * + * @since 1.0 + * @see http://simplepie.org/wiki/faq/supported_xml_namespaces + * @param string $namespace The URL of the XML namespace of the elements you're trying to access + * @param string $tag Tag name + * @return array>|null + */ + public function get_image_tags(string $namespace, string $tag) + { + $type = $this->get_type(); + if ($type & self::TYPE_RSS_10) { + if ($image = $this->get_feed_tags(self::NAMESPACE_RSS_10, 'image')) { + if (isset($image[0]['child'][$namespace][$tag])) { + return $image[0]['child'][$namespace][$tag]; + } + } + } + if ($type & self::TYPE_RSS_090) { + if ($image = $this->get_feed_tags(self::NAMESPACE_RSS_090, 'image')) { + if (isset($image[0]['child'][$namespace][$tag])) { + return $image[0]['child'][$namespace][$tag]; + } + } + } + if ($type & self::TYPE_RSS_SYNDICATION) { + if ($image = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'image')) { + if (isset($image[0]['child'][$namespace][$tag])) { + return $image[0]['child'][$namespace][$tag]; + } + } + } + return null; + } + + /** + * Get the base URL value from the feed + * + * Uses `` if available, + * otherwise uses the first 'self' link or the first 'alternate' link of the feed, + * or failing that, the URL of the feed itself. + * + * @see get_link + * @see subscribe_url + * + * @param array $element + * @return string + */ + public function get_base(array $element = []) + { + if (!empty($element['xml_base_explicit']) && isset($element['xml_base'])) { + return $element['xml_base']; + } + if (($link = $this->get_link(0, 'alternate')) !== null) { + return $link; + } + if (($link = $this->get_link(0, 'self')) !== null) { + return $link; + } + + return $this->subscribe_url() ?? ''; + } + + /** + * Sanitize feed data + * + * @access private + * @see Sanitize::sanitize() + * @param string $data Data to sanitize + * @param int-mask-of $type + * @param string $base Base URL to resolve URLs against + * @return string Sanitized data + */ + public function sanitize(string $data, int $type, string $base = '') + { + try { + // This really returns string|false but changing encoding is uncommon and we are going to deprecate it, so let’s just lie to PHPStan in the interest of cleaner annotations. + return $this->sanitize->sanitize($data, $type, $base); + } catch (SimplePieException $e) { + if (!$this->enable_exceptions) { + $this->error = $e->getMessage(); + $this->registry->call(Misc::class, 'error', [$this->error, E_USER_WARNING, $e->getFile(), $e->getLine()]); + return ''; + } + + throw $e; + } + } + + /** + * Get the title of the feed + * + * Uses ``, `` or `<dc:title>` + * + * @since 1.0 (previously called `get_feed_title` since 0.8) + * @return string|null + */ + public function get_title() + { + if ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'title')) { + return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'title')) { + return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_10, 'title')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_090, 'title')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'title')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_11, 'title')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_10, 'title')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); + } + + return null; + } + + /** + * Get a category for the feed + * + * @since Unknown + * @param int $key The category that you want to return. Remember that arrays begin with 0, not 1 + * @return Category|null + */ + public function get_category(int $key = 0) + { + $categories = $this->get_categories(); + if (isset($categories[$key])) { + return $categories[$key]; + } + + return null; + } + + /** + * Get all categories for the feed + * + * Uses `<atom:category>`, `<category>` or `<dc:subject>` + * + * @since Unknown + * @return array<Category>|null List of {@see Category} objects + */ + public function get_categories() + { + $categories = []; + + foreach ((array) $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'category') as $category) { + $term = null; + $scheme = null; + $label = null; + if (isset($category['attribs']['']['term'])) { + $term = $this->sanitize($category['attribs']['']['term'], self::CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['scheme'])) { + $scheme = $this->sanitize($category['attribs']['']['scheme'], self::CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['label'])) { + $label = $this->sanitize($category['attribs']['']['label'], self::CONSTRUCT_TEXT); + } + $categories[] = $this->registry->create(Category::class, [$term, $scheme, $label]); + } + foreach ((array) $this->get_channel_tags(self::NAMESPACE_RSS_20, 'category') as $category) { + // This is really the label, but keep this as the term also for BC. + // Label will also work on retrieving because that falls back to term. + $term = $this->sanitize($category['data'], self::CONSTRUCT_TEXT); + if (isset($category['attribs']['']['domain'])) { + $scheme = $this->sanitize($category['attribs']['']['domain'], self::CONSTRUCT_TEXT); + } else { + $scheme = null; + } + $categories[] = $this->registry->create(Category::class, [$term, $scheme, null]); + } + foreach ((array) $this->get_channel_tags(self::NAMESPACE_DC_11, 'subject') as $category) { + $categories[] = $this->registry->create(Category::class, [$this->sanitize($category['data'], self::CONSTRUCT_TEXT), null, null]); + } + foreach ((array) $this->get_channel_tags(self::NAMESPACE_DC_10, 'subject') as $category) { + $categories[] = $this->registry->create(Category::class, [$this->sanitize($category['data'], self::CONSTRUCT_TEXT), null, null]); + } + + if (!empty($categories)) { + return array_unique($categories); + } + + return null; + } + + /** + * Get an author for the feed + * + * @since 1.1 + * @param int $key The author that you want to return. Remember that arrays begin with 0, not 1 + * @return Author|null + */ + public function get_author(int $key = 0) + { + $authors = $this->get_authors(); + if (isset($authors[$key])) { + return $authors[$key]; + } + + return null; + } + + /** + * Get all authors for the feed + * + * Uses `<atom:author>`, `<author>`, `<dc:creator>` or `<itunes:author>` + * + * @since 1.1 + * @return array<Author>|null List of {@see Author} objects + */ + public function get_authors() + { + $authors = []; + foreach ((array) $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'author') as $author) { + $name = null; + $uri = null; + $email = null; + if (isset($author['child'][self::NAMESPACE_ATOM_10]['name'][0]['data'])) { + $name = $this->sanitize($author['child'][self::NAMESPACE_ATOM_10]['name'][0]['data'], self::CONSTRUCT_TEXT); + } + if (isset($author['child'][self::NAMESPACE_ATOM_10]['uri'][0]['data'])) { + $uri = $author['child'][self::NAMESPACE_ATOM_10]['uri'][0]; + $uri = $this->sanitize($uri['data'], self::CONSTRUCT_IRI, $this->get_base($uri)); + } + if (isset($author['child'][self::NAMESPACE_ATOM_10]['email'][0]['data'])) { + $email = $this->sanitize($author['child'][self::NAMESPACE_ATOM_10]['email'][0]['data'], self::CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $uri !== null) { + $authors[] = $this->registry->create(Author::class, [$name, $uri, $email]); + } + } + if ($author = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'author')) { + $name = null; + $url = null; + $email = null; + if (isset($author[0]['child'][self::NAMESPACE_ATOM_03]['name'][0]['data'])) { + $name = $this->sanitize($author[0]['child'][self::NAMESPACE_ATOM_03]['name'][0]['data'], self::CONSTRUCT_TEXT); + } + if (isset($author[0]['child'][self::NAMESPACE_ATOM_03]['url'][0]['data'])) { + $url = $author[0]['child'][self::NAMESPACE_ATOM_03]['url'][0]; + $url = $this->sanitize($url['data'], self::CONSTRUCT_IRI, $this->get_base($url)); + } + if (isset($author[0]['child'][self::NAMESPACE_ATOM_03]['email'][0]['data'])) { + $email = $this->sanitize($author[0]['child'][self::NAMESPACE_ATOM_03]['email'][0]['data'], self::CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $url !== null) { + $authors[] = $this->registry->create(Author::class, [$name, $url, $email]); + } + } + foreach ((array) $this->get_channel_tags(self::NAMESPACE_DC_11, 'creator') as $author) { + $authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], self::CONSTRUCT_TEXT), null, null]); + } + foreach ((array) $this->get_channel_tags(self::NAMESPACE_DC_10, 'creator') as $author) { + $authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], self::CONSTRUCT_TEXT), null, null]); + } + foreach ((array) $this->get_channel_tags(self::NAMESPACE_ITUNES, 'author') as $author) { + $authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], self::CONSTRUCT_TEXT), null, null]); + } + + if (!empty($authors)) { + return array_unique($authors); + } + + return null; + } + + /** + * Get a contributor for the feed + * + * @since 1.1 + * @param int $key The contrbutor that you want to return. Remember that arrays begin with 0, not 1 + * @return Author|null + */ + public function get_contributor(int $key = 0) + { + $contributors = $this->get_contributors(); + if (isset($contributors[$key])) { + return $contributors[$key]; + } + + return null; + } + + /** + * Get all contributors for the feed + * + * Uses `<atom:contributor>` + * + * @since 1.1 + * @return array<Author>|null List of {@see Author} objects + */ + public function get_contributors() + { + $contributors = []; + foreach ((array) $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'contributor') as $contributor) { + $name = null; + $uri = null; + $email = null; + if (isset($contributor['child'][self::NAMESPACE_ATOM_10]['name'][0]['data'])) { + $name = $this->sanitize($contributor['child'][self::NAMESPACE_ATOM_10]['name'][0]['data'], self::CONSTRUCT_TEXT); + } + if (isset($contributor['child'][self::NAMESPACE_ATOM_10]['uri'][0]['data'])) { + $uri = $contributor['child'][self::NAMESPACE_ATOM_10]['uri'][0]; + $uri = $this->sanitize($uri['data'], self::CONSTRUCT_IRI, $this->get_base($uri)); + } + if (isset($contributor['child'][self::NAMESPACE_ATOM_10]['email'][0]['data'])) { + $email = $this->sanitize($contributor['child'][self::NAMESPACE_ATOM_10]['email'][0]['data'], self::CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $uri !== null) { + $contributors[] = $this->registry->create(Author::class, [$name, $uri, $email]); + } + } + foreach ((array) $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'contributor') as $contributor) { + $name = null; + $url = null; + $email = null; + if (isset($contributor['child'][self::NAMESPACE_ATOM_03]['name'][0]['data'])) { + $name = $this->sanitize($contributor['child'][self::NAMESPACE_ATOM_03]['name'][0]['data'], self::CONSTRUCT_TEXT); + } + if (isset($contributor['child'][self::NAMESPACE_ATOM_03]['url'][0]['data'])) { + $url = $contributor['child'][self::NAMESPACE_ATOM_03]['url'][0]; + $url = $this->sanitize($url['data'], self::CONSTRUCT_IRI, $this->get_base($url)); + } + if (isset($contributor['child'][self::NAMESPACE_ATOM_03]['email'][0]['data'])) { + $email = $this->sanitize($contributor['child'][self::NAMESPACE_ATOM_03]['email'][0]['data'], self::CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $url !== null) { + $contributors[] = $this->registry->create(Author::class, [$name, $url, $email]); + } + } + + if (!empty($contributors)) { + return array_unique($contributors); + } + + return null; + } + + /** + * Get a single link for the feed + * + * @since 1.0 (previously called `get_feed_link` since Preview Release, `get_feed_permalink()` since 0.8) + * @param int $key The link that you want to return. Remember that arrays begin with 0, not 1 + * @param string $rel The relationship of the link to return + * @return string|null Link URL + */ + public function get_link(int $key = 0, string $rel = 'alternate') + { + $links = $this->get_links($rel); + if (isset($links[$key])) { + return $links[$key]; + } + + return null; + } + + /** + * Get the permalink for the item + * + * Returns the first link available with a relationship of "alternate". + * Identical to {@see get_link()} with key 0 + * + * @see get_link + * @since 1.0 (previously called `get_feed_link` since Preview Release, `get_feed_permalink()` since 0.8) + * @internal Added for parity between the parent-level and the item/entry-level. + * @return string|null Link URL + */ + public function get_permalink() + { + return $this->get_link(0); + } + + /** + * Get all links for the feed + * + * Uses `<atom:link>` or `<link>` + * + * @since Beta 2 + * @param string $rel The relationship of links to return + * @return array<string>|null Links found for the feed (strings) + */ + public function get_links(string $rel = 'alternate') + { + if (!isset($this->data['links'])) { + $this->data['links'] = []; + if ($links = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'link')) { + foreach ($links as $link) { + if (isset($link['attribs']['']['href'])) { + $link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate'; + $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], self::CONSTRUCT_IRI, $this->get_base($link)); + } + } + } + if ($links = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'link')) { + foreach ($links as $link) { + if (isset($link['attribs']['']['href'])) { + $link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate'; + $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], self::CONSTRUCT_IRI, $this->get_base($link)); + } + } + } + if ($links = $this->get_channel_tags(self::NAMESPACE_RSS_10, 'link')) { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], self::CONSTRUCT_IRI, $this->get_base($links[0])); + } + if ($links = $this->get_channel_tags(self::NAMESPACE_RSS_090, 'link')) { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], self::CONSTRUCT_IRI, $this->get_base($links[0])); + } + if ($links = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'link')) { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], self::CONSTRUCT_IRI, $this->get_base($links[0])); + } + + $keys = array_keys($this->data['links']); + foreach ($keys as $key) { + if ($this->registry->call(Misc::class, 'is_isegment_nz_nc', [$key])) { + if (isset($this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key])) { + $this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key] = array_merge($this->data['links'][$key], $this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key]); + $this->data['links'][$key] = &$this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key]; + } else { + $this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key] = &$this->data['links'][$key]; + } + } elseif (substr($key, 0, 41) === self::IANA_LINK_RELATIONS_REGISTRY) { + $this->data['links'][substr($key, 41)] = &$this->data['links'][$key]; + } + $this->data['links'][$key] = array_unique($this->data['links'][$key]); + } + } + + if (isset($this->data['headers']['link'])) { + $link_headers = $this->data['headers']['link']; + if (is_array($link_headers)) { + $link_headers = implode(',', $link_headers); + } + // https://datatracker.ietf.org/doc/html/rfc8288 + if (is_string($link_headers) && + preg_match_all('/<(?P<uri>[^>]+)>\s*;\s*rel\s*=\s*(?P<quote>"?)' . preg_quote($rel) . '(?P=quote)\s*(?=,|$)/i', $link_headers, $matches)) { + return $matches['uri']; + } + } + + if (isset($this->data['links'][$rel])) { + return $this->data['links'][$rel]; + } + + return null; + } + + /** + * @return ?array<Response> + */ + public function get_all_discovered_feeds() + { + return $this->all_discovered_feeds; + } + + /** + * Get the content for the item + * + * Uses `<atom:subtitle>`, `<atom:tagline>`, `<description>`, + * `<dc:description>`, `<itunes:summary>` or `<itunes:subtitle>` + * + * @since 1.0 (previously called `get_feed_description()` since 0.8) + * @return string|null + */ + public function get_description() + { + if ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'subtitle')) { + return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'tagline')) { + return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_10, 'description')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_090, 'description')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'description')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_11, 'description')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_10, 'description')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ITUNES, 'summary')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ITUNES, 'subtitle')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_HTML, $this->get_base($return[0])); + } + + return null; + } + + /** + * Get the copyright info for the feed + * + * Uses `<atom:rights>`, `<atom:copyright>` or `<dc:rights>` + * + * @since 1.0 (previously called `get_feed_copyright()` since 0.8) + * @return string|null + */ + public function get_copyright() + { + if ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'rights')) { + return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'copyright')) { + return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'copyright')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_11, 'rights')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_10, 'rights')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); + } + + return null; + } + + /** + * Get the language for the feed + * + * Uses `<language>`, `<dc:language>`, or @xml_lang + * + * @since 1.0 (previously called `get_feed_language()` since 0.8) + * @return string|null + */ + public function get_language() + { + if ($return = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'language')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_11, 'language')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_10, 'language')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); + } elseif (isset($this->data['child'][self::NAMESPACE_ATOM_10]['feed'][0]['xml_lang'])) { + return $this->sanitize($this->data['child'][self::NAMESPACE_ATOM_10]['feed'][0]['xml_lang'], self::CONSTRUCT_TEXT); + } elseif (isset($this->data['child'][self::NAMESPACE_ATOM_03]['feed'][0]['xml_lang'])) { + return $this->sanitize($this->data['child'][self::NAMESPACE_ATOM_03]['feed'][0]['xml_lang'], self::CONSTRUCT_TEXT); + } elseif (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['xml_lang'])) { + return $this->sanitize($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['xml_lang'], self::CONSTRUCT_TEXT); + } elseif (isset($this->data['headers']['content-language'])) { + return $this->sanitize($this->data['headers']['content-language'], self::CONSTRUCT_TEXT); + } + + return null; + } + + /** + * Get the latitude coordinates for the item + * + * Compatible with the W3C WGS84 Basic Geo and GeoRSS specifications + * + * Uses `<geo:lat>` or `<georss:point>` + * + * @since 1.0 + * @link http://www.w3.org/2003/01/geo/ W3C WGS84 Basic Geo + * @link http://www.georss.org/ GeoRSS + * @return float|null + */ + public function get_latitude() + { + if ($return = $this->get_channel_tags(self::NAMESPACE_W3C_BASIC_GEO, 'lat')) { + return (float) $return[0]['data']; + } elseif (($return = $this->get_channel_tags(self::NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) { + return (float) $match[1]; + } + + return null; + } + + /** + * Get the longitude coordinates for the feed + * + * Compatible with the W3C WGS84 Basic Geo and GeoRSS specifications + * + * Uses `<geo:long>`, `<geo:lon>` or `<georss:point>` + * + * @since 1.0 + * @link http://www.w3.org/2003/01/geo/ W3C WGS84 Basic Geo + * @link http://www.georss.org/ GeoRSS + * @return float|null + */ + public function get_longitude() + { + if ($return = $this->get_channel_tags(self::NAMESPACE_W3C_BASIC_GEO, 'long')) { + return (float) $return[0]['data']; + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_W3C_BASIC_GEO, 'lon')) { + return (float) $return[0]['data']; + } elseif (($return = $this->get_channel_tags(self::NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) { + return (float) $match[2]; + } + + return null; + } + + /** + * Get the feed logo's title + * + * RSS 0.9.0, 1.0 and 2.0 feeds are allowed to have a "feed logo" title. + * + * Uses `<image><title>` or `<image><dc:title>` + * + * @return string|null + */ + public function get_image_title() + { + if ($return = $this->get_image_tags(self::NAMESPACE_RSS_10, 'title')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); + } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_090, 'title')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); + } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_20, 'title')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); + } elseif ($return = $this->get_image_tags(self::NAMESPACE_DC_11, 'title')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); + } elseif ($return = $this->get_image_tags(self::NAMESPACE_DC_10, 'title')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); + } + + return null; + } + + /** + * Get the feed logo's URL + * + * RSS 0.9.0, 2.0, Atom 1.0, and feeds with iTunes RSS tags are allowed to + * have a "feed logo" URL. This points directly to the image itself. + * + * Uses `<itunes:image>`, `<atom:logo>`, `<atom:icon>`, + * `<image><title>` or `<image><dc:title>` + * + * @return string|null + */ + public function get_image_url() + { + if ($return = $this->get_channel_tags(self::NAMESPACE_ITUNES, 'image')) { + return $this->sanitize($return[0]['attribs']['']['href'], self::CONSTRUCT_IRI); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'logo')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0])); + } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'icon')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0])); + } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_10, 'url')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0])); + } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_090, 'url')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0])); + } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_20, 'url')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0])); + } + + return null; + } + + /** + * Get the feed logo's link + * + * RSS 0.9.0, 1.0 and 2.0 feeds are allowed to have a "feed logo" link. This + * points to a human-readable page that the image should link to. + * + * Uses `<itunes:image>`, `<atom:logo>`, `<atom:icon>`, + * `<image><title>` or `<image><dc:title>` + * + * @return string|null + */ + public function get_image_link() + { + if ($return = $this->get_image_tags(self::NAMESPACE_RSS_10, 'link')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0])); + } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_090, 'link')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0])); + } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_20, 'link')) { + return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0])); + } + + return null; + } + + /** + * Get the feed logo's link + * + * RSS 2.0 feeds are allowed to have a "feed logo" width. + * + * Uses `<image><width>` or defaults to 88 if no width is specified and + * the feed is an RSS 2.0 feed. + * + * @return int|null + */ + public function get_image_width() + { + if ($return = $this->get_image_tags(self::NAMESPACE_RSS_20, 'width')) { + return intval($return[0]['data']); + } elseif ($this->get_type() & self::TYPE_RSS_SYNDICATION && $this->get_image_tags(self::NAMESPACE_RSS_20, 'url')) { + return 88; + } + + return null; + } + + /** + * Get the feed logo's height + * + * RSS 2.0 feeds are allowed to have a "feed logo" height. + * + * Uses `<image><height>` or defaults to 31 if no height is specified and + * the feed is an RSS 2.0 feed. + * + * @return int|null + */ + public function get_image_height() + { + if ($return = $this->get_image_tags(self::NAMESPACE_RSS_20, 'height')) { + return intval($return[0]['data']); + } elseif ($this->get_type() & self::TYPE_RSS_SYNDICATION && $this->get_image_tags(self::NAMESPACE_RSS_20, 'url')) { + return 31; + } + + return null; + } + + /** + * Get the number of items in the feed + * + * This is well-suited for {@link http://php.net/for for()} loops with + * {@see get_item()} + * + * @param int $max Maximum value to return. 0 for no limit + * @return int Number of items in the feed + */ + public function get_item_quantity(int $max = 0) + { + $qty = count($this->get_items()); + if ($max === 0) { + return $qty; + } + + return min($qty, $max); + } + + /** + * Get a single item from the feed + * + * This is better suited for {@link http://php.net/for for()} loops, whereas + * {@see get_items()} is better suited for + * {@link http://php.net/foreach foreach()} loops. + * + * @see get_item_quantity() + * @since Beta 2 + * @param int $key The item that you want to return. Remember that arrays begin with 0, not 1 + * @return Item|null + */ + public function get_item(int $key = 0) + { + $items = $this->get_items(); + if (isset($items[$key])) { + return $items[$key]; + } + + return null; + } + + /** + * Get all items from the feed + * + * This is better suited for {@link http://php.net/for for()} loops, whereas + * {@see get_items()} is better suited for + * {@link http://php.net/foreach foreach()} loops. + * + * @see get_item_quantity + * @since Beta 2 + * @param int $start Index to start at + * @param int $end Number of items to return. 0 for all items after `$start` + * @return Item[] List of {@see Item} objects + */ + public function get_items(int $start = 0, int $end = 0) + { + if (!isset($this->data['items'])) { + if (!empty($this->multifeed_objects)) { + $this->data['items'] = SimplePie::merge_items($this->multifeed_objects, $start, $end, $this->item_limit); + if (empty($this->data['items'])) { + return []; + } + return $this->data['items']; + } + $this->data['items'] = []; + if ($items = $this->get_feed_tags(self::NAMESPACE_ATOM_10, 'entry')) { + $keys = array_keys($items); + foreach ($keys as $key) { + $this->data['items'][] = $this->make_item($items[$key]); + } + } + if ($items = $this->get_feed_tags(self::NAMESPACE_ATOM_03, 'entry')) { + $keys = array_keys($items); + foreach ($keys as $key) { + $this->data['items'][] = $this->make_item($items[$key]); + } + } + if ($items = $this->get_feed_tags(self::NAMESPACE_RSS_10, 'item')) { + $keys = array_keys($items); + foreach ($keys as $key) { + $this->data['items'][] = $this->make_item($items[$key]); + } + } + if ($items = $this->get_feed_tags(self::NAMESPACE_RSS_090, 'item')) { + $keys = array_keys($items); + foreach ($keys as $key) { + $this->data['items'][] = $this->make_item($items[$key]); + } + } + if ($items = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'item')) { + $keys = array_keys($items); + foreach ($keys as $key) { + $this->data['items'][] = $this->make_item($items[$key]); + } + } + } + + if (empty($this->data['items'])) { + return []; + } + + if ($this->order_by_date) { + if (!isset($this->data['ordered_items'])) { + $this->data['ordered_items'] = $this->data['items']; + usort($this->data['ordered_items'], [get_class($this), 'sort_items']); + } + $items = $this->data['ordered_items']; + } else { + $items = $this->data['items']; + } + // Slice the data as desired + if ($end === 0) { + return array_slice($items, $start); + } + + return array_slice($items, $start, $end); + } + + /** + * Set the favicon handler + * + * @deprecated Use your own favicon handling instead + * @param string|false $page + * @return bool + */ + public function set_favicon_handler($page = false, string $qs = 'i') + { + trigger_error('Favicon handling has been removed since SimplePie 1.3, please use your own handling', \E_USER_DEPRECATED); + return false; + } + + /** + * Get the favicon for the current feed + * + * @deprecated Use your own favicon handling instead + * @return string|bool + */ + public function get_favicon() + { + trigger_error('Favicon handling has been removed since SimplePie 1.3, please use your own handling', \E_USER_DEPRECATED); + + if (($url = $this->get_link()) !== null) { + return 'https://www.google.com/s2/favicons?domain=' . urlencode($url); + } + + return false; + } + + /** + * Magic method handler + * + * @param string $method Method name + * @param array<mixed> $args Arguments to the method + * @return mixed + */ + public function __call(string $method, array $args) + { + if (strpos($method, 'subscribe_') === 0) { + trigger_error('subscribe_*() has been deprecated since SimplePie 1.3, implement the callback yourself', \E_USER_DEPRECATED); + return ''; + } + if ($method === 'enable_xml_dump') { + trigger_error('enable_xml_dump() has been deprecated since SimplePie 1.3, use get_raw_data() instead', \E_USER_DEPRECATED); + return false; + } + + $class = get_class($this); + $trace = debug_backtrace(); + $file = $trace[0]['file'] ?? ''; + $line = $trace[0]['line'] ?? ''; + throw new SimplePieException("Call to undefined method $class::$method() in $file on line $line"); + } + + /** + * Item factory + * + * @param array<string, mixed> $data + */ + private function make_item(array $data): Item + { + $item = $this->registry->create(Item::class, [$this, $data]); + $item->set_sanitize($this->sanitize); + + return $item; + } + + /** + * Sorting callback for items + * + * @access private + * @param Item $a + * @param Item $b + * @return -1|0|1 + */ + public static function sort_items(Item $a, Item $b) + { + $a_date = $a->get_date('U'); + $b_date = $b->get_date('U'); + if ($a_date && $b_date) { + return $a_date > $b_date ? -1 : 1; + } + // Sort items without dates to the top. + if ($a_date) { + return 1; + } + if ($b_date) { + return -1; + } + return 0; + } + + /** + * Merge items from several feeds into one + * + * If you're merging multiple feeds together, they need to all have dates + * for the items or else SimplePie will refuse to sort them. + * + * @link http://simplepie.org/wiki/tutorial/sort_multiple_feeds_by_time_and_date#if_feeds_require_separate_per-feed_settings + * @param array<SimplePie> $urls List of SimplePie feed objects to merge + * @param int $start Starting item + * @param int $end Number of items to return + * @param int $limit Maximum number of items per feed + * @return array<Item> + */ + public static function merge_items(array $urls, int $start = 0, int $end = 0, int $limit = 0) + { + if (count($urls) > 0) { + $items = []; + foreach ($urls as $arg) { + if ($arg instanceof SimplePie) { + $items = array_merge($items, $arg->get_items(0, $limit)); + + // @phpstan-ignore-next-line Enforce PHPDoc type. + } else { + trigger_error('Arguments must be SimplePie objects', E_USER_WARNING); + } + } + + usort($items, [get_class($urls[0]), 'sort_items']); + + if ($end === 0) { + return array_slice($items, $start); + } + + return array_slice($items, $start, $end); + } + + trigger_error('Cannot merge zero SimplePie objects', E_USER_WARNING); + return []; + } + + /** + * Store PubSubHubbub links as headers + * + * There is no way to find PuSH links in the body of a microformats feed, + * so they are added to the headers when found, to be used later by get_links. + */ + private function store_links(Response $file, ?string $hub, ?string $self): Response + { + $linkHeaderLine = $file->get_header_line('link'); + $linkHeader = $file->get_header('link'); + + if ($hub && !preg_match('/rel=hub/', $linkHeaderLine)) { + $linkHeader[] = '<'.$hub.'>; rel=hub'; + } + + if ($self && !preg_match('/rel=self/', $linkHeaderLine)) { + $linkHeader[] = '<'.$self.'>; rel=self'; + } + + if (count($linkHeader) > 0) { + $file = $file->with_header('link', $linkHeader); + } + + return $file; + } + + /** + * Get a DataCache + * + * @param string $feed_url Only needed for BC, can be removed in SimplePie 2.0.0 + * + * @return DataCache + */ + private function get_cache(string $feed_url = ''): DataCache + { + if ($this->cache === null) { + // @trigger_error(sprintf('Not providing as PSR-16 cache implementation is deprecated since SimplePie 1.8.0, please use "SimplePie\SimplePie::set_cache()".'), \E_USER_DEPRECATED); + $cache = $this->registry->call(Cache::class, 'get_handler', [ + $this->cache_location, + $this->get_cache_filename($feed_url), + Base::TYPE_FEED + ]); + + return new BaseDataCache($cache); + } + + return $this->cache; + } + + /** + * Get a HTTP client + */ + private function get_http_client(): Client + { + if ($this->http_client === null) { + $this->http_client = new FileClient( + $this->get_registry(), + [ + 'timeout' => $this->timeout, + 'redirects' => 5, + 'useragent' => $this->useragent, + 'force_fsockopen' => $this->force_fsockopen, + 'curl_options' => $this->curl_options, + ] + ); + $this->http_client_injected = true; + } + + return $this->http_client; + } +} + +class_alias('SimplePie\SimplePie', 'SimplePie'); + +} + +namespace SimplePie { + +/** + * Manages all author-related data + * + * Used by {@see Item::get_author()} and {@see SimplePie::get_authors()} + * + * This class can be overloaded with {@see SimplePie::set_author_class()} + */ +class Author +{ + /** + * Author's name + * + * @var ?string + * @see get_name() + */ + public $name; + + /** + * Author's link + * + * @var ?string + * @see get_link() + */ + public $link; + + /** + * Author's email address + * + * @var ?string + * @see get_email() + */ + public $email; + + /** + * Constructor, used to input the data + */ + public function __construct( + ?string $name = null, + ?string $link = null, + ?string $email = null + ) { + $this->name = $name; + $this->link = $link; + $this->email = $email; + } + + /** + * String-ified version + * + * @return string + */ + public function __toString() + { + // There is no $this->data here + return md5(serialize($this)); + } + + /** + * Author's name + * + * @return string|null + */ + public function get_name() + { + if ($this->name !== null) { + return $this->name; + } + + return null; + } + + /** + * Author's link + * + * @return string|null + */ + public function get_link() + { + if ($this->link !== null) { + return $this->link; + } + + return null; + } + + /** + * Author's email address + * + * @return string|null + */ + public function get_email() + { + if ($this->email !== null) { + return $this->email; + } + + return null; + } +} + +class_alias('SimplePie\Author', 'SimplePie_Author'); + +} + +namespace SimplePie { + +use SimplePie\Cache\Base; + +/** + * Used to create cache objects + * + * This class can be overloaded with {@see SimplePie::set_cache_class()}, + * although the preferred way is to create your own handler + * via {@see register()} + * + * @deprecated since SimplePie 1.8.0, use "SimplePie\SimplePie::set_cache()" instead + */ +class Cache +{ + /** + * Cache handler classes + * + * These receive 3 parameters to their constructor, as documented in + * {@see register()} + * @var array<string, class-string<Base>> + */ + protected static $handlers = [ + 'mysql' => Cache\MySQL::class, + 'memcache' => Cache\Memcache::class, + 'memcached' => Cache\Memcached::class, + 'redis' => Cache\Redis::class, + ]; + + /** + * Don't call the constructor. Please. + */ + private function __construct() + { + } + + /** + * Create a new SimplePie\Cache object + * + * @param string $location URL location (scheme is used to determine handler) + * @param string $filename Unique identifier for cache object + * @param Base::TYPE_FEED|Base::TYPE_IMAGE $extension 'spi' or 'spc' + * @return Base Type of object depends on scheme of `$location` + */ + public static function get_handler(string $location, string $filename, $extension) + { + $type = explode(':', $location, 2); + $type = $type[0]; + if (!empty(self::$handlers[$type])) { + $class = self::$handlers[$type]; + return new $class($location, $filename, $extension); + } + + return new \SimplePie\Cache\File($location, $filename, $extension); + } + + /** + * Create a new SimplePie\Cache object + * + * @deprecated since SimplePie 1.3.1, use {@see get_handler()} instead + * @param string $location + * @param string $filename + * @param Base::TYPE_FEED|Base::TYPE_IMAGE $extension + * @return Base + */ + public function create(string $location, string $filename, $extension) + { + trigger_error('Cache::create() has been replaced with Cache::get_handler() since SimplePie 1.3.1, use the registry system instead.', \E_USER_DEPRECATED); + + return self::get_handler($location, $filename, $extension); + } + + /** + * Register a handler + * + * @param string $type DSN type to register for + * @param class-string<Base> $class Name of handler class. Must implement Base + * @return void + */ + public static function register(string $type, $class) + { + self::$handlers[$type] = $class; + } + + /** + * Parse a URL into an array + * + * @param string $url + * @return array<string, mixed> + */ + public static function parse_URL(string $url) + { + $parsedUrl = parse_url($url); + + if ($parsedUrl === false) { + return []; + } + + $params = array_merge($parsedUrl, ['extras' => []]); + if (isset($params['query'])) { + parse_str($params['query'], $params['extras']); + } + return $params; + } +} + +class_alias('SimplePie\Cache', 'SimplePie_Cache'); + +} + +namespace SimplePie\Cache { + +/** + * Base for cache objects + * + * Classes to be used with {@see \SimplePie\Cache::register()} are expected + * to implement this interface. + * + * @deprecated since SimplePie 1.8.0, use "Psr\SimpleCache\CacheInterface" instead + */ +interface Base +{ + /** + * Feed cache type + * + * @var string + */ + public const TYPE_FEED = 'spc'; + + /** + * Image cache type + * + * @var string + */ + public const TYPE_IMAGE = 'spi'; + + /** + * Create a new cache object + * + * @param string $location Location string (from SimplePie::$cache_location) + * @param string $name Unique ID for the cache + * @param Base::TYPE_FEED|Base::TYPE_IMAGE $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data + */ + public function __construct(string $location, string $name, $type); + + /** + * Save data to the cache + * + * @param array<mixed>|\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @return bool Successfulness + */ + public function save($data); + + /** + * Retrieve the data saved to the cache + * + * @return array<mixed> Data for SimplePie::$data + */ + public function load(); + + /** + * Retrieve the last modified time for the cache + * + * @return int Timestamp + */ + public function mtime(); + + /** + * Set the last modified time to the current time + * + * @return bool Success status + */ + public function touch(); + + /** + * Remove the cache + * + * @return bool Success status + */ + public function unlink(); +} + +class_alias('SimplePie\Cache\Base', 'SimplePie_Cache_Base'); + +} + +namespace SimplePie\Cache { + +use InvalidArgumentException; + +/** + * Adapter for deprecated \SimplePie\Cache\Base implementations + * + * @internal + */ +final class BaseDataCache implements DataCache +{ + /** + * @var Base + */ + private $cache; + + public function __construct(Base $cache) + { + $this->cache = $cache; + } + + /** + * Fetches a value from the cache. + * + * Equivalent to \Psr\SimpleCache\CacheInterface::get() + * <code> + * public function get(string $key, mixed $default = null): mixed; + * </code> + * + * @param string $key The unique key of this item in the cache. + * @param mixed $default Default value to return if the key does not exist. + * + * @return array|mixed The value of the item from the cache, or $default in case of cache miss. + * + * @throws InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. + */ + public function get_data(string $key, $default = null) + { + $data = $this->cache->load(); + + if (!is_array($data)) { + return $default; + } + + // ignore data if internal cache expiration time is not set + if (!array_key_exists('__cache_expiration_time', $data)) { + return $default; + } + + // ignore data if internal cache expiration time is expired + if ($data['__cache_expiration_time'] < time()) { + return $default; + } + + // remove internal cache expiration time + unset($data['__cache_expiration_time']); + + return $data; + } + + /** + * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. + * + * Equivalent to \Psr\SimpleCache\CacheInterface::set() + * <code> + * public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool; + * </code> + * + * @param string $key The key of the item to store. + * @param array<mixed> $value The value of the item to store, must be serializable. + * @param null|int $ttl Optional. The TTL value of this item. If no value is sent and + * the driver supports TTL then the library may set a default value + * for it or let the driver take care of that. + * + * @return bool True on success and false on failure. + * + * @throws InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. + */ + public function set_data(string $key, array $value, ?int $ttl = null): bool + { + if ($ttl === null) { + $ttl = 3600; + } + + // place internal cache expiration time + $value['__cache_expiration_time'] = time() + $ttl; + + return $this->cache->save($value); + } + + /** + * Delete an item from the cache by its unique key. + * + * Equivalent to \Psr\SimpleCache\CacheInterface::delete() + * <code> + * public function delete(string $key): bool; + * </code> + * + * @param string $key The unique cache key of the item to delete. + * + * @return bool True if the item was successfully removed. False if there was an error. + * + * @throws InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. + */ + public function delete_data(string $key): bool + { + return $this->cache->unlink(); + } +} + +} + +namespace SimplePie\Cache { + +/** + * Creating a cache filename with callables + */ +final class CallableNameFilter implements NameFilter +{ + /** + * @var callable(string): string + */ + private $callable; + + /** + * @param callable(string): string $callable + */ + public function __construct(callable $callable) + { + $this->callable = $callable; + } + + /** + * Method to create cache filename with. + * + * The returning name MUST follow the rules for keys in PSR-16. + * + * @link https://www.php-fig.org/psr/psr-16/ + * + * The returning name MUST be a string of at least one character + * that uniquely identifies a cached item, MUST only contain the + * characters A-Z, a-z, 0-9, _, and . in any order in UTF-8 encoding + * and MUST not longer then 64 characters. The following characters + * are reserved for future extensions and MUST NOT be used: {}()/\@: + * + * A provided implementing library MAY support additional characters + * and encodings or longer lengths, but MUST support at least that + * minimum. + * + * @param string $name The name for the cache will be most likely an url with query string + * + * @return string the new cache name + */ + public function filter(string $name): string + { + return call_user_func($this->callable, $name); + } +} + +} + +namespace SimplePie\Cache { + +use SimplePie\Item; + +/** + * Base class for database-based caches + * + * @deprecated since SimplePie 1.8.0, use implementation of "Psr\SimpleCache\CacheInterface" instead + */ +abstract class DB implements Base +{ + /** + * Helper for database conversion + * + * Converts a given {@see SimplePie} object into data to be stored + * + * @param \SimplePie\SimplePie $data + * @return array{string, array<string, Item>} First item is the serialized data for storage, second item is the unique ID for this item + */ + protected static function prepare_simplepie_object_for_cache(\SimplePie\SimplePie $data) + { + $items = $data->get_items(); + $items_by_id = []; + + if (!empty($items)) { + foreach ($items as $item) { + $items_by_id[$item->get_id()] = $item; + } + + if (count($items_by_id) !== count($items)) { + $items_by_id = []; + foreach ($items as $item) { + $items_by_id[$item->get_id(true)] = $item; + } + } + + if (isset($data->data['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['feed'][0])) { + $channel = &$data->data['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['feed'][0]; + } elseif (isset($data->data['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['feed'][0])) { + $channel = &$data->data['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['feed'][0]; + } elseif (isset($data->data['child'][\SimplePie\SimplePie::NAMESPACE_RDF]['RDF'][0])) { + $channel = &$data->data['child'][\SimplePie\SimplePie::NAMESPACE_RDF]['RDF'][0]; + } elseif (isset($data->data['child'][\SimplePie\SimplePie::NAMESPACE_RSS_20]['rss'][0]['child'][\SimplePie\SimplePie::NAMESPACE_RSS_20]['channel'][0])) { + $channel = &$data->data['child'][\SimplePie\SimplePie::NAMESPACE_RSS_20]['rss'][0]['child'][\SimplePie\SimplePie::NAMESPACE_RSS_20]['channel'][0]; + } else { + $channel = null; + } + + if ($channel !== null) { + if (isset($channel['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['entry'])) { + unset($channel['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['entry']); + } + if (isset($channel['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['entry'])) { + unset($channel['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['entry']); + } + if (isset($channel['child'][\SimplePie\SimplePie::NAMESPACE_RSS_10]['item'])) { + unset($channel['child'][\SimplePie\SimplePie::NAMESPACE_RSS_10]['item']); + } + if (isset($channel['child'][\SimplePie\SimplePie::NAMESPACE_RSS_090]['item'])) { + unset($channel['child'][\SimplePie\SimplePie::NAMESPACE_RSS_090]['item']); + } + if (isset($channel['child'][\SimplePie\SimplePie::NAMESPACE_RSS_20]['item'])) { + unset($channel['child'][\SimplePie\SimplePie::NAMESPACE_RSS_20]['item']); + } + } + if (isset($data->data['items'])) { + unset($data->data['items']); + } + if (isset($data->data['ordered_items'])) { + unset($data->data['ordered_items']); + } + } + return [serialize($data->data), $items_by_id]; + } +} + +class_alias('SimplePie\Cache\DB', 'SimplePie_Cache_DB'); + +} + +namespace SimplePie\Cache { + +use InvalidArgumentException; + +/** + * Subset of PSR-16 Cache client for caching data arrays + * + * Only get(), set() and delete() methods are used, + * but not has(), getMultiple(), setMultiple() or deleteMultiple(). + * + * The methods names must be different, but should be compatible to the + * methods of \Psr\SimpleCache\CacheInterface. + * + * @internal + */ +interface DataCache +{ + /** + * Fetches a value from the cache. + * + * Equivalent to \Psr\SimpleCache\CacheInterface::get() + * <code> + * public function get(string $key, mixed $default = null): mixed; + * </code> + * + * @param string $key The unique key of this item in the cache. + * @param mixed $default Default value to return if the key does not exist. + * + * @return array|mixed The value of the item from the cache, or $default in case of cache miss. + * + * @throws InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. + */ + public function get_data(string $key, $default = null); + + /** + * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. + * + * Equivalent to \Psr\SimpleCache\CacheInterface::set() + * <code> + * public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool; + * </code> + * + * @param string $key The key of the item to store. + * @param array<mixed> $value The value of the item to store, must be serializable. + * @param null|int $ttl Optional. The TTL value of this item. If no value is sent and + * the driver supports TTL then the library may set a default value + * for it or let the driver take care of that. + * + * @return bool True on success and false on failure. + * + * @throws InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. + */ + public function set_data(string $key, array $value, ?int $ttl = null): bool; + + /** + * Delete an item from the cache by its unique key. + * + * Equivalent to \Psr\SimpleCache\CacheInterface::delete() + * <code> + * public function delete(string $key): bool; + * </code> + * + * @param string $key The unique cache key of the item to delete. + * + * @return bool True if the item was successfully removed. False if there was an error. + * + * @throws InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. + */ + public function delete_data(string $key): bool; +} + +} + +namespace SimplePie\Cache { + +/** + * Caches data to the filesystem + * + * @deprecated since SimplePie 1.8.0, use implementation of "Psr\SimpleCache\CacheInterface" instead + */ +class File implements Base +{ + /** + * Location string + * + * @see SimplePie::$cache_location + * @var string + */ + protected $location; + + /** + * Filename + * + * @var string + */ + protected $filename; + + /** + * File extension + * + * @var string + */ + protected $extension; + + /** + * File path + * + * @var string + */ + protected $name; + + /** + * Create a new cache object + * + * @param string $location Location string (from SimplePie::$cache_location) + * @param string $name Unique ID for the cache + * @param Base::TYPE_FEED|Base::TYPE_IMAGE $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data + */ + public function __construct(string $location, string $name, $type) + { + $this->location = $location; + $this->filename = $name; + $this->extension = $type; + $this->name = "$this->location/$this->filename.$this->extension"; + } + + /** + * Save data to the cache + * + * @param array<mixed>|\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @return bool Successfulness + */ + public function save($data) + { + if (file_exists($this->name) && is_writable($this->name) || file_exists($this->location) && is_writable($this->location)) { + if ($data instanceof \SimplePie\SimplePie) { + $data = $data->data; + } + + $data = serialize($data); + return (bool) file_put_contents($this->name, $data); + } + return false; + } + + /** + * Retrieve the data saved to the cache + * + * @return array<mixed>|false Data for SimplePie::$data + */ + public function load() + { + if (file_exists($this->name) && is_readable($this->name)) { + return unserialize((string) file_get_contents($this->name)); + } + return false; + } + + /** + * Retrieve the last modified time for the cache + * + * @return int|false Timestamp + */ + public function mtime() + { + return @filemtime($this->name); + } + + /** + * Set the last modified time to the current time + * + * @return bool Success status + */ + public function touch() + { + return @touch($this->name); + } + + /** + * Remove the cache + * + * @return bool Success status + */ + public function unlink() + { + if (file_exists($this->name)) { + return unlink($this->name); + } + return false; + } +} + +class_alias('SimplePie\Cache\File', 'SimplePie_Cache_File'); + +} + +namespace SimplePie\Cache { + +use Memcache as NativeMemcache; + +/** + * Caches data to memcache + * + * Registered for URLs with the "memcache" protocol + * + * For example, `memcache://localhost:11211/?timeout=3600&prefix=sp_` will + * connect to memcache on `localhost` on port 11211. All tables will be + * prefixed with `sp_` and data will expire after 3600 seconds + * + * @uses Memcache + * @deprecated since SimplePie 1.8.0, use implementation of "Psr\SimpleCache\CacheInterface" instead + */ +class Memcache implements Base +{ + /** + * Memcache instance + * + * @var NativeMemcache + */ + protected $cache; + + /** + * Options + * + * @var array<string, mixed> + */ + protected $options; + + /** + * Cache name + * + * @var string + */ + protected $name; + + /** + * Create a new cache object + * + * @param string $location Location string (from SimplePie::$cache_location) + * @param string $name Unique ID for the cache + * @param Base::TYPE_FEED|Base::TYPE_IMAGE $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data + */ + public function __construct(string $location, string $name, $type) + { + $this->options = [ + 'host' => '127.0.0.1', + 'port' => 11211, + 'extras' => [ + 'timeout' => 3600, // one hour + 'prefix' => 'simplepie_', + ], + ]; + $this->options = array_replace_recursive($this->options, \SimplePie\Cache::parse_URL($location)); + + $this->name = $this->options['extras']['prefix'] . md5("$name:$type"); + + $this->cache = new NativeMemcache(); + $this->cache->addServer($this->options['host'], (int) $this->options['port']); + } + + /** + * Save data to the cache + * + * @param array<mixed>|\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @return bool Successfulness + */ + public function save($data) + { + if ($data instanceof \SimplePie\SimplePie) { + $data = $data->data; + } + return $this->cache->set($this->name, serialize($data), MEMCACHE_COMPRESSED, (int) $this->options['extras']['timeout']); + } + + /** + * Retrieve the data saved to the cache + * + * @return array<mixed>|false Data for SimplePie::$data + */ + public function load() + { + $data = $this->cache->get($this->name); + + if ($data !== false) { + return unserialize($data); + } + return false; + } + + /** + * Retrieve the last modified time for the cache + * + * @return int|false Timestamp + */ + public function mtime() + { + $data = $this->cache->get($this->name); + + if ($data !== false) { + // essentially ignore the mtime because Memcache expires on its own + return time(); + } + + return false; + } + + /** + * Set the last modified time to the current time + * + * @return bool Success status + */ + public function touch() + { + $data = $this->cache->get($this->name); + + if ($data !== false) { + return $this->cache->set($this->name, $data, MEMCACHE_COMPRESSED, (int) $this->options['extras']['timeout']); + } + + return false; + } + + /** + * Remove the cache + * + * @return bool Success status + */ + public function unlink() + { + return $this->cache->delete($this->name, 0); + } +} + +class_alias('SimplePie\Cache\Memcache', 'SimplePie_Cache_Memcache'); + +} + +namespace SimplePie\Cache { + +use Memcached as NativeMemcached; + +/** + * Caches data to memcached + * + * Registered for URLs with the "memcached" protocol + * + * For example, `memcached://localhost:11211/?timeout=3600&prefix=sp_` will + * connect to memcached on `localhost` on port 11211. All tables will be + * prefixed with `sp_` and data will expire after 3600 seconds + * + * @uses Memcached + * @deprecated since SimplePie 1.8.0, use implementation of "Psr\SimpleCache\CacheInterface" instead + */ +class Memcached implements Base +{ + /** + * NativeMemcached instance + * @var NativeMemcached + */ + protected $cache; + + /** + * Options + * @var array<string, mixed> + */ + protected $options; + + /** + * Cache name + * @var string + */ + protected $name; + + /** + * Create a new cache object + * @param string $location Location string (from SimplePie::$cache_location) + * @param string $name Unique ID for the cache + * @param Base::TYPE_FEED|Base::TYPE_IMAGE $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data + */ + public function __construct(string $location, string $name, $type) + { + $this->options = [ + 'host' => '127.0.0.1', + 'port' => 11211, + 'extras' => [ + 'timeout' => 3600, // one hour + 'prefix' => 'simplepie_', + ], + ]; + $this->options = array_replace_recursive($this->options, \SimplePie\Cache::parse_URL($location)); + + $this->name = $this->options['extras']['prefix'] . md5("$name:$type"); + + $this->cache = new NativeMemcached(); + $this->cache->addServer($this->options['host'], (int)$this->options['port']); + } + + /** + * Save data to the cache + * @param array<mixed>|\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @return bool Successfulness + */ + public function save($data) + { + if ($data instanceof \SimplePie\SimplePie) { + $data = $data->data; + } + + return $this->setData(serialize($data)); + } + + /** + * Retrieve the data saved to the cache + * @return array<mixed>|false Data for SimplePie::$data + */ + public function load() + { + $data = $this->cache->get($this->name); + + if ($data !== false) { + return unserialize($data); + } + return false; + } + + /** + * Retrieve the last modified time for the cache + * @return int Timestamp + */ + public function mtime() + { + $data = $this->cache->get($this->name . '_mtime'); + return (int) $data; + } + + /** + * Set the last modified time to the current time + * @return bool Success status + */ + public function touch() + { + $data = $this->cache->get($this->name); + return $this->setData($data); + } + + /** + * Remove the cache + * @return bool Success status + */ + public function unlink() + { + return $this->cache->delete($this->name, 0); + } + + /** + * Set the last modified time and data to NativeMemcached + * @param string|false $data + * @return bool Success status + */ + private function setData($data): bool + { + if ($data !== false) { + $this->cache->set($this->name . '_mtime', time(), (int)$this->options['extras']['timeout']); + return $this->cache->set($this->name, $data, (int)$this->options['extras']['timeout']); + } + + return false; + } +} + +class_alias('SimplePie\Cache\Memcached', 'SimplePie_Cache_Memcached'); + +} + +namespace SimplePie\Cache { + +/** + * Caches data to a MySQL database + * + * Registered for URLs with the "mysql" protocol + * + * For example, `mysql://root:password@localhost:3306/mydb?prefix=sp_` will + * connect to the `mydb` database on `localhost` on port 3306, with the user + * `root` and the password `password`. All tables will be prefixed with `sp_` + * + * @deprecated since SimplePie 1.8.0, use implementation of "Psr\SimpleCache\CacheInterface" instead + */ +class MySQL extends DB +{ + /** + * PDO instance + * + * @var \PDO|null + */ + protected $mysql; + + /** + * Options + * + * @var array<string, mixed> + */ + protected $options; + + /** + * Cache ID + * + * @var string + */ + protected $id; + + /** + * Create a new cache object + * + * @param string $location Location string (from SimplePie::$cache_location) + * @param string $name Unique ID for the cache + * @param Base::TYPE_FEED|Base::TYPE_IMAGE $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data + */ + public function __construct(string $location, string $name, $type) + { + $this->options = [ + 'user' => null, + 'pass' => null, + 'host' => '127.0.0.1', + 'port' => '3306', + 'path' => '', + 'extras' => [ + 'prefix' => '', + 'cache_purge_time' => 2592000 + ], + ]; + + $this->options = array_replace_recursive($this->options, \SimplePie\Cache::parse_URL($location)); + + // Path is prefixed with a "/" + $this->options['dbname'] = substr($this->options['path'], 1); + + try { + $this->mysql = new \PDO("mysql:dbname={$this->options['dbname']};host={$this->options['host']};port={$this->options['port']}", $this->options['user'], $this->options['pass'], [\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8']); + } catch (\PDOException $e) { + $this->mysql = null; + return; + } + + $this->id = $name . $type; + + if (!$query = $this->mysql->query('SHOW TABLES')) { + $this->mysql = null; + return; + } + + $db = []; + while ($row = $query->fetchColumn()) { + $db[] = $row; + } + + if (!in_array($this->options['extras']['prefix'] . 'cache_data', $db)) { + $query = $this->mysql->exec('CREATE TABLE `' . $this->options['extras']['prefix'] . 'cache_data` (`id` TEXT CHARACTER SET utf8 NOT NULL, `items` SMALLINT NOT NULL DEFAULT 0, `data` BLOB NOT NULL, `mtime` INT UNSIGNED NOT NULL, UNIQUE (`id`(125)))'); + if ($query === false) { + trigger_error("Can't create " . $this->options['extras']['prefix'] . "cache_data table, check permissions", \E_USER_WARNING); + $this->mysql = null; + return; + } + } + + if (!in_array($this->options['extras']['prefix'] . 'items', $db)) { + $query = $this->mysql->exec('CREATE TABLE `' . $this->options['extras']['prefix'] . 'items` (`feed_id` TEXT CHARACTER SET utf8 NOT NULL, `id` TEXT CHARACTER SET utf8 NOT NULL, `data` MEDIUMBLOB NOT NULL, `posted` INT UNSIGNED NOT NULL, INDEX `feed_id` (`feed_id`(125)))'); + if ($query === false) { + trigger_error("Can't create " . $this->options['extras']['prefix'] . "items table, check permissions", \E_USER_WARNING); + $this->mysql = null; + return; + } + } + } + + /** + * Save data to the cache + * + * @param array<string>|\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @return bool Successfulness + */ + public function save($data) + { + if ($this->mysql === null) { + return false; + } + + $query = $this->mysql->prepare('DELETE i, cd FROM `' . $this->options['extras']['prefix'] . 'cache_data` cd, ' . + '`' . $this->options['extras']['prefix'] . 'items` i ' . + 'WHERE cd.id = i.feed_id ' . + 'AND cd.mtime < (unix_timestamp() - :purge_time)'); + $query->bindValue(':purge_time', $this->options['extras']['cache_purge_time']); + + if (!$query->execute()) { + return false; + } + + if ($data instanceof \SimplePie\SimplePie) { + $data = clone $data; + + $prepared = self::prepare_simplepie_object_for_cache($data); + + $query = $this->mysql->prepare('SELECT COUNT(*) FROM `' . $this->options['extras']['prefix'] . 'cache_data` WHERE `id` = :feed'); + $query->bindValue(':feed', $this->id); + if ($query->execute()) { + if ($query->fetchColumn() > 0) { + $items = count($prepared[1]); + if ($items) { + $sql = 'UPDATE `' . $this->options['extras']['prefix'] . 'cache_data` SET `items` = :items, `data` = :data, `mtime` = :time WHERE `id` = :feed'; + $query = $this->mysql->prepare($sql); + $query->bindValue(':items', $items); + } else { + $sql = 'UPDATE `' . $this->options['extras']['prefix'] . 'cache_data` SET `data` = :data, `mtime` = :time WHERE `id` = :feed'; + $query = $this->mysql->prepare($sql); + } + + $query->bindValue(':data', $prepared[0]); + $query->bindValue(':time', time()); + $query->bindValue(':feed', $this->id); + if (!$query->execute()) { + return false; + } + } else { + $query = $this->mysql->prepare('INSERT INTO `' . $this->options['extras']['prefix'] . 'cache_data` (`id`, `items`, `data`, `mtime`) VALUES(:feed, :count, :data, :time)'); + $query->bindValue(':feed', $this->id); + $query->bindValue(':count', count($prepared[1])); + $query->bindValue(':data', $prepared[0]); + $query->bindValue(':time', time()); + if (!$query->execute()) { + return false; + } + } + + $ids = array_keys($prepared[1]); + if (!empty($ids)) { + foreach ($ids as $id) { + $database_ids[] = $this->mysql->quote($id); + } + + $query = $this->mysql->prepare('SELECT `id` FROM `' . $this->options['extras']['prefix'] . 'items` WHERE `id` = ' . implode(' OR `id` = ', $database_ids) . ' AND `feed_id` = :feed'); + $query->bindValue(':feed', $this->id); + + if ($query->execute()) { + $existing_ids = []; + while ($row = $query->fetchColumn()) { + $existing_ids[] = $row; + } + + $new_ids = array_diff($ids, $existing_ids); + + foreach ($new_ids as $new_id) { + if (!($date = $prepared[1][$new_id]->get_date('U'))) { + $date = time(); + } + + $query = $this->mysql->prepare('INSERT INTO `' . $this->options['extras']['prefix'] . 'items` (`feed_id`, `id`, `data`, `posted`) VALUES(:feed, :id, :data, :date)'); + $query->bindValue(':feed', $this->id); + $query->bindValue(':id', $new_id); + $query->bindValue(':data', serialize($prepared[1][$new_id]->data)); + $query->bindValue(':date', $date); + if (!$query->execute()) { + return false; + } + } + return true; + } + } else { + return true; + } + } + } else { + $query = $this->mysql->prepare('SELECT `id` FROM `' . $this->options['extras']['prefix'] . 'cache_data` WHERE `id` = :feed'); + $query->bindValue(':feed', $this->id); + if ($query->execute()) { + if ($query->rowCount() > 0) { + $query = $this->mysql->prepare('UPDATE `' . $this->options['extras']['prefix'] . 'cache_data` SET `items` = 0, `data` = :data, `mtime` = :time WHERE `id` = :feed'); + $query->bindValue(':data', serialize($data)); + $query->bindValue(':time', time()); + $query->bindValue(':feed', $this->id); + if ($query->execute()) { + return true; + } + } else { + $query = $this->mysql->prepare('INSERT INTO `' . $this->options['extras']['prefix'] . 'cache_data` (`id`, `items`, `data`, `mtime`) VALUES(:id, 0, :data, :time)'); + $query->bindValue(':id', $this->id); + $query->bindValue(':data', serialize($data)); + $query->bindValue(':time', time()); + if ($query->execute()) { + return true; + } + } + } + } + return false; + } + + /** + * Retrieve the data saved to the cache + * + * @return array<string>|false Data for SimplePie::$data + */ + public function load() + { + if ($this->mysql === null) { + return false; + } + + $query = $this->mysql->prepare('SELECT `items`, `data` FROM `' . $this->options['extras']['prefix'] . 'cache_data` WHERE `id` = :id'); + $query->bindValue(':id', $this->id); + if ($query->execute() && ($row = $query->fetch())) { + $data = unserialize($row[1]); + + if (isset($this->options['items'][0])) { + $items = (int) $this->options['items'][0]; + } else { + $items = (int) $row[0]; + } + + if ($items !== 0) { + if (isset($data['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['feed'][0])) { + $feed = &$data['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['feed'][0]; + } elseif (isset($data['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['feed'][0])) { + $feed = &$data['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['feed'][0]; + } elseif (isset($data['child'][\SimplePie\SimplePie::NAMESPACE_RDF]['RDF'][0])) { + $feed = &$data['child'][\SimplePie\SimplePie::NAMESPACE_RDF]['RDF'][0]; + } elseif (isset($data['child'][\SimplePie\SimplePie::NAMESPACE_RSS_20]['rss'][0])) { + $feed = &$data['child'][\SimplePie\SimplePie::NAMESPACE_RSS_20]['rss'][0]; + } else { + $feed = null; + } + + if ($feed !== null) { + $sql = 'SELECT `data` FROM `' . $this->options['extras']['prefix'] . 'items` WHERE `feed_id` = :feed ORDER BY `posted` DESC'; + if ($items > 0) { + $sql .= ' LIMIT ' . $items; + } + + $query = $this->mysql->prepare($sql); + $query->bindValue(':feed', $this->id); + if ($query->execute()) { + while ($row = $query->fetchColumn()) { + $feed['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['entry'][] = unserialize((string) $row); + } + } else { + return false; + } + } + } + return $data; + } + return false; + } + + /** + * Retrieve the last modified time for the cache + * + * @return int|false Timestamp + */ + public function mtime() + { + if ($this->mysql === null) { + return false; + } + + $query = $this->mysql->prepare('SELECT `mtime` FROM `' . $this->options['extras']['prefix'] . 'cache_data` WHERE `id` = :id'); + $query->bindValue(':id', $this->id); + if ($query->execute() && ($time = $query->fetchColumn())) { + return (int) $time; + } + + return false; + } + + /** + * Set the last modified time to the current time + * + * @return bool Success status + */ + public function touch() + { + if ($this->mysql === null) { + return false; + } + + $query = $this->mysql->prepare('UPDATE `' . $this->options['extras']['prefix'] . 'cache_data` SET `mtime` = :time WHERE `id` = :id'); + $query->bindValue(':time', time()); + $query->bindValue(':id', $this->id); + + return $query->execute() && $query->rowCount() > 0; + } + + /** + * Remove the cache + * + * @return bool Success status + */ + public function unlink() + { + if ($this->mysql === null) { + return false; + } + + $query = $this->mysql->prepare('DELETE FROM `' . $this->options['extras']['prefix'] . 'cache_data` WHERE `id` = :id'); + $query->bindValue(':id', $this->id); + $query2 = $this->mysql->prepare('DELETE FROM `' . $this->options['extras']['prefix'] . 'items` WHERE `feed_id` = :id'); + $query2->bindValue(':id', $this->id); + + return $query->execute() && $query2->execute(); + } +} + +class_alias('SimplePie\Cache\MySQL', 'SimplePie_Cache_MySQL'); + +} + +namespace SimplePie\Cache { + +/** + * Interface for creating a cache filename + */ +interface NameFilter +{ + /** + * Method to create cache filename with. + * + * The returning name MUST follow the rules for keys in PSR-16. + * + * @link https://www.php-fig.org/psr/psr-16/ + * + * The returning name MUST be a string of at least one character + * that uniquely identifies a cached item, MUST only contain the + * characters A-Z, a-z, 0-9, _, and . in any order in UTF-8 encoding + * and MUST not longer then 64 characters. The following characters + * are reserved for future extensions and MUST NOT be used: {}()/\@: + * + * A provided implementing library MAY support additional characters + * and encodings or longer lengths, but MUST support at least that + * minimum. + * + * @param string $name The name for the cache will be most likely an url with query string + * + * @return string the new cache name + */ + public function filter(string $name): string; +} + +} + +namespace SimplePie\Cache { + +use Psr\SimpleCache\CacheInterface; +use Psr\SimpleCache\InvalidArgumentException; +use Throwable; + +/** + * Caches data into a PSR-16 cache implementation + * + * @internal + */ +final class Psr16 implements DataCache +{ + /** + * PSR-16 cache implementation + * + * @var CacheInterface + */ + private $cache; + + /** + * PSR-16 cache implementation + * + * @param CacheInterface $cache + */ + public function __construct(CacheInterface $cache) + { + $this->cache = $cache; + } + + /** + * Fetches a value from the cache. + * + * Equivalent to \Psr\SimpleCache\CacheInterface::get() + * <code> + * public function get(string $key, mixed $default = null): mixed; + * </code> + * + * @param string $key The unique key of this item in the cache. + * @param mixed $default Default value to return if the key does not exist. + * + * @return array|mixed The value of the item from the cache, or $default in case of cache miss. + * + * @throws InvalidArgumentException&Throwable + * MUST be thrown if the $key string is not a legal value. + */ + public function get_data(string $key, $default = null) + { + $data = $this->cache->get($key, $default); + + if (!is_array($data) || $data === $default) { + return $default; + } + + return $data; + } + + /** + * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. + * + * Equivalent to \Psr\SimpleCache\CacheInterface::set() + * <code> + * public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool; + * </code> + * + * @param string $key The key of the item to store. + * @param array<mixed> $value The value of the item to store, must be serializable. + * @param null|int $ttl Optional. The TTL value of this item. If no value is sent and + * the driver supports TTL then the library may set a default value + * for it or let the driver take care of that. + * + * @return bool True on success and false on failure. + * + * @throws InvalidArgumentException&Throwable + * MUST be thrown if the $key string is not a legal value. + */ + public function set_data(string $key, array $value, ?int $ttl = null): bool + { + return $this->cache->set($key, $value, $ttl); + } + + /** + * Delete an item from the cache by its unique key. + * + * Equivalent to \Psr\SimpleCache\CacheInterface::delete() + * <code> + * public function delete(string $key): bool; + * </code> + * + * @param string $key The unique cache key of the item to delete. + * + * @return bool True if the item was successfully removed. False if there was an error. + * + * @throws InvalidArgumentException&Throwable + * MUST be thrown if the $key string is not a legal value. + */ + public function delete_data(string $key): bool + { + return $this->cache->delete($key); + } +} + +} + +namespace SimplePie\Cache { + +use Redis as NativeRedis; + +/** + * Caches data to redis + * + * Registered for URLs with the "redis" protocol + * + * For example, `redis://localhost:6379/?timeout=3600&prefix=sp_&dbIndex=0` will + * connect to redis on `localhost` on port 6379. All tables will be + * prefixed with `simple_primary-` and data will expire after 3600 seconds + * + * @uses Redis + * @deprecated since SimplePie 1.8.0, use implementation of "Psr\SimpleCache\CacheInterface" instead + */ +class Redis implements Base +{ + /** + * Redis instance + * + * @var NativeRedis + */ + protected $cache; + + /** + * Options + * + * @var array<string, mixed> + */ + protected $options; + + /** + * Cache name + * + * @var string + */ + protected $name; + + /** + * Create a new cache object + * + * @param string $location Location string (from SimplePie::$cache_location) + * @param string $name Unique ID for the cache + * @param Base::TYPE_FEED|Base::TYPE_IMAGE|array<string, mixed>|null $options Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data + */ + public function __construct(string $location, string $name, $options = null) + { + //$this->cache = \flow\simple\cache\Redis::getRedisClientInstance(); + $parsed = \SimplePie\Cache::parse_URL($location); + $redis = new NativeRedis(); + $redis->connect($parsed['host'], $parsed['port']); + if (isset($parsed['pass'])) { + $redis->auth($parsed['pass']); + } + if (isset($parsed['path'])) { + $redis->select((int)substr($parsed['path'], 1)); + } + $this->cache = $redis; + + if (!is_null($options) && is_array($options)) { + $this->options = $options; + } else { + $this->options = [ + 'prefix' => 'rss:simple_primary:', + 'expire' => 0, + ]; + } + + $this->name = $this->options['prefix'] . $name; + } + + /** + * @param NativeRedis $cache + * @return void + */ + public function setRedisClient(NativeRedis $cache) + { + $this->cache = $cache; + } + + /** + * Save data to the cache + * + * @param array<mixed>|\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @return bool Successfulness + */ + public function save($data) + { + if ($data instanceof \SimplePie\SimplePie) { + $data = $data->data; + } + $response = $this->cache->set($this->name, serialize($data)); + if ($this->options['expire']) { + $this->cache->expire($this->name, $this->options['expire']); + } + + return $response; + } + + /** + * Retrieve the data saved to the cache + * + * @return array<mixed>|false Data for SimplePie::$data + */ + public function load() + { + $data = $this->cache->get($this->name); + + if ($data !== false) { + return unserialize($data); + } + return false; + } + + /** + * Retrieve the last modified time for the cache + * + * @return int|false Timestamp + */ + public function mtime() + { + $data = $this->cache->get($this->name); + + if ($data !== false) { + return time(); + } + + return false; + } + + /** + * Set the last modified time to the current time + * + * @return bool Success status + */ + public function touch() + { + $data = $this->cache->get($this->name); + + if ($data !== false) { + $return = $this->cache->set($this->name, $data); + if ($this->options['expire']) { + return $this->cache->expire($this->name, $this->options['expire']); + } + return $return; + } + + return false; + } + + /** + * Remove the cache + * + * @return bool Success status + */ + public function unlink() + { + return $this->cache->set($this->name, null); + } +} + +class_alias('SimplePie\Cache\Redis', 'SimplePie_Cache_Redis'); + +} + +namespace SimplePie { + +/** + * Handles `<media:text>` captions as defined in Media RSS. + * + * Used by {@see \SimplePie\Enclosure::get_caption()} and {@see \SimplePie\Enclosure::get_captions()} + * + * This class can be overloaded with {@see \SimplePie\SimplePie::set_caption_class()} + */ +class Caption +{ + /** + * Content type + * + * @var ?string + * @see get_type() + */ + public $type; + + /** + * Language + * + * @var ?string + * @see get_language() + */ + public $lang; + + /** + * Start time + * + * @var ?string + * @see get_starttime() + */ + public $startTime; + + /** + * End time + * + * @var ?string + * @see get_endtime() + */ + public $endTime; + + /** + * Caption text + * + * @var ?string + * @see get_text() + */ + public $text; + + /** + * Constructor, used to input the data + * + * For documentation on all the parameters, see the corresponding + * properties and their accessors + */ + public function __construct( + ?string $type = null, + ?string $lang = null, + ?string $startTime = null, + ?string $endTime = null, + ?string $text = null + ) { + $this->type = $type; + $this->lang = $lang; + $this->startTime = $startTime; + $this->endTime = $endTime; + $this->text = $text; + } + + /** + * String-ified version + * + * @return string + */ + public function __toString() + { + // There is no $this->data here + return md5(serialize($this)); + } + + /** + * Get the end time + * + * @return string|null Time in the format 'hh:mm:ss.SSS' + */ + public function get_endtime() + { + if ($this->endTime !== null) { + return $this->endTime; + } + + return null; + } + + /** + * Get the language + * + * @link http://tools.ietf.org/html/rfc3066 + * @return string|null Language code as per RFC 3066 + */ + public function get_language() + { + if ($this->lang !== null) { + return $this->lang; + } + + return null; + } + + /** + * Get the start time + * + * @return string|null Time in the format 'hh:mm:ss.SSS' + */ + public function get_starttime() + { + if ($this->startTime !== null) { + return $this->startTime; + } + + return null; + } + + /** + * Get the text of the caption + * + * @return string|null + */ + public function get_text() + { + if ($this->text !== null) { + return $this->text; + } + + return null; + } + + /** + * Get the content type (not MIME type) + * + * @return string|null Either 'text' or 'html' + */ + public function get_type() + { + if ($this->type !== null) { + return $this->type; + } + + return null; + } +} + +class_alias('SimplePie\Caption', 'SimplePie_Caption'); + +} + +namespace SimplePie { + +/** + * Manages all category-related data + * + * Used by {@see \SimplePie\Item::get_category()} and {@see \SimplePie\Item::get_categories()} + * + * This class can be overloaded with {@see \SimplePie\SimplePie::set_category_class()} + */ +class Category +{ + /** + * Category identifier + * + * @var string|null + * @see get_term + */ + public $term; + + /** + * Categorization scheme identifier + * + * @var string|null + * @see get_scheme() + */ + public $scheme; + + /** + * Human readable label + * + * @var string|null + * @see get_label() + */ + public $label; + + /** + * Category type + * + * category for <category> + * subject for <dc:subject> + * + * @var string|null + * @see get_type() + */ + public $type; + + /** + * Constructor, used to input the data + * + * @param string|null $term + * @param string|null $scheme + * @param string|null $label + * @param string|null $type + */ + public function __construct(?string $term = null, ?string $scheme = null, ?string $label = null, ?string $type = null) + { + $this->term = $term; + $this->scheme = $scheme; + $this->label = $label; + $this->type = $type; + } + + /** + * String-ified version + * + * @return string + */ + public function __toString() + { + // There is no $this->data here + return md5(serialize($this)); + } + + /** + * Get the category identifier + * + * @return string|null + */ + public function get_term() + { + return $this->term; + } + + /** + * Get the categorization scheme identifier + * + * @return string|null + */ + public function get_scheme() + { + return $this->scheme; + } + + /** + * Get the human readable label + * + * @param bool $strict + * @return string|null + */ + public function get_label(bool $strict = false) + { + if ($this->label === null && $strict !== true) { + return $this->get_term(); + } + return $this->label; + } + + /** + * Get the category type + * + * @return string|null + */ + public function get_type() + { + return $this->type; + } +} + +class_alias('SimplePie\Category', 'SimplePie_Category'); + +} + +namespace SimplePie\Content\Type { + +use InvalidArgumentException; +use SimplePie\File; +use SimplePie\HTTP\Response; + +/** + * Content-type sniffing + * + * Based on the rules in http://tools.ietf.org/html/draft-abarth-mime-sniff-06 + * + * This is used since we can't always trust Content-Type headers, and is based + * upon the HTML5 parsing rules. + * + * + * This class can be overloaded with {@see \SimplePie\SimplePie::set_content_type_sniffer_class()} + */ +class Sniffer +{ + /** + * File object + * + * @var File|Response + */ + public $file; + + /** + * Create an instance of the class with the input file + * + * @param File|Response $file Input file + */ + public function __construct(/* File */ $file) + { + if (!is_object($file) || !$file instanceof Response) { + // For BC we're asking for `File`, but internally we accept every `Response` implementation + throw new InvalidArgumentException(sprintf( + '%s(): Argument #1 ($file) must be of type %s', + __METHOD__, + File::class + ), 1); + } + + $this->file = $file; + } + + /** + * Get the Content-Type of the specified file + * + * @return string Actual Content-Type + */ + public function get_type() + { + $content_type = $this->file->has_header('content-type') ? $this->file->get_header_line('content-type') : null; + $content_encoding = $this->file->has_header('content-encoding') ? $this->file->get_header_line('content-encoding') : null; + if ($content_type !== null) { + if ($content_encoding === null + && ($content_type === 'text/plain' + || $content_type === 'text/plain; charset=ISO-8859-1' + || $content_type === 'text/plain; charset=iso-8859-1' + || $content_type === 'text/plain; charset=UTF-8')) { + return $this->text_or_binary(); + } + + if (($pos = strpos($content_type, ';')) !== false) { + $official = substr($content_type, 0, $pos); + } else { + $official = $content_type; + } + $official = trim(strtolower($official)); + + if ($official === 'unknown/unknown' + || $official === 'application/unknown') { + return $this->unknown(); + } elseif (substr($official, -4) === '+xml' + || $official === 'text/xml' + || $official === 'application/xml') { + return $official; + } elseif (substr($official, 0, 6) === 'image/') { + if ($return = $this->image()) { + return $return; + } + + return $official; + } elseif ($official === 'text/html') { + return $this->feed_or_html(); + } + + return $official; + } + + return $this->unknown(); + } + + /** + * Sniff text or binary + * + * @return string Actual Content-Type + */ + public function text_or_binary() + { + $body = $this->file->get_body_content(); + + if (substr($body, 0, 2) === "\xFE\xFF" + || substr($body, 0, 2) === "\xFF\xFE" + || substr($body, 0, 4) === "\x00\x00\xFE\xFF" + || substr($body, 0, 3) === "\xEF\xBB\xBF") { + return 'text/plain'; + } elseif (preg_match('/[\x00-\x08\x0E-\x1A\x1C-\x1F]/', $body)) { + return 'application/octet-stream'; + } + + return 'text/plain'; + } + + /** + * Sniff unknown + * + * @return string Actual Content-Type + */ + public function unknown() + { + $body = $this->file->get_body_content(); + + $ws = strspn($body, "\x09\x0A\x0B\x0C\x0D\x20"); + if (strtolower(substr($body, $ws, 14)) === '<!doctype html' + || strtolower(substr($body, $ws, 5)) === '<html' + || strtolower(substr($body, $ws, 7)) === '<script') { + return 'text/html'; + } elseif (substr($body, 0, 5) === '%PDF-') { + return 'application/pdf'; + } elseif (substr($body, 0, 11) === '%!PS-Adobe-') { + return 'application/postscript'; + } elseif (substr($body, 0, 6) === 'GIF87a' + || substr($body, 0, 6) === 'GIF89a') { + return 'image/gif'; + } elseif (substr($body, 0, 8) === "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A") { + return 'image/png'; + } elseif (substr($body, 0, 3) === "\xFF\xD8\xFF") { + return 'image/jpeg'; + } elseif (substr($body, 0, 2) === "\x42\x4D") { + return 'image/bmp'; + } elseif (substr($body, 0, 4) === "\x00\x00\x01\x00") { + return 'image/vnd.microsoft.icon'; + } + + return $this->text_or_binary(); + } + + /** + * Sniff images + * + * @return string|false Actual Content-Type + */ + public function image() + { + $body = $this->file->get_body_content(); + + if (substr($body, 0, 6) === 'GIF87a' + || substr($body, 0, 6) === 'GIF89a') { + return 'image/gif'; + } elseif (substr($body, 0, 8) === "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A") { + return 'image/png'; + } elseif (substr($body, 0, 3) === "\xFF\xD8\xFF") { + return 'image/jpeg'; + } elseif (substr($body, 0, 2) === "\x42\x4D") { + return 'image/bmp'; + } elseif (substr($body, 0, 4) === "\x00\x00\x01\x00") { + return 'image/vnd.microsoft.icon'; + } + + return false; + } + + /** + * Sniff HTML + * + * @return string Actual Content-Type + */ + public function feed_or_html() + { + $body = $this->file->get_body_content(); + + $len = strlen($body); + $pos = strspn($body, "\x09\x0A\x0D\x20\xEF\xBB\xBF"); + + while ($pos < $len) { + switch ($body[$pos]) { + case "\x09": + case "\x0A": + case "\x0D": + case "\x20": + $pos += strspn($body, "\x09\x0A\x0D\x20", $pos); + continue 2; + + case '<': + $pos++; + break; + + default: + return 'text/html'; + } + + if (substr($body, $pos, 3) === '!--') { + $pos += 3; + if ($pos < $len && ($pos = strpos($body, '-->', $pos)) !== false) { + $pos += 3; + } else { + return 'text/html'; + } + } elseif (substr($body, $pos, 1) === '!') { + if ($pos < $len && ($pos = strpos($body, '>', $pos)) !== false) { + $pos++; + } else { + return 'text/html'; + } + } elseif (substr($body, $pos, 1) === '?') { + if ($pos < $len && ($pos = strpos($body, '?>', $pos)) !== false) { + $pos += 2; + } else { + return 'text/html'; + } + } elseif (substr($body, $pos, 3) === 'rss' + || substr($body, $pos, 7) === 'rdf:RDF') { + return 'application/rss+xml'; + } elseif (substr($body, $pos, 4) === 'feed') { + return 'application/atom+xml'; + } else { + return 'text/html'; + } + } + + return 'text/html'; + } +} + +class_alias('SimplePie\Content\Type\Sniffer', 'SimplePie_Content_Type_Sniffer'); + +} + +namespace SimplePie { + +/** + * Manages `<media:copyright>` copyright tags as defined in Media RSS + * + * Used by {@see \SimplePie\Enclosure::get_copyright()} + * + * This class can be overloaded with {@see \SimplePie\SimplePie::set_copyright_class()} + */ +class Copyright +{ + /** + * Copyright URL + * + * @var ?string + * @see get_url() + */ + public $url; + + /** + * Attribution + * + * @var ?string + * @see get_attribution() + */ + public $label; + + /** + * Constructor, used to input the data + * + * For documentation on all the parameters, see the corresponding + * properties and their accessors + */ + public function __construct( + ?string $url = null, + ?string $label = null + ) { + $this->url = $url; + $this->label = $label; + } + + /** + * String-ified version + * + * @return string + */ + public function __toString() + { + // There is no $this->data here + return md5(serialize($this)); + } + + /** + * Get the copyright URL + * + * @return string|null URL to copyright information + */ + public function get_url() + { + if ($this->url !== null) { + return $this->url; + } + + return null; + } + + /** + * Get the attribution text + * + * @return string|null + */ + public function get_attribution() + { + if ($this->label !== null) { + return $this->label; + } + + return null; + } +} + +class_alias('SimplePie\Copyright', 'SimplePie_Copyright'); + +} + +namespace SimplePie { + +/** + * Handles `<media:credit>` as defined in Media RSS + * + * Used by {@see \SimplePie\Enclosure::get_credit()} and {@see \SimplePie\Enclosure::get_credits()} + * + * This class can be overloaded with {@see \SimplePie\SimplePie::set_credit_class()} + */ +class Credit +{ + /** + * Credited role + * + * @var ?string + * @see get_role() + */ + public $role; + + /** + * Organizational scheme + * + * @var ?string + * @see get_scheme() + */ + public $scheme; + + /** + * Credited name + * + * @var ?string + * @see get_name() + */ + public $name; + + /** + * Constructor, used to input the data + * + * For documentation on all the parameters, see the corresponding + * properties and their accessors + */ + public function __construct( + ?string $role = null, + ?string $scheme = null, + ?string $name = null + ) { + $this->role = $role; + $this->scheme = $scheme; + $this->name = $name; + } + + /** + * String-ified version + * + * @return string + */ + public function __toString() + { + // There is no $this->data here + return md5(serialize($this)); + } + + /** + * Get the role of the person receiving credit + * + * @return string|null + */ + public function get_role() + { + if ($this->role !== null) { + return $this->role; + } + + return null; + } + + /** + * Get the organizational scheme + * + * @return string|null + */ + public function get_scheme() + { + if ($this->scheme !== null) { + return $this->scheme; + } + + return null; + } + + /** + * Get the credited person/entity's name + * + * @return string|null + */ + public function get_name() + { + if ($this->name !== null) { + return $this->name; + } + + return null; + } +} + +class_alias('SimplePie\Credit', 'SimplePie_Credit'); + +} + +namespace SimplePie { + +/** + * Handles everything related to enclosures (including Media RSS and iTunes RSS) + * + * Used by {@see \SimplePie\Item::get_enclosure()} and {@see \SimplePie\Item::get_enclosures()} + * + * This class can be overloaded with {@see \SimplePie\SimplePie::set_enclosure_class()} + */ +class Enclosure +{ + /** + * @var ?string + * @see get_bitrate() + */ + public $bitrate; + + /** + * @var Caption[]|null + * @see get_captions() + */ + public $captions; + + /** + * @var Category[]|null + * @see get_categories() + */ + public $categories; + + /** + * @var ?int + * @see get_channels() + */ + public $channels; + + /** + * @var ?Copyright + * @see get_copyright() + */ + public $copyright; + + /** + * @var Credit[]|null + * @see get_credits() + */ + public $credits; + + /** + * @var ?string + * @see get_description() + */ + public $description; + + /** + * @var ?int + * @see get_duration() + */ + public $duration; + + /** + * @var ?string + * @see get_expression() + */ + public $expression; + + /** + * @var ?string + * @see get_framerate() + */ + public $framerate; + + /** + * @var ?string + * @see get_handler() + */ + public $handler; + + /** + * @var string[]|null + * @see get_hashes() + */ + public $hashes; + + /** + * @var ?string + * @see get_height() + */ + public $height; + + /** + * @deprecated + * @var null + */ + public $javascript; + + /** + * @var string[]|null + * @see get_keywords() + */ + public $keywords; + + /** + * @var ?string + * @see get_language() + */ + public $lang; + + /** + * @var ?int + * @see get_length() + */ + public $length; + + /** + * @var ?string + * @see get_link() + */ + public $link; + + /** + * @var ?string + * @see get_medium() + */ + public $medium; + + /** + * @var ?string + * @see get_player() + */ + public $player; + + /** + * @var Rating[]|null + * @see get_ratings() + */ + public $ratings; + + /** + * @var ?Restriction[] + * @see get_restrictions() + */ + public $restrictions; + + /** + * @var ?string + * @see get_sampling_rate() + */ + public $samplingrate; + + /** + * @var string[]|null + * @see get_thumbnails() + */ + public $thumbnails; + + /** + * @var ?string + * @see get_title() + */ + public $title; + + /** + * @var ?string + * @see get_type() + */ + public $type; + + /** + * @var ?string + * @see get_width() + */ + public $width; + + /** + * Constructor, used to input the data + * + * For documentation on all the parameters, see the corresponding + * properties and their accessors + * + * @uses idn_to_ascii If available, this will convert an IDN + * + * @param null $javascript + * @param Caption[]|null $captions + * @param Category[]|null $categories + * @param Credit[]|null $credits + * @param string[]|null $hashes + * @param string[]|null $keywords + * @param Rating[]|null $ratings + * @param Restriction[]|null $restrictions + * @param string[]|null $thumbnails + */ + public function __construct( + ?string $link = null, + ?string $type = null, + ?int $length = null, + $javascript = null, + ?string $bitrate = null, + ?array $captions = null, + ?array $categories = null, + ?int $channels = null, + ?Copyright $copyright = null, + ?array $credits = null, + ?string $description = null, + ?int $duration = null, + ?string $expression = null, + ?string $framerate = null, + ?array $hashes = null, + ?string $height = null, + ?array $keywords = null, + ?string $lang = null, + ?string $medium = null, + ?string $player = null, + ?array $ratings = null, + ?array $restrictions = null, + ?string $samplingrate = null, + ?array $thumbnails = null, + ?string $title = null, + ?string $width = null + ) { + $this->bitrate = $bitrate; + $this->captions = $captions; + $this->categories = $categories; + $this->channels = $channels; + $this->copyright = $copyright; + $this->credits = $credits; + $this->description = $description; + $this->duration = $duration; + $this->expression = $expression; + $this->framerate = $framerate; + $this->hashes = $hashes; + $this->height = $height; + $this->keywords = $keywords; + $this->lang = $lang; + $this->length = $length; + $this->link = $link; + $this->medium = $medium; + $this->player = $player; + $this->ratings = $ratings; + $this->restrictions = $restrictions; + $this->samplingrate = $samplingrate; + $this->thumbnails = $thumbnails; + $this->title = $title; + $this->type = $type; + $this->width = $width; + + if (function_exists('idn_to_ascii')) { + $parsed = \SimplePie\Misc::parse_url($link ?? ''); + if ($parsed['authority'] !== '' && !ctype_print($parsed['authority'])) { + $authority = (string) \idn_to_ascii($parsed['authority'], \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46); + $this->link = \SimplePie\Misc::compress_parse_url($parsed['scheme'], $authority, $parsed['path'], $parsed['query'], $parsed['fragment']); + } + } + $this->handler = $this->get_handler(); // Needs to load last + } + + /** + * String-ified version + * + * @return string + */ + public function __toString() + { + // There is no $this->data here + return md5(serialize($this)); + } + + /** + * Get the bitrate + * + * @return string|null + */ + public function get_bitrate() + { + if ($this->bitrate !== null) { + return $this->bitrate; + } + + return null; + } + + /** + * Get a single caption + * + * @param int $key + * @return \SimplePie\Caption|null + */ + public function get_caption(int $key = 0) + { + $captions = $this->get_captions(); + if (isset($captions[$key])) { + return $captions[$key]; + } + + return null; + } + + /** + * Get all captions + * + * @return Caption[]|null + */ + public function get_captions() + { + if ($this->captions !== null) { + return $this->captions; + } + + return null; + } + + /** + * Get a single category + * + * @param int $key + * @return \SimplePie\Category|null + */ + public function get_category(int $key = 0) + { + $categories = $this->get_categories(); + if (isset($categories[$key])) { + return $categories[$key]; + } + + return null; + } + + /** + * Get all categories + * + * @return \SimplePie\Category[]|null + */ + public function get_categories() + { + if ($this->categories !== null) { + return $this->categories; + } + + return null; + } + + /** + * Get the number of audio channels + * + * @return int|null + */ + public function get_channels() + { + if ($this->channels !== null) { + return $this->channels; + } + + return null; + } + + /** + * Get the copyright information + * + * @return \SimplePie\Copyright|null + */ + public function get_copyright() + { + if ($this->copyright !== null) { + return $this->copyright; + } + + return null; + } + + /** + * Get a single credit + * + * @param int $key + * @return \SimplePie\Credit|null + */ + public function get_credit(int $key = 0) + { + $credits = $this->get_credits(); + if (isset($credits[$key])) { + return $credits[$key]; + } + + return null; + } + + /** + * Get all credits + * + * @return Credit[]|null + */ + public function get_credits() + { + if ($this->credits !== null) { + return $this->credits; + } + + return null; + } + + /** + * Get the description of the enclosure + * + * @return string|null + */ + public function get_description() + { + if ($this->description !== null) { + return $this->description; + } + + return null; + } + + /** + * Get the duration of the enclosure + * + * @param bool $convert Convert seconds into hh:mm:ss + * @return string|int|null 'hh:mm:ss' string if `$convert` was specified, otherwise integer (or null if none found) + */ + public function get_duration(bool $convert = false) + { + if ($this->duration !== null) { + if ($convert) { + $time = \SimplePie\Misc::time_hms($this->duration); + return $time; + } + + return $this->duration; + } + + return null; + } + + /** + * Get the expression + * + * @return string Probably one of 'sample', 'full', 'nonstop', 'clip'. Defaults to 'full' + */ + public function get_expression() + { + if ($this->expression !== null) { + return $this->expression; + } + + return 'full'; + } + + /** + * Get the file extension + * + * @return string|null + */ + public function get_extension() + { + if ($this->link !== null) { + $url = \SimplePie\Misc::parse_url($this->link); + if ($url['path'] !== '') { + return pathinfo($url['path'], PATHINFO_EXTENSION); + } + } + return null; + } + + /** + * Get the framerate (in frames-per-second) + * + * @return string|null + */ + public function get_framerate() + { + if ($this->framerate !== null) { + return $this->framerate; + } + + return null; + } + + /** + * Get the preferred handler + * + * @return string|null One of 'flash', 'fmedia', 'quicktime', 'wmedia', 'mp3' + */ + public function get_handler() + { + return $this->get_real_type(true); + } + + /** + * Get a single hash + * + * @link http://www.rssboard.org/media-rss#media-hash + * @param int $key + * @return string|null Hash as per `media:hash`, prefixed with "$algo:" + */ + public function get_hash(int $key = 0) + { + $hashes = $this->get_hashes(); + if (isset($hashes[$key])) { + return $hashes[$key]; + } + + return null; + } + + /** + * Get all credits + * + * @return string[]|null Array of strings, see {@see get_hash()} + */ + public function get_hashes() + { + if ($this->hashes !== null) { + return $this->hashes; + } + + return null; + } + + /** + * Get the height + * + * @return string|null + */ + public function get_height() + { + if ($this->height !== null) { + return $this->height; + } + + return null; + } + + /** + * Get the language + * + * @link http://tools.ietf.org/html/rfc3066 + * @return string|null Language code as per RFC 3066 + */ + public function get_language() + { + if ($this->lang !== null) { + return $this->lang; + } + + return null; + } + + /** + * Get a single keyword + * + * @param int $key + * @return string|null + */ + public function get_keyword(int $key = 0) + { + $keywords = $this->get_keywords(); + if (isset($keywords[$key])) { + return $keywords[$key]; + } + + return null; + } + + /** + * Get all keywords + * + * @return string[]|null + */ + public function get_keywords() + { + if ($this->keywords !== null) { + return $this->keywords; + } + + return null; + } + + /** + * Get length + * + * @return ?int Length in bytes + */ + public function get_length() + { + if ($this->length !== null) { + return $this->length; + } + + return null; + } + + /** + * Get the URL + * + * @return string|null + */ + public function get_link() + { + if ($this->link !== null) { + return $this->link; + } + + return null; + } + + /** + * Get the medium + * + * @link http://www.rssboard.org/media-rss#media-content + * @return string|null Should be one of 'image', 'audio', 'video', 'document', 'executable' + */ + public function get_medium() + { + if ($this->medium !== null) { + return $this->medium; + } + + return null; + } + + /** + * Get the player URL + * + * Typically the same as {@see get_permalink()} + * @return string|null Player URL + */ + public function get_player() + { + if ($this->player !== null) { + return $this->player; + } + + return null; + } + + /** + * Get a single rating + * + * @param int $key + * @return \SimplePie\Rating|null + */ + public function get_rating(int $key = 0) + { + $ratings = $this->get_ratings(); + if (isset($ratings[$key])) { + return $ratings[$key]; + } + + return null; + } + + /** + * Get all ratings + * + * @return Rating[]|null + */ + public function get_ratings() + { + if ($this->ratings !== null) { + return $this->ratings; + } + + return null; + } + + /** + * Get a single restriction + * + * @param int $key + * @return \SimplePie\Restriction|null + */ + public function get_restriction(int $key = 0) + { + $restrictions = $this->get_restrictions(); + if (isset($restrictions[$key])) { + return $restrictions[$key]; + } + + return null; + } + + /** + * Get all restrictions + * + * @return Restriction[]|null + */ + public function get_restrictions() + { + if ($this->restrictions !== null) { + return $this->restrictions; + } + + return null; + } + + /** + * Get the sampling rate (in kHz) + * + * @return string|null + */ + public function get_sampling_rate() + { + if ($this->samplingrate !== null) { + return $this->samplingrate; + } + + return null; + } + + /** + * Get the file size (in MiB) + * + * @return float|null File size in mebibytes (1048 bytes) + */ + public function get_size() + { + $length = $this->get_length(); + if ($length !== null) { + return round($length / 1048576, 2); + } + + return null; + } + + /** + * Get a single thumbnail + * + * @param int $key + * @return string|null Thumbnail URL + */ + public function get_thumbnail(int $key = 0) + { + $thumbnails = $this->get_thumbnails(); + if (isset($thumbnails[$key])) { + return $thumbnails[$key]; + } + + return null; + } + + /** + * Get all thumbnails + * + * @return string[]|null Array of thumbnail URLs + */ + public function get_thumbnails() + { + if ($this->thumbnails !== null) { + return $this->thumbnails; + } + + return null; + } + + /** + * Get the title + * + * @return string|null + */ + public function get_title() + { + if ($this->title !== null) { + return $this->title; + } + + return null; + } + + /** + * Get mimetype of the enclosure + * + * @see get_real_type() + * @return string|null MIME type + */ + public function get_type() + { + if ($this->type !== null) { + return $this->type; + } + + return null; + } + + /** + * Get the width + * + * @return string|null + */ + public function get_width() + { + if ($this->width !== null) { + return $this->width; + } + + return null; + } + + /** + * Embed the enclosure using `<embed>` + * + * @deprecated Use the second parameter to {@see embed} instead + * + * @param array<string, mixed>|string $options See first parameter to {@see embed} + * @return string HTML string to output + */ + public function native_embed($options = '') + { + return $this->embed($options, true); + } + + /** + * Embed the enclosure using Javascript + * + * `$options` is an array or comma-separated key:value string, with the + * following properties: + * + * - `alt` (string): Alternate content for when an end-user does not have + * the appropriate handler installed or when a file type is + * unsupported. Can be any text or HTML. Defaults to blank. + * - `altclass` (string): If a file type is unsupported, the end-user will + * see the alt text (above) linked directly to the content. That link + * will have this value as its class name. Defaults to blank. + * - `audio` (string): This is an image that should be used as a + * placeholder for audio files before they're loaded (QuickTime-only). + * Can be any relative or absolute URL. Defaults to blank. + * - `bgcolor` (string): The background color for the media, if not + * already transparent. Defaults to `#ffffff`. + * - `height` (integer): The height of the embedded media. Accepts any + * numeric pixel value (such as `360`) or `auto`. Defaults to `auto`, + * and it is recommended that you use this default. + * - `loop` (boolean): Do you want the media to loop when it's done? + * Defaults to `false`. + * - `mediaplayer` (string): The location of the included + * `mediaplayer.swf` file. This allows for the playback of Flash Video + * (`.flv`) files, and is the default handler for non-Odeo MP3's. + * Defaults to blank. + * - `video` (string): This is an image that should be used as a + * placeholder for video files before they're loaded (QuickTime-only). + * Can be any relative or absolute URL. Defaults to blank. + * - `width` (integer): The width of the embedded media. Accepts any + * numeric pixel value (such as `480`) or `auto`. Defaults to `auto`, + * and it is recommended that you use this default. + * - `widescreen` (boolean): Is the enclosure widescreen or standard? + * This applies only to video enclosures, and will automatically resize + * the content appropriately. Defaults to `false`, implying 4:3 mode. + * + * Note: Non-widescreen (4:3) mode with `width` and `height` set to `auto` + * will default to 480x360 video resolution. Widescreen (16:9) mode with + * `width` and `height` set to `auto` will default to 480x270 video resolution. + * + * @todo If the dimensions for media:content are defined, use them when width/height are set to 'auto'. + * @param array<string, mixed>|string $options Comma-separated key:value list, or array + * @param bool $native Use `<embed>` + * @return string HTML string to output + */ + public function embed($options = '', bool $native = false) + { + // Set up defaults + $audio = ''; + $video = ''; + $alt = ''; + $altclass = ''; + $loop = 'false'; + $width = 'auto'; + $height = 'auto'; + $bgcolor = '#ffffff'; + $mediaplayer = ''; + $widescreen = false; + $handler = $this->get_handler(); + $type = $this->get_real_type(); + $placeholder = ''; + + // Process options and reassign values as necessary + if (is_array($options)) { + extract($options); + } else { + $options = explode(',', $options); + foreach ($options as $option) { + $opt = explode(':', $option, 2); + if (isset($opt[0], $opt[1])) { + $opt[0] = trim($opt[0]); + $opt[1] = trim($opt[1]); + switch ($opt[0]) { + case 'audio': + $audio = $opt[1]; + break; + + case 'video': + $video = $opt[1]; + break; + + case 'alt': + $alt = $opt[1]; + break; + + case 'altclass': + $altclass = $opt[1]; + break; + + case 'loop': + $loop = $opt[1]; + break; + + case 'width': + $width = $opt[1]; + break; + + case 'height': + $height = $opt[1]; + break; + + case 'bgcolor': + $bgcolor = $opt[1]; + break; + + case 'mediaplayer': + $mediaplayer = $opt[1]; + break; + + case 'widescreen': + $widescreen = $opt[1]; + break; + } + } + } + } + + $mime = explode('/', (string) $type, 2); + $mime = $mime[0]; + + // Process values for 'auto' + if ($width === 'auto') { + if ($mime === 'video') { + if ($height === 'auto') { + $width = 480; + } elseif ($widescreen) { + $width = round((intval($height) / 9) * 16); + } else { + $width = round((intval($height) / 3) * 4); + } + } else { + $width = '100%'; + } + } + + if ($height === 'auto') { + if ($mime === 'audio') { + $height = 0; + } elseif ($mime === 'video') { + if ($width === 'auto') { + if ($widescreen) { + $height = 270; + } else { + $height = 360; + } + } elseif ($widescreen) { + $height = round((intval($width) / 16) * 9); + } else { + $height = round((intval($width) / 4) * 3); + } + } else { + $height = 376; + } + } elseif ($mime === 'audio') { + $height = 0; + } + + // Set proper placeholder value + if ($mime === 'audio') { + $placeholder = $audio; + } elseif ($mime === 'video') { + $placeholder = $video; + } + + $embed = ''; + + // Flash + if ($handler === 'flash') { + if ($native) { + $embed .= "<embed src=\"" . $this->get_link() . "\" pluginspage=\"http://adobe.com/go/getflashplayer\" type=\"$type\" quality=\"high\" width=\"$width\" height=\"$height\" bgcolor=\"$bgcolor\" loop=\"$loop\"></embed>"; + } else { + $embed .= "<script type='text/javascript'>embed_flash('$bgcolor', '$width', '$height', '" . $this->get_link() . "', '$loop', '$type');</script>"; + } + } + + // Flash Media Player file types. + // Preferred handler for MP3 file types. + elseif ($handler === 'fmedia' || ($handler === 'mp3' && $mediaplayer !== '')) { + if (is_numeric($height)) { + $height += 20; + } + + if ($native) { + $embed .= "<embed src=\"$mediaplayer\" pluginspage=\"http://adobe.com/go/getflashplayer\" type=\"application/x-shockwave-flash\" quality=\"high\" width=\"$width\" height=\"$height\" wmode=\"transparent\" flashvars=\"file=" . rawurlencode($this->get_link().'?file_extension=.'.$this->get_extension()) . "&autostart=false&repeat=$loop&showdigits=true&showfsbutton=false\"></embed>"; + } else { + $embed .= "<script type='text/javascript'>embed_flv('$width', '$height', '" . rawurlencode($this->get_link().'?file_extension=.'.$this->get_extension()) . "', '$placeholder', '$loop', '$mediaplayer');</script>"; + } + } + + // QuickTime 7 file types. Need to test with QuickTime 6. + // Only handle MP3's if the Flash Media Player is not present. + elseif ($handler === 'quicktime' || ($handler === 'mp3' && $mediaplayer === '')) { + if (is_numeric($height)) { + $height += 16; + } + + if ($native) { + if ($placeholder !== '') { + $embed .= "<embed type=\"$type\" style=\"cursor:hand; cursor:pointer;\" href=\"" . $this->get_link() . "\" src=\"$placeholder\" width=\"$width\" height=\"$height\" autoplay=\"false\" target=\"myself\" controller=\"false\" loop=\"$loop\" scale=\"aspect\" bgcolor=\"$bgcolor\" pluginspage=\"http://apple.com/quicktime/download/\"></embed>"; + } else { + $embed .= "<embed type=\"$type\" style=\"cursor:hand; cursor:pointer;\" src=\"" . $this->get_link() . "\" width=\"$width\" height=\"$height\" autoplay=\"false\" target=\"myself\" controller=\"true\" loop=\"$loop\" scale=\"aspect\" bgcolor=\"$bgcolor\" pluginspage=\"http://apple.com/quicktime/download/\"></embed>"; + } + } else { + $embed .= "<script type='text/javascript'>embed_quicktime('$type', '$bgcolor', '$width', '$height', '" . $this->get_link() . "', '$placeholder', '$loop');</script>"; + } + } + + // Windows Media + elseif ($handler === 'wmedia') { + if (is_numeric($height)) { + $height += 45; + } + + if ($native) { + $embed .= "<embed type=\"application/x-mplayer2\" src=\"" . $this->get_link() . "\" autosize=\"1\" width=\"$width\" height=\"$height\" showcontrols=\"1\" showstatusbar=\"0\" showdisplay=\"0\" autostart=\"0\"></embed>"; + } else { + $embed .= "<script type='text/javascript'>embed_wmedia('$width', '$height', '" . $this->get_link() . "');</script>"; + } + } + + // Everything else + else { + $embed .= '<a href="' . $this->get_link() . '" class="' . $altclass . '">' . $alt . '</a>'; + } + + return $embed; + } + + /** + * Get the real media type + * + * Often, feeds lie to us, necessitating a bit of deeper inspection. This + * converts types to their canonical representations based on the file + * extension + * + * @see get_type() + * @param bool $find_handler Internal use only, use {@see get_handler()} instead + * @return string|null MIME type + */ + public function get_real_type(bool $find_handler = false) + { + // Mime-types by handler. + $types_flash = ['application/x-shockwave-flash', 'application/futuresplash']; // Flash + $types_fmedia = ['video/flv', 'video/x-flv','flv-application/octet-stream']; // Flash Media Player + $types_quicktime = ['audio/3gpp', 'audio/3gpp2', 'audio/aac', 'audio/x-aac', 'audio/aiff', 'audio/x-aiff', 'audio/mid', 'audio/midi', 'audio/x-midi', 'audio/mp4', 'audio/m4a', 'audio/x-m4a', 'audio/wav', 'audio/x-wav', 'video/3gpp', 'video/3gpp2', 'video/m4v', 'video/x-m4v', 'video/mp4', 'video/mpeg', 'video/x-mpeg', 'video/quicktime', 'video/sd-video']; // QuickTime + $types_wmedia = ['application/asx', 'application/x-mplayer2', 'audio/x-ms-wma', 'audio/x-ms-wax', 'video/x-ms-asf-plugin', 'video/x-ms-asf', 'video/x-ms-wm', 'video/x-ms-wmv', 'video/x-ms-wvx']; // Windows Media + $types_mp3 = ['audio/mp3', 'audio/x-mp3', 'audio/mpeg', 'audio/x-mpeg']; // MP3 + + $type = $this->get_type(); + if ($type !== null) { + $type = strtolower($type); + } + + // If we encounter an unsupported mime-type, check the file extension and guess intelligently. + if (!in_array($type, array_merge($types_flash, $types_fmedia, $types_quicktime, $types_wmedia, $types_mp3))) { + $extension = $this->get_extension(); + if ($extension === null) { + return null; + } + + switch (strtolower($extension)) { + // Audio mime-types + case 'aac': + case 'adts': + $type = 'audio/acc'; + break; + + case 'aif': + case 'aifc': + case 'aiff': + case 'cdda': + $type = 'audio/aiff'; + break; + + case 'bwf': + $type = 'audio/wav'; + break; + + case 'kar': + case 'mid': + case 'midi': + case 'smf': + $type = 'audio/midi'; + break; + + case 'm4a': + $type = 'audio/x-m4a'; + break; + + case 'mp3': + case 'swa': + $type = 'audio/mp3'; + break; + + case 'wav': + $type = 'audio/wav'; + break; + + case 'wax': + $type = 'audio/x-ms-wax'; + break; + + case 'wma': + $type = 'audio/x-ms-wma'; + break; + + case '3gp': + case '3gpp': + // Video mime-types + $type = 'video/3gpp'; + break; + + case '3g2': + case '3gp2': + $type = 'video/3gpp2'; + break; + + case 'asf': + $type = 'video/x-ms-asf'; + break; + + case 'flv': + $type = 'video/x-flv'; + break; + + case 'm1a': + case 'm1s': + case 'm1v': + case 'm15': + case 'm75': + case 'mp2': + case 'mpa': + case 'mpeg': + case 'mpg': + case 'mpm': + case 'mpv': + $type = 'video/mpeg'; + break; + + case 'm4v': + $type = 'video/x-m4v'; + break; + + case 'mov': + case 'qt': + $type = 'video/quicktime'; + break; + + case 'mp4': + case 'mpg4': + $type = 'video/mp4'; + break; + + case 'sdv': + $type = 'video/sd-video'; + break; + + case 'wm': + $type = 'video/x-ms-wm'; + break; + + case 'wmv': + $type = 'video/x-ms-wmv'; + break; + + case 'wvx': + $type = 'video/x-ms-wvx'; + break; + + case 'spl': + // Flash mime-types + $type = 'application/futuresplash'; + break; + + case 'swf': + $type = 'application/x-shockwave-flash'; + break; + } + } + + if ($find_handler) { + if (in_array($type, $types_flash)) { + return 'flash'; + } elseif (in_array($type, $types_fmedia)) { + return 'fmedia'; + } elseif (in_array($type, $types_quicktime)) { + return 'quicktime'; + } elseif (in_array($type, $types_wmedia)) { + return 'wmedia'; + } elseif (in_array($type, $types_mp3)) { + return 'mp3'; + } + + return null; + } + + return $type; + } +} + +class_alias('SimplePie\Enclosure', 'SimplePie_Enclosure'); + +} + +namespace SimplePie { + +use Exception as NativeException; + +/** + * General SimplePie exception class + */ +class Exception extends NativeException +{ +} + +class_alias('SimplePie\Exception', 'SimplePie_Exception'); + +} + +namespace SimplePie { + +use SimplePie\HTTP\Response; + +/** + * Used for fetching remote files and reading local files + * + * Supports HTTP 1.0 via cURL or fsockopen, with spotty HTTP 1.1 support + * + * This class can be overloaded with {@see \SimplePie\SimplePie::set_file_class()} + * + * @todo Move to properly supporting RFC2616 (HTTP/1.1) + */ +class File implements Response +{ + /** + * @var string The final URL after following all redirects + * @deprecated Use `get_final_requested_uri()` method. + */ + public $url; + + /** + * @var ?string User agent to use in requests + * @deprecated Set the user agent in constructor. + */ + public $useragent; + + /** @var bool */ + public $success = true; + + /** @var array<string, non-empty-array<string>> Canonical representation of headers */ + private $parsed_headers = []; + /** @var array<string, string> Last known value of $headers property (used to detect external modification) */ + private $last_headers = []; + /** + * @var array<string, string> Headers as string for BC + * @deprecated Use `get_headers()` method. + */ + public $headers = []; + + /** + * @var ?string Body of the HTTP response + * @deprecated Use `get_body_content()` method. + */ + public $body; + + /** + * @var int Status code of the HTTP response + * @deprecated Use `get_status_code()` method. + */ + public $status_code = 0; + + /** @var non-negative-int Number of redirect that were already performed during this request sequence. */ + public $redirects = 0; + + /** @var ?string */ + public $error; + + /** + * @var int-mask-of<SimplePie::FILE_SOURCE_*> Bit mask representing the method used to fetch the file and whether it is a local file or remote file obtained over HTTP. + * @deprecated Backend is implementation detail which you should not care about; to see if the file was retrieved over HTTP, check if `get_final_requested_uri()` with `Misc::is_remote_uri()`. + */ + public $method = \SimplePie\SimplePie::FILE_SOURCE_NONE; + + /** + * @var string The permanent URL or the resource (first URL after the prefix of (only) permanent redirects) + * @deprecated Use `get_permanent_uri()` method. + */ + public $permanent_url; + /** @var bool Whether the permanent URL is still writeable (prefix of permanent redirects has not ended) */ + private $permanentUrlMutable = true; + + /** + * @param string $url + * @param int $timeout + * @param int $redirects + * @param ?array<string, string> $headers + * @param ?string $useragent + * @param bool $force_fsockopen + * @param array<int, mixed> $curl_options + */ + public function __construct(string $url, int $timeout = 10, int $redirects = 5, ?array $headers = null, ?string $useragent = null, bool $force_fsockopen = false, array $curl_options = []) + { + if (function_exists('idn_to_ascii')) { + $parsed = \SimplePie\Misc::parse_url($url); + if ($parsed['authority'] !== '' && !ctype_print($parsed['authority'])) { + $authority = (string) \idn_to_ascii($parsed['authority'], \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46); + $url = \SimplePie\Misc::compress_parse_url($parsed['scheme'], $authority, $parsed['path'], $parsed['query'], null); + } + } + $this->url = $url; + if ($this->permanentUrlMutable) { + $this->permanent_url = $url; + } + $this->useragent = $useragent; + if (preg_match('/^http(s)?:\/\//i', $url)) { + if ($useragent === null) { + $useragent = (string) ini_get('user_agent'); + $this->useragent = $useragent; + } + if (!is_array($headers)) { + $headers = []; + } + if (!$force_fsockopen && function_exists('curl_exec')) { + $this->method = \SimplePie\SimplePie::FILE_SOURCE_REMOTE | \SimplePie\SimplePie::FILE_SOURCE_CURL; + $fp = curl_init(); + $headers2 = []; + foreach ($headers as $key => $value) { + $headers2[] = "$key: $value"; + } + if (isset($curl_options[CURLOPT_HTTPHEADER])) { + if (is_array($curl_options[CURLOPT_HTTPHEADER])) { + $headers2 = array_merge($headers2, $curl_options[CURLOPT_HTTPHEADER]); + } + unset($curl_options[CURLOPT_HTTPHEADER]); + } + if (version_compare(\SimplePie\Misc::get_curl_version(), '7.10.5', '>=')) { + curl_setopt($fp, CURLOPT_ENCODING, ''); + } + curl_setopt($fp, CURLOPT_URL, $url); + curl_setopt($fp, CURLOPT_HEADER, 1); + curl_setopt($fp, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($fp, CURLOPT_FAILONERROR, 1); + curl_setopt($fp, CURLOPT_TIMEOUT, $timeout); + curl_setopt($fp, CURLOPT_CONNECTTIMEOUT, $timeout); + curl_setopt($fp, CURLOPT_REFERER, \SimplePie\Misc::url_remove_credentials($url)); + curl_setopt($fp, CURLOPT_USERAGENT, $useragent); + curl_setopt($fp, CURLOPT_HTTPHEADER, $headers2); + foreach ($curl_options as $curl_param => $curl_value) { + curl_setopt($fp, $curl_param, $curl_value); + } + + $responseHeaders = curl_exec($fp); + if (curl_errno($fp) === CURLE_WRITE_ERROR || curl_errno($fp) === CURLE_BAD_CONTENT_ENCODING) { + curl_setopt($fp, CURLOPT_ENCODING, 'none'); + $responseHeaders = curl_exec($fp); + } + $this->status_code = curl_getinfo($fp, CURLINFO_HTTP_CODE); + if (curl_errno($fp) !== CURLE_OK) { + $this->error = 'cURL error ' . curl_errno($fp) . ': ' . curl_error($fp); + $this->success = false; + } else { + // For PHPStan: `curl_exec` returns `false` only on error so the `is_string` check will always pass. + \assert(is_string($responseHeaders)); + if (curl_getinfo($fp, CURLINFO_HTTP_CONNECTCODE) !== 0) { + // TODO: Replace with `CURLOPT_SUPPRESS_CONNECT_HEADERS` once PHP 7.2 support is dropped. + $responseHeaders = \SimplePie\HTTP\Parser::prepareHeaders($responseHeaders); + } + if (\PHP_VERSION_ID < 80000) { + curl_close($fp); + } + $parser = new \SimplePie\HTTP\Parser($responseHeaders, true); + if ($parser->parse()) { + $this->set_headers($parser->headers); + $this->body = $parser->body; + if ((in_array($this->status_code, [300, 301, 302, 303, 307]) || $this->status_code > 307 && $this->status_code < 400) && ($locationHeader = $this->get_header_line('location')) !== '' && $this->redirects < $redirects) { + $this->redirects++; + $location = \SimplePie\Misc::absolutize_url($locationHeader, $url); + if ($location === false) { + $this->error = "Invalid redirect location, trying to base “{$locationHeader}” onto “{$url}”"; + $this->success = false; + return; + } + $this->permanentUrlMutable = $this->permanentUrlMutable && ($this->status_code == 301 || $this->status_code == 308); + $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen, $curl_options); + return; + } + } + } + } else { + $this->method = \SimplePie\SimplePie::FILE_SOURCE_REMOTE | \SimplePie\SimplePie::FILE_SOURCE_FSOCKOPEN; + if (($url_parts = parse_url($url)) === false) { + throw new \InvalidArgumentException('Malformed URL: ' . $url); + } + if (!isset($url_parts['host'])) { + throw new \InvalidArgumentException('Missing hostname: ' . $url); + } + $socket_host = $url_parts['host']; + if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') { + $socket_host = 'ssl://' . $socket_host; + $url_parts['port'] = 443; + } + if (!isset($url_parts['port'])) { + $url_parts['port'] = 80; + } + $fp = @fsockopen($socket_host, $url_parts['port'], $errno, $errstr, $timeout); + if (!$fp) { + $this->error = 'fsockopen error: ' . $errstr; + $this->success = false; + } else { + stream_set_timeout($fp, $timeout); + if (isset($url_parts['path'])) { + if (isset($url_parts['query'])) { + $get = "$url_parts[path]?$url_parts[query]"; + } else { + $get = $url_parts['path']; + } + } else { + $get = '/'; + } + $out = "GET $get HTTP/1.1\r\n"; + $out .= "Host: $url_parts[host]\r\n"; + $out .= "User-Agent: $useragent\r\n"; + if (extension_loaded('zlib')) { + $out .= "Accept-Encoding: x-gzip,gzip,deflate\r\n"; + } + + if (isset($url_parts['user']) && isset($url_parts['pass'])) { + $out .= "Authorization: Basic " . base64_encode("$url_parts[user]:$url_parts[pass]") . "\r\n"; + } + foreach ($headers as $key => $value) { + $out .= "$key: $value\r\n"; + } + $out .= "Connection: Close\r\n\r\n"; + fwrite($fp, $out); + + $info = stream_get_meta_data($fp); + + $responseHeaders = ''; + while (!$info['eof'] && !$info['timed_out']) { + $responseHeaders .= fread($fp, 1160); + $info = stream_get_meta_data($fp); + } + if (!$info['timed_out']) { + $parser = new \SimplePie\HTTP\Parser($responseHeaders, true); + if ($parser->parse()) { + $this->set_headers($parser->headers); + $this->body = $parser->body; + $this->status_code = $parser->status_code; + if ((in_array($this->status_code, [300, 301, 302, 303, 307]) || $this->status_code > 307 && $this->status_code < 400) && ($locationHeader = $this->get_header_line('location')) !== '' && $this->redirects < $redirects) { + $this->redirects++; + $location = \SimplePie\Misc::absolutize_url($locationHeader, $url); + $this->permanentUrlMutable = $this->permanentUrlMutable && ($this->status_code == 301 || $this->status_code == 308); + if ($location === false) { + $this->error = "Invalid redirect location, trying to base “{$locationHeader}” onto “{$url}”"; + $this->success = false; + return; + } + $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen, $curl_options); + return; + } + if (($contentEncodingHeader = $this->get_header_line('content-encoding')) !== '') { + // Hey, we act dumb elsewhere, so let's do that here too + switch (strtolower(trim($contentEncodingHeader, "\x09\x0A\x0D\x20"))) { + case 'gzip': + case 'x-gzip': + if (($decompressed = gzdecode($this->body)) === false) { + $this->error = 'Unable to decode HTTP "gzip" stream'; + $this->success = false; + } else { + $this->body = $decompressed; + } + break; + + case 'deflate': + if (($decompressed = gzinflate($this->body)) !== false) { + $this->body = $decompressed; + } elseif (($decompressed = gzuncompress($this->body)) !== false) { + $this->body = $decompressed; + } elseif (($decompressed = gzdecode($this->body)) !== false) { + $this->body = $decompressed; + } else { + $this->error = 'Unable to decode HTTP "deflate" stream'; + $this->success = false; + } + break; + + default: + $this->error = 'Unknown content coding'; + $this->success = false; + } + } + } + } else { + $this->error = 'fsocket timed out'; + $this->success = false; + } + fclose($fp); + } + } + } else { + $this->method = \SimplePie\SimplePie::FILE_SOURCE_LOCAL | \SimplePie\SimplePie::FILE_SOURCE_FILE_GET_CONTENTS; + if (empty($url) || !is_readable($url) || false === $filebody = file_get_contents($url)) { + $this->body = ''; + $this->error = sprintf('file "%s" is not readable', $url); + $this->success = false; + } else { + $this->body = $filebody; + $this->status_code = 200; + } + } + if ($this->success) { + assert($this->body !== null); // For PHPStan + // Leading whitespace may cause XML parsing errors (XML declaration cannot be preceded by anything other than BOM) so we trim it. + // Note that unlike built-in `trim` function’s default settings, we do not trim `\x00` to avoid breaking characters in UTF-16 or UTF-32 encoded strings. + // We also only do that when the whitespace is followed by `<`, so that we do not break e.g. UTF-16LE encoded whitespace like `\n\x00` in half. + $this->body = preg_replace('/^[ \n\r\t\v]+</', '<', $this->body); + } + } + + public function get_permanent_uri(): string + { + return (string) $this->permanent_url; + } + + public function get_final_requested_uri(): string + { + return (string) $this->url; + } + + public function get_status_code(): int + { + return (int) $this->status_code; + } + + public function get_headers(): array + { + $this->maybe_update_headers(); + return $this->parsed_headers; + } + + public function has_header(string $name): bool + { + $this->maybe_update_headers(); + return $this->get_header($name) !== []; + } + + public function get_header(string $name): array + { + $this->maybe_update_headers(); + return $this->parsed_headers[strtolower($name)] ?? []; + } + + public function with_header(string $name, $value) + { + $this->maybe_update_headers(); + $new = clone $this; + + $newHeader = [ + strtolower($name) => (array) $value, + ]; + $new->set_headers($newHeader + $this->get_headers()); + + return $new; + } + + public function get_header_line(string $name): string + { + $this->maybe_update_headers(); + return implode(', ', $this->get_header($name)); + } + + public function get_body_content(): string + { + return (string) $this->body; + } + + /** + * Check if the $headers property was changed and update the internal state accordingly. + */ + private function maybe_update_headers(): void + { + if ($this->headers !== $this->last_headers) { + $this->parsed_headers = array_map( + function (string $header_line): array { + if (strpos($header_line, ',') === false) { + return [$header_line]; + } else { + return array_map('trim', explode(',', $header_line)); + } + }, + $this->headers + ); + } + $this->last_headers = $this->headers; + } + + /** + * Sets headers internally. + * + * @param array<string, non-empty-array<string>> $headers + */ + private function set_headers(array $headers): void + { + $this->parsed_headers = $headers; + $this->headers = self::flatten_headers($headers); + $this->last_headers = $this->headers; + } + + /** + * Converts PSR-7 compatible headers into a legacy format. + * + * @param array<string, non-empty-array<string>> $headers + * + * @return array<string, string> + */ + private function flatten_headers(array $headers): array + { + return array_map(function (array $values): string { + return implode(',', $values); + }, $headers); + } + + /** + * Create a File instance from another Response + * + * For BC reasons in some places there MUST be a `File` instance + * instead of a `Response` implementation + * + * @see Locator::__construct() + * @internal + */ + final public static function fromResponse(Response $response): self + { + $headers = []; + + foreach ($response->get_headers() as $name => $header) { + $headers[$name] = implode(', ', $header); + } + + /** @var File */ + $file = (new \ReflectionClass(File::class))->newInstanceWithoutConstructor(); + + $file->url = $response->get_final_requested_uri(); + $file->useragent = null; + $file->headers = $headers; + $file->body = $response->get_body_content(); + $file->status_code = $response->get_status_code(); + $file->permanent_url = $response->get_permanent_uri(); + + return $file; + } +} + +class_alias('SimplePie\File', 'SimplePie_File'); + +} + +namespace SimplePie { + +/** + * Decode 'gzip' encoded HTTP data + * + * @link http://www.gzip.org/format.txt + * @link https://www.php.net/manual/en/function.gzdecode.php + * @deprecated since SimplePie 1.9.0, use `gzdecode` function instead. + */ +class Gzdecode +{ + /** + * Compressed data + * + * @access private + * @var string + * @see gzdecode::$data + */ + public $compressed_data; + + /** + * Size of compressed data + * + * @access private + * @var int + */ + public $compressed_size; + + /** + * Minimum size of a valid gzip string + * + * @access private + * @var int + */ + public $min_compressed_size = 18; + + /** + * Current position of pointer + * + * @access private + * @var int + */ + public $position = 0; + + /** + * Flags (FLG) + * + * @access private + * @var int + */ + public $flags; + + /** + * Uncompressed data + * + * @access public + * @see gzdecode::$compressed_data + * @var string + */ + public $data; + + /** + * Modified time + * + * @access public + * @var int + */ + public $MTIME; + + /** + * Extra Flags + * + * @access public + * @var int + */ + public $XFL; + + /** + * Operating System + * + * @access public + * @var int + */ + public $OS; + + /** + * Subfield ID 1 + * + * @access public + * @see gzdecode::$extra_field + * @see gzdecode::$SI2 + * @var string + */ + public $SI1; + + /** + * Subfield ID 2 + * + * @access public + * @see gzdecode::$extra_field + * @see gzdecode::$SI1 + * @var string + */ + public $SI2; + + /** + * Extra field content + * + * @access public + * @see gzdecode::$SI1 + * @see gzdecode::$SI2 + * @var string + */ + public $extra_field; + + /** + * Original filename + * + * @access public + * @var string + */ + public $filename; + + /** + * Human readable comment + * + * @access public + * @var string + */ + public $comment; + + /** + * Don't allow anything to be set + * + * @param string $name + * @param mixed $value + */ + public function __set(string $name, $value) + { + throw new Exception("Cannot write property $name"); + } + + /** + * Set the compressed string and related properties + * + * @param string $data + */ + public function __construct(string $data) + { + $this->compressed_data = $data; + $this->compressed_size = strlen($data); + } + + /** + * Decode the GZIP stream + * + * @return bool Successfulness + */ + public function parse() + { + if ($this->compressed_size >= $this->min_compressed_size) { + $len = 0; + + // Check ID1, ID2, and CM + if (substr($this->compressed_data, 0, 3) !== "\x1F\x8B\x08") { + return false; + } + + // Get the FLG (FLaGs) + $this->flags = ord($this->compressed_data[3]); + + // FLG bits above (1 << 4) are reserved + if ($this->flags > 0x1F) { + return false; + } + + // Advance the pointer after the above + $this->position += 4; + + // MTIME + $mtime = substr($this->compressed_data, $this->position, 4); + // Reverse the string if we're on a big-endian arch because l is the only signed long and is machine endianness + if (current((array) unpack('S', "\x00\x01")) === 1) { + $mtime = strrev($mtime); + } + $this->MTIME = current((array) unpack('l', $mtime)); + $this->position += 4; + + // Get the XFL (eXtra FLags) + $this->XFL = ord($this->compressed_data[$this->position++]); + + // Get the OS (Operating System) + $this->OS = ord($this->compressed_data[$this->position++]); + + // Parse the FEXTRA + if ($this->flags & 4) { + // Read subfield IDs + $this->SI1 = $this->compressed_data[$this->position++]; + $this->SI2 = $this->compressed_data[$this->position++]; + + // SI2 set to zero is reserved for future use + if ($this->SI2 === "\x00") { + return false; + } + + // Get the length of the extra field + $len = current((array) unpack('v', substr($this->compressed_data, $this->position, 2))); + $this->position += 2; + + // Check the length of the string is still valid + $this->min_compressed_size += $len + 4; + if ($this->compressed_size >= $this->min_compressed_size) { + // Set the extra field to the given data + $this->extra_field = substr($this->compressed_data, $this->position, $len); + $this->position += $len; + } else { + return false; + } + } + + // Parse the FNAME + if ($this->flags & 8) { + // Get the length of the filename + $len = strcspn($this->compressed_data, "\x00", $this->position); + + // Check the length of the string is still valid + $this->min_compressed_size += $len + 1; + if ($this->compressed_size >= $this->min_compressed_size) { + // Set the original filename to the given string + $this->filename = substr($this->compressed_data, $this->position, $len); + $this->position += $len + 1; + } else { + return false; + } + } + + // Parse the FCOMMENT + if ($this->flags & 16) { + // Get the length of the comment + $len = strcspn($this->compressed_data, "\x00", $this->position); + + // Check the length of the string is still valid + $this->min_compressed_size += $len + 1; + if ($this->compressed_size >= $this->min_compressed_size) { + // Set the original comment to the given string + $this->comment = substr($this->compressed_data, $this->position, $len); + $this->position += $len + 1; + } else { + return false; + } + } + + // Parse the FHCRC + if ($this->flags & 2) { + // Check the length of the string is still valid + $this->min_compressed_size += $len + 2; + if ($this->compressed_size >= $this->min_compressed_size) { + // Read the CRC + $crc = current((array) unpack('v', substr($this->compressed_data, $this->position, 2))); + + // Check the CRC matches + if ((crc32(substr($this->compressed_data, 0, $this->position)) & 0xFFFF) === $crc) { + $this->position += 2; + } else { + return false; + } + } else { + return false; + } + } + + // Decompress the actual data + if (($data = gzinflate(substr($this->compressed_data, $this->position, -8))) === false) { + return false; + } + + $this->data = $data; + $this->position = $this->compressed_size - 8; + + // Check CRC of data + $crc = current((array) unpack('V', substr($this->compressed_data, $this->position, 4))); + $this->position += 4; + /*if (extension_loaded('hash') && sprintf('%u', current(unpack('V', hash('crc32b', $this->data)))) !== sprintf('%u', $crc)) + { + return false; + }*/ + + // Check ISIZE of data + $isize = current((array) unpack('V', substr($this->compressed_data, $this->position, 4))); + $this->position += 4; + if (sprintf('%u', strlen($this->data) & 0xFFFFFFFF) !== sprintf('%u', $isize)) { + return false; + } + + // Wow, against all odds, we've actually got a valid gzip string + return true; + } + + return false; + } +} + +class_alias('SimplePie\Gzdecode', 'SimplePie_gzdecode'); + +} + +namespace SimplePie\HTTP { + +/** + * HTTP Client interface + * + * @internal + */ +interface Client +{ + public const METHOD_GET = 'GET'; + + /** + * send a request and return the response + * + * @param Client::METHOD_* $method + * @param array<string, string> $headers + * + * @throws ClientException if anything goes wrong requesting the data + */ + public function request(string $method, string $url, array $headers = []): Response; +} + +} + +namespace SimplePie\HTTP { + +use SimplePie\Exception as SimplePieException; + +/** + * Client exception class + * + * @internal + */ +final class ClientException extends SimplePieException +{ +} + +} + +namespace SimplePie\HTTP { + +use InvalidArgumentException; +use SimplePie\File; +use SimplePie\Misc; +use SimplePie\Registry; +use Throwable; + +/** + * HTTP Client based on \SimplePie\File + * + * @internal + */ +final class FileClient implements Client +{ + /** @var Registry */ + private $registry; + + /** @var array{timeout?: int, redirects?: int, useragent?: string, force_fsockopen?: bool, curl_options?: array<mixed>} */ + private $options; + + /** + * @param array{timeout?: int, redirects?: int, useragent?: string, force_fsockopen?: bool, curl_options?: array<mixed>} $options + */ + public function __construct(Registry $registry, array $options = []) + { + $this->registry = $registry; + $this->options = $options; + } + + /** + * send a request and return the response + * + * @param Client::METHOD_* $method + * @param array<string, string> $headers + * + * @throws ClientException if anything goes wrong requesting the data + */ + public function request(string $method, string $url, array $headers = []): Response + { + // @phpstan-ignore-next-line Enforce PHPDoc type. + if ($method !== self::METHOD_GET) { + throw new InvalidArgumentException(sprintf( + '%s(): Argument #1 ($method) only supports method "%s".', + __METHOD__, + self::METHOD_GET + ), 1); + } + + try { + $file = $this->registry->create(File::class, [ + $url, + $this->options['timeout'] ?? 10, + $this->options['redirects'] ?? 5, + $headers, + $this->options['useragent'] ?? Misc::get_default_useragent(), + $this->options['force_fsockopen'] ?? false, + $this->options['curl_options'] ?? [] + ]); + } catch (Throwable $th) { + throw new ClientException($th->getMessage(), $th->getCode(), $th); + } + + if ($file->error !== null && $file->get_status_code() === 0) { + throw new ClientException($file->error); + } + + return $file; + } +} + +} + +namespace SimplePie\HTTP { + +/** + * HTTP Response Parser + * @template Psr7Compatible of bool + */ +class Parser +{ + /** + * HTTP Version + * + * @var float + */ + public $http_version = 0.0; + + /** + * Status code + * + * @var int + */ + public $status_code = 0; + + /** + * Reason phrase + * + * @var string + */ + public $reason = ''; + + /** + * @var Psr7Compatible whether headers are compatible with PSR-7 format. + */ + private $psr7Compatible; + + /** + * Key/value pairs of the headers + * + * @phpstan-var (Psr7Compatible is true ? array<string, non-empty-array<string>> : array<string, string>) + */ + public $headers = []; + + /** + * Body of the response + * + * @var string + */ + public $body = ''; + + private const STATE_HTTP_VERSION = 'http_version'; + + private const STATE_STATUS = 'status'; + + private const STATE_REASON = 'reason'; + + private const STATE_NEW_LINE = 'new_line'; + + private const STATE_BODY = 'body'; + + private const STATE_NAME = 'name'; + + private const STATE_VALUE = 'value'; + + private const STATE_VALUE_CHAR = 'value_char'; + + private const STATE_QUOTE = 'quote'; + + private const STATE_QUOTE_ESCAPED = 'quote_escaped'; + + private const STATE_QUOTE_CHAR = 'quote_char'; + + private const STATE_CHUNKED = 'chunked'; + + private const STATE_EMIT = 'emit'; + + private const STATE_ERROR = false; + + /** + * Current state of the state machine + * + * @var self::STATE_* + */ + protected $state = self::STATE_HTTP_VERSION; + + /** + * Input data + * + * @var string + */ + protected $data = ''; + + /** + * Input data length (to avoid calling strlen() everytime this is needed) + * + * @var int + */ + protected $data_length = 0; + + /** + * Current position of the pointer + * + * @var int + */ + protected $position = 0; + + /** + * Name of the header currently being parsed + * + * @var string + */ + protected $name = ''; + + /** + * Value of the header currently being parsed + * + * @var string + */ + protected $value = ''; + + /** + * Create an instance of the class with the input data + * + * @param string $data Input data + * @param Psr7Compatible $psr7Compatible Whether the data types are in format compatible with PSR-7. + */ + public function __construct(string $data, bool $psr7Compatible = false) + { + $this->data = $data; + $this->data_length = strlen($this->data); + $this->psr7Compatible = $psr7Compatible; + } + + /** + * Parse the input data + * + * @return bool true on success, false on failure + */ + public function parse() + { + while ($this->state && $this->state !== self::STATE_EMIT && $this->has_data()) { + $state = $this->state; + $this->$state(); + } + $this->data = ''; + if ($this->state === self::STATE_EMIT || $this->state === self::STATE_BODY) { + return true; + } + + // Reset the parser state. + $this->http_version = 0.0; + $this->status_code = 0; + $this->reason = ''; + $this->headers = []; + $this->body = ''; + return false; + } + + /** + * Check whether there is data beyond the pointer + * + * @return bool true if there is further data, false if not + */ + protected function has_data() + { + return (bool) ($this->position < $this->data_length); + } + + /** + * See if the next character is LWS + * + * @return bool true if the next character is LWS, false if not + */ + protected function is_linear_whitespace() + { + return (bool) ($this->data[$this->position] === "\x09" + || $this->data[$this->position] === "\x20" + || ($this->data[$this->position] === "\x0A" + && isset($this->data[$this->position + 1]) + && ($this->data[$this->position + 1] === "\x09" || $this->data[$this->position + 1] === "\x20"))); + } + + /** + * Parse the HTTP version + * @return void + */ + protected function http_version() + { + if (strpos($this->data, "\x0A") !== false && strtoupper(substr($this->data, 0, 5)) === 'HTTP/') { + $len = strspn($this->data, '0123456789.', 5); + $http_version = substr($this->data, 5, $len); + $this->position += 5 + $len; + if (substr_count($http_version, '.') <= 1) { + $this->http_version = (float) $http_version; + $this->position += strspn($this->data, "\x09\x20", $this->position); + $this->state = self::STATE_STATUS; + } else { + $this->state = self::STATE_ERROR; + } + } else { + $this->state = self::STATE_ERROR; + } + } + + /** + * Parse the status code + * @return void + */ + protected function status() + { + if ($len = strspn($this->data, '0123456789', $this->position)) { + $this->status_code = (int) substr($this->data, $this->position, $len); + $this->position += $len; + $this->state = self::STATE_REASON; + } else { + $this->state = self::STATE_ERROR; + } + } + + /** + * Parse the reason phrase + * @return void + */ + protected function reason() + { + $len = strcspn($this->data, "\x0A", $this->position); + $this->reason = trim(substr($this->data, $this->position, $len), "\x09\x0D\x20"); + $this->position += $len + 1; + $this->state = self::STATE_NEW_LINE; + } + + private function add_header(string $name, string $value): void + { + if ($this->psr7Compatible) { + // For PHPStan: should be enforced by template parameter but PHPStan is not smart enough. + /** @var array<string, non-empty-array<string>> */ + $headers = &$this->headers; + $headers[$name][] = $value; + } else { + // For PHPStan: should be enforced by template parameter but PHPStan is not smart enough. + /** @var array<string, string>) */ + $headers = &$this->headers; + $headers[$name] .= ', ' . $value; + } + } + + private function replace_header(string $name, string $value): void + { + if ($this->psr7Compatible) { + // For PHPStan: should be enforced by template parameter but PHPStan is not smart enough. + /** @var array<string, non-empty-array<string>> */ + $headers = &$this->headers; + $headers[$name] = [$value]; + } else { + // For PHPStan: should be enforced by template parameter but PHPStan is not smart enough. + /** @var array<string, string>) */ + $headers = &$this->headers; + $headers[$name] = $value; + } + } + + /** + * Deal with a new line, shifting data around as needed + * @return void + */ + protected function new_line() + { + $this->value = trim($this->value, "\x0D\x20"); + if ($this->name !== '' && $this->value !== '') { + $this->name = strtolower($this->name); + // We should only use the last Content-Type header. c.f. issue #1 + if (isset($this->headers[$this->name]) && $this->name !== 'content-type') { + $this->add_header($this->name, $this->value); + } else { + $this->replace_header($this->name, $this->value); + } + } + $this->name = ''; + $this->value = ''; + if (substr($this->data[$this->position], 0, 2) === "\x0D\x0A") { + $this->position += 2; + $this->state = self::STATE_BODY; + } elseif ($this->data[$this->position] === "\x0A") { + $this->position++; + $this->state = self::STATE_BODY; + } else { + $this->state = self::STATE_NAME; + } + } + + /** + * Parse a header name + * @return void + */ + protected function name() + { + $len = strcspn($this->data, "\x0A:", $this->position); + if (isset($this->data[$this->position + $len])) { + if ($this->data[$this->position + $len] === "\x0A") { + $this->position += $len; + $this->state = self::STATE_NEW_LINE; + } else { + $this->name = substr($this->data, $this->position, $len); + $this->position += $len + 1; + $this->state = self::STATE_VALUE; + } + } else { + $this->state = self::STATE_ERROR; + } + } + + /** + * Parse LWS, replacing consecutive LWS characters with a single space + * @return void + */ + protected function linear_whitespace() + { + do { + if (substr($this->data, $this->position, 2) === "\x0D\x0A") { + $this->position += 2; + } elseif ($this->data[$this->position] === "\x0A") { + $this->position++; + } + $this->position += strspn($this->data, "\x09\x20", $this->position); + } while ($this->has_data() && $this->is_linear_whitespace()); + $this->value .= "\x20"; + } + + /** + * See what state to move to while within non-quoted header values + * @return void + */ + protected function value() + { + if ($this->is_linear_whitespace()) { + $this->linear_whitespace(); + } else { + switch ($this->data[$this->position]) { + case '"': + // Workaround for ETags: we have to include the quotes as + // part of the tag. + if (strtolower($this->name) === 'etag') { + $this->value .= '"'; + $this->position++; + $this->state = self::STATE_VALUE_CHAR; + break; + } + $this->position++; + $this->state = self::STATE_QUOTE; + break; + + case "\x0A": + $this->position++; + $this->state = self::STATE_NEW_LINE; + break; + + default: + $this->state = self::STATE_VALUE_CHAR; + break; + } + } + } + + /** + * Parse a header value while outside quotes + * @return void + */ + protected function value_char() + { + $len = strcspn($this->data, "\x09\x20\x0A\"", $this->position); + $this->value .= substr($this->data, $this->position, $len); + $this->position += $len; + $this->state = self::STATE_VALUE; + } + + /** + * See what state to move to while within quoted header values + * @return void + */ + protected function quote() + { + if ($this->is_linear_whitespace()) { + $this->linear_whitespace(); + } else { + switch ($this->data[$this->position]) { + case '"': + $this->position++; + $this->state = self::STATE_VALUE; + break; + + case "\x0A": + $this->position++; + $this->state = self::STATE_NEW_LINE; + break; + + case '\\': + $this->position++; + $this->state = self::STATE_QUOTE_ESCAPED; + break; + + default: + $this->state = self::STATE_QUOTE_CHAR; + break; + } + } + } + + /** + * Parse a header value while within quotes + * @return void + */ + protected function quote_char() + { + $len = strcspn($this->data, "\x09\x20\x0A\"\\", $this->position); + $this->value .= substr($this->data, $this->position, $len); + $this->position += $len; + $this->state = self::STATE_VALUE; + } + + /** + * Parse an escaped character within quotes + * @return void + */ + protected function quote_escaped() + { + $this->value .= $this->data[$this->position]; + $this->position++; + $this->state = self::STATE_QUOTE; + } + + /** + * Parse the body + * @return void + */ + protected function body() + { + $this->body = substr($this->data, $this->position); + if (!empty($this->headers['transfer-encoding'])) { + unset($this->headers['transfer-encoding']); + $this->state = self::STATE_CHUNKED; + } else { + $this->state = self::STATE_EMIT; + } + } + + /** + * Parsed a "Transfer-Encoding: chunked" body + * @return void + */ + protected function chunked() + { + if (!preg_match('/^([0-9a-f]+)[^\r\n]*\r\n/i', trim($this->body))) { + $this->state = self::STATE_EMIT; + return; + } + + $decoded = ''; + $encoded = $this->body; + + while (true) { + $is_chunked = (bool) preg_match('/^([0-9a-f]+)[^\r\n]*\r\n/i', $encoded, $matches); + if (!$is_chunked) { + // Looks like it's not chunked after all + $this->state = self::STATE_EMIT; + return; + } + + $length = hexdec(trim($matches[1])); + // For PHPStan: this will only be float when larger than PHP_INT_MAX. + // But even on 32-bit systems, it would mean 2GiB chunk, which sounds unlikely. + \assert(\is_int($length), "Length needs to be shorter than PHP_INT_MAX"); + if ($length === 0) { + // Ignore trailer headers + $this->state = self::STATE_EMIT; + $this->body = $decoded; + return; + } + + $chunk_length = strlen($matches[0]); + $decoded .= substr($encoded, $chunk_length, $length); + $encoded = substr($encoded, $chunk_length + $length + 2); + + // BC for PHP < 8.0: substr() can return bool instead of string + $encoded = ($encoded === false) ? '' : $encoded; + + if (trim($encoded) === '0' || empty($encoded)) { + $this->state = self::STATE_EMIT; + $this->body = $decoded; + return; + } + } + } + + /** + * Prepare headers (take care of proxies headers) + * + * @param string $headers Raw headers + * @param non-negative-int $count Redirection count. Default to 1. + * + * @return string + */ + public static function prepareHeaders(string $headers, int $count = 1) + { + $data = explode("\r\n\r\n", $headers, $count); + $data = array_pop($data); + if (false !== stripos($data, "HTTP/1.0 200 Connection established\r\n")) { + $exploded = explode("\r\n\r\n", $data, 2); + $data = end($exploded); + } + if (false !== stripos($data, "HTTP/1.1 200 Connection established\r\n")) { + $exploded = explode("\r\n\r\n", $data, 2); + $data = end($exploded); + } + return $data; + } +} + +class_alias('SimplePie\HTTP\Parser', 'SimplePie_HTTP_Parser'); + +} + +namespace SimplePie\HTTP { + +use Psr\Http\Message\ResponseInterface; + +/** + * HTTP Response based on a PSR-7 response + * + * This interface must be interoperable with Psr\Http\Message\ResponseInterface + * @see https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface + * + * @internal + */ +final class Psr7Response implements Response +{ + /** + * @var ResponseInterface + */ + private $response; + + /** + * @var string + */ + private $permanent_url; + + /** + * @var string + */ + private $requested_url; + + public function __construct(ResponseInterface $response, string $permanent_url, string $requested_url) + { + $this->response = $response; + $this->permanent_url = $permanent_url; + $this->requested_url = $requested_url; + } + + public function get_permanent_uri(): string + { + return $this->permanent_url; + } + + public function get_final_requested_uri(): string + { + return $this->requested_url; + } + + public function get_status_code(): int + { + return $this->response->getStatusCode(); + } + + public function get_headers(): array + { + // The filtering is probably redundant but let’s make PHPStan happy. + return array_filter($this->response->getHeaders(), function (array $header): bool { + return count($header) >= 1; + }); + } + + public function has_header(string $name): bool + { + return $this->response->hasHeader($name); + } + + public function with_header(string $name, $value) + { + return new self($this->response->withHeader($name, $value), $this->permanent_url, $this->requested_url); + } + + public function get_header(string $name): array + { + return $this->response->getHeader($name); + } + + public function get_header_line(string $name): string + { + return $this->response->getHeaderLine($name); + } + + public function get_body_content(): string + { + return $this->response->getBody()->__toString(); + } +} + +} + +namespace SimplePie\HTTP { + +use InvalidArgumentException; +use Psr\Http\Client\ClientExceptionInterface; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\UriFactoryInterface; +use Throwable; + +/** + * HTTP Client based on PSR-18 and PSR-17 implementations + * + * @internal + */ +final class Psr18Client implements Client +{ + /** + * @var ClientInterface + */ + private $httpClient; + + /** + * @var RequestFactoryInterface + */ + private $requestFactory; + + /** + * @var UriFactoryInterface + */ + private $uriFactory; + + /** + * @var int + */ + private $allowedRedirects = 5; + + public function __construct(ClientInterface $httpClient, RequestFactoryInterface $requestFactory, UriFactoryInterface $uriFactory) + { + $this->httpClient = $httpClient; + $this->requestFactory = $requestFactory; + $this->uriFactory = $uriFactory; + } + + public function getHttpClient(): ClientInterface + { + return $this->httpClient; + } + + public function getRequestFactory(): RequestFactoryInterface + { + return $this->requestFactory; + } + + public function getUriFactory(): UriFactoryInterface + { + return $this->uriFactory; + } + + /** + * send a request and return the response + * + * @param Client::METHOD_* $method + * @param string $url + * @param array<string,string|string[]> $headers + * + * @throws ClientException if anything goes wrong requesting the data + */ + public function request(string $method, string $url, array $headers = []): Response + { + if ($method !== self::METHOD_GET) { + throw new InvalidArgumentException(sprintf( + '%s(): Argument #1 ($method) only supports method "%s".', + __METHOD__, + self::METHOD_GET + ), 1); + } + + if (preg_match('/^http(s)?:\/\//i', $url)) { + return $this->requestUrl($method, $url, $headers); + } + + return $this->requestLocalFile($url); + } + + /** + * @param array<string,string|string[]> $headers + */ + private function requestUrl(string $method, string $url, array $headers): Response + { + $permanentUrl = $url; + $requestedUrl = $url; + $remainingRedirects = $this->allowedRedirects; + + $request = $this->requestFactory->createRequest( + $method, + $this->uriFactory->createUri($requestedUrl) + ); + + foreach ($headers as $name => $value) { + $request = $request->withHeader($name, $value); + } + + do { + $followRedirect = false; + + try { + $response = $this->httpClient->sendRequest($request); + } catch (ClientExceptionInterface $th) { + throw new ClientException($th->getMessage(), $th->getCode(), $th); + } + + $statusCode = $response->getStatusCode(); + + // If we have a redirect + if (in_array($statusCode, [300, 301, 302, 303, 307]) && $response->hasHeader('Location')) { + // Prevent infinity redirect loops + if ($remainingRedirects <= 0) { + break; + } + + $remainingRedirects--; + $followRedirect = true; + + $requestedUrl = $response->getHeaderLine('Location'); + + if ($statusCode === 301) { + $permanentUrl = $requestedUrl; + } + + $request = $request->withUri($this->uriFactory->createUri($requestedUrl)); + } + } while ($followRedirect); + + return new Psr7Response($response, $permanentUrl, $requestedUrl); + } + + private function requestLocalFile(string $path): Response + { + if (!is_readable($path)) { + throw new ClientException(sprintf('file "%s" is not readable', $path)); + } + + try { + $raw = file_get_contents($path); + } catch (Throwable $th) { + throw new ClientException($th->getMessage(), $th->getCode(), $th); + } + + if ($raw === false) { + throw new ClientException('file_get_contents() could not read the file', 1); + } + + return new RawTextResponse($raw, $path); + } +} + +} + +namespace SimplePie\HTTP { + +/** + * HTTP Response for rax text + * + * This interface must be interoperable with Psr\Http\Message\ResponseInterface + * @see https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface + * + * @internal + */ +final class RawTextResponse implements Response +{ + /** + * @var string + */ + private $raw_text; + + /** + * @var string + */ + private $permanent_url; + + /** + * @var array<non-empty-array<string>> + */ + private $headers = []; + + /** + * @var string + */ + private $requested_url; + + public function __construct(string $raw_text, string $filepath) + { + $this->raw_text = $raw_text; + $this->permanent_url = $filepath; + $this->requested_url = $filepath; + } + + public function get_permanent_uri(): string + { + return $this->permanent_url; + } + + public function get_final_requested_uri(): string + { + return $this->requested_url; + } + + public function get_status_code(): int + { + return 200; + } + + public function get_headers(): array + { + return $this->headers; + } + + public function has_header(string $name): bool + { + return isset($this->headers[strtolower($name)]); + } + + public function get_header(string $name): array + { + return isset($this->headers[strtolower($name)]) ? $this->headers[$name] : []; + } + + public function with_header(string $name, $value) + { + $new = clone $this; + + $newHeader = [ + strtolower($name) => (array) $value, + ]; + $new->headers = $newHeader + $this->headers; + + return $new; + } + + public function get_header_line(string $name): string + { + return isset($this->headers[strtolower($name)]) ? implode(", ", $this->headers[$name]) : ''; + } + + public function get_body_content(): string + { + return $this->raw_text; + } +} + +} + +namespace SimplePie\HTTP { + +/** + * HTTP Response interface + * + * This interface must be interoperable with Psr\Http\Message\ResponseInterface + * @see https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface + * + * @internal + */ +interface Response +{ + /** + * Return the string representation of the permanent URI of the requested resource + * (the first location after a prefix of (only) permanent redirects). + * + * Depending on which components of the URI are present, the resulting + * string is either a full URI or relative reference according to RFC 3986, + * Section 4.1. The method concatenates the various components of the URI, + * using the appropriate delimiters: + * + * - If a scheme is present, it MUST be suffixed by ":". + * - If an authority is present, it MUST be prefixed by "//". + * - The path can be concatenated without delimiters. But there are two + * cases where the path has to be adjusted to make the URI reference + * valid as PHP does not allow to throw an exception in __toString(): + * - If the path is rootless and an authority is present, the path MUST + * be prefixed by "/". + * - If the path is starting with more than one "/" and no authority is + * present, the starting slashes MUST be reduced to one. + * - If a query is present, it MUST be prefixed by "?". + * - If a fragment is present, it MUST be prefixed by "#". + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + */ + public function get_permanent_uri(): string; + + /** + * Return the string representation of the final requested URL after following all redirects. + * + * Depending on which components of the URI are present, the resulting + * string is either a full URI or relative reference according to RFC 3986, + * Section 4.1. The method concatenates the various components of the URI, + * using the appropriate delimiters: + * + * - If a scheme is present, it MUST be suffixed by ":". + * - If an authority is present, it MUST be prefixed by "//". + * - The path can be concatenated without delimiters. But there are two + * cases where the path has to be adjusted to make the URI reference + * valid as PHP does not allow to throw an exception in __toString(): + * - If the path is rootless and an authority is present, the path MUST + * be prefixed by "/". + * - If the path is starting with more than one "/" and no authority is + * present, the starting slashes MUST be reduced to one. + * - If a query is present, it MUST be prefixed by "?". + * - If a fragment is present, it MUST be prefixed by "#". + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + */ + public function get_final_requested_uri(): string; + + /** + * Gets the response status code. + * + * The status code is a 3-digit integer result code of the server's attempt + * to understand and satisfy the request. + * + * @return int Status code. + */ + public function get_status_code(): int; + + /** + * Retrieves all message header values. + * + * The keys represent the header name as it will be sent over the wire, and + * each value is an array of strings associated with the header. + * + * // Represent the headers as a string + * foreach ($message->get_headers() as $name => $values) { + * echo $name . ': ' . implode(', ', $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->get_headers() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * @return array<non-empty-array<string>> Returns an associative array of the message's headers. + * Each key MUST be a header name, and each value MUST be an array of + * strings for that header. + */ + public function get_headers(): array; + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function has_header(string $name): bool; + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function get_header(string $name): array; + + /** + * Return an instance with the provided value replacing the specified header. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|non-empty-array<string> $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function with_header(string $name, $value); + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function get_header_line(string $name): string; + + /** + * get the body as string + * + * @return string + */ + public function get_body_content(): string; +} + +} + +namespace SimplePie { + +/** + * IRI parser/serialiser/normaliser + * + * @property ?string $scheme + * @property ?string $userinfo + * @property ?string $host + * @property ?int $port + * @property-write int|string|null $port + * @property ?string $authority + * @property string $path + * @property ?string $query + * @property ?string $fragment + */ +class IRI +{ + /** + * Scheme + * + * @var ?string + */ + protected $scheme = null; + + /** + * User Information + * + * @var ?string + */ + protected $iuserinfo = null; + + /** + * ihost + * + * @var ?string + */ + protected $ihost = null; + + /** + * Port + * + * @var ?int + */ + protected $port = null; + + /** + * ipath + * + * @var string + */ + protected $ipath = ''; + + /** + * iquery + * + * @var ?string + */ + protected $iquery = null; + + /** + * ifragment + * + * @var ?string + */ + protected $ifragment = null; + + /** + * Normalization database + * + * Each key is the scheme, each value is an array with each key as the IRI + * part and value as the default value for that part. + * + * @var array<string, array<string, mixed>> + */ + protected $normalization = [ + 'acap' => [ + 'port' => 674 + ], + 'dict' => [ + 'port' => 2628 + ], + 'file' => [ + 'ihost' => 'localhost' + ], + 'http' => [ + 'port' => 80, + 'ipath' => '/' + ], + 'https' => [ + 'port' => 443, + 'ipath' => '/' + ], + ]; + + /** + * Return the entire IRI when you try and read the object as a string + * + * @return string + */ + public function __toString() + { + return (string) $this->get_iri(); + } + + /** + * Overload __set() to provide access via properties + * + * @param string $name Property name + * @param mixed $value Property value + * @return void + */ + public function __set(string $name, $value) + { + $callable = [$this, 'set_' . $name]; + if (is_callable($callable)) { + call_user_func($callable, $value); + } elseif ( + $name === 'iauthority' + || $name === 'iuserinfo' + || $name === 'ihost' + || $name === 'ipath' + || $name === 'iquery' + || $name === 'ifragment' + ) { + call_user_func([$this, 'set_' . substr($name, 1)], $value); + } + } + + /** + * Overload __get() to provide access via properties + * + * @param string $name Property name + * @return mixed + */ + public function __get(string $name) + { + // isset() returns false for null, we don't want to do that + // Also why we use array_key_exists below instead of isset() + $props = get_object_vars($this); + + if ( + $name === 'iri' || + $name === 'uri' || + $name === 'iauthority' || + $name === 'authority' + ) { + $return = $this->{"get_$name"}(); + } elseif (array_key_exists($name, $props)) { + $return = $this->$name; + } + // host -> ihost + elseif (array_key_exists($prop = 'i' . $name, $props)) { + $name = $prop; + $return = $this->$prop; + } + // ischeme -> scheme + elseif (($prop = substr($name, 1)) && array_key_exists($prop, $props)) { + $name = $prop; + $return = $this->$prop; + } else { + trigger_error('Undefined property: ' . get_class($this) . '::' . $name, E_USER_NOTICE); + $return = null; + } + + if ($return === null && isset($this->normalization[$this->scheme][$name])) { + return $this->normalization[$this->scheme][$name]; + } + + return $return; + } + + /** + * Overload __isset() to provide access via properties + * + * @param string $name Property name + * @return bool + */ + public function __isset(string $name) + { + return method_exists($this, 'get_' . $name) || isset($this->$name); + } + + /** + * Overload __unset() to provide access via properties + * + * @param string $name Property name + * @return void + */ + public function __unset(string $name) + { + $callable = [$this, 'set_' . $name]; + if (is_callable($callable)) { + call_user_func($callable, ''); + } + } + + /** + * Create a new IRI object, from a specified string + * + * @param string|null $iri + */ + public function __construct(?string $iri = null) + { + $this->set_iri($iri); + } + + /** + * Clean up + * @return void + */ + public function __destruct() + { + $this->set_iri(null, true); + $this->set_path(null, true); + $this->set_authority(null, true); + } + + /** + * Create a new IRI object by resolving a relative IRI + * + * Returns false if $base is not absolute, otherwise an IRI. + * + * @param IRI|string $base (Absolute) Base IRI + * @param IRI|string $relative Relative IRI + * @return IRI|false + */ + public static function absolutize($base, $relative) + { + if (!($relative instanceof IRI)) { + $relative = new IRI($relative); + } + if (!$relative->is_valid()) { + return false; + } elseif ($relative->scheme !== null) { + return clone $relative; + } else { + if (!($base instanceof IRI)) { + $base = new IRI($base); + } + if ($base->scheme !== null && $base->is_valid()) { + if ($relative->get_iri() !== '') { + if ($relative->iuserinfo !== null || $relative->ihost !== null || $relative->port !== null) { + $target = clone $relative; + $target->scheme = $base->scheme; + } else { + $target = new IRI(); + $target->scheme = $base->scheme; + $target->iuserinfo = $base->iuserinfo; + $target->ihost = $base->ihost; + $target->port = $base->port; + if ($relative->ipath !== '') { + if ($relative->ipath[0] === '/') { + $target->ipath = $relative->ipath; + } elseif (($base->iuserinfo !== null || $base->ihost !== null || $base->port !== null) && $base->ipath === '') { + $target->ipath = '/' . $relative->ipath; + } elseif (($last_segment = strrpos($base->ipath, '/')) !== false) { + $target->ipath = substr($base->ipath, 0, $last_segment + 1) . $relative->ipath; + } else { + $target->ipath = $relative->ipath; + } + $target->ipath = $target->remove_dot_segments($target->ipath); + $target->iquery = $relative->iquery; + } else { + $target->ipath = $base->ipath; + if ($relative->iquery !== null) { + $target->iquery = $relative->iquery; + } elseif ($base->iquery !== null) { + $target->iquery = $base->iquery; + } + } + $target->ifragment = $relative->ifragment; + } + } else { + $target = clone $base; + $target->ifragment = null; + } + $target->scheme_normalization(); + return $target; + } + + return false; + } + } + + /** + * Parse an IRI into scheme/authority/path/query/fragment segments + * + * @param string $iri + * @return array{ + * scheme: string|null, + * authority: string|null, + * path: string, + * query: string|null, + * fragment: string|null, + * }|false + */ + protected function parse_iri(string $iri) + { + $iri = trim($iri, "\x20\x09\x0A\x0C\x0D"); + if (preg_match('/^(?:(?P<scheme>[^:\/?#]+):)?(:?\/\/(?P<authority>[^\/?#]*))?(?P<path>[^?#]*)(?:\?(?P<query>[^#]*))?(?:#(?P<fragment>.*))?$/', $iri, $match, \PREG_UNMATCHED_AS_NULL)) { + // TODO: Remove once we require PHP ≥ 7.4. + $match['query'] = $match['query'] ?? null; + $match['fragment'] = $match['fragment'] ?? null; + return $match; + } + + // This can occur when a paragraph is accidentally parsed as a URI + return false; + } + + /** + * Remove dot segments from a path + * + * @param string $input + * @return string + */ + protected function remove_dot_segments(string $input) + { + $output = ''; + while (strpos($input, './') !== false || strpos($input, '/.') !== false || $input === '.' || $input === '..') { + // A: If the input buffer begins with a prefix of "../" or "./", then remove that prefix from the input buffer; otherwise, + if (strpos($input, '../') === 0) { + $input = substr($input, 3); + } elseif (strpos($input, './') === 0) { + $input = substr($input, 2); + } + // B: if the input buffer begins with a prefix of "/./" or "/.", where "." is a complete path segment, then replace that prefix with "/" in the input buffer; otherwise, + elseif (strpos($input, '/./') === 0) { + $input = substr($input, 2); + } elseif ($input === '/.') { + $input = '/'; + } + // C: if the input buffer begins with a prefix of "/../" or "/..", where ".." is a complete path segment, then replace that prefix with "/" in the input buffer and remove the last segment and its preceding "/" (if any) from the output buffer; otherwise, + elseif (strpos($input, '/../') === 0) { + $input = substr($input, 3); + $output = substr_replace($output, '', intval(strrpos($output, '/'))); + } elseif ($input === '/..') { + $input = '/'; + $output = substr_replace($output, '', intval(strrpos($output, '/'))); + } + // D: if the input buffer consists only of "." or "..", then remove that from the input buffer; otherwise, + elseif ($input === '.' || $input === '..') { + $input = ''; + } + // E: move the first path segment in the input buffer to the end of the output buffer, including the initial "/" character (if any) and any subsequent characters up to, but not including, the next "/" character or the end of the input buffer + elseif (($pos = strpos($input, '/', 1)) !== false) { + $output .= substr($input, 0, $pos); + $input = substr_replace($input, '', 0, $pos); + } else { + $output .= $input; + $input = ''; + } + } + return $output . $input; + } + + /** + * Replace invalid character with percent encoding + * + * @param string $string Input string + * @param string $extra_chars Valid characters not in iunreserved or + * iprivate (this is ASCII-only) + * @param bool $iprivate Allow iprivate + * @return string + */ + protected function replace_invalid_with_pct_encoding(string $string, string $extra_chars, bool $iprivate = false) + { + // Normalize as many pct-encoded sections as possible + $string = preg_replace_callback('/(?:%[A-Fa-f0-9]{2})+/', [$this, 'remove_iunreserved_percent_encoded'], $string); + \assert(\is_string($string), "For PHPStan: Should not occur, the regex is valid"); + + // Replace invalid percent characters + $string = preg_replace('/%(?![A-Fa-f0-9]{2})/', '%25', $string); + \assert(\is_string($string), "For PHPStan: Should not occur, the regex is valid"); + + // Add unreserved and % to $extra_chars (the latter is safe because all + // pct-encoded sections are now valid). + $extra_chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~%'; + + // Now replace any bytes that aren't allowed with their pct-encoded versions + $position = 0; + $strlen = strlen($string); + while (($position += strspn($string, $extra_chars, $position)) < $strlen) { + $value = ord($string[$position]); + $character = 0; + + // Start position + $start = $position; + + // By default we are valid + $valid = true; + + // No one byte sequences are valid due to the while. + // Two byte sequence: + if (($value & 0xE0) === 0xC0) { + $character = ($value & 0x1F) << 6; + $length = 2; + $remaining = 1; + } + // Three byte sequence: + elseif (($value & 0xF0) === 0xE0) { + $character = ($value & 0x0F) << 12; + $length = 3; + $remaining = 2; + } + // Four byte sequence: + elseif (($value & 0xF8) === 0xF0) { + $character = ($value & 0x07) << 18; + $length = 4; + $remaining = 3; + } + // Invalid byte: + else { + $valid = false; + $length = 1; + $remaining = 0; + } + + if ($remaining) { + if ($position + $length <= $strlen) { + for ($position++; $remaining; $position++) { + $value = ord($string[$position]); + + // Check that the byte is valid, then add it to the character: + if (($value & 0xC0) === 0x80) { + $character |= ($value & 0x3F) << (--$remaining * 6); + } + // If it is invalid, count the sequence as invalid and reprocess the current byte: + else { + $valid = false; + $position--; + break; + } + } + } else { + $position = $strlen - 1; + $valid = false; + } + } + + // Percent encode anything invalid or not in ucschar + if ( + // Invalid sequences + !$valid + // Non-shortest form sequences are invalid + || $length > 1 && $character <= 0x7F + || $length > 2 && $character <= 0x7FF + || $length > 3 && $character <= 0xFFFF + // Outside of range of ucschar codepoints + // Noncharacters + || ($character & 0xFFFE) === 0xFFFE + || $character >= 0xFDD0 && $character <= 0xFDEF + || ( + // Everything else not in ucschar + $character > 0xD7FF && $character < 0xF900 + || $character < 0xA0 + || $character > 0xEFFFD + ) + && ( + // Everything not in iprivate, if it applies + !$iprivate + || $character < 0xE000 + || $character > 0x10FFFD + ) + ) { + // If we were a character, pretend we weren't, but rather an error. + if ($valid) { + $position--; + } + + for ($j = $start; $j <= $position; $j++) { + $string = substr_replace($string, sprintf('%%%02X', ord($string[$j])), $j, 1); + $j += 2; + $position += 2; + $strlen += 2; + } + } + } + + return $string; + } + + /** + * Callback function for preg_replace_callback. + * + * Removes sequences of percent encoded bytes that represent UTF-8 + * encoded characters in iunreserved + * + * @param array{string} $match PCRE match, a capture group #0 consisting of a sequence of valid percent-encoded bytes + * @return string Replacement + */ + protected function remove_iunreserved_percent_encoded(array $match) + { + // As we just have valid percent encoded sequences we can just explode + // and ignore the first member of the returned array (an empty string). + $bytes = explode('%', $match[0]); + + // Initialize the new string (this is what will be returned) and that + // there are no bytes remaining in the current sequence (unsurprising + // at the first byte!). + $string = ''; + $remaining = 0; + + // these variables will be initialized in the loop but PHPStan is not able to detect it currently + $start = 0; + $character = 0; + $length = 0; + $valid = true; + + // Loop over each and every byte, and set $value to its value + for ($i = 1, $len = count($bytes); $i < $len; $i++) { + $value = hexdec($bytes[$i]); + + // If we're the first byte of sequence: + if (!$remaining) { + // Start position + $start = $i; + + // By default we are valid + $valid = true; + + // One byte sequence: + if ($value <= 0x7F) { + $character = $value; + $length = 1; + } + // Two byte sequence: + elseif (($value & 0xE0) === 0xC0) { + $character = ($value & 0x1F) << 6; + $length = 2; + $remaining = 1; + } + // Three byte sequence: + elseif (($value & 0xF0) === 0xE0) { + $character = ($value & 0x0F) << 12; + $length = 3; + $remaining = 2; + } + // Four byte sequence: + elseif (($value & 0xF8) === 0xF0) { + $character = ($value & 0x07) << 18; + $length = 4; + $remaining = 3; + } + // Invalid byte: + else { + $valid = false; + $remaining = 0; + } + } + // Continuation byte: + else { + // Check that the byte is valid, then add it to the character: + if (($value & 0xC0) === 0x80) { + $remaining--; + $character |= ($value & 0x3F) << ($remaining * 6); + } + // If it is invalid, count the sequence as invalid and reprocess the current byte as the start of a sequence: + else { + $valid = false; + $remaining = 0; + $i--; + } + } + + // If we've reached the end of the current byte sequence, append it to Unicode::$data + if (!$remaining) { + // Percent encode anything invalid or not in iunreserved + if ( + // Invalid sequences + !$valid + // Non-shortest form sequences are invalid + || $length > 1 && $character <= 0x7F + || $length > 2 && $character <= 0x7FF + || $length > 3 && $character <= 0xFFFF + // Outside of range of iunreserved codepoints + || $character < 0x2D + || $character > 0xEFFFD + // Noncharacters + || ($character & 0xFFFE) === 0xFFFE + || $character >= 0xFDD0 && $character <= 0xFDEF + // Everything else not in iunreserved (this is all BMP) + || $character === 0x2F + || $character > 0x39 && $character < 0x41 + || $character > 0x5A && $character < 0x61 + || $character > 0x7A && $character < 0x7E + || $character > 0x7E && $character < 0xA0 + || $character > 0xD7FF && $character < 0xF900 + ) { + for ($j = $start; $j <= $i; $j++) { + $string .= '%' . strtoupper($bytes[$j]); + } + } else { + for ($j = $start; $j <= $i; $j++) { + // Cast for PHPStan, this will always be a number between 0 and 0xFF so hexdec will return int. + $string .= chr((int) hexdec($bytes[$j])); + } + } + } + } + + // If we have any bytes left over they are invalid (i.e., we are + // mid-way through a multi-byte sequence) + if ($remaining) { + for ($j = $start; $j < $len; $j++) { + $string .= '%' . strtoupper($bytes[$j]); + } + } + + return $string; + } + + /** + * @return void + */ + protected function scheme_normalization() + { + if (isset($this->normalization[$this->scheme]['iuserinfo']) && $this->iuserinfo === $this->normalization[$this->scheme]['iuserinfo']) { + $this->iuserinfo = null; + } + if (isset($this->normalization[$this->scheme]['ihost']) && $this->ihost === $this->normalization[$this->scheme]['ihost']) { + $this->ihost = null; + } + if (isset($this->normalization[$this->scheme]['port']) && $this->port === $this->normalization[$this->scheme]['port']) { + $this->port = null; + } + if (isset($this->normalization[$this->scheme]['ipath']) && $this->ipath === $this->normalization[$this->scheme]['ipath']) { + $this->ipath = ''; + } + if (isset($this->normalization[$this->scheme]['iquery']) && $this->iquery === $this->normalization[$this->scheme]['iquery']) { + $this->iquery = null; + } + if (isset($this->normalization[$this->scheme]['ifragment']) && $this->ifragment === $this->normalization[$this->scheme]['ifragment']) { + $this->ifragment = null; + } + } + + /** + * Check if the object represents a valid IRI. This needs to be done on each + * call as some things change depending on another part of the IRI. + * + * @return bool + */ + public function is_valid() + { + if ($this->ipath === '') { + return true; + } + + $isauthority = $this->iuserinfo !== null || $this->ihost !== null || + $this->port !== null; + if ($isauthority && $this->ipath[0] === '/') { + return true; + } + + if (!$isauthority && (substr($this->ipath, 0, 2) === '//')) { + return false; + } + + // Relative urls cannot have a colon in the first path segment (and the + // slashes themselves are not included so skip the first character). + if (!$this->scheme && !$isauthority && + strpos($this->ipath, ':') !== false && + strpos($this->ipath, '/', 1) !== false && + strpos($this->ipath, ':') < strpos($this->ipath, '/', 1)) { + return false; + } + + return true; + } + + /** + * Set the entire IRI. Returns true on success, false on failure (if there + * are any invalid characters). + * + * @param string|null $iri + * @return bool + */ + public function set_iri(?string $iri, bool $clear_cache = false) + { + static $cache; + if ($clear_cache) { + $cache = null; + return false; + } + if (!$cache) { + $cache = []; + } + + if ($iri === null) { + return true; + } elseif (isset($cache[$iri])) { + [ + $this->scheme, + $this->iuserinfo, + $this->ihost, + $this->port, + $this->ipath, + $this->iquery, + $this->ifragment, + $return + ] = $cache[$iri]; + + return $return; + } + + $parsed = $this->parse_iri((string) $iri); + if (!$parsed) { + return false; + } + + $return = $this->set_scheme($parsed['scheme']) + && $this->set_authority($parsed['authority']) + && $this->set_path($parsed['path']) + && $this->set_query($parsed['query']) + && $this->set_fragment($parsed['fragment']); + + $cache[$iri] = [ + $this->scheme, + $this->iuserinfo, + $this->ihost, + $this->port, + $this->ipath, + $this->iquery, + $this->ifragment, + $return + ]; + + return $return; + } + + /** + * Set the scheme. Returns true on success, false on failure (if there are + * any invalid characters). + * + * @param string|null $scheme + * @return bool + */ + public function set_scheme(?string $scheme) + { + if ($scheme === null) { + $this->scheme = null; + } elseif (!preg_match('/^[A-Za-z][0-9A-Za-z+\-.]*$/', $scheme)) { + $this->scheme = null; + return false; + } else { + $this->scheme = strtolower($scheme); + } + return true; + } + + /** + * Set the authority. Returns true on success, false on failure (if there are + * any invalid characters). + * + * @param string|null $authority + * @return bool + */ + public function set_authority(?string $authority, bool $clear_cache = false) + { + static $cache; + if ($clear_cache) { + $cache = null; + return false; + } + if (!$cache) { + $cache = []; + } + + if ($authority === null) { + $this->iuserinfo = null; + $this->ihost = null; + $this->port = null; + return true; + } elseif (isset($cache[$authority])) { + [ + $this->iuserinfo, + $this->ihost, + $this->port, + $return + ] = $cache[$authority]; + + return $return; + } + + $remaining = $authority; + if (($iuserinfo_end = strrpos($remaining, '@')) !== false) { + // Cast for PHPStan on PHP < 8.0. It does not detect that + // the range is not flipped so substr cannot return false. + $iuserinfo = (string) substr($remaining, 0, $iuserinfo_end); + $remaining = substr($remaining, $iuserinfo_end + 1); + } else { + $iuserinfo = null; + } + if (($port_start = strpos($remaining, ':', intval(strpos($remaining, ']')))) !== false) { + $port = substr($remaining, $port_start + 1); + if ($port === false) { + $port = null; + } + $remaining = substr($remaining, 0, $port_start); + } else { + $port = null; + } + + $return = $this->set_userinfo($iuserinfo) && + $this->set_host($remaining) && + $this->set_port($port); + + $cache[$authority] = [ + $this->iuserinfo, + $this->ihost, + $this->port, + $return + ]; + + return $return; + } + + /** + * Set the iuserinfo. + * + * @param string|null $iuserinfo + * @return bool + */ + public function set_userinfo(?string $iuserinfo) + { + if ($iuserinfo === null) { + $this->iuserinfo = null; + } else { + $this->iuserinfo = $this->replace_invalid_with_pct_encoding($iuserinfo, '!$&\'()*+,;=:'); + $this->scheme_normalization(); + } + + return true; + } + + /** + * Set the ihost. Returns true on success, false on failure (if there are + * any invalid characters). + * + * @param string|null $ihost + * @return bool + */ + public function set_host(?string $ihost) + { + if ($ihost === null) { + $this->ihost = null; + return true; + } elseif (substr($ihost, 0, 1) === '[' && substr($ihost, -1) === ']') { + if (\SimplePie\Net\IPv6::check_ipv6(substr($ihost, 1, -1))) { + $this->ihost = '[' . \SimplePie\Net\IPv6::compress(substr($ihost, 1, -1)) . ']'; + } else { + $this->ihost = null; + return false; + } + } else { + $ihost = $this->replace_invalid_with_pct_encoding($ihost, '!$&\'()*+,;='); + + // Lowercase, but ignore pct-encoded sections (as they should + // remain uppercase). This must be done after the previous step + // as that can add unescaped characters. + $position = 0; + $strlen = strlen($ihost); + while (($position += strcspn($ihost, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ%', $position)) < $strlen) { + if ($ihost[$position] === '%') { + $position += 3; + } else { + $ihost[$position] = strtolower($ihost[$position]); + $position++; + } + } + + $this->ihost = $ihost; + } + + $this->scheme_normalization(); + + return true; + } + + /** + * Set the port. Returns true on success, false on failure (if there are + * any invalid characters). + * + * @param string|int|null $port + * @return bool + */ + public function set_port($port) + { + if ($port === null) { + $this->port = null; + return true; + } elseif (strspn((string) $port, '0123456789') === strlen((string) $port)) { + $this->port = (int) $port; + $this->scheme_normalization(); + return true; + } + + $this->port = null; + return false; + } + + /** + * Set the ipath. + * + * @param string|null $ipath + * @return bool + */ + public function set_path(?string $ipath, bool $clear_cache = false) + { + static $cache; + if ($clear_cache) { + $cache = null; + return false; + } + if (!$cache) { + $cache = []; + } + + $ipath = (string) $ipath; + + if (isset($cache[$ipath])) { + $this->ipath = $cache[$ipath][(int) ($this->scheme !== null)]; + } else { + $valid = $this->replace_invalid_with_pct_encoding($ipath, '!$&\'()*+,;=@:/'); + $removed = $this->remove_dot_segments($valid); + + $cache[$ipath] = [$valid, $removed]; + $this->ipath = ($this->scheme !== null) ? $removed : $valid; + } + + $this->scheme_normalization(); + return true; + } + + /** + * Set the iquery. + * + * @param string|null $iquery + * @return bool + */ + public function set_query(?string $iquery) + { + if ($iquery === null) { + $this->iquery = null; + } else { + $this->iquery = $this->replace_invalid_with_pct_encoding($iquery, '!$&\'()*+,;=:@/?', true); + $this->scheme_normalization(); + } + return true; + } + + /** + * Set the ifragment. + * + * @param string|null $ifragment + * @return bool + */ + public function set_fragment(?string $ifragment) + { + if ($ifragment === null) { + $this->ifragment = null; + } else { + $this->ifragment = $this->replace_invalid_with_pct_encoding($ifragment, '!$&\'()*+,;=:@/?'); + $this->scheme_normalization(); + } + return true; + } + + /** + * Convert an IRI to a URI (or parts thereof) + * + * @param string $string + * @return string + */ + public function to_uri(string $string) + { + static $non_ascii; + if (!$non_ascii) { + $non_ascii = implode('', range("\x80", "\xFF")); + } + + $position = 0; + $strlen = strlen($string); + while (($position += strcspn($string, $non_ascii, $position)) < $strlen) { + $string = substr_replace($string, sprintf('%%%02X', ord($string[$position])), $position, 1); + $position += 3; + $strlen += 2; + } + + return $string; + } + + /** + * Get the complete IRI + * + * @return string|false + */ + public function get_iri() + { + if (!$this->is_valid()) { + return false; + } + + $iri = ''; + if ($this->scheme !== null) { + $iri .= $this->scheme . ':'; + } + if (($iauthority = $this->get_iauthority()) !== null) { + $iri .= '//' . $iauthority; + } + if ($this->ipath !== '') { + $iri .= $this->ipath; + } elseif (!empty($this->normalization[$this->scheme]['ipath']) && $iauthority !== null && $iauthority !== '') { + $iri .= $this->normalization[$this->scheme]['ipath']; + } + if ($this->iquery !== null) { + $iri .= '?' . $this->iquery; + } + if ($this->ifragment !== null) { + $iri .= '#' . $this->ifragment; + } + + return $iri; + } + + /** + * Get the complete URI + * + * @return string + */ + public function get_uri() + { + return $this->to_uri((string) $this->get_iri()); + } + + /** + * Get the complete iauthority + * + * @return ?string + */ + protected function get_iauthority() + { + if ($this->iuserinfo !== null || $this->ihost !== null || $this->port !== null) { + $iauthority = ''; + if ($this->iuserinfo !== null) { + $iauthority .= $this->iuserinfo . '@'; + } + if ($this->ihost !== null) { + $iauthority .= $this->ihost; + } + if ($this->port !== null && $this->port !== 0) { + $iauthority .= ':' . $this->port; + } + return $iauthority; + } + + return null; + } + + /** + * Get the complete authority + * + * @return ?string + */ + protected function get_authority() + { + $iauthority = $this->get_iauthority(); + if (is_string($iauthority)) { + return $this->to_uri($iauthority); + } + + return $iauthority; + } +} + +class_alias('SimplePie\IRI', 'SimplePie_IRI'); + +} + +namespace SimplePie { + +/** + * Manages all item-related data + * + * Used by {@see \SimplePie\SimplePie::get_item()} and {@see \SimplePie\SimplePie::get_items()} + * + * This class can be overloaded with {@see \SimplePie\SimplePie::set_item_class()} + */ +class Item implements RegistryAware +{ + /** + * Parent feed + * + * @access private + * @var \SimplePie\SimplePie + */ + public $feed; + + /** + * Raw data + * + * @access private + * @var array<string, mixed> + */ + public $data = []; + + /** + * Registry object + * + * @see set_registry + * @var \SimplePie\Registry + */ + protected $registry; + + /** + * @var Sanitize|null + */ + private $sanitize = null; + + /** + * Create a new item object + * + * This is usually used by {@see \SimplePie\SimplePie::get_items} and + * {@see \SimplePie\SimplePie::get_item}. Avoid creating this manually. + * + * @param \SimplePie\SimplePie $feed Parent feed + * @param array<string, mixed> $data Raw data + */ + public function __construct(\SimplePie\SimplePie $feed, array $data) + { + $this->feed = $feed; + $this->data = $data; + } + + /** + * Set the registry handler + * + * This is usually used by {@see \SimplePie\Registry::create} + * + * @since 1.3 + * @param \SimplePie\Registry $registry + * @return void + */ + public function set_registry(\SimplePie\Registry $registry) + { + $this->registry = $registry; + } + + /** + * Get a string representation of the item + * + * @return string + */ + public function __toString() + { + return md5(serialize($this->data)); + } + + /** + * Remove items that link back to this before destroying this object + */ + public function __destruct() + { + if (!gc_enabled()) { + unset($this->feed); + } + } + + /** + * Get data for an item-level element + * + * This method allows you to get access to ANY element/attribute that is a + * sub-element of the item/entry tag. + * + * See {@see \SimplePie\SimplePie::get_feed_tags()} for a description of the return value + * + * @since 1.0 + * @see http://simplepie.org/wiki/faq/supported_xml_namespaces + * @param string $namespace The URL of the XML namespace of the elements you're trying to access + * @param string $tag Tag name + * @return array<array<string, mixed>>|null + */ + public function get_item_tags(string $namespace, string $tag) + { + if (isset($this->data['child'][$namespace][$tag])) { + return $this->data['child'][$namespace][$tag]; + } + + return null; + } + + /** + * Get base URL of the item itself. + * Returns `<xml:base>` or feed base URL. + * Similar to `Item::get_base()` but can safely be used during initialisation methods + * such as `Item::get_links()` (`Item::get_base()` and `Item::get_links()` call each-other) + * and is not affected by enclosures. + * + * @param array<string, mixed> $element + * @see get_base + */ + private function get_own_base(array $element = []): string + { + if (!empty($element['xml_base_explicit']) && isset($element['xml_base'])) { + return $element['xml_base']; + } + return $this->feed->get_base(); + } + + /** + * Get the base URL value. + * Uses `<xml:base>`, or item link, or enclosure link, or feed base URL. + * + * @param array<string, mixed> $element + * @return string + */ + public function get_base(array $element = []) + { + if (!empty($element['xml_base_explicit']) && isset($element['xml_base'])) { + return $element['xml_base']; + } + $link = $this->get_permalink(); + if ($link != null) { + return $link; + } + return $this->feed->get_base($element); + } + + /** + * Sanitize feed data + * + * @access private + * @see \SimplePie\SimplePie::sanitize() + * @param string $data Data to sanitize + * @param int-mask-of<SimplePie::CONSTRUCT_*> $type + * @param string $base Base URL to resolve URLs against + * @return string Sanitized data + */ + public function sanitize(string $data, int $type, string $base = '') + { + // This really returns string|false but changing encoding is uncommon and we are going to deprecate it, so let’s just lie to PHPStan in the interest of cleaner annotations. + return $this->feed->sanitize($data, $type, $base); + } + + /** + * Get the parent feed + * + * Note: this may not work as you think for multifeeds! + * + * @link http://simplepie.org/faq/typical_multifeed_gotchas#missing_data_from_feed + * @since 1.0 + * @return \SimplePie\SimplePie + */ + public function get_feed() + { + return $this->feed; + } + + /** + * Get the unique identifier for the item + * + * This is usually used when writing code to check for new items in a feed. + * + * Uses `<atom:id>`, `<guid>`, `<dc:identifier>` or the `about` attribute + * for RDF. If none of these are supplied (or `$hash` is true), creates an + * MD5 hash based on the permalink, title and content. + * + * @since Beta 2 + * @param bool $hash Should we force using a hash instead of the supplied ID? + * @param string|false $fn User-supplied function to generate an hash + * @return string|null + */ + public function get_id(bool $hash = false, $fn = 'md5') + { + if (!$hash) { + if ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'id')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'id')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, 'guid')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_DC_11, 'identifier')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_DC_10, 'identifier')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif (isset($this->data['attribs'][\SimplePie\SimplePie::NAMESPACE_RDF]['about'])) { + return $this->sanitize($this->data['attribs'][\SimplePie\SimplePie::NAMESPACE_RDF]['about'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + } + if ($fn === false) { + return null; + } elseif (!is_callable($fn)) { + trigger_error('User-supplied function $fn must be callable', E_USER_WARNING); + $fn = 'md5'; + } + return call_user_func( + $fn, + $this->get_permalink().$this->get_title().$this->get_content() + ); + } + + /** + * Get the title of the item + * + * Uses `<atom:title>`, `<title>` or `<dc:title>` + * + * @since Beta 2 (previously called `get_item_title` since 0.8) + * @return string|null + */ + public function get_title() + { + if (!isset($this->data['title'])) { + if ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'title')) { + $this->data['title'] = $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'title')) { + $this->data['title'] = $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_10, 'title')) { + $this->data['title'] = $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_090, 'title')) { + $this->data['title'] = $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, 'title')) { + $this->data['title'] = $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_DC_11, 'title')) { + $this->data['title'] = $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_DC_10, 'title')) { + $this->data['title'] = $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $this->data['title'] = null; + } + } + return $this->data['title']; + } + + /** + * Get the content for the item + * + * Prefers summaries over full content , but will return full content if a + * summary does not exist. + * + * To prefer full content instead, use {@see get_content} + * + * Uses `<atom:summary>`, `<description>`, `<dc:description>` or + * `<itunes:subtitle>` + * + * @since 0.8 + * @param bool $description_only Should we avoid falling back to the content? + * @return string|null + */ + public function get_description(bool $description_only = false) + { + if (($tags = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'summary')) && + ($return = $this->sanitize($tags[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$tags[0]['attribs']]), $this->get_base($tags[0])))) { + return $return; + } elseif (($tags = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'summary')) && + ($return = $this->sanitize($tags[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$tags[0]['attribs']]), $this->get_base($tags[0])))) { + return $return; + } elseif (($tags = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_10, 'description')) && + ($return = $this->sanitize($tags[0]['data'], \SimplePie\SimplePie::CONSTRUCT_MAYBE_HTML, $this->get_base($tags[0])))) { + return $return; + } elseif (($tags = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, 'description')) && + ($return = $this->sanitize($tags[0]['data'], \SimplePie\SimplePie::CONSTRUCT_HTML, $this->get_base($tags[0])))) { + return $return; + } elseif (($tags = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_DC_11, 'description')) && + ($return = $this->sanitize($tags[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT))) { + return $return; + } elseif (($tags = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_DC_10, 'description')) && + ($return = $this->sanitize($tags[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT))) { + return $return; + } elseif (($tags = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'summary')) && + ($return = $this->sanitize($tags[0]['data'], \SimplePie\SimplePie::CONSTRUCT_HTML, $this->get_base($tags[0])))) { + return $return; + } elseif (($tags = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'subtitle')) && + ($return = $this->sanitize($tags[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT))) { + return $return; + } elseif (($tags = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_090, 'description')) && + ($return = $this->sanitize($tags[0]['data'], \SimplePie\SimplePie::CONSTRUCT_HTML))) { + return $return; + } elseif (!$description_only) { + return $this->get_content(true); + } + + return null; + } + + /** + * Get the content for the item + * + * Prefers full content over summaries, but will return a summary if full + * content does not exist. + * + * To prefer summaries instead, use {@see get_description} + * + * Uses `<atom:content>` or `<content:encoded>` (RSS 1.0 Content Module) + * + * @since 1.0 + * @param bool $content_only Should we avoid falling back to the description? + * @return string|null + */ + public function get_content(bool $content_only = false) + { + if (($tags = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'content')) && + ($return = $this->sanitize($tags[0]['data'], $this->registry->call(Misc::class, 'atom_10_content_construct_type', [$tags[0]['attribs']]), $this->get_base($tags[0])))) { + return $return; + } elseif (($tags = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'content')) && + ($return = $this->sanitize($tags[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$tags[0]['attribs']]), $this->get_base($tags[0])))) { + return $return; + } elseif (($tags = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_10_MODULES_CONTENT, 'encoded')) && + ($return = $this->sanitize($tags[0]['data'], \SimplePie\SimplePie::CONSTRUCT_HTML, $this->get_base($tags[0])))) { + return $return; + } elseif (!$content_only) { + return $this->get_description(true); + } + + return null; + } + + /** + * Get the media:thumbnail of the item + * + * Uses `<media:thumbnail>` + * + * + * @return array{url: string, height?: string, width?: string, time?: string}|null + */ + public function get_thumbnail() + { + if (!isset($this->data['thumbnail'])) { + if ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'thumbnail')) { + $thumbnail = $return[0]['attribs']['']; + if (empty($thumbnail['url'])) { + $this->data['thumbnail'] = null; + } else { + $thumbnail['url'] = $this->sanitize($thumbnail['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($return[0])); + $this->data['thumbnail'] = $thumbnail; + } + } else { + $this->data['thumbnail'] = null; + } + } + return $this->data['thumbnail']; + } + + /** + * Get a category for the item + * + * @since Beta 3 (previously called `get_categories()` since Beta 2) + * @param int $key The category that you want to return. Remember that arrays begin with 0, not 1 + * @return \SimplePie\Category|null + */ + public function get_category(int $key = 0) + { + $categories = $this->get_categories(); + if (isset($categories[$key])) { + return $categories[$key]; + } + + return null; + } + + /** + * Get all categories for the item + * + * Uses `<atom:category>`, `<category>` or `<dc:subject>` + * + * @since Beta 3 + * @return \SimplePie\Category[]|null List of {@see \SimplePie\Category} objects + */ + public function get_categories() + { + $categories = []; + + $type = 'category'; + foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, $type) as $category) { + $term = null; + $scheme = null; + $label = null; + if (isset($category['attribs']['']['term'])) { + $term = $this->sanitize($category['attribs']['']['term'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['scheme'])) { + $scheme = $this->sanitize($category['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['label'])) { + $label = $this->sanitize($category['attribs']['']['label'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $categories[] = $this->registry->create(Category::class, [$term, $scheme, $label, $type]); + } + foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, $type) as $category) { + // This is really the label, but keep this as the term also for BC. + // Label will also work on retrieving because that falls back to term. + $term = $this->sanitize($category['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + if (isset($category['attribs']['']['domain'])) { + $scheme = $this->sanitize($category['attribs']['']['domain'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $scheme = null; + } + $categories[] = $this->registry->create(Category::class, [$term, $scheme, null, $type]); + } + + $type = 'subject'; + foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_DC_11, $type) as $category) { + $categories[] = $this->registry->create(Category::class, [$this->sanitize($category['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT), null, null, $type]); + } + foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_DC_10, $type) as $category) { + $categories[] = $this->registry->create(Category::class, [$this->sanitize($category['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT), null, null, $type]); + } + + if (!empty($categories)) { + return array_unique($categories); + } + + return null; + } + + /** + * Get an author for the item + * + * @since Beta 2 + * @param int $key The author that you want to return. Remember that arrays begin with 0, not 1 + * @return \SimplePie\Author|null + */ + public function get_author(int $key = 0) + { + $authors = $this->get_authors(); + if (isset($authors[$key])) { + return $authors[$key]; + } + + return null; + } + + /** + * Get a contributor for the item + * + * @since 1.1 + * @param int $key The contrbutor that you want to return. Remember that arrays begin with 0, not 1 + * @return \SimplePie\Author|null + */ + public function get_contributor(int $key = 0) + { + $contributors = $this->get_contributors(); + if (isset($contributors[$key])) { + return $contributors[$key]; + } + + return null; + } + + /** + * Get all contributors for the item + * + * Uses `<atom:contributor>` + * + * @since 1.1 + * @return \SimplePie\Author[]|null List of {@see \SimplePie\Author} objects + */ + public function get_contributors() + { + $contributors = []; + foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'contributor') as $contributor) { + $name = null; + $uri = null; + $email = null; + if (isset($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['name'][0]['data'])) { + $name = $this->sanitize($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['name'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['uri'][0]['data'])) { + $uri = $contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['uri'][0]; + $uri = $this->sanitize($uri['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($uri)); + } + if (isset($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['email'][0]['data'])) { + $email = $this->sanitize($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['email'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $uri !== null) { + $contributors[] = $this->registry->create(Author::class, [$name, $uri, $email]); + } + } + foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'contributor') as $contributor) { + $name = null; + $url = null; + $email = null; + if (isset($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['name'][0]['data'])) { + $name = $this->sanitize($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['name'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['url'][0]['data'])) { + $url = $contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['url'][0]; + $url = $this->sanitize($url['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($url)); + } + if (isset($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['email'][0]['data'])) { + $email = $this->sanitize($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['email'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $url !== null) { + $contributors[] = $this->registry->create(Author::class, [$name, $url, $email]); + } + } + + if (!empty($contributors)) { + return array_unique($contributors); + } + + return null; + } + + /** + * Get all authors for the item + * + * Uses `<atom:author>`, `<author>`, `<dc:creator>` or `<itunes:author>` + * + * @since Beta 2 + * @return \SimplePie\Author[]|null List of {@see \SimplePie\Author} objects + */ + public function get_authors() + { + $authors = []; + foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'author') as $author) { + $name = null; + $uri = null; + $email = null; + if (isset($author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['name'][0]['data'])) { + $name = $this->sanitize($author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['name'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['uri'][0]['data'])) { + $uri = $author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['uri'][0]; + $uri = $this->sanitize($uri['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($uri)); + } + if (isset($author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['email'][0]['data'])) { + $email = $this->sanitize($author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['email'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $uri !== null) { + $authors[] = $this->registry->create(Author::class, [$name, $uri, $email]); + } + } + if ($author = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'author')) { + $name = null; + $url = null; + $email = null; + if (isset($author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['name'][0]['data'])) { + $name = $this->sanitize($author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['name'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['url'][0]['data'])) { + $url = $author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['url'][0]; + $url = $this->sanitize($url['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($url)); + } + if (isset($author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['email'][0]['data'])) { + $email = $this->sanitize($author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['email'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $url !== null) { + $authors[] = $this->registry->create(Author::class, [$name, $url, $email]); + } + } + if ($author = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, 'author')) { + $authors[] = $this->registry->create(Author::class, [null, null, $this->sanitize($author[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT)]); + } + foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_DC_11, 'creator') as $author) { + $authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT), null, null]); + } + foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_DC_10, 'creator') as $author) { + $authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT), null, null]); + } + foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'author') as $author) { + $authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT), null, null]); + } + + if (!empty($authors)) { + return array_unique($authors); + } elseif (($source = $this->get_source()) && ($authors = $source->get_authors())) { + return $authors; + } elseif ($authors = $this->feed->get_authors()) { + return $authors; + } + + return null; + } + + /** + * Get the copyright info for the item + * + * Uses `<atom:rights>` or `<dc:rights>` + * + * @since 1.1 + * @return string|null + */ + public function get_copyright() + { + if ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'rights')) { + return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_DC_11, 'rights')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_DC_10, 'rights')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + + return null; + } + + /** + * Get the posting date/time for the item + * + * Uses `<atom:published>`, `<atom:updated>`, `<atom:issued>`, + * `<atom:modified>`, `<pubDate>` or `<dc:date>` + * + * Note: obeys PHP's timezone setting. To get a UTC date/time, use + * {@see get_gmdate} + * + * @since Beta 2 (previously called `get_item_date` since 0.8) + * + * @param string $date_format Supports any PHP date format from {@see http://php.net/date} (empty for the raw data) + * @return ($date_format is 'U' ? ?int : ?string) + */ + public function get_date(string $date_format = 'j F Y, g:i a') + { + if (!isset($this->data['date'])) { + if ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'published')) { + $this->data['date']['raw'] = $return[0]['data']; + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, 'pubDate')) { + $this->data['date']['raw'] = $return[0]['data']; + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_DC_11, 'date')) { + $this->data['date']['raw'] = $return[0]['data']; + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_DC_10, 'date')) { + $this->data['date']['raw'] = $return[0]['data']; + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'updated')) { + $this->data['date']['raw'] = $return[0]['data']; + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'issued')) { + $this->data['date']['raw'] = $return[0]['data']; + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'created')) { + $this->data['date']['raw'] = $return[0]['data']; + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'modified')) { + $this->data['date']['raw'] = $return[0]['data']; + } + + if (!empty($this->data['date']['raw'])) { + $parser = $this->registry->call(Parse\Date::class, 'get'); + $this->data['date']['parsed'] = $parser->parse($this->data['date']['raw']) ?: null; + } else { + $this->data['date'] = null; + } + } + if ($this->data['date']) { + switch ($date_format) { + case '': + return $this->sanitize($this->data['date']['raw'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + + case 'U': + return $this->data['date']['parsed']; + + default: + return date($date_format, $this->data['date']['parsed']); + } + } + + return null; + } + + /** + * Get the update date/time for the item + * + * Uses `<atom:updated>` + * + * Note: obeys PHP's timezone setting. To get a UTC date/time, use + * {@see get_gmdate} + * + * @param string $date_format Supports any PHP date format from {@see http://php.net/date} (empty for the raw data) + * @return ($date_format is 'U' ? ?int : ?string) + */ + public function get_updated_date(string $date_format = 'j F Y, g:i a') + { + if (!isset($this->data['updated'])) { + if ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'updated')) { + $this->data['updated']['raw'] = $return[0]['data']; + } + + if (!empty($this->data['updated']['raw'])) { + $parser = $this->registry->call(Parse\Date::class, 'get'); + $this->data['updated']['parsed'] = $parser->parse($this->data['updated']['raw']) ?: null; + } else { + $this->data['updated'] = null; + } + } + if ($this->data['updated']) { + switch ($date_format) { + case '': + return $this->sanitize($this->data['updated']['raw'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + + case 'U': + return $this->data['updated']['parsed']; + + default: + return date($date_format, $this->data['updated']['parsed']); + } + } + + return null; + } + + /** + * Get the localized posting date/time for the item + * + * Returns the date formatted in the localized language. To display in + * languages other than the server's default, you need to change the locale + * with {@link http://php.net/setlocale setlocale()}. The available + * localizations depend on which ones are installed on your web server. + * + * @since 1.0 + * + * @param string $date_format Supports any PHP date format from {@see http://php.net/strftime} (empty for the raw data) + * @return string|null|false see `strftime` for when this can return `false` + */ + public function get_local_date(string $date_format = '%c') + { + if ($date_format === '') { + if (($raw_date = $this->get_date('')) === null) { + return null; + } + + return $this->sanitize($raw_date, \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif (($date = $this->get_date('U')) !== null && $date !== false) { + return strftime($date_format, $date); + } + + return null; + } + + /** + * Get the posting date/time for the item (UTC time) + * + * @see get_date + * @param string $date_format Supports any PHP date format from {@see http://php.net/date} + * @return string|null + */ + public function get_gmdate(string $date_format = 'j F Y, g:i a') + { + $date = $this->get_date('U'); + if ($date === null) { + return null; + } + + return gmdate($date_format, $date); + } + + /** + * Get the update date/time for the item (UTC time) + * + * @see get_updated_date + * @param string $date_format Supports any PHP date format from {@see http://php.net/date} + * @return string|null + */ + public function get_updated_gmdate(string $date_format = 'j F Y, g:i a') + { + $date = $this->get_updated_date('U'); + if ($date === null) { + return null; + } + + return gmdate($date_format, $date); + } + + /** + * Get the permalink for the item + * + * Returns the first link available with a relationship of "alternate". + * Identical to {@see get_link()} with key 0 + * + * @see get_link + * @since 0.8 + * @return string|null Permalink URL + */ + public function get_permalink() + { + $link = $this->get_link(); + $enclosure = $this->get_enclosure(0); + if ($link !== null) { + return $link; + } elseif ($enclosure !== null) { + return $enclosure->get_link(); + } + + return null; + } + + /** + * Get a single link for the item + * + * @since Beta 3 + * @param int $key The link that you want to return. Remember that arrays begin with 0, not 1 + * @param string $rel The relationship of the link to return + * @return string|null Link URL + */ + public function get_link(int $key = 0, string $rel = 'alternate') + { + $links = $this->get_links($rel); + if ($links && $links[$key] !== null) { + return $links[$key]; + } + + return null; + } + + /** + * Get all links for the item + * + * Uses `<atom:link>`, `<link>` or `<guid>` + * + * @since Beta 2 + * @param string $rel The relationship of links to return + * @return array<string>|null Links found for the item (strings) + */ + public function get_links(string $rel = 'alternate') + { + if (!isset($this->data['links'])) { + $this->data['links'] = []; + foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'link') as $link) { + if (isset($link['attribs']['']['href'])) { + $link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate'; + $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($link)); + } + } + foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'link') as $link) { + if (isset($link['attribs']['']['href'])) { + $link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate'; + $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($link)); + } + } + if ($links = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_10, 'link')) { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($links[0])); + } + if ($links = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_090, 'link')) { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($links[0])); + } + if ($links = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, 'link')) { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($links[0])); + } + if ($links = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, 'guid')) { + if (!isset($links[0]['attribs']['']['isPermaLink']) || strtolower(trim($links[0]['attribs']['']['isPermaLink'])) === 'true') { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($links[0])); + } + } + + $keys = array_keys($this->data['links']); + foreach ($keys as $key) { + if ($this->registry->call(Misc::class, 'is_isegment_nz_nc', [$key])) { + if (isset($this->data['links'][\SimplePie\SimplePie::IANA_LINK_RELATIONS_REGISTRY . $key])) { + $this->data['links'][\SimplePie\SimplePie::IANA_LINK_RELATIONS_REGISTRY . $key] = array_merge($this->data['links'][$key], $this->data['links'][\SimplePie\SimplePie::IANA_LINK_RELATIONS_REGISTRY . $key]); + $this->data['links'][$key] = &$this->data['links'][\SimplePie\SimplePie::IANA_LINK_RELATIONS_REGISTRY . $key]; + } else { + $this->data['links'][\SimplePie\SimplePie::IANA_LINK_RELATIONS_REGISTRY . $key] = &$this->data['links'][$key]; + } + } elseif (substr((string) $key, 0, 41) === \SimplePie\SimplePie::IANA_LINK_RELATIONS_REGISTRY) { + $this->data['links'][substr((string) $key, 41)] = &$this->data['links'][$key]; + } + $this->data['links'][$key] = array_unique($this->data['links'][$key]); + } + } + if (isset($this->data['links'][$rel])) { + return $this->data['links'][$rel]; + } + + return null; + } + + /** + * Get an enclosure from the item + * + * Supports the <enclosure> RSS tag, as well as Media RSS and iTunes RSS. + * + * @since Beta 2 + * @todo Add ability to prefer one type of content over another (in a media group). + * @param int $key The enclosure that you want to return. Remember that arrays begin with 0, not 1 + * @return \SimplePie\Enclosure|null + */ + public function get_enclosure(int $key = 0) + { + $enclosures = $this->get_enclosures(); + if (isset($enclosures[$key])) { + return $enclosures[$key]; + } + + return null; + } + + /** + * Get all available enclosures (podcasts, etc.) + * + * Supports the <enclosure> RSS tag, as well as Media RSS and iTunes RSS. + * + * At this point, we're pretty much assuming that all enclosures for an item + * are the same content. Anything else is too complicated to + * properly support. + * + * @since Beta 2 + * @todo Add support for end-user defined sorting of enclosures by type/handler (so we can prefer the faster-loading FLV over MP4). + * @todo If an element exists at a level, but its value is empty, we should fall back to the value from the parent (if it exists). + * @return \SimplePie\Enclosure[]|null List of \SimplePie\Enclosure items + */ + public function get_enclosures() + { + if (!isset($this->data['enclosures'])) { + $this->data['enclosures'] = []; + + // Elements + $captions_parent = null; + $categories_parent = null; + $copyrights_parent = null; + $credits_parent = null; + $description_parent = null; + $duration_parent = null; + $hashes_parent = null; + $keywords_parent = null; + $player_parent = null; + $ratings_parent = null; + $restrictions_parent = []; + $thumbnails_parent = null; + $title_parent = null; + + // Let's do the channel and item-level ones first, and just re-use them if we need to. + $parent = $this->get_feed(); + + // CAPTIONS + if ($captions = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'text')) { + foreach ($captions as $caption) { + $caption_type = null; + $caption_lang = null; + $caption_startTime = null; + $caption_endTime = null; + $caption_text = null; + if (isset($caption['attribs']['']['type'])) { + $caption_type = $this->sanitize($caption['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['lang'])) { + $caption_lang = $this->sanitize($caption['attribs']['']['lang'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['start'])) { + $caption_startTime = $this->sanitize($caption['attribs']['']['start'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['end'])) { + $caption_endTime = $this->sanitize($caption['attribs']['']['end'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['data'])) { + $caption_text = $this->sanitize($caption['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $captions_parent[] = $this->registry->create(Caption::class, [$caption_type, $caption_lang, $caption_startTime, $caption_endTime, $caption_text]); + } + } elseif ($captions = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'text')) { + foreach ($captions as $caption) { + $caption_type = null; + $caption_lang = null; + $caption_startTime = null; + $caption_endTime = null; + $caption_text = null; + if (isset($caption['attribs']['']['type'])) { + $caption_type = $this->sanitize($caption['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['lang'])) { + $caption_lang = $this->sanitize($caption['attribs']['']['lang'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['start'])) { + $caption_startTime = $this->sanitize($caption['attribs']['']['start'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['end'])) { + $caption_endTime = $this->sanitize($caption['attribs']['']['end'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['data'])) { + $caption_text = $this->sanitize($caption['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $captions_parent[] = $this->registry->create(Caption::class, [$caption_type, $caption_lang, $caption_startTime, $caption_endTime, $caption_text]); + } + } + if (is_array($captions_parent)) { + $captions_parent = array_values(array_unique($captions_parent)); + } + + // CATEGORIES + foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'category') as $category) { + $term = null; + $scheme = null; + $label = null; + if (isset($category['data'])) { + $term = $this->sanitize($category['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['scheme'])) { + $scheme = $this->sanitize($category['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $scheme = 'http://search.yahoo.com/mrss/category_schema'; + } + if (isset($category['attribs']['']['label'])) { + $label = $this->sanitize($category['attribs']['']['label'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $categories_parent[] = $this->registry->create(Category::class, [$term, $scheme, $label]); + } + foreach ((array) $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'category') as $category) { + $term = null; + $scheme = null; + $label = null; + if (isset($category['data'])) { + $term = $this->sanitize($category['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['scheme'])) { + $scheme = $this->sanitize($category['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $scheme = 'http://search.yahoo.com/mrss/category_schema'; + } + if (isset($category['attribs']['']['label'])) { + $label = $this->sanitize($category['attribs']['']['label'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $categories_parent[] = $this->registry->create(Category::class, [$term, $scheme, $label]); + } + foreach ((array) $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'category') as $category) { + $term = null; + $scheme = 'http://www.itunes.com/dtds/podcast-1.0.dtd'; + $label = null; + if (isset($category['attribs']['']['text'])) { + $label = $this->sanitize($category['attribs']['']['text'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $categories_parent[] = $this->registry->create(Category::class, [$term, $scheme, $label]); + + if (isset($category['child'][\SimplePie\SimplePie::NAMESPACE_ITUNES]['category'])) { + foreach ((array) $category['child'][\SimplePie\SimplePie::NAMESPACE_ITUNES]['category'] as $subcategory) { + if (isset($subcategory['attribs']['']['text'])) { + $label = $this->sanitize($subcategory['attribs']['']['text'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $categories_parent[] = $this->registry->create(Category::class, [$term, $scheme, $label]); + } + } + } + if (is_array($categories_parent)) { + $categories_parent = array_values(array_unique($categories_parent)); + } + + // COPYRIGHT + if ($copyright = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'copyright')) { + $copyright_url = null; + $copyright_label = null; + if (isset($copyright[0]['attribs']['']['url'])) { + $copyright_url = $this->sanitize($copyright[0]['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($copyright[0]['data'])) { + $copyright_label = $this->sanitize($copyright[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $copyrights_parent = $this->registry->create(Copyright::class, [$copyright_url, $copyright_label]); + } elseif ($copyright = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'copyright')) { + $copyright_url = null; + $copyright_label = null; + if (isset($copyright[0]['attribs']['']['url'])) { + $copyright_url = $this->sanitize($copyright[0]['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($copyright[0]['data'])) { + $copyright_label = $this->sanitize($copyright[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $copyrights_parent = $this->registry->create(Copyright::class, [$copyright_url, $copyright_label]); + } + + // CREDITS + if ($credits = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'credit')) { + foreach ($credits as $credit) { + $credit_role = null; + $credit_scheme = null; + $credit_name = null; + if (isset($credit['attribs']['']['role'])) { + $credit_role = $this->sanitize($credit['attribs']['']['role'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($credit['attribs']['']['scheme'])) { + $credit_scheme = $this->sanitize($credit['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $credit_scheme = 'urn:ebu'; + } + if (isset($credit['data'])) { + $credit_name = $this->sanitize($credit['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $credits_parent[] = $this->registry->create(Credit::class, [$credit_role, $credit_scheme, $credit_name]); + } + } elseif ($credits = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'credit')) { + foreach ($credits as $credit) { + $credit_role = null; + $credit_scheme = null; + $credit_name = null; + if (isset($credit['attribs']['']['role'])) { + $credit_role = $this->sanitize($credit['attribs']['']['role'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($credit['attribs']['']['scheme'])) { + $credit_scheme = $this->sanitize($credit['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $credit_scheme = 'urn:ebu'; + } + if (isset($credit['data'])) { + $credit_name = $this->sanitize($credit['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $credits_parent[] = $this->registry->create(Credit::class, [$credit_role, $credit_scheme, $credit_name]); + } + } + if (is_array($credits_parent)) { + $credits_parent = array_values(array_unique($credits_parent)); + } + + // DESCRIPTION + if ($description_parent = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'description')) { + if (isset($description_parent[0]['data'])) { + $description_parent = $this->sanitize($description_parent[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + } elseif ($description_parent = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'description')) { + if (isset($description_parent[0]['data'])) { + $description_parent = $this->sanitize($description_parent[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + } + + // DURATION + $duration_tags = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'duration'); + if ($duration_tags !== null) { + $seconds = null; + $minutes = null; + $hours = null; + if (isset($duration_tags[0]['data'])) { + $temp = explode(':', $this->sanitize($duration_tags[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT)); + $seconds = (int) array_pop($temp); + if (count($temp) > 0) { + $minutes = (int) array_pop($temp); + $seconds += $minutes * 60; + } + if (count($temp) > 0) { + $hours = (int) array_pop($temp); + $seconds += $hours * 3600; + } + unset($temp); + $duration_parent = $seconds; + } + } + + // HASHES + if ($hashes_iterator = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'hash')) { + foreach ($hashes_iterator as $hash) { + $value = null; + $algo = null; + if (isset($hash['data'])) { + $value = $this->sanitize($hash['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($hash['attribs']['']['algo'])) { + $algo = $this->sanitize($hash['attribs']['']['algo'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $algo = 'md5'; + } + $hashes_parent[] = $algo.':'.$value; + } + } elseif ($hashes_iterator = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'hash')) { + foreach ($hashes_iterator as $hash) { + $value = null; + $algo = null; + if (isset($hash['data'])) { + $value = $this->sanitize($hash['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($hash['attribs']['']['algo'])) { + $algo = $this->sanitize($hash['attribs']['']['algo'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $algo = 'md5'; + } + $hashes_parent[] = $algo.':'.$value; + } + } + if (is_array($hashes_parent)) { + $hashes_parent = array_values(array_unique($hashes_parent)); + } + + // KEYWORDS + if ($keywords = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'keywords')) { + if (isset($keywords[0]['data'])) { + $temp = explode(',', $this->sanitize($keywords[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT)); + foreach ($temp as $word) { + $keywords_parent[] = trim($word); + } + } + unset($temp); + } elseif ($keywords = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'keywords')) { + if (isset($keywords[0]['data'])) { + $temp = explode(',', $this->sanitize($keywords[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT)); + foreach ($temp as $word) { + $keywords_parent[] = trim($word); + } + } + unset($temp); + } elseif ($keywords = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'keywords')) { + if (isset($keywords[0]['data'])) { + $temp = explode(',', $this->sanitize($keywords[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT)); + foreach ($temp as $word) { + $keywords_parent[] = trim($word); + } + } + unset($temp); + } elseif ($keywords = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'keywords')) { + if (isset($keywords[0]['data'])) { + $temp = explode(',', $this->sanitize($keywords[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT)); + foreach ($temp as $word) { + $keywords_parent[] = trim($word); + } + } + unset($temp); + } + if (is_array($keywords_parent)) { + $keywords_parent = array_values(array_unique($keywords_parent)); + } + + // PLAYER + if ($player_parent = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'player')) { + if (isset($player_parent[0]['attribs']['']['url'])) { + $player_parent = $this->sanitize($player_parent[0]['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($player_parent[0])); + } + } elseif ($player_parent = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'player')) { + if (isset($player_parent[0]['attribs']['']['url'])) { + $player_parent = $this->sanitize($player_parent[0]['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($player_parent[0])); + } + } + + // RATINGS + if ($ratings = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'rating')) { + foreach ($ratings as $rating) { + $rating_scheme = null; + $rating_value = null; + if (isset($rating['attribs']['']['scheme'])) { + $rating_scheme = $this->sanitize($rating['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $rating_scheme = 'urn:simple'; + } + if (isset($rating['data'])) { + $rating_value = $this->sanitize($rating['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $ratings_parent[] = $this->registry->create(Rating::class, [$rating_scheme, $rating_value]); + } + } elseif ($ratings = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'explicit')) { + foreach ($ratings as $rating) { + $rating_scheme = 'urn:itunes'; + $rating_value = null; + if (isset($rating['data'])) { + $rating_value = $this->sanitize($rating['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $ratings_parent[] = $this->registry->create(Rating::class, [$rating_scheme, $rating_value]); + } + } elseif ($ratings = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'rating')) { + foreach ($ratings as $rating) { + $rating_scheme = null; + $rating_value = null; + if (isset($rating['attribs']['']['scheme'])) { + $rating_scheme = $this->sanitize($rating['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $rating_scheme = 'urn:simple'; + } + if (isset($rating['data'])) { + $rating_value = $this->sanitize($rating['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $ratings_parent[] = $this->registry->create(Rating::class, [$rating_scheme, $rating_value]); + } + } elseif ($ratings = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'explicit')) { + foreach ($ratings as $rating) { + $rating_scheme = 'urn:itunes'; + $rating_value = null; + if (isset($rating['data'])) { + $rating_value = $this->sanitize($rating['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $ratings_parent[] = $this->registry->create(Rating::class, [$rating_scheme, $rating_value]); + } + } + if (is_array($ratings_parent)) { + $ratings_parent = array_values(array_unique($ratings_parent)); + } + + // RESTRICTIONS + if ($restrictions = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'restriction')) { + foreach ($restrictions as $restriction) { + $restriction_relationship = null; + $restriction_type = null; + $restriction_value = null; + if (isset($restriction['attribs']['']['relationship'])) { + $restriction_relationship = $this->sanitize($restriction['attribs']['']['relationship'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($restriction['attribs']['']['type'])) { + $restriction_type = $this->sanitize($restriction['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($restriction['data'])) { + $restriction_value = $this->sanitize($restriction['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $restrictions_parent[] = $this->registry->create(Restriction::class, [$restriction_relationship, $restriction_type, $restriction_value]); + } + } elseif ($restrictions = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'block')) { + foreach ($restrictions as $restriction) { + $restriction_relationship = Restriction::RELATIONSHIP_ALLOW; + $restriction_type = null; + $restriction_value = 'itunes'; + if (isset($restriction['data']) && strtolower($restriction['data']) === 'yes') { + $restriction_relationship = Restriction::RELATIONSHIP_DENY; + } + $restrictions_parent[] = $this->registry->create(Restriction::class, [$restriction_relationship, $restriction_type, $restriction_value]); + } + } elseif ($restrictions = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'restriction')) { + foreach ($restrictions as $restriction) { + $restriction_relationship = null; + $restriction_type = null; + $restriction_value = null; + if (isset($restriction['attribs']['']['relationship'])) { + $restriction_relationship = $this->sanitize($restriction['attribs']['']['relationship'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($restriction['attribs']['']['type'])) { + $restriction_type = $this->sanitize($restriction['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($restriction['data'])) { + $restriction_value = $this->sanitize($restriction['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $restrictions_parent[] = $this->registry->create(Restriction::class, [$restriction_relationship, $restriction_type, $restriction_value]); + } + } elseif ($restrictions = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'block')) { + foreach ($restrictions as $restriction) { + $restriction_relationship = Restriction::RELATIONSHIP_ALLOW; + $restriction_type = null; + $restriction_value = 'itunes'; + if (isset($restriction['data']) && strtolower($restriction['data']) === 'yes') { + $restriction_relationship = Restriction::RELATIONSHIP_DENY; + } + $restrictions_parent[] = $this->registry->create(Restriction::class, [$restriction_relationship, $restriction_type, $restriction_value]); + } + } + if (count($restrictions_parent) > 0) { + $restrictions_parent = array_values(array_unique($restrictions_parent)); + } else { + $restrictions_parent = [new \SimplePie\Restriction(Restriction::RELATIONSHIP_ALLOW, null, 'default')]; + } + + // THUMBNAILS + if ($thumbnails = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'thumbnail')) { + foreach ($thumbnails as $thumbnail) { + if (isset($thumbnail['attribs']['']['url'])) { + $thumbnails_parent[] = $this->sanitize($thumbnail['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($thumbnail)); + } + } + } elseif ($thumbnails = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'thumbnail')) { + foreach ($thumbnails as $thumbnail) { + if (isset($thumbnail['attribs']['']['url'])) { + $thumbnails_parent[] = $this->sanitize($thumbnail['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($thumbnail)); + } + } + } + + // TITLES + if ($title_parent = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'title')) { + if (isset($title_parent[0]['data'])) { + $title_parent = $this->sanitize($title_parent[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + } elseif ($title_parent = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'title')) { + if (isset($title_parent[0]['data'])) { + $title_parent = $this->sanitize($title_parent[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + } + + // Clear the memory + unset($parent); + + // Attributes + $bitrate = null; + $channels = null; + $duration = null; + $expression = null; + $framerate = null; + $height = null; + $javascript = null; + $lang = null; + $length = null; + $medium = null; + $samplingrate = null; + $type = null; + $url = null; + $width = null; + + // Elements + $captions = null; + $categories = null; + $copyrights = null; + $credits = null; + $description = null; + $hashes = null; + $keywords = null; + $player = null; + $ratings = null; + $restrictions = null; + $thumbnails = null; + $title = null; + + // If we have media:group tags, loop through them. + foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'group') as $group) { + if (isset($group['child']) && isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['content'])) { + // If we have media:content tags, loop through them. + foreach ((array) $group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['content'] as $content) { + if (isset($content['attribs']['']['url'])) { + // Attributes + $bitrate = null; + $channels = null; + $duration = null; + $expression = null; + $framerate = null; + $height = null; + $javascript = null; + $lang = null; + $length = null; + $medium = null; + $samplingrate = null; + $type = null; + $url = null; + $width = null; + + // Elements + $captions = null; + $categories = null; + $copyrights = null; + $credits = null; + $description = null; + $hashes = null; + $keywords = null; + $player = null; + $ratings = null; + $restrictions = null; + $thumbnails = null; + $title = null; + + // Start checking the attributes of media:content + if (isset($content['attribs']['']['bitrate'])) { + $bitrate = $this->sanitize($content['attribs']['']['bitrate'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['channels'])) { + $channels = $this->sanitize($content['attribs']['']['channels'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['duration'])) { + $duration = $this->sanitize($content['attribs']['']['duration'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $duration = $duration_parent; + } + if (isset($content['attribs']['']['expression'])) { + $expression = $this->sanitize($content['attribs']['']['expression'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['framerate'])) { + $framerate = $this->sanitize($content['attribs']['']['framerate'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['height'])) { + $height = $this->sanitize($content['attribs']['']['height'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['lang'])) { + $lang = $this->sanitize($content['attribs']['']['lang'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['fileSize'])) { + $length = intval($content['attribs']['']['fileSize']); + } + if (isset($content['attribs']['']['medium'])) { + $medium = $this->sanitize($content['attribs']['']['medium'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['samplingrate'])) { + $samplingrate = $this->sanitize($content['attribs']['']['samplingrate'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['type'])) { + $type = $this->sanitize($content['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['width'])) { + $width = $this->sanitize($content['attribs']['']['width'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $url = $this->sanitize($content['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($content)); + + // Checking the other optional media: elements. Priority: media:content, media:group, item, channel + + // CAPTIONS + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['text'])) { + foreach ($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['text'] as $caption) { + $caption_type = null; + $caption_lang = null; + $caption_startTime = null; + $caption_endTime = null; + $caption_text = null; + if (isset($caption['attribs']['']['type'])) { + $caption_type = $this->sanitize($caption['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['lang'])) { + $caption_lang = $this->sanitize($caption['attribs']['']['lang'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['start'])) { + $caption_startTime = $this->sanitize($caption['attribs']['']['start'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['end'])) { + $caption_endTime = $this->sanitize($caption['attribs']['']['end'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['data'])) { + $caption_text = $this->sanitize($caption['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $captions[] = $this->registry->create(Caption::class, [$caption_type, $caption_lang, $caption_startTime, $caption_endTime, $caption_text]); + } + if (is_array($captions)) { + $captions = array_values(array_unique($captions)); + } + } elseif (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['text'])) { + foreach ($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['text'] as $caption) { + $caption_type = null; + $caption_lang = null; + $caption_startTime = null; + $caption_endTime = null; + $caption_text = null; + if (isset($caption['attribs']['']['type'])) { + $caption_type = $this->sanitize($caption['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['lang'])) { + $caption_lang = $this->sanitize($caption['attribs']['']['lang'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['start'])) { + $caption_startTime = $this->sanitize($caption['attribs']['']['start'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['end'])) { + $caption_endTime = $this->sanitize($caption['attribs']['']['end'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['data'])) { + $caption_text = $this->sanitize($caption['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $captions[] = $this->registry->create(Caption::class, [$caption_type, $caption_lang, $caption_startTime, $caption_endTime, $caption_text]); + } + if (is_array($captions)) { + $captions = array_values(array_unique($captions)); + } + } else { + $captions = $captions_parent; + } + + // CATEGORIES + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['category'])) { + foreach ((array) $content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['category'] as $category) { + $term = null; + $scheme = null; + $label = null; + if (isset($category['data'])) { + $term = $this->sanitize($category['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['scheme'])) { + $scheme = $this->sanitize($category['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $scheme = 'http://search.yahoo.com/mrss/category_schema'; + } + if (isset($category['attribs']['']['label'])) { + $label = $this->sanitize($category['attribs']['']['label'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $categories[] = $this->registry->create(Category::class, [$term, $scheme, $label]); + } + } + if (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['category'])) { + foreach ((array) $group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['category'] as $category) { + $term = null; + $scheme = null; + $label = null; + if (isset($category['data'])) { + $term = $this->sanitize($category['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['scheme'])) { + $scheme = $this->sanitize($category['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $scheme = 'http://search.yahoo.com/mrss/category_schema'; + } + if (isset($category['attribs']['']['label'])) { + $label = $this->sanitize($category['attribs']['']['label'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $categories[] = $this->registry->create(Category::class, [$term, $scheme, $label]); + } + } + if (is_array($categories) && is_array($categories_parent)) { + $categories = array_values(array_unique(array_merge($categories, $categories_parent))); + } elseif (is_array($categories)) { + $categories = array_values(array_unique($categories)); + } elseif (is_array($categories_parent)) { + $categories = array_values(array_unique($categories_parent)); + } + + // COPYRIGHTS + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['copyright'])) { + $copyright_url = null; + $copyright_label = null; + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['copyright'][0]['attribs']['']['url'])) { + $copyright_url = $this->sanitize($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['copyright'][0]['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['copyright'][0]['data'])) { + $copyright_label = $this->sanitize($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['copyright'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $copyrights = $this->registry->create(Copyright::class, [$copyright_url, $copyright_label]); + } elseif (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['copyright'])) { + $copyright_url = null; + $copyright_label = null; + if (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['copyright'][0]['attribs']['']['url'])) { + $copyright_url = $this->sanitize($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['copyright'][0]['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['copyright'][0]['data'])) { + $copyright_label = $this->sanitize($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['copyright'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $copyrights = $this->registry->create(Copyright::class, [$copyright_url, $copyright_label]); + } else { + $copyrights = $copyrights_parent; + } + + // CREDITS + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['credit'])) { + foreach ($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['credit'] as $credit) { + $credit_role = null; + $credit_scheme = null; + $credit_name = null; + if (isset($credit['attribs']['']['role'])) { + $credit_role = $this->sanitize($credit['attribs']['']['role'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($credit['attribs']['']['scheme'])) { + $credit_scheme = $this->sanitize($credit['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $credit_scheme = 'urn:ebu'; + } + if (isset($credit['data'])) { + $credit_name = $this->sanitize($credit['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $credits[] = $this->registry->create(Credit::class, [$credit_role, $credit_scheme, $credit_name]); + } + if (is_array($credits)) { + $credits = array_values(array_unique($credits)); + } + } elseif (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['credit'])) { + foreach ($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['credit'] as $credit) { + $credit_role = null; + $credit_scheme = null; + $credit_name = null; + if (isset($credit['attribs']['']['role'])) { + $credit_role = $this->sanitize($credit['attribs']['']['role'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($credit['attribs']['']['scheme'])) { + $credit_scheme = $this->sanitize($credit['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $credit_scheme = 'urn:ebu'; + } + if (isset($credit['data'])) { + $credit_name = $this->sanitize($credit['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $credits[] = $this->registry->create(Credit::class, [$credit_role, $credit_scheme, $credit_name]); + } + if (is_array($credits)) { + $credits = array_values(array_unique($credits)); + } + } else { + $credits = $credits_parent; + } + + // DESCRIPTION + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['description'])) { + $description = $this->sanitize($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['description'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['description'])) { + $description = $this->sanitize($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['description'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $description = $description_parent; + } + + // HASHES + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['hash'])) { + foreach ($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['hash'] as $hash) { + $value = null; + $algo = null; + if (isset($hash['data'])) { + $value = $this->sanitize($hash['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($hash['attribs']['']['algo'])) { + $algo = $this->sanitize($hash['attribs']['']['algo'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $algo = 'md5'; + } + $hashes[] = $algo.':'.$value; + } + if (is_array($hashes)) { + $hashes = array_values(array_unique($hashes)); + } + } elseif (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['hash'])) { + foreach ($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['hash'] as $hash) { + $value = null; + $algo = null; + if (isset($hash['data'])) { + $value = $this->sanitize($hash['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($hash['attribs']['']['algo'])) { + $algo = $this->sanitize($hash['attribs']['']['algo'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $algo = 'md5'; + } + $hashes[] = $algo.':'.$value; + } + if (is_array($hashes)) { + $hashes = array_values(array_unique($hashes)); + } + } else { + $hashes = $hashes_parent; + } + + // KEYWORDS + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['keywords'])) { + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['keywords'][0]['data'])) { + $temp = explode(',', $this->sanitize($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['keywords'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT)); + foreach ($temp as $word) { + $keywords[] = trim($word); + } + unset($temp); + } + if (is_array($keywords)) { + $keywords = array_values(array_unique($keywords)); + } + } elseif (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['keywords'])) { + if (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['keywords'][0]['data'])) { + $temp = explode(',', $this->sanitize($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['keywords'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT)); + foreach ($temp as $word) { + $keywords[] = trim($word); + } + unset($temp); + } + if (is_array($keywords)) { + $keywords = array_values(array_unique($keywords)); + } + } else { + $keywords = $keywords_parent; + } + + // PLAYER + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'])) { + $playerElem = $content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'][0]; + $player = $this->sanitize($playerElem['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($playerElem)); + } elseif (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'])) { + $playerElem = $group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'][0]; + $player = $this->sanitize($playerElem['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($playerElem)); + } else { + $player = $player_parent; + } + + // RATINGS + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['rating'])) { + foreach ($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['rating'] as $rating) { + $rating_scheme = null; + $rating_value = null; + if (isset($rating['attribs']['']['scheme'])) { + $rating_scheme = $this->sanitize($rating['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $rating_scheme = 'urn:simple'; + } + if (isset($rating['data'])) { + $rating_value = $this->sanitize($rating['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $ratings[] = $this->registry->create(Rating::class, [$rating_scheme, $rating_value]); + } + if (is_array($ratings)) { + $ratings = array_values(array_unique($ratings)); + } + } elseif (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['rating'])) { + foreach ($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['rating'] as $rating) { + $rating_scheme = null; + $rating_value = null; + if (isset($rating['attribs']['']['scheme'])) { + $rating_scheme = $this->sanitize($rating['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $rating_scheme = 'urn:simple'; + } + if (isset($rating['data'])) { + $rating_value = $this->sanitize($rating['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $ratings[] = $this->registry->create(Rating::class, [$rating_scheme, $rating_value]); + } + if (is_array($ratings)) { + $ratings = array_values(array_unique($ratings)); + } + } else { + $ratings = $ratings_parent; + } + + // RESTRICTIONS + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['restriction'])) { + foreach ($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['restriction'] as $restriction) { + $restriction_relationship = null; + $restriction_type = null; + $restriction_value = null; + if (isset($restriction['attribs']['']['relationship'])) { + $restriction_relationship = $this->sanitize($restriction['attribs']['']['relationship'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($restriction['attribs']['']['type'])) { + $restriction_type = $this->sanitize($restriction['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($restriction['data'])) { + $restriction_value = $this->sanitize($restriction['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $restrictions[] = $this->registry->create(Restriction::class, [$restriction_relationship, $restriction_type, $restriction_value]); + } + if (is_array($restrictions)) { + $restrictions = array_values(array_unique($restrictions)); + } + } elseif (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['restriction'])) { + foreach ($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['restriction'] as $restriction) { + $restriction_relationship = null; + $restriction_type = null; + $restriction_value = null; + if (isset($restriction['attribs']['']['relationship'])) { + $restriction_relationship = $this->sanitize($restriction['attribs']['']['relationship'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($restriction['attribs']['']['type'])) { + $restriction_type = $this->sanitize($restriction['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($restriction['data'])) { + $restriction_value = $this->sanitize($restriction['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $restrictions[] = $this->registry->create(Restriction::class, [$restriction_relationship, $restriction_type, $restriction_value]); + } + if (is_array($restrictions)) { + $restrictions = array_values(array_unique($restrictions)); + } + } else { + $restrictions = $restrictions_parent; + } + + // THUMBNAILS + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['thumbnail'])) { + foreach ($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['thumbnail'] as $thumbnail) { + $thumbnails[] = $this->sanitize($thumbnail['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($thumbnail)); + } + if (is_array($thumbnails)) { + $thumbnails = array_values(array_unique($thumbnails)); + } + } elseif (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['thumbnail'])) { + foreach ($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['thumbnail'] as $thumbnail) { + $thumbnails[] = $this->sanitize($thumbnail['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($thumbnail)); + } + if (is_array($thumbnails)) { + $thumbnails = array_values(array_unique($thumbnails)); + } + } else { + $thumbnails = $thumbnails_parent; + } + + // TITLES + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['title'])) { + $title = $this->sanitize($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['title'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['title'])) { + $title = $this->sanitize($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['title'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $title = $title_parent; + } + + $this->data['enclosures'][] = $this->registry->create(Enclosure::class, [$url, $type, $length, null, $bitrate, $captions, $categories, $channels, $copyrights, $credits, $description, $duration, $expression, $framerate, $hashes, $height, $keywords, $lang, $medium, $player, $ratings, $restrictions, $samplingrate, $thumbnails, $title, $width]); + } + } + } + } + + // If we have standalone media:content tags, loop through them. + if (isset($this->data['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['content'])) { + foreach ((array) $this->data['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['content'] as $content) { + if (isset($content['attribs']['']['url']) || isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'])) { + // Attributes + $bitrate = null; + $channels = null; + $duration = null; + $expression = null; + $framerate = null; + $height = null; + $javascript = null; + $lang = null; + $length = null; + $medium = null; + $samplingrate = null; + $type = null; + $url = null; + $width = null; + + // Elements + $captions = null; + $categories = null; + $copyrights = null; + $credits = null; + $description = null; + $hashes = null; + $keywords = null; + $player = null; + $ratings = null; + $restrictions = null; + $thumbnails = null; + $title = null; + + // Start checking the attributes of media:content + if (isset($content['attribs']['']['bitrate'])) { + $bitrate = $this->sanitize($content['attribs']['']['bitrate'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['channels'])) { + $channels = $this->sanitize($content['attribs']['']['channels'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['duration'])) { + $duration = $this->sanitize($content['attribs']['']['duration'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $duration = $duration_parent; + } + if (isset($content['attribs']['']['expression'])) { + $expression = $this->sanitize($content['attribs']['']['expression'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['framerate'])) { + $framerate = $this->sanitize($content['attribs']['']['framerate'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['height'])) { + $height = $this->sanitize($content['attribs']['']['height'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['lang'])) { + $lang = $this->sanitize($content['attribs']['']['lang'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['fileSize'])) { + $length = intval($content['attribs']['']['fileSize']); + } + if (isset($content['attribs']['']['medium'])) { + $medium = $this->sanitize($content['attribs']['']['medium'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['samplingrate'])) { + $samplingrate = $this->sanitize($content['attribs']['']['samplingrate'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['type'])) { + $type = $this->sanitize($content['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['width'])) { + $width = $this->sanitize($content['attribs']['']['width'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['url'])) { + $url = $this->sanitize($content['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($content)); + } + // Checking the other optional media: elements. Priority: media:content, media:group, item, channel + + // CAPTIONS + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['text'])) { + foreach ($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['text'] as $caption) { + $caption_type = null; + $caption_lang = null; + $caption_startTime = null; + $caption_endTime = null; + $caption_text = null; + if (isset($caption['attribs']['']['type'])) { + $caption_type = $this->sanitize($caption['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['lang'])) { + $caption_lang = $this->sanitize($caption['attribs']['']['lang'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['start'])) { + $caption_startTime = $this->sanitize($caption['attribs']['']['start'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['end'])) { + $caption_endTime = $this->sanitize($caption['attribs']['']['end'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($caption['data'])) { + $caption_text = $this->sanitize($caption['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $captions[] = $this->registry->create(Caption::class, [$caption_type, $caption_lang, $caption_startTime, $caption_endTime, $caption_text]); + } + if (is_array($captions)) { + $captions = array_values(array_unique($captions)); + } + } else { + $captions = $captions_parent; + } + + // CATEGORIES + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['category'])) { + foreach ((array) $content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['category'] as $category) { + $term = null; + $scheme = null; + $label = null; + if (isset($category['data'])) { + $term = $this->sanitize($category['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['scheme'])) { + $scheme = $this->sanitize($category['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $scheme = 'http://search.yahoo.com/mrss/category_schema'; + } + if (isset($category['attribs']['']['label'])) { + $label = $this->sanitize($category['attribs']['']['label'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $categories[] = $this->registry->create(Category::class, [$term, $scheme, $label]); + } + } + if (is_array($categories) && is_array($categories_parent)) { + $categories = array_values(array_unique(array_merge($categories, $categories_parent))); + } elseif (is_array($categories)) { + $categories = array_values(array_unique($categories)); + } elseif (is_array($categories_parent)) { + $categories = array_values(array_unique($categories_parent)); + } else { + $categories = null; + } + + // COPYRIGHTS + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['copyright'])) { + $copyright_url = null; + $copyright_label = null; + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['copyright'][0]['attribs']['']['url'])) { + $copyright_url = $this->sanitize($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['copyright'][0]['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['copyright'][0]['data'])) { + $copyright_label = $this->sanitize($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['copyright'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $copyrights = $this->registry->create(Copyright::class, [$copyright_url, $copyright_label]); + } else { + $copyrights = $copyrights_parent; + } + + // CREDITS + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['credit'])) { + foreach ($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['credit'] as $credit) { + $credit_role = null; + $credit_scheme = null; + $credit_name = null; + if (isset($credit['attribs']['']['role'])) { + $credit_role = $this->sanitize($credit['attribs']['']['role'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($credit['attribs']['']['scheme'])) { + $credit_scheme = $this->sanitize($credit['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $credit_scheme = 'urn:ebu'; + } + if (isset($credit['data'])) { + $credit_name = $this->sanitize($credit['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $credits[] = $this->registry->create(Credit::class, [$credit_role, $credit_scheme, $credit_name]); + } + if (is_array($credits)) { + $credits = array_values(array_unique($credits)); + } + } else { + $credits = $credits_parent; + } + + // DESCRIPTION + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['description'])) { + $description = $this->sanitize($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['description'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $description = $description_parent; + } + + // HASHES + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['hash'])) { + foreach ($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['hash'] as $hash) { + $value = null; + $algo = null; + if (isset($hash['data'])) { + $value = $this->sanitize($hash['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($hash['attribs']['']['algo'])) { + $algo = $this->sanitize($hash['attribs']['']['algo'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $algo = 'md5'; + } + $hashes[] = $algo.':'.$value; + } + if (is_array($hashes)) { + $hashes = array_values(array_unique($hashes)); + } + } else { + $hashes = $hashes_parent; + } + + // KEYWORDS + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['keywords'])) { + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['keywords'][0]['data'])) { + $temp = explode(',', $this->sanitize($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['keywords'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT)); + foreach ($temp as $word) { + $keywords[] = trim($word); + } + unset($temp); + } + if (is_array($keywords)) { + $keywords = array_values(array_unique($keywords)); + } + } else { + $keywords = $keywords_parent; + } + + // PLAYER + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'])) { + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'][0]['attribs']['']['url'])) { + $playerElem = $content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'][0]; + $player = $this->sanitize($playerElem['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($playerElem)); + } + } else { + $player = $player_parent; + } + + // RATINGS + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['rating'])) { + foreach ($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['rating'] as $rating) { + $rating_scheme = null; + $rating_value = null; + if (isset($rating['attribs']['']['scheme'])) { + $rating_scheme = $this->sanitize($rating['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $rating_scheme = 'urn:simple'; + } + if (isset($rating['data'])) { + $rating_value = $this->sanitize($rating['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $ratings[] = $this->registry->create(Rating::class, [$rating_scheme, $rating_value]); + } + if (is_array($ratings)) { + $ratings = array_values(array_unique($ratings)); + } + } else { + $ratings = $ratings_parent; + } + + // RESTRICTIONS + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['restriction'])) { + foreach ($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['restriction'] as $restriction) { + $restriction_relationship = null; + $restriction_type = null; + $restriction_value = null; + if (isset($restriction['attribs']['']['relationship'])) { + $restriction_relationship = $this->sanitize($restriction['attribs']['']['relationship'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($restriction['attribs']['']['type'])) { + $restriction_type = $this->sanitize($restriction['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($restriction['data'])) { + $restriction_value = $this->sanitize($restriction['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $restrictions[] = $this->registry->create(Restriction::class, [$restriction_relationship, $restriction_type, $restriction_value]); + } + if (is_array($restrictions)) { + $restrictions = array_values(array_unique($restrictions)); + } + } else { + $restrictions = $restrictions_parent; + } + + // THUMBNAILS + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['thumbnail'])) { + foreach ($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['thumbnail'] as $thumbnail) { + if (isset($thumbnail['attribs']['']['url'])) { + $thumbnails[] = $this->sanitize($thumbnail['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($thumbnail)); + } + } + if (is_array($thumbnails)) { + $thumbnails = array_values(array_unique($thumbnails)); + } + } else { + $thumbnails = $thumbnails_parent; + } + + // TITLES + if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['title'])) { + $title = $this->sanitize($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['title'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $title = $title_parent; + } + + $this->data['enclosures'][] = $this->registry->create(Enclosure::class, [$url, $type, $length, null, $bitrate, $captions, $categories, $channels, $copyrights, $credits, $description, $duration, $expression, $framerate, $hashes, $height, $keywords, $lang, $medium, $player, $ratings, $restrictions, $samplingrate, $thumbnails, $title, $width]); + } + } + } + + foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'link') as $link) { + if (isset($link['attribs']['']['href']) && !empty($link['attribs']['']['rel']) && $link['attribs']['']['rel'] === 'enclosure') { + // Attributes + $bitrate = null; + $channels = null; + $duration = null; + $expression = null; + $framerate = null; + $height = null; + $javascript = null; + $lang = null; + $length = null; + $medium = null; + $samplingrate = null; + $type = null; + $url = null; + $width = null; + + $url = $this->sanitize($link['attribs']['']['href'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($link)); + if (isset($link['attribs']['']['type'])) { + $type = $this->sanitize($link['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($link['attribs']['']['length'])) { + $length = intval($link['attribs']['']['length']); + } + if (isset($link['attribs']['']['title'])) { + $title = $this->sanitize($link['attribs']['']['title'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $title = $title_parent; + } + + // Since we don't have group or content for these, we'll just pass the '*_parent' variables directly to the constructor + $this->data['enclosures'][] = $this->registry->create(Enclosure::class, [$url, $type, $length, null, $bitrate, $captions_parent, $categories_parent, $channels, $copyrights_parent, $credits_parent, $description_parent, $duration_parent, $expression, $framerate, $hashes_parent, $height, $keywords_parent, $lang, $medium, $player_parent, $ratings_parent, $restrictions_parent, $samplingrate, $thumbnails_parent, $title, $width]); + } + } + + foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'link') as $link) { + if (isset($link['attribs']['']['href']) && !empty($link['attribs']['']['rel']) && $link['attribs']['']['rel'] === 'enclosure') { + // Attributes + $bitrate = null; + $channels = null; + $duration = null; + $expression = null; + $framerate = null; + $height = null; + $javascript = null; + $lang = null; + $length = null; + $medium = null; + $samplingrate = null; + $type = null; + $url = null; + $width = null; + + $url = $this->sanitize($link['attribs']['']['href'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($link)); + if (isset($link['attribs']['']['type'])) { + $type = $this->sanitize($link['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($link['attribs']['']['length'])) { + $length = intval($link['attribs']['']['length']); + } + + // Since we don't have group or content for these, we'll just pass the '*_parent' variables directly to the constructor + $this->data['enclosures'][] = $this->registry->create(Enclosure::class, [$url, $type, $length, null, $bitrate, $captions_parent, $categories_parent, $channels, $copyrights_parent, $credits_parent, $description_parent, $duration_parent, $expression, $framerate, $hashes_parent, $height, $keywords_parent, $lang, $medium, $player_parent, $ratings_parent, $restrictions_parent, $samplingrate, $thumbnails_parent, $title_parent, $width]); + } + } + + foreach ($this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, 'enclosure') ?? [] as $enclosure) { + if (isset($enclosure['attribs']['']['url'])) { + // Attributes + $bitrate = null; + $channels = null; + $duration = null; + $expression = null; + $framerate = null; + $height = null; + $javascript = null; + $lang = null; + $length = null; + $medium = null; + $samplingrate = null; + $type = null; + $url = null; + $width = null; + + $url = $this->sanitize($enclosure['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($enclosure)); + $url = $this->get_sanitize()->https_url($url); + if (isset($enclosure['attribs']['']['type'])) { + $type = $this->sanitize($enclosure['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($enclosure['attribs']['']['length'])) { + $length = intval($enclosure['attribs']['']['length']); + } + + // Since we don't have group or content for these, we'll just pass the '*_parent' variables directly to the constructor + $this->data['enclosures'][] = $this->registry->create(Enclosure::class, [$url, $type, $length, null, $bitrate, $captions_parent, $categories_parent, $channels, $copyrights_parent, $credits_parent, $description_parent, $duration_parent, $expression, $framerate, $hashes_parent, $height, $keywords_parent, $lang, $medium, $player_parent, $ratings_parent, $restrictions_parent, $samplingrate, $thumbnails_parent, $title_parent, $width]); + } + } + + if (count($this->data['enclosures']) === 0 && ($url || $type || $length || $bitrate || $captions_parent || $categories_parent || $channels || $copyrights_parent || $credits_parent || $description_parent || $duration_parent || $expression || $framerate || $hashes_parent || $height || $keywords_parent || $lang || $medium || $player_parent || $ratings_parent || $samplingrate || $thumbnails_parent || $title_parent || $width)) { + // Since we don't have group or content for these, we'll just pass the '*_parent' variables directly to the constructor + $this->data['enclosures'][] = $this->registry->create(Enclosure::class, [$url, $type, $length, null, $bitrate, $captions_parent, $categories_parent, $channels, $copyrights_parent, $credits_parent, $description_parent, $duration_parent, $expression, $framerate, $hashes_parent, $height, $keywords_parent, $lang, $medium, $player_parent, $ratings_parent, $restrictions_parent, $samplingrate, $thumbnails_parent, $title_parent, $width]); + } + + $this->data['enclosures'] = array_values(array_unique($this->data['enclosures'])); + } + if (!empty($this->data['enclosures'])) { + return $this->data['enclosures']; + } + + return null; + } + + /** + * Get the latitude coordinates for the item + * + * Compatible with the W3C WGS84 Basic Geo and GeoRSS specifications + * + * Uses `<geo:lat>` or `<georss:point>` + * + * @since 1.0 + * @link http://www.w3.org/2003/01/geo/ W3C WGS84 Basic Geo + * @link http://www.georss.org/ GeoRSS + * @return float|null + */ + public function get_latitude() + { + if ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_W3C_BASIC_GEO, 'lat')) { + return (float) $return[0]['data']; + } elseif (($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) { + return (float) $match[1]; + } + + return null; + } + + /** + * Get the longitude coordinates for the item + * + * Compatible with the W3C WGS84 Basic Geo and GeoRSS specifications + * + * Uses `<geo:long>`, `<geo:lon>` or `<georss:point>` + * + * @since 1.0 + * @link http://www.w3.org/2003/01/geo/ W3C WGS84 Basic Geo + * @link http://www.georss.org/ GeoRSS + * @return float|null + */ + public function get_longitude() + { + if ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_W3C_BASIC_GEO, 'long')) { + return (float) $return[0]['data']; + } elseif ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_W3C_BASIC_GEO, 'lon')) { + return (float) $return[0]['data']; + } elseif (($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) { + return (float) $match[2]; + } + + return null; + } + + /** + * Get the `<atom:source>` for the item + * + * @since 1.1 + * @return \SimplePie\Source|null + */ + public function get_source() + { + if ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'source')) { + return $this->registry->create(Source::class, [$this, $return[0]]); + } + + return null; + } + + public function set_sanitize(Sanitize $sanitize): void + { + $this->sanitize = $sanitize; + } + + protected function get_sanitize(): Sanitize + { + if ($this->sanitize === null) { + $this->sanitize = new Sanitize(); + } + + return $this->sanitize; + } +} + +class_alias('SimplePie\Item', 'SimplePie_Item'); + +} + +namespace SimplePie { + +use DomDocument; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\UriFactoryInterface; +use SimplePie\HTTP\Client; +use SimplePie\HTTP\ClientException; +use SimplePie\HTTP\FileClient; +use SimplePie\HTTP\Psr18Client; +use SimplePie\HTTP\Response; + +/** + * Used for feed auto-discovery + * + * + * This class can be overloaded with {@see \SimplePie\SimplePie::set_locator_class()} + */ +class Locator implements RegistryAware +{ + /** @var ?string */ + public $useragent = null; + /** @var int */ + public $timeout = 10; + /** @var File */ + public $file; + /** @var string[] */ + public $local = []; + /** @var string[] */ + public $elsewhere = []; + /** @var array<mixed> */ + public $cached_entities = []; + /** @var string */ + public $http_base; + /** @var string */ + public $base; + /** @var int */ + public $base_location = 0; + /** @var int */ + public $checked_feeds = 0; + /** @var int */ + public $max_checked_feeds = 10; + /** @var bool */ + public $force_fsockopen = false; + /** @var array<int, mixed> */ + public $curl_options = []; + /** @var ?\DomDocument */ + public $dom; + /** @var ?Registry */ + protected $registry; + + /** + * @var Client|null + */ + private $http_client = null; + + /** + * @param array<int, mixed> $curl_options + */ + public function __construct(File $file, int $timeout = 10, ?string $useragent = null, int $max_checked_feeds = 10, bool $force_fsockopen = false, array $curl_options = []) + { + $this->file = $file; + $this->useragent = $useragent; + $this->timeout = $timeout; + $this->max_checked_feeds = $max_checked_feeds; + $this->force_fsockopen = $force_fsockopen; + $this->curl_options = $curl_options; + + $body = $this->file->get_body_content(); + + if (class_exists('DOMDocument') && $body != '') { + $this->dom = new \DOMDocument(); + + set_error_handler([Misc::class, 'silence_errors']); + try { + $this->dom->loadHTML($body); + } catch (\Throwable $ex) { + $this->dom = null; + } + restore_error_handler(); + } else { + $this->dom = null; + } + } + + /** + * Set a PSR-18 client and PSR-17 factories + * + * Allows you to use your own HTTP client implementations. + */ + final public function set_http_client( + ClientInterface $http_client, + RequestFactoryInterface $request_factory, + UriFactoryInterface $uri_factory + ): void { + $this->http_client = new Psr18Client($http_client, $request_factory, $uri_factory); + } + + /** + * @return void + */ + public function set_registry(\SimplePie\Registry $registry) + { + $this->registry = $registry; + } + + /** + * @param int-mask-of<SimplePie::LOCATOR_*> $type + * @param array<Response>|null $working + * @return Response|null + */ + public function find(int $type = \SimplePie\SimplePie::LOCATOR_ALL, ?array &$working = null) + { + assert($this->registry !== null); + + if ($this->is_feed($this->file)) { + return $this->file; + } + + if (Misc::is_remote_uri($this->file->get_final_requested_uri())) { + $sniffer = $this->registry->create(Content\Type\Sniffer::class, [$this->file]); + if ($sniffer->get_type() !== 'text/html') { + return null; + } + } + + if ($type & ~\SimplePie\SimplePie::LOCATOR_NONE) { + $this->get_base(); + } + + if ($type & \SimplePie\SimplePie::LOCATOR_AUTODISCOVERY && $working = $this->autodiscovery()) { + return $working[0]; + } + + if ($type & (\SimplePie\SimplePie::LOCATOR_LOCAL_EXTENSION | \SimplePie\SimplePie::LOCATOR_LOCAL_BODY | \SimplePie\SimplePie::LOCATOR_REMOTE_EXTENSION | \SimplePie\SimplePie::LOCATOR_REMOTE_BODY) && $this->get_links()) { + if ($type & \SimplePie\SimplePie::LOCATOR_LOCAL_EXTENSION && $working = $this->extension($this->local)) { + return $working[0]; + } + + if ($type & \SimplePie\SimplePie::LOCATOR_LOCAL_BODY && $working = $this->body($this->local)) { + return $working[0]; + } + + if ($type & \SimplePie\SimplePie::LOCATOR_REMOTE_EXTENSION && $working = $this->extension($this->elsewhere)) { + return $working[0]; + } + + if ($type & \SimplePie\SimplePie::LOCATOR_REMOTE_BODY && $working = $this->body($this->elsewhere)) { + return $working[0]; + } + } + return null; + } + + /** + * @return bool + */ + public function is_feed(Response $file, bool $check_html = false) + { + assert($this->registry !== null); + + if (Misc::is_remote_uri($file->get_final_requested_uri())) { + $sniffer = $this->registry->create(Content\Type\Sniffer::class, [$file]); + $sniffed = $sniffer->get_type(); + $mime_types = ['application/rss+xml', 'application/rdf+xml', + 'text/rdf', 'application/atom+xml', 'text/xml', + 'application/xml', 'application/x-rss+xml']; + if ($check_html) { + $mime_types[] = 'text/html'; + } + + return in_array($sniffed, $mime_types); + } elseif (is_file($file->get_final_requested_uri())) { + return true; + } else { + return false; + } + } + + /** + * @return void + */ + public function get_base() + { + assert($this->registry !== null); + + if ($this->dom === null) { + throw new \SimplePie\Exception('DOMDocument not found, unable to use locator'); + } + $this->http_base = $this->file->get_final_requested_uri(); + $this->base = $this->http_base; + $elements = $this->dom->getElementsByTagName('base'); + foreach ($elements as $element) { + if ($element->hasAttribute('href')) { + $base = $this->registry->call(Misc::class, 'absolutize_url', [trim($element->getAttribute('href')), $this->http_base]); + if ($base === false) { + continue; + } + $this->base = $base; + $this->base_location = method_exists($element, 'getLineNo') ? $element->getLineNo() : 0; + break; + } + } + } + + /** + * @return array<Response>|null + */ + public function autodiscovery() + { + $done = []; + $feeds = []; + $feeds = array_merge($feeds, $this->search_elements_by_tag('link', $done, $feeds)); + $feeds = array_merge($feeds, $this->search_elements_by_tag('a', $done, $feeds)); + $feeds = array_merge($feeds, $this->search_elements_by_tag('area', $done, $feeds)); + + if (!empty($feeds)) { + return array_values($feeds); + } + + return null; + } + + /** + * @param string[] $done + * @param array<string, Response> $feeds + * @return array<string, Response> + */ + protected function search_elements_by_tag(string $name, array &$done, array $feeds) + { + assert($this->registry !== null); + + if ($this->dom === null) { + throw new \SimplePie\Exception('DOMDocument not found, unable to use locator'); + } + + $links = $this->dom->getElementsByTagName($name); + foreach ($links as $link) { + if ($this->checked_feeds === $this->max_checked_feeds) { + break; + } + if ($link->hasAttribute('href') && $link->hasAttribute('rel')) { + $rel = array_unique($this->registry->call(Misc::class, 'space_separated_tokens', [strtolower($link->getAttribute('rel'))])); + $line = method_exists($link, 'getLineNo') ? $link->getLineNo() : 1; + + if ($this->base_location < $line) { + $href = $this->registry->call(Misc::class, 'absolutize_url', [trim($link->getAttribute('href')), $this->base]); + } else { + $href = $this->registry->call(Misc::class, 'absolutize_url', [trim($link->getAttribute('href')), $this->http_base]); + } + if ($href === false) { + continue; + } + + if (!in_array($href, $done) && in_array('feed', $rel) || (in_array('alternate', $rel) && !in_array('stylesheet', $rel) && $link->hasAttribute('type') && in_array(strtolower($this->registry->call(Misc::class, 'parse_mime', [$link->getAttribute('type')])), ['text/html', 'application/rss+xml', 'application/atom+xml'])) && !isset($feeds[$href])) { + $this->checked_feeds++; + $headers = [ + 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER, + ]; + + try { + $feed = $this->get_http_client()->request(Client::METHOD_GET, $href, $headers); + + if ((!Misc::is_remote_uri($feed->get_final_requested_uri()) || ($feed->get_status_code() === 200 || $feed->get_status_code() > 206 && $feed->get_status_code() < 300)) && $this->is_feed($feed, true)) { + $feeds[$href] = $feed; + } + } catch (ClientException $th) { + // Just mark it as done and continue. + } + } + $done[] = $href; + } + } + + return $feeds; + } + + /** + * @return true|null + */ + public function get_links() + { + assert($this->registry !== null); + + if ($this->dom === null) { + throw new \SimplePie\Exception('DOMDocument not found, unable to use locator'); + } + + $links = $this->dom->getElementsByTagName('a'); + foreach ($links as $link) { + if ($link->hasAttribute('href')) { + $href = trim($link->getAttribute('href')); + $parsed = $this->registry->call(Misc::class, 'parse_url', [$href]); + if ($parsed['scheme'] === '' || preg_match('/^(https?|feed)?$/i', $parsed['scheme'])) { + if (method_exists($link, 'getLineNo') && $this->base_location < $link->getLineNo()) { + $href = $this->registry->call(Misc::class, 'absolutize_url', [trim($link->getAttribute('href')), $this->base]); + } else { + $href = $this->registry->call(Misc::class, 'absolutize_url', [trim($link->getAttribute('href')), $this->http_base]); + } + if ($href === false) { + continue; + } + + $current = $this->registry->call(Misc::class, 'parse_url', [$this->file->get_final_requested_uri()]); + + if ($parsed['authority'] === '' || $parsed['authority'] === $current['authority']) { + $this->local[] = $href; + } else { + $this->elsewhere[] = $href; + } + } + } + } + $this->local = array_unique($this->local); + $this->elsewhere = array_unique($this->elsewhere); + if (!empty($this->local) || !empty($this->elsewhere)) { + return true; + } + return null; + } + + /** + * Extracts first `link` element with given `rel` attribute inside the `head` element. + * + * @return string|null + */ + public function get_rel_link(string $rel) + { + assert($this->registry !== null); + + if ($this->dom === null) { + throw new \SimplePie\Exception('DOMDocument not found, unable to use '. + 'locator'); + } + if (!class_exists('DOMXpath')) { + throw new \SimplePie\Exception('DOMXpath not found, unable to use '. + 'get_rel_link'); + } + + $xpath = new \DOMXpath($this->dom); + $query = '(//head)[1]/link[@rel and @href]'; + /** @var \DOMNodeList<\DOMElement> */ + $queryResult = $xpath->query($query); + foreach ($queryResult as $link) { + $href = trim($link->getAttribute('href')); + $parsed = $this->registry->call(Misc::class, 'parse_url', [$href]); + if ($parsed['scheme'] === '' || + preg_match('/^https?$/i', $parsed['scheme'])) { + if (method_exists($link, 'getLineNo') && + $this->base_location < $link->getLineNo()) { + $href = $this->registry->call( + Misc::class, + 'absolutize_url', + [trim($link->getAttribute('href')), $this->base] + ); + } else { + $href = $this->registry->call( + Misc::class, + 'absolutize_url', + [trim($link->getAttribute('href')), $this->http_base] + ); + } + if ($href === false) { + return null; + } + $rel_values = explode(' ', strtolower($link->getAttribute('rel'))); + if (in_array($rel, $rel_values)) { + return $href; + } + } + } + + return null; + } + + /** + * @param string[] $array + * @return array<Response>|null + */ + public function extension(array &$array) + { + foreach ($array as $key => $value) { + if ($this->checked_feeds === $this->max_checked_feeds) { + break; + } + $extension = strrchr($value, '.'); + if ($extension !== false && in_array(strtolower($extension), ['.rss', '.rdf', '.atom', '.xml'])) { + $this->checked_feeds++; + + $headers = [ + 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER, + ]; + + try { + $feed = $this->get_http_client()->request(Client::METHOD_GET, $value, $headers); + + if ((!Misc::is_remote_uri($feed->get_final_requested_uri()) || ($feed->get_status_code() === 200 || $feed->get_status_code() > 206 && $feed->get_status_code() < 300)) && $this->is_feed($feed)) { + return [$feed]; + } + } catch (ClientException $th) { + // Just unset and continue. + } + + unset($array[$key]); + } + } + return null; + } + + /** + * @param string[] $array + * @return array<Response>|null + */ + public function body(array &$array) + { + foreach ($array as $key => $value) { + if ($this->checked_feeds === $this->max_checked_feeds) { + break; + } + if (preg_match('/(feed|rss|rdf|atom|xml)/i', $value)) { + $this->checked_feeds++; + $headers = [ + 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER, + ]; + + try { + $feed = $this->get_http_client()->request(Client::METHOD_GET, $value, $headers); + + if ((!Misc::is_remote_uri($feed->get_final_requested_uri()) || ($feed->get_status_code() === 200 || $feed->get_status_code() > 206 && $feed->get_status_code() < 300)) && $this->is_feed($feed)) { + return [$feed]; + } + } catch (ClientException $th) { + // Just unset and continue. + } + + unset($array[$key]); + } + } + return null; + } + + /** + * Get a HTTP client + */ + private function get_http_client(): Client + { + assert($this->registry !== null); + + if ($this->http_client === null) { + $options = [ + 'timeout' => $this->timeout, + 'redirects' => 5, + 'force_fsockopen' => $this->force_fsockopen, + 'curl_options' => $this->curl_options, + ]; + + if ($this->useragent !== null) { + $options['useragent'] = $this->useragent; + } + + return new FileClient( + $this->registry, + $options + ); + } + + return $this->http_client; + } +} + +class_alias('SimplePie\Locator', 'SimplePie_Locator', false); + +} + +namespace SimplePie { + +use SimplePie\XML\Declaration\Parser; + +/** + * Miscellaneous utilities + */ +class Misc +{ + /** @var int|null */ + private static $SIMPLEPIE_BUILD = null; + + /** + * @return string + */ + public static function time_hms(int $seconds) + { + $time = ''; + + $hours = floor($seconds / 3600); + $remainder = $seconds % 3600; + if ($hours > 0) { + $time .= $hours.':'; + } + + $minutes = floor($remainder / 60); + $seconds = $remainder % 60; + if ($minutes < 10 && $hours > 0) { + $minutes = '0' . $minutes; + } + if ($seconds < 10) { + $seconds = '0' . $seconds; + } + + $time .= $minutes.':'; + $time .= $seconds; + + return $time; + } + + /** + * @return string|false + */ + public static function absolutize_url(string $relative, string $base) + { + $iri = \SimplePie\IRI::absolutize(new \SimplePie\IRI($base), $relative); + if ($iri === false) { + return false; + } + return $iri->get_uri(); + } + + /** + * @internal + */ + public static function is_remote_uri(string $uri): bool + { + return preg_match('/^https?:\/\//i', $uri) === 1; + } + + /** + * Get a HTML/XML element from a HTML string + * + * @deprecated since SimplePie 1.3, use DOMDocument instead (parsing HTML with regex is bad!) + * @param string $realname Element name (including namespace prefix if applicable) + * @param string $string HTML document + * @return array<array{tag: string, self_closing: bool, attribs: array<string, array{data: string}>, content?: string}> + */ + public static function get_element(string $realname, string $string) + { + // trigger_error(sprintf('Using method "' . __METHOD__ . '" is deprecated since SimplePie 1.3, use "DOMDocument" instead.'), \E_USER_DEPRECATED); + + $return = []; + $name = preg_quote($realname, '/'); + if (preg_match_all("/<($name)" . \SimplePie\SimplePie::PCRE_HTML_ATTRIBUTE . "(>(.*)<\/$name>|(\/)?>)/siU", $string, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { + for ($i = 0, $total_matches = count($matches); $i < $total_matches; $i++) { + $return[$i]['tag'] = $realname; + $return[$i]['full'] = $matches[$i][0][0]; + $return[$i]['offset'] = $matches[$i][0][1]; + if (strlen($matches[$i][3][0]) <= 2) { + $return[$i]['self_closing'] = true; + } else { + $return[$i]['self_closing'] = false; + $return[$i]['content'] = $matches[$i][4][0]; + } + $return[$i]['attribs'] = []; + if (isset($matches[$i][2][0]) && preg_match_all('/[\x09\x0A\x0B\x0C\x0D\x20]+([^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3D\x3E]*)(?:[\x09\x0A\x0B\x0C\x0D\x20]*=[\x09\x0A\x0B\x0C\x0D\x20]*(?:"([^"]*)"|\'([^\']*)\'|([^\x09\x0A\x0B\x0C\x0D\x20\x22\x27\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x3E]*)?))?/', ' ' . $matches[$i][2][0] . ' ', $attribs, PREG_SET_ORDER)) { + foreach ($attribs as $attrib) { + if (count($attrib) === 2) { + $attrib[2] = $attrib[1]; + } + $return[$i]['attribs'][strtolower($attrib[1])]['data'] = Misc::entities_decode(end($attrib)); + } + } + } + } + return $return; + } + + /** + * @deprecated since SimplePie 1.9.0. If you need it, you can copy the function to your codebase. But you should consider using `DOMDocument` for any DOM wrangling. + * @param array{tag: string, self_closing: bool, attribs: array<string, array{data: string}>, content: string} $element + * @return string + */ + public static function element_implode(array $element) + { + // trigger_error(sprintf('Using method "' . __METHOD__ . '" is deprecated since SimplePie 1.9.'), \E_USER_DEPRECATED); + + $full = "<{$element['tag']}"; + foreach ($element['attribs'] as $key => $value) { + $key = strtolower($key); + $full .= " $key=\"" . htmlspecialchars($value['data'], ENT_COMPAT, 'UTF-8') . '"'; + } + if ($element['self_closing']) { + $full .= ' />'; + } else { + $full .= ">{$element['content']}</{$element['tag']}>"; + } + return $full; + } + + /** + * @param string $message + * @param int $level + * @param string $file + * @param int $line + * @return string + */ + public static function error(string $message, int $level, string $file, int $line) + { + if ((error_reporting() & $level) > 0) { + switch ($level) { + case E_USER_ERROR: + $note = 'PHP Error'; + break; + case E_USER_WARNING: + $note = 'PHP Warning'; + break; + case E_USER_NOTICE: + $note = 'PHP Notice'; + break; + default: + $note = 'Unknown Error'; + break; + } + + $log_error = true; + if (!function_exists('error_log')) { + $log_error = false; + } + + $log_file = @ini_get('error_log'); + if (!empty($log_file) && ('syslog' !== $log_file) && !@is_writable($log_file)) { + $log_error = false; + } + + if ($log_error) { + @error_log("$note: $message in $file on line $line", 0); + } + } + + return $message; + } + + /** + * @return string + */ + public static function fix_protocol(string $url, int $http = 1) + { + $url = Misc::normalize_url($url); + $parsed = Misc::parse_url($url); + if ($parsed['scheme'] !== '' && $parsed['scheme'] !== 'http' && $parsed['scheme'] !== 'https') { + return Misc::fix_protocol(Misc::compress_parse_url('http', $parsed['authority'], $parsed['path'], $parsed['query'], $parsed['fragment']), $http); + } + + if ($parsed['scheme'] === '' && $parsed['authority'] === '' && !file_exists($url)) { + return Misc::fix_protocol(Misc::compress_parse_url('http', $parsed['path'], '', $parsed['query'], $parsed['fragment']), $http); + } + + if ($http === 2 && $parsed['scheme'] !== '') { + return "feed:$url"; + } elseif ($http === 3 && strtolower($parsed['scheme']) === 'http') { + return substr_replace($url, 'podcast', 0, 4); + } elseif ($http === 4 && strtolower($parsed['scheme']) === 'http') { + return substr_replace($url, 'itpc', 0, 4); + } + + return $url; + } + + /** + * @deprecated since SimplePie 1.8.0, use PHP native array_replace_recursive() instead. + * @param array<mixed> $array1 + * @param array<mixed> $array2 + * @return array<mixed> + */ + public static function array_merge_recursive(array $array1, array $array2) + { + foreach ($array2 as $key => $value) { + if (is_array($value)) { + $array1[$key] = Misc::array_merge_recursive($array1[$key], $value); + } else { + $array1[$key] = $value; + } + } + + return $array1; + } + + /** + * @return array<string, string> + */ + public static function parse_url(string $url) + { + $iri = new \SimplePie\IRI($url); + return [ + 'scheme' => (string) $iri->scheme, + 'authority' => (string) $iri->authority, + 'path' => (string) $iri->path, + 'query' => (string) $iri->query, + 'fragment' => (string) $iri->fragment + ]; + } + + /** + * @return string + */ + public static function compress_parse_url(string $scheme = '', string $authority = '', string $path = '', string $query = '', ?string $fragment = '') + { + $iri = new \SimplePie\IRI(''); + $iri->scheme = $scheme; + $iri->authority = $authority; + $iri->path = $path; + $iri->query = $query; + $iri->fragment = $fragment; + return $iri->get_uri(); + } + + /** + * @return string + */ + public static function normalize_url(string $url) + { + $iri = new \SimplePie\IRI($url); + return $iri->get_uri(); + } + + /** + * @deprecated since SimplePie 1.9.0. This functionality is part of `IRI` – if you need it standalone, consider copying the function to your codebase. + * @param array<int, string> $match + * @return string + */ + public static function percent_encoding_normalization(array $match) + { + $integer = hexdec($match[1]); + if ($integer >= 0x41 && $integer <= 0x5A || $integer >= 0x61 && $integer <= 0x7A || $integer >= 0x30 && $integer <= 0x39 || $integer === 0x2D || $integer === 0x2E || $integer === 0x5F || $integer === 0x7E) { + // Cast for PHPStan, the value would only be float when above PHP_INT_MAX, which would not go in this branch. + return chr((int) $integer); + } + + return strtoupper($match[0]); + } + + /** + * Converts a Windows-1252 encoded string to a UTF-8 encoded string + * + * @static + * @param string $string Windows-1252 encoded string + * @return string UTF-8 encoded string + */ + public static function windows_1252_to_utf8(string $string) + { + static $convert_table = ["\x80" => "\xE2\x82\xAC", "\x81" => "\xEF\xBF\xBD", "\x82" => "\xE2\x80\x9A", "\x83" => "\xC6\x92", "\x84" => "\xE2\x80\x9E", "\x85" => "\xE2\x80\xA6", "\x86" => "\xE2\x80\xA0", "\x87" => "\xE2\x80\xA1", "\x88" => "\xCB\x86", "\x89" => "\xE2\x80\xB0", "\x8A" => "\xC5\xA0", "\x8B" => "\xE2\x80\xB9", "\x8C" => "\xC5\x92", "\x8D" => "\xEF\xBF\xBD", "\x8E" => "\xC5\xBD", "\x8F" => "\xEF\xBF\xBD", "\x90" => "\xEF\xBF\xBD", "\x91" => "\xE2\x80\x98", "\x92" => "\xE2\x80\x99", "\x93" => "\xE2\x80\x9C", "\x94" => "\xE2\x80\x9D", "\x95" => "\xE2\x80\xA2", "\x96" => "\xE2\x80\x93", "\x97" => "\xE2\x80\x94", "\x98" => "\xCB\x9C", "\x99" => "\xE2\x84\xA2", "\x9A" => "\xC5\xA1", "\x9B" => "\xE2\x80\xBA", "\x9C" => "\xC5\x93", "\x9D" => "\xEF\xBF\xBD", "\x9E" => "\xC5\xBE", "\x9F" => "\xC5\xB8", "\xA0" => "\xC2\xA0", "\xA1" => "\xC2\xA1", "\xA2" => "\xC2\xA2", "\xA3" => "\xC2\xA3", "\xA4" => "\xC2\xA4", "\xA5" => "\xC2\xA5", "\xA6" => "\xC2\xA6", "\xA7" => "\xC2\xA7", "\xA8" => "\xC2\xA8", "\xA9" => "\xC2\xA9", "\xAA" => "\xC2\xAA", "\xAB" => "\xC2\xAB", "\xAC" => "\xC2\xAC", "\xAD" => "\xC2\xAD", "\xAE" => "\xC2\xAE", "\xAF" => "\xC2\xAF", "\xB0" => "\xC2\xB0", "\xB1" => "\xC2\xB1", "\xB2" => "\xC2\xB2", "\xB3" => "\xC2\xB3", "\xB4" => "\xC2\xB4", "\xB5" => "\xC2\xB5", "\xB6" => "\xC2\xB6", "\xB7" => "\xC2\xB7", "\xB8" => "\xC2\xB8", "\xB9" => "\xC2\xB9", "\xBA" => "\xC2\xBA", "\xBB" => "\xC2\xBB", "\xBC" => "\xC2\xBC", "\xBD" => "\xC2\xBD", "\xBE" => "\xC2\xBE", "\xBF" => "\xC2\xBF", "\xC0" => "\xC3\x80", "\xC1" => "\xC3\x81", "\xC2" => "\xC3\x82", "\xC3" => "\xC3\x83", "\xC4" => "\xC3\x84", "\xC5" => "\xC3\x85", "\xC6" => "\xC3\x86", "\xC7" => "\xC3\x87", "\xC8" => "\xC3\x88", "\xC9" => "\xC3\x89", "\xCA" => "\xC3\x8A", "\xCB" => "\xC3\x8B", "\xCC" => "\xC3\x8C", "\xCD" => "\xC3\x8D", "\xCE" => "\xC3\x8E", "\xCF" => "\xC3\x8F", "\xD0" => "\xC3\x90", "\xD1" => "\xC3\x91", "\xD2" => "\xC3\x92", "\xD3" => "\xC3\x93", "\xD4" => "\xC3\x94", "\xD5" => "\xC3\x95", "\xD6" => "\xC3\x96", "\xD7" => "\xC3\x97", "\xD8" => "\xC3\x98", "\xD9" => "\xC3\x99", "\xDA" => "\xC3\x9A", "\xDB" => "\xC3\x9B", "\xDC" => "\xC3\x9C", "\xDD" => "\xC3\x9D", "\xDE" => "\xC3\x9E", "\xDF" => "\xC3\x9F", "\xE0" => "\xC3\xA0", "\xE1" => "\xC3\xA1", "\xE2" => "\xC3\xA2", "\xE3" => "\xC3\xA3", "\xE4" => "\xC3\xA4", "\xE5" => "\xC3\xA5", "\xE6" => "\xC3\xA6", "\xE7" => "\xC3\xA7", "\xE8" => "\xC3\xA8", "\xE9" => "\xC3\xA9", "\xEA" => "\xC3\xAA", "\xEB" => "\xC3\xAB", "\xEC" => "\xC3\xAC", "\xED" => "\xC3\xAD", "\xEE" => "\xC3\xAE", "\xEF" => "\xC3\xAF", "\xF0" => "\xC3\xB0", "\xF1" => "\xC3\xB1", "\xF2" => "\xC3\xB2", "\xF3" => "\xC3\xB3", "\xF4" => "\xC3\xB4", "\xF5" => "\xC3\xB5", "\xF6" => "\xC3\xB6", "\xF7" => "\xC3\xB7", "\xF8" => "\xC3\xB8", "\xF9" => "\xC3\xB9", "\xFA" => "\xC3\xBA", "\xFB" => "\xC3\xBB", "\xFC" => "\xC3\xBC", "\xFD" => "\xC3\xBD", "\xFE" => "\xC3\xBE", "\xFF" => "\xC3\xBF"]; + + return strtr($string, $convert_table); + } + + /** + * Change a string from one encoding to another + * + * @param string $data Raw data in $input encoding + * @param string $input Encoding of $data + * @param string $output Encoding you want + * @return string|false False if we can't convert it + */ + public static function change_encoding(string $data, string $input, string $output) + { + $input = Misc::encoding($input); + $output = Misc::encoding($output); + + // We fail to fail on non US-ASCII bytes + if ($input === 'US-ASCII') { + static $non_ascii_octets = ''; + if (!$non_ascii_octets) { + for ($i = 0x80; $i <= 0xFF; $i++) { + $non_ascii_octets .= chr($i); + } + } + $data = substr($data, 0, strcspn($data, $non_ascii_octets)); + } + + // This is first, as behaviour of this is completely predictable + if ($input === 'windows-1252' && $output === 'UTF-8') { + return Misc::windows_1252_to_utf8($data); + } + // This is second, as behaviour of this varies only with PHP version (the middle part of this expression checks the encoding is supported). + elseif (function_exists('mb_convert_encoding') && ($return = Misc::change_encoding_mbstring($data, $input, $output))) { + return $return; + } + // This is third, as behaviour of this varies with OS userland and PHP version + elseif (function_exists('iconv') && ($return = Misc::change_encoding_iconv($data, $input, $output))) { + return $return; + } + // This is last, as behaviour of this varies with OS userland and PHP version + elseif (class_exists('\UConverter') && ($return = Misc::change_encoding_uconverter($data, $input, $output))) { + return $return; + } + + // If we can't do anything, just fail + return false; + } + + /** + * @return string|false + */ + protected static function change_encoding_mbstring(string $data, string $input, string $output) + { + if ($input === 'windows-949') { + $input = 'EUC-KR'; + } + if ($output === 'windows-949') { + $output = 'EUC-KR'; + } + if ($input === 'Windows-31J') { + $input = 'SJIS'; + } + if ($output === 'Windows-31J') { + $output = 'SJIS'; + } + + // Check that the encoding is supported + if (!in_array($input, mb_list_encodings())) { + return false; + } + + if (@mb_convert_encoding("\x80", 'UTF-16BE', $input) === "\x00\x80") { + return false; + } + + // Let's do some conversion + if ($return = @mb_convert_encoding($data, $output, $input)) { + return $return; + } + + return false; + } + + /** + * @return string|false + */ + protected static function change_encoding_iconv(string $data, string $input, string $output) + { + return @iconv($input, $output, $data); + } + + /** + * @return string|false + */ + protected static function change_encoding_uconverter(string $data, string $input, string $output) + { + return @\UConverter::transcode($data, $output, $input); + } + + /** + * Normalize an encoding name + * + * This is automatically generated by create.php + * + * To generate it, run `php create.php` on the command line, and copy the + * output to replace this function. + * + * @param string $charset Character set to standardise + * @return string Standardised name + */ + public static function encoding(string $charset) + { + // Normalization from UTS #22 + // Cast for PHPStan, the regex should not fail. + switch (strtolower((string) preg_replace('/(?:[^a-zA-Z0-9]+|([^0-9])0+)/', '\1', $charset))) { + case 'adobestandardencoding': + case 'csadobestandardencoding': + return 'Adobe-Standard-Encoding'; + + case 'adobesymbolencoding': + case 'cshppsmath': + return 'Adobe-Symbol-Encoding'; + + case 'ami1251': + case 'amiga1251': + return 'Amiga-1251'; + + case 'ansix31101983': + case 'csat5001983': + case 'csiso99naplps': + case 'isoir99': + case 'naplps': + return 'ANSI_X3.110-1983'; + + case 'arabic7': + case 'asmo449': + case 'csiso89asmo449': + case 'iso9036': + case 'isoir89': + return 'ASMO_449'; + + case 'big5': + case 'csbig5': + return 'Big5'; + + case 'big5hkscs': + return 'Big5-HKSCS'; + + case 'bocu1': + case 'csbocu1': + return 'BOCU-1'; + + case 'brf': + case 'csbrf': + return 'BRF'; + + case 'bs4730': + case 'csiso4unitedkingdom': + case 'gb': + case 'iso646gb': + case 'isoir4': + case 'uk': + return 'BS_4730'; + + case 'bsviewdata': + case 'csiso47bsviewdata': + case 'isoir47': + return 'BS_viewdata'; + + case 'cesu8': + case 'cscesu8': + return 'CESU-8'; + + case 'ca': + case 'csa71': + case 'csaz243419851': + case 'csiso121canadian1': + case 'iso646ca': + case 'isoir121': + return 'CSA_Z243.4-1985-1'; + + case 'csa72': + case 'csaz243419852': + case 'csiso122canadian2': + case 'iso646ca2': + case 'isoir122': + return 'CSA_Z243.4-1985-2'; + + case 'csaz24341985gr': + case 'csiso123csaz24341985gr': + case 'isoir123': + return 'CSA_Z243.4-1985-gr'; + + case 'csiso139csn369103': + case 'csn369103': + case 'isoir139': + return 'CSN_369103'; + + case 'csdecmcs': + case 'dec': + case 'decmcs': + return 'DEC-MCS'; + + case 'csiso21german': + case 'de': + case 'din66003': + case 'iso646de': + case 'isoir21': + return 'DIN_66003'; + + case 'csdkus': + case 'dkus': + return 'dk-us'; + + case 'csiso646danish': + case 'dk': + case 'ds2089': + case 'iso646dk': + return 'DS_2089'; + + case 'csibmebcdicatde': + case 'ebcdicatde': + return 'EBCDIC-AT-DE'; + + case 'csebcdicatdea': + case 'ebcdicatdea': + return 'EBCDIC-AT-DE-A'; + + case 'csebcdiccafr': + case 'ebcdiccafr': + return 'EBCDIC-CA-FR'; + + case 'csebcdicdkno': + case 'ebcdicdkno': + return 'EBCDIC-DK-NO'; + + case 'csebcdicdknoa': + case 'ebcdicdknoa': + return 'EBCDIC-DK-NO-A'; + + case 'csebcdices': + case 'ebcdices': + return 'EBCDIC-ES'; + + case 'csebcdicesa': + case 'ebcdicesa': + return 'EBCDIC-ES-A'; + + case 'csebcdicess': + case 'ebcdicess': + return 'EBCDIC-ES-S'; + + case 'csebcdicfise': + case 'ebcdicfise': + return 'EBCDIC-FI-SE'; + + case 'csebcdicfisea': + case 'ebcdicfisea': + return 'EBCDIC-FI-SE-A'; + + case 'csebcdicfr': + case 'ebcdicfr': + return 'EBCDIC-FR'; + + case 'csebcdicit': + case 'ebcdicit': + return 'EBCDIC-IT'; + + case 'csebcdicpt': + case 'ebcdicpt': + return 'EBCDIC-PT'; + + case 'csebcdicuk': + case 'ebcdicuk': + return 'EBCDIC-UK'; + + case 'csebcdicus': + case 'ebcdicus': + return 'EBCDIC-US'; + + case 'csiso111ecmacyrillic': + case 'ecmacyrillic': + case 'isoir111': + case 'koi8e': + return 'ECMA-cyrillic'; + + case 'csiso17spanish': + case 'es': + case 'iso646es': + case 'isoir17': + return 'ES'; + + case 'csiso85spanish2': + case 'es2': + case 'iso646es2': + case 'isoir85': + return 'ES2'; + + case 'cseucpkdfmtjapanese': + case 'eucjp': + case 'extendedunixcodepackedformatforjapanese': + return 'EUC-JP'; + + case 'cseucfixwidjapanese': + case 'extendedunixcodefixedwidthforjapanese': + return 'Extended_UNIX_Code_Fixed_Width_for_Japanese'; + + case 'gb18030': + return 'GB18030'; + + case 'chinese': + case 'cp936': + case 'csgb2312': + case 'csiso58gb231280': + case 'gb2312': + case 'gb231280': + case 'gbk': + case 'isoir58': + case 'ms936': + case 'windows936': + return 'GBK'; + + case 'cn': + case 'csiso57gb1988': + case 'gb198880': + case 'iso646cn': + case 'isoir57': + return 'GB_1988-80'; + + case 'csiso153gost1976874': + case 'gost1976874': + case 'isoir153': + case 'stsev35888': + return 'GOST_19768-74'; + + case 'csiso150': + case 'csiso150greekccitt': + case 'greekccitt': + case 'isoir150': + return 'greek-ccitt'; + + case 'csiso88greek7': + case 'greek7': + case 'isoir88': + return 'greek7'; + + case 'csiso18greek7old': + case 'greek7old': + case 'isoir18': + return 'greek7-old'; + + case 'cshpdesktop': + case 'hpdesktop': + return 'HP-DeskTop'; + + case 'cshplegal': + case 'hplegal': + return 'HP-Legal'; + + case 'cshpmath8': + case 'hpmath8': + return 'HP-Math8'; + + case 'cshppifont': + case 'hppifont': + return 'HP-Pi-font'; + + case 'cshproman8': + case 'hproman8': + case 'r8': + case 'roman8': + return 'hp-roman8'; + + case 'hzgb2312': + return 'HZ-GB-2312'; + + case 'csibmsymbols': + case 'ibmsymbols': + return 'IBM-Symbols'; + + case 'csibmthai': + case 'ibmthai': + return 'IBM-Thai'; + + case 'cp37': + case 'csibm37': + case 'ebcdiccpca': + case 'ebcdiccpnl': + case 'ebcdiccpus': + case 'ebcdiccpwt': + case 'ibm37': + return 'IBM037'; + + case 'cp38': + case 'csibm38': + case 'ebcdicint': + case 'ibm38': + return 'IBM038'; + + case 'cp273': + case 'csibm273': + case 'ibm273': + return 'IBM273'; + + case 'cp274': + case 'csibm274': + case 'ebcdicbe': + case 'ibm274': + return 'IBM274'; + + case 'cp275': + case 'csibm275': + case 'ebcdicbr': + case 'ibm275': + return 'IBM275'; + + case 'csibm277': + case 'ebcdiccpdk': + case 'ebcdiccpno': + case 'ibm277': + return 'IBM277'; + + case 'cp278': + case 'csibm278': + case 'ebcdiccpfi': + case 'ebcdiccpse': + case 'ibm278': + return 'IBM278'; + + case 'cp280': + case 'csibm280': + case 'ebcdiccpit': + case 'ibm280': + return 'IBM280'; + + case 'cp281': + case 'csibm281': + case 'ebcdicjpe': + case 'ibm281': + return 'IBM281'; + + case 'cp284': + case 'csibm284': + case 'ebcdiccpes': + case 'ibm284': + return 'IBM284'; + + case 'cp285': + case 'csibm285': + case 'ebcdiccpgb': + case 'ibm285': + return 'IBM285'; + + case 'cp290': + case 'csibm290': + case 'ebcdicjpkana': + case 'ibm290': + return 'IBM290'; + + case 'cp297': + case 'csibm297': + case 'ebcdiccpfr': + case 'ibm297': + return 'IBM297'; + + case 'cp420': + case 'csibm420': + case 'ebcdiccpar1': + case 'ibm420': + return 'IBM420'; + + case 'cp423': + case 'csibm423': + case 'ebcdiccpgr': + case 'ibm423': + return 'IBM423'; + + case 'cp424': + case 'csibm424': + case 'ebcdiccphe': + case 'ibm424': + return 'IBM424'; + + case '437': + case 'cp437': + case 'cspc8codepage437': + case 'ibm437': + return 'IBM437'; + + case 'cp500': + case 'csibm500': + case 'ebcdiccpbe': + case 'ebcdiccpch': + case 'ibm500': + return 'IBM500'; + + case 'cp775': + case 'cspc775baltic': + case 'ibm775': + return 'IBM775'; + + case '850': + case 'cp850': + case 'cspc850multilingual': + case 'ibm850': + return 'IBM850'; + + case '851': + case 'cp851': + case 'csibm851': + case 'ibm851': + return 'IBM851'; + + case '852': + case 'cp852': + case 'cspcp852': + case 'ibm852': + return 'IBM852'; + + case '855': + case 'cp855': + case 'csibm855': + case 'ibm855': + return 'IBM855'; + + case '857': + case 'cp857': + case 'csibm857': + case 'ibm857': + return 'IBM857'; + + case 'ccsid858': + case 'cp858': + case 'ibm858': + case 'pcmultilingual850euro': + return 'IBM00858'; + + case '860': + case 'cp860': + case 'csibm860': + case 'ibm860': + return 'IBM860'; + + case '861': + case 'cp861': + case 'cpis': + case 'csibm861': + case 'ibm861': + return 'IBM861'; + + case '862': + case 'cp862': + case 'cspc862latinhebrew': + case 'ibm862': + return 'IBM862'; + + case '863': + case 'cp863': + case 'csibm863': + case 'ibm863': + return 'IBM863'; + + case 'cp864': + case 'csibm864': + case 'ibm864': + return 'IBM864'; + + case '865': + case 'cp865': + case 'csibm865': + case 'ibm865': + return 'IBM865'; + + case '866': + case 'cp866': + case 'csibm866': + case 'ibm866': + return 'IBM866'; + + case 'cp868': + case 'cpar': + case 'csibm868': + case 'ibm868': + return 'IBM868'; + + case '869': + case 'cp869': + case 'cpgr': + case 'csibm869': + case 'ibm869': + return 'IBM869'; + + case 'cp870': + case 'csibm870': + case 'ebcdiccproece': + case 'ebcdiccpyu': + case 'ibm870': + return 'IBM870'; + + case 'cp871': + case 'csibm871': + case 'ebcdiccpis': + case 'ibm871': + return 'IBM871'; + + case 'cp880': + case 'csibm880': + case 'ebcdiccyrillic': + case 'ibm880': + return 'IBM880'; + + case 'cp891': + case 'csibm891': + case 'ibm891': + return 'IBM891'; + + case 'cp903': + case 'csibm903': + case 'ibm903': + return 'IBM903'; + + case '904': + case 'cp904': + case 'csibbm904': + case 'ibm904': + return 'IBM904'; + + case 'cp905': + case 'csibm905': + case 'ebcdiccptr': + case 'ibm905': + return 'IBM905'; + + case 'cp918': + case 'csibm918': + case 'ebcdiccpar2': + case 'ibm918': + return 'IBM918'; + + case 'ccsid924': + case 'cp924': + case 'ebcdiclatin9euro': + case 'ibm924': + return 'IBM00924'; + + case 'cp1026': + case 'csibm1026': + case 'ibm1026': + return 'IBM1026'; + + case 'ibm1047': + return 'IBM1047'; + + case 'ccsid1140': + case 'cp1140': + case 'ebcdicus37euro': + case 'ibm1140': + return 'IBM01140'; + + case 'ccsid1141': + case 'cp1141': + case 'ebcdicde273euro': + case 'ibm1141': + return 'IBM01141'; + + case 'ccsid1142': + case 'cp1142': + case 'ebcdicdk277euro': + case 'ebcdicno277euro': + case 'ibm1142': + return 'IBM01142'; + + case 'ccsid1143': + case 'cp1143': + case 'ebcdicfi278euro': + case 'ebcdicse278euro': + case 'ibm1143': + return 'IBM01143'; + + case 'ccsid1144': + case 'cp1144': + case 'ebcdicit280euro': + case 'ibm1144': + return 'IBM01144'; + + case 'ccsid1145': + case 'cp1145': + case 'ebcdices284euro': + case 'ibm1145': + return 'IBM01145'; + + case 'ccsid1146': + case 'cp1146': + case 'ebcdicgb285euro': + case 'ibm1146': + return 'IBM01146'; + + case 'ccsid1147': + case 'cp1147': + case 'ebcdicfr297euro': + case 'ibm1147': + return 'IBM01147'; + + case 'ccsid1148': + case 'cp1148': + case 'ebcdicinternational500euro': + case 'ibm1148': + return 'IBM01148'; + + case 'ccsid1149': + case 'cp1149': + case 'ebcdicis871euro': + case 'ibm1149': + return 'IBM01149'; + + case 'csiso143iecp271': + case 'iecp271': + case 'isoir143': + return 'IEC_P27-1'; + + case 'csiso49inis': + case 'inis': + case 'isoir49': + return 'INIS'; + + case 'csiso50inis8': + case 'inis8': + case 'isoir50': + return 'INIS-8'; + + case 'csiso51iniscyrillic': + case 'iniscyrillic': + case 'isoir51': + return 'INIS-cyrillic'; + + case 'csinvariant': + case 'invariant': + return 'INVARIANT'; + + case 'iso2022cn': + return 'ISO-2022-CN'; + + case 'iso2022cnext': + return 'ISO-2022-CN-EXT'; + + case 'csiso2022jp': + case 'iso2022jp': + return 'ISO-2022-JP'; + + case 'csiso2022jp2': + case 'iso2022jp2': + return 'ISO-2022-JP-2'; + + case 'csiso2022kr': + case 'iso2022kr': + return 'ISO-2022-KR'; + + case 'cswindows30latin1': + case 'iso88591windows30latin1': + return 'ISO-8859-1-Windows-3.0-Latin-1'; + + case 'cswindows31latin1': + case 'iso88591windows31latin1': + return 'ISO-8859-1-Windows-3.1-Latin-1'; + + case 'csisolatin2': + case 'iso88592': + case 'iso885921987': + case 'isoir101': + case 'l2': + case 'latin2': + return 'ISO-8859-2'; + + case 'cswindows31latin2': + case 'iso88592windowslatin2': + return 'ISO-8859-2-Windows-Latin-2'; + + case 'csisolatin3': + case 'iso88593': + case 'iso885931988': + case 'isoir109': + case 'l3': + case 'latin3': + return 'ISO-8859-3'; + + case 'csisolatin4': + case 'iso88594': + case 'iso885941988': + case 'isoir110': + case 'l4': + case 'latin4': + return 'ISO-8859-4'; + + case 'csisolatincyrillic': + case 'cyrillic': + case 'iso88595': + case 'iso885951988': + case 'isoir144': + return 'ISO-8859-5'; + + case 'arabic': + case 'asmo708': + case 'csisolatinarabic': + case 'ecma114': + case 'iso88596': + case 'iso885961987': + case 'isoir127': + return 'ISO-8859-6'; + + case 'csiso88596e': + case 'iso88596e': + return 'ISO-8859-6-E'; + + case 'csiso88596i': + case 'iso88596i': + return 'ISO-8859-6-I'; + + case 'csisolatingreek': + case 'ecma118': + case 'elot928': + case 'greek': + case 'greek8': + case 'iso88597': + case 'iso885971987': + case 'isoir126': + return 'ISO-8859-7'; + + case 'csisolatinhebrew': + case 'hebrew': + case 'iso88598': + case 'iso885981988': + case 'isoir138': + return 'ISO-8859-8'; + + case 'csiso88598e': + case 'iso88598e': + return 'ISO-8859-8-E'; + + case 'csiso88598i': + case 'iso88598i': + return 'ISO-8859-8-I'; + + case 'cswindows31latin5': + case 'iso88599windowslatin5': + return 'ISO-8859-9-Windows-Latin-5'; + + case 'csisolatin6': + case 'iso885910': + case 'iso8859101992': + case 'isoir157': + case 'l6': + case 'latin6': + return 'ISO-8859-10'; + + case 'iso885913': + return 'ISO-8859-13'; + + case 'iso885914': + case 'iso8859141998': + case 'isoceltic': + case 'isoir199': + case 'l8': + case 'latin8': + return 'ISO-8859-14'; + + case 'iso885915': + case 'latin9': + return 'ISO-8859-15'; + + case 'iso885916': + case 'iso8859162001': + case 'isoir226': + case 'l10': + case 'latin10': + return 'ISO-8859-16'; + + case 'iso10646j1': + return 'ISO-10646-J-1'; + + case 'csunicode': + case 'iso10646ucs2': + return 'ISO-10646-UCS-2'; + + case 'csucs4': + case 'iso10646ucs4': + return 'ISO-10646-UCS-4'; + + case 'csunicodeascii': + case 'iso10646ucsbasic': + return 'ISO-10646-UCS-Basic'; + + case 'csunicodelatin1': + case 'iso10646': + case 'iso10646unicodelatin1': + return 'ISO-10646-Unicode-Latin1'; + + case 'csiso10646utf1': + case 'iso10646utf1': + return 'ISO-10646-UTF-1'; + + case 'csiso115481': + case 'iso115481': + case 'isotr115481': + return 'ISO-11548-1'; + + case 'csiso90': + case 'isoir90': + return 'iso-ir-90'; + + case 'csunicodeibm1261': + case 'isounicodeibm1261': + return 'ISO-Unicode-IBM-1261'; + + case 'csunicodeibm1264': + case 'isounicodeibm1264': + return 'ISO-Unicode-IBM-1264'; + + case 'csunicodeibm1265': + case 'isounicodeibm1265': + return 'ISO-Unicode-IBM-1265'; + + case 'csunicodeibm1268': + case 'isounicodeibm1268': + return 'ISO-Unicode-IBM-1268'; + + case 'csunicodeibm1276': + case 'isounicodeibm1276': + return 'ISO-Unicode-IBM-1276'; + + case 'csiso646basic1983': + case 'iso646basic1983': + case 'ref': + return 'ISO_646.basic:1983'; + + case 'csiso2intlrefversion': + case 'irv': + case 'iso646irv1983': + case 'isoir2': + return 'ISO_646.irv:1983'; + + case 'csiso2033': + case 'e13b': + case 'iso20331983': + case 'isoir98': + return 'ISO_2033-1983'; + + case 'csiso5427cyrillic': + case 'iso5427': + case 'isoir37': + return 'ISO_5427'; + + case 'iso5427cyrillic1981': + case 'iso54271981': + case 'isoir54': + return 'ISO_5427:1981'; + + case 'csiso5428greek': + case 'iso54281980': + case 'isoir55': + return 'ISO_5428:1980'; + + case 'csiso6937add': + case 'iso6937225': + case 'isoir152': + return 'ISO_6937-2-25'; + + case 'csisotextcomm': + case 'iso69372add': + case 'isoir142': + return 'ISO_6937-2-add'; + + case 'csiso8859supp': + case 'iso8859supp': + case 'isoir154': + case 'latin125': + return 'ISO_8859-supp'; + + case 'csiso10367box': + case 'iso10367box': + case 'isoir155': + return 'ISO_10367-box'; + + case 'csiso15italian': + case 'iso646it': + case 'isoir15': + case 'it': + return 'IT'; + + case 'csiso13jisc6220jp': + case 'isoir13': + case 'jisc62201969': + case 'jisc62201969jp': + case 'katakana': + case 'x2017': + return 'JIS_C6220-1969-jp'; + + case 'csiso14jisc6220ro': + case 'iso646jp': + case 'isoir14': + case 'jisc62201969ro': + case 'jp': + return 'JIS_C6220-1969-ro'; + + case 'csiso42jisc62261978': + case 'isoir42': + case 'jisc62261978': + return 'JIS_C6226-1978'; + + case 'csiso87jisx208': + case 'isoir87': + case 'jisc62261983': + case 'jisx2081983': + case 'x208': + return 'JIS_C6226-1983'; + + case 'csiso91jisc62291984a': + case 'isoir91': + case 'jisc62291984a': + case 'jpocra': + return 'JIS_C6229-1984-a'; + + case 'csiso92jisc62991984b': + case 'iso646jpocrb': + case 'isoir92': + case 'jisc62291984b': + case 'jpocrb': + return 'JIS_C6229-1984-b'; + + case 'csiso93jis62291984badd': + case 'isoir93': + case 'jisc62291984badd': + case 'jpocrbadd': + return 'JIS_C6229-1984-b-add'; + + case 'csiso94jis62291984hand': + case 'isoir94': + case 'jisc62291984hand': + case 'jpocrhand': + return 'JIS_C6229-1984-hand'; + + case 'csiso95jis62291984handadd': + case 'isoir95': + case 'jisc62291984handadd': + case 'jpocrhandadd': + return 'JIS_C6229-1984-hand-add'; + + case 'csiso96jisc62291984kana': + case 'isoir96': + case 'jisc62291984kana': + return 'JIS_C6229-1984-kana'; + + case 'csjisencoding': + case 'jisencoding': + return 'JIS_Encoding'; + + case 'cshalfwidthkatakana': + case 'jisx201': + case 'x201': + return 'JIS_X0201'; + + case 'csiso159jisx2121990': + case 'isoir159': + case 'jisx2121990': + case 'x212': + return 'JIS_X0212-1990'; + + case 'csiso141jusib1002': + case 'iso646yu': + case 'isoir141': + case 'js': + case 'jusib1002': + case 'yu': + return 'JUS_I.B1.002'; + + case 'csiso147macedonian': + case 'isoir147': + case 'jusib1003mac': + case 'macedonian': + return 'JUS_I.B1.003-mac'; + + case 'csiso146serbian': + case 'isoir146': + case 'jusib1003serb': + case 'serbian': + return 'JUS_I.B1.003-serb'; + + case 'koi7switched': + return 'KOI7-switched'; + + case 'cskoi8r': + case 'koi8r': + return 'KOI8-R'; + + case 'koi8u': + return 'KOI8-U'; + + case 'csksc5636': + case 'iso646kr': + case 'ksc5636': + return 'KSC5636'; + + case 'cskz1048': + case 'kz1048': + case 'rk1048': + case 'strk10482002': + return 'KZ-1048'; + + case 'csiso19latingreek': + case 'isoir19': + case 'latingreek': + return 'latin-greek'; + + case 'csiso27latingreek1': + case 'isoir27': + case 'latingreek1': + return 'Latin-greek-1'; + + case 'csiso158lap': + case 'isoir158': + case 'lap': + case 'latinlap': + return 'latin-lap'; + + case 'csmacintosh': + case 'mac': + case 'macintosh': + return 'macintosh'; + + case 'csmicrosoftpublishing': + case 'microsoftpublishing': + return 'Microsoft-Publishing'; + + case 'csmnem': + case 'mnem': + return 'MNEM'; + + case 'csmnemonic': + case 'mnemonic': + return 'MNEMONIC'; + + case 'csiso86hungarian': + case 'hu': + case 'iso646hu': + case 'isoir86': + case 'msz77953': + return 'MSZ_7795.3'; + + case 'csnatsdano': + case 'isoir91': + case 'natsdano': + return 'NATS-DANO'; + + case 'csnatsdanoadd': + case 'isoir92': + case 'natsdanoadd': + return 'NATS-DANO-ADD'; + + case 'csnatssefi': + case 'isoir81': + case 'natssefi': + return 'NATS-SEFI'; + + case 'csnatssefiadd': + case 'isoir82': + case 'natssefiadd': + return 'NATS-SEFI-ADD'; + + case 'csiso151cuba': + case 'cuba': + case 'iso646cu': + case 'isoir151': + case 'ncnc1081': + return 'NC_NC00-10:81'; + + case 'csiso69french': + case 'fr': + case 'iso646fr': + case 'isoir69': + case 'nfz62010': + return 'NF_Z_62-010'; + + case 'csiso25french': + case 'iso646fr1': + case 'isoir25': + case 'nfz620101973': + return 'NF_Z_62-010_(1973)'; + + case 'csiso60danishnorwegian': + case 'csiso60norwegian1': + case 'iso646no': + case 'isoir60': + case 'no': + case 'ns45511': + return 'NS_4551-1'; + + case 'csiso61norwegian2': + case 'iso646no2': + case 'isoir61': + case 'no2': + case 'ns45512': + return 'NS_4551-2'; + + case 'osdebcdicdf3irv': + return 'OSD_EBCDIC_DF03_IRV'; + + case 'osdebcdicdf41': + return 'OSD_EBCDIC_DF04_1'; + + case 'osdebcdicdf415': + return 'OSD_EBCDIC_DF04_15'; + + case 'cspc8danishnorwegian': + case 'pc8danishnorwegian': + return 'PC8-Danish-Norwegian'; + + case 'cspc8turkish': + case 'pc8turkish': + return 'PC8-Turkish'; + + case 'csiso16portuguese': + case 'iso646pt': + case 'isoir16': + case 'pt': + return 'PT'; + + case 'csiso84portuguese2': + case 'iso646pt2': + case 'isoir84': + case 'pt2': + return 'PT2'; + + case 'cp154': + case 'csptcp154': + case 'cyrillicasian': + case 'pt154': + case 'ptcp154': + return 'PTCP154'; + + case 'scsu': + return 'SCSU'; + + case 'csiso10swedish': + case 'fi': + case 'iso646fi': + case 'iso646se': + case 'isoir10': + case 'se': + case 'sen850200b': + return 'SEN_850200_B'; + + case 'csiso11swedishfornames': + case 'iso646se2': + case 'isoir11': + case 'se2': + case 'sen850200c': + return 'SEN_850200_C'; + + case 'csiso102t617bit': + case 'isoir102': + case 't617bit': + return 'T.61-7bit'; + + case 'csiso103t618bit': + case 'isoir103': + case 't61': + case 't618bit': + return 'T.61-8bit'; + + case 'csiso128t101g2': + case 'isoir128': + case 't101g2': + return 'T.101-G2'; + + case 'cstscii': + case 'tscii': + return 'TSCII'; + + case 'csunicode11': + case 'unicode11': + return 'UNICODE-1-1'; + + case 'csunicode11utf7': + case 'unicode11utf7': + return 'UNICODE-1-1-UTF-7'; + + case 'csunknown8bit': + case 'unknown8bit': + return 'UNKNOWN-8BIT'; + + case 'ansix341968': + case 'ansix341986': + case 'ascii': + case 'cp367': + case 'csascii': + case 'ibm367': + case 'iso646irv1991': + case 'iso646us': + case 'isoir6': + case 'us': + case 'usascii': + return 'US-ASCII'; + + case 'csusdk': + case 'usdk': + return 'us-dk'; + + case 'utf7': + return 'UTF-7'; + + case 'utf8': + return 'UTF-8'; + + case 'utf16': + return 'UTF-16'; + + case 'utf16be': + return 'UTF-16BE'; + + case 'utf16le': + return 'UTF-16LE'; + + case 'utf32': + return 'UTF-32'; + + case 'utf32be': + return 'UTF-32BE'; + + case 'utf32le': + return 'UTF-32LE'; + + case 'csventurainternational': + case 'venturainternational': + return 'Ventura-International'; + + case 'csventuramath': + case 'venturamath': + return 'Ventura-Math'; + + case 'csventuraus': + case 'venturaus': + return 'Ventura-US'; + + case 'csiso70videotexsupp1': + case 'isoir70': + case 'videotexsuppl': + return 'videotex-suppl'; + + case 'csviqr': + case 'viqr': + return 'VIQR'; + + case 'csviscii': + case 'viscii': + return 'VISCII'; + + case 'csshiftjis': + case 'cswindows31j': + case 'mskanji': + case 'shiftjis': + case 'windows31j': + return 'Windows-31J'; + + case 'iso885911': + case 'tis620': + return 'windows-874'; + + case 'cseuckr': + case 'csksc56011987': + case 'euckr': + case 'isoir149': + case 'korean': + case 'ksc5601': + case 'ksc56011987': + case 'ksc56011989': + case 'windows949': + return 'windows-949'; + + case 'windows1250': + return 'windows-1250'; + + case 'windows1251': + return 'windows-1251'; + + case 'cp819': + case 'csisolatin1': + case 'ibm819': + case 'iso88591': + case 'iso885911987': + case 'isoir100': + case 'l1': + case 'latin1': + case 'windows1252': + return 'windows-1252'; + + case 'windows1253': + return 'windows-1253'; + + case 'csisolatin5': + case 'iso88599': + case 'iso885991989': + case 'isoir148': + case 'l5': + case 'latin5': + case 'windows1254': + return 'windows-1254'; + + case 'windows1255': + return 'windows-1255'; + + case 'windows1256': + return 'windows-1256'; + + case 'windows1257': + return 'windows-1257'; + + case 'windows1258': + return 'windows-1258'; + + default: + return $charset; + } + } + + /** + * @return string + */ + public static function get_curl_version() + { + if (is_array($curl = curl_version())) { + $curl = $curl['version']; + } else { + $curl = '0'; + } + return $curl; + } + + /** + * Strip HTML comments + * + * @deprecated since SimplePie 1.9.0. If you need it, you can copy the function to your codebase. But you should consider using `DOMDocument` for any DOM wrangling. + * @param string $data Data to strip comments from + * @return string Comment stripped string + */ + public static function strip_comments(string $data) + { + // trigger_error(sprintf('Using method "' . __METHOD__ . '" is deprecated since SimplePie 1.9.'), \E_USER_DEPRECATED); + + $output = ''; + while (($start = strpos($data, '<!--')) !== false) { + $output .= substr($data, 0, $start); + if (($end = strpos($data, '-->', $start)) !== false) { + $data = substr_replace($data, '', 0, $end + 3); + } else { + $data = ''; + } + } + return $output . $data; + } + + /** + * @return int|false + */ + public static function parse_date(string $dt) + { + $parser = \SimplePie\Parse\Date::get(); + return $parser->parse($dt); + } + + /** + * Decode HTML entities + * + * @deprecated since SimplePie 1.3, use DOMDocument instead + * @param string $data Input data + * @return string Output data + */ + public static function entities_decode(string $data) + { + // trigger_error(sprintf('Using method "' . __METHOD__ . '" is deprecated since SimplePie 1.3, use "DOMDocument" instead.'), \E_USER_DEPRECATED); + + $decoder = new \SimplePie_Decode_HTML_Entities($data); + return $decoder->parse(); + } + + /** + * Remove RFC822 comments + * + * @deprecated since SimplePie 1.9.0. If you need it, consider copying the function to your codebase. + * @param string $string Data to strip comments from + * @return string Comment stripped string + */ + public static function uncomment_rfc822(string $string) + { + // trigger_error(sprintf('Using method "' . __METHOD__ . '" is deprecated since SimplePie 1.9.'), \E_USER_DEPRECATED); + + $position = 0; + $length = strlen($string); + $depth = 0; + + $output = ''; + + while ($position < $length && ($pos = strpos($string, '(', $position)) !== false) { + $output .= substr($string, $position, $pos - $position); + $position = $pos + 1; + if ($string[$pos - 1] !== '\\') { + $depth++; + while ($depth && $position < $length) { + $position += strcspn($string, '()', $position); + if ($string[$position - 1] === '\\') { + $position++; + continue; + } elseif (isset($string[$position])) { + switch ($string[$position]) { + case '(': + $depth++; + break; + + case ')': + $depth--; + break; + } + $position++; + } else { + break; + } + } + } else { + $output .= '('; + } + } + $output .= substr($string, $position); + + return $output; + } + + /** + * @return string + */ + public static function parse_mime(string $mime) + { + if (($pos = strpos($mime, ';')) === false) { + return trim($mime); + } + + return trim(substr($mime, 0, $pos)); + } + + /** + * @param array<string, array<string, string>> $attribs + * @return int-mask-of<SimplePie::CONSTRUCT_*> + */ + public static function atom_03_construct_type(array $attribs) + { + if (isset($attribs['']['mode']) && strtolower(trim($attribs['']['mode'])) === 'base64') { + $mode = \SimplePie\SimplePie::CONSTRUCT_BASE64; + } else { + $mode = \SimplePie\SimplePie::CONSTRUCT_NONE; + } + if (isset($attribs['']['type'])) { + switch (strtolower(trim($attribs['']['type']))) { + case 'text': + case 'text/plain': + return \SimplePie\SimplePie::CONSTRUCT_TEXT | $mode; + + case 'html': + case 'text/html': + return \SimplePie\SimplePie::CONSTRUCT_HTML | $mode; + + case 'xhtml': + case 'application/xhtml+xml': + return \SimplePie\SimplePie::CONSTRUCT_XHTML | $mode; + + default: + return \SimplePie\SimplePie::CONSTRUCT_NONE | $mode; + } + } + + return \SimplePie\SimplePie::CONSTRUCT_TEXT | $mode; + } + + /** + * @param array<string, array<string, string>> $attribs + * @return int-mask-of<SimplePie::CONSTRUCT_*> + */ + public static function atom_10_construct_type(array $attribs) + { + if (isset($attribs['']['type'])) { + switch (strtolower(trim($attribs['']['type']))) { + case 'text': + return \SimplePie\SimplePie::CONSTRUCT_TEXT; + + case 'html': + return \SimplePie\SimplePie::CONSTRUCT_HTML; + + case 'xhtml': + return \SimplePie\SimplePie::CONSTRUCT_XHTML; + + default: + return \SimplePie\SimplePie::CONSTRUCT_NONE; + } + } + return \SimplePie\SimplePie::CONSTRUCT_TEXT; + } + + /** + * @param array<string, array<string, string>> $attribs + * @return int-mask-of<SimplePie::CONSTRUCT_*> + */ + public static function atom_10_content_construct_type(array $attribs) + { + if (isset($attribs['']['type'])) { + $type = strtolower(trim($attribs['']['type'])); + switch ($type) { + case 'text': + return \SimplePie\SimplePie::CONSTRUCT_TEXT; + + case 'html': + return \SimplePie\SimplePie::CONSTRUCT_HTML; + + case 'xhtml': + return \SimplePie\SimplePie::CONSTRUCT_XHTML; + } + if (in_array(substr($type, -4), ['+xml', '/xml']) || substr($type, 0, 5) === 'text/') { + return \SimplePie\SimplePie::CONSTRUCT_NONE; + } else { + return \SimplePie\SimplePie::CONSTRUCT_BASE64; + } + } + + return \SimplePie\SimplePie::CONSTRUCT_TEXT; + } + + /** + * @return bool + */ + public static function is_isegment_nz_nc(string $string) + { + return (bool) preg_match('/^([A-Za-z0-9\-._~\x{A0}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFEF}\x{10000}-\x{1FFFD}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}\x{40000}-\x{4FFFD}\x{50000}-\x{5FFFD}\x{60000}-\x{6FFFD}\x{70000}-\x{7FFFD}\x{80000}-\x{8FFFD}\x{90000}-\x{9FFFD}\x{A0000}-\x{AFFFD}\x{B0000}-\x{BFFFD}\x{C0000}-\x{CFFFD}\x{D0000}-\x{DFFFD}\x{E1000}-\x{EFFFD}!$&\'()*+,;=@]|(%[0-9ABCDEF]{2}))+$/u', $string); + } + + /** + * @return string[] + */ + public static function space_separated_tokens(string $string) + { + $space_characters = "\x20\x09\x0A\x0B\x0C\x0D"; + $string_length = strlen($string); + + $position = strspn($string, $space_characters); + $tokens = []; + + while ($position < $string_length) { + $len = strcspn($string, $space_characters, $position); + $tokens[] = substr($string, $position, $len); + $position += $len; + $position += strspn($string, $space_characters, $position); + } + + return $tokens; + } + + /** + * Converts a unicode codepoint to a UTF-8 character + * + * @static + * @param int $codepoint Unicode codepoint + * @return string|false UTF-8 character + */ + public static function codepoint_to_utf8(int $codepoint) + { + if ($codepoint < 0) { + return false; + } elseif ($codepoint <= 0x7f) { + return chr($codepoint); + } elseif ($codepoint <= 0x7ff) { + return chr(0xc0 | ($codepoint >> 6)) . chr(0x80 | ($codepoint & 0x3f)); + } elseif ($codepoint <= 0xffff) { + return chr(0xe0 | ($codepoint >> 12)) . chr(0x80 | (($codepoint >> 6) & 0x3f)) . chr(0x80 | ($codepoint & 0x3f)); + } elseif ($codepoint <= 0x10ffff) { + return chr(0xf0 | ($codepoint >> 18)) . chr(0x80 | (($codepoint >> 12) & 0x3f)) . chr(0x80 | (($codepoint >> 6) & 0x3f)) . chr(0x80 | ($codepoint & 0x3f)); + } + + // U+FFFD REPLACEMENT CHARACTER + return "\xEF\xBF\xBD"; + } + + /** + * Similar to parse_str() + * + * Returns an associative array of name/value pairs, where the value is an + * array of values that have used the same name + * + * @deprecated since SimplePie 1.9.0. If you need it, consider copying the function to your codebase. + * @static + * @param string $str The input string. + * @return array<string, array<string|null>> + */ + public static function parse_str(string $str) + { + // trigger_error(sprintf('Using method "' . __METHOD__ . '" is deprecated since SimplePie 1.9.'), \E_USER_DEPRECATED); + + $return = []; + $str = explode('&', $str); + + foreach ($str as $section) { + if (strpos($section, '=') !== false) { + [$name, $value] = explode('=', $section, 2); + $return[urldecode($name)][] = urldecode($value); + } else { + $return[urldecode($section)][] = null; + } + } + + return $return; + } + + /** + * Detect XML encoding, as per XML 1.0 Appendix F.1 + * + * @todo Add support for EBCDIC + * @param string $data XML data + * @param \SimplePie\Registry $registry Class registry + * @return array<string> Possible encodings + */ + public static function xml_encoding(string $data, \SimplePie\Registry $registry) + { + // UTF-32 Big Endian BOM + if (substr($data, 0, 4) === "\x00\x00\xFE\xFF") { + $encoding[] = 'UTF-32BE'; + } + // UTF-32 Little Endian BOM + elseif (substr($data, 0, 4) === "\xFF\xFE\x00\x00") { + $encoding[] = 'UTF-32LE'; + } + // UTF-16 Big Endian BOM + elseif (substr($data, 0, 2) === "\xFE\xFF") { + $encoding[] = 'UTF-16BE'; + } + // UTF-16 Little Endian BOM + elseif (substr($data, 0, 2) === "\xFF\xFE") { + $encoding[] = 'UTF-16LE'; + } + // UTF-8 BOM + elseif (substr($data, 0, 3) === "\xEF\xBB\xBF") { + $encoding[] = 'UTF-8'; + } + // UTF-32 Big Endian Without BOM + elseif (substr($data, 0, 20) === "\x00\x00\x00\x3C\x00\x00\x00\x3F\x00\x00\x00\x78\x00\x00\x00\x6D\x00\x00\x00\x6C") { + if ($pos = strpos($data, "\x00\x00\x00\x3F\x00\x00\x00\x3E")) { + $parser = $registry->create(Parser::class, [Misc::change_encoding(substr($data, 20, $pos - 20), 'UTF-32BE', 'UTF-8')]); + if ($parser->parse()) { + $encoding[] = $parser->encoding; + } + } + $encoding[] = 'UTF-32BE'; + } + // UTF-32 Little Endian Without BOM + elseif (substr($data, 0, 20) === "\x3C\x00\x00\x00\x3F\x00\x00\x00\x78\x00\x00\x00\x6D\x00\x00\x00\x6C\x00\x00\x00") { + if ($pos = strpos($data, "\x3F\x00\x00\x00\x3E\x00\x00\x00")) { + $parser = $registry->create(Parser::class, [Misc::change_encoding(substr($data, 20, $pos - 20), 'UTF-32LE', 'UTF-8')]); + if ($parser->parse()) { + $encoding[] = $parser->encoding; + } + } + $encoding[] = 'UTF-32LE'; + } + // UTF-16 Big Endian Without BOM + elseif (substr($data, 0, 10) === "\x00\x3C\x00\x3F\x00\x78\x00\x6D\x00\x6C") { + if ($pos = strpos($data, "\x00\x3F\x00\x3E")) { + $parser = $registry->create(Parser::class, [Misc::change_encoding(substr($data, 20, $pos - 10), 'UTF-16BE', 'UTF-8')]); + if ($parser->parse()) { + $encoding[] = $parser->encoding; + } + } + $encoding[] = 'UTF-16BE'; + } + // UTF-16 Little Endian Without BOM + elseif (substr($data, 0, 10) === "\x3C\x00\x3F\x00\x78\x00\x6D\x00\x6C\x00") { + if ($pos = strpos($data, "\x3F\x00\x3E\x00")) { + $parser = $registry->create(Parser::class, [Misc::change_encoding(substr($data, 20, $pos - 10), 'UTF-16LE', 'UTF-8')]); + if ($parser->parse()) { + $encoding[] = $parser->encoding; + } + } + $encoding[] = 'UTF-16LE'; + } + // US-ASCII (or superset) + elseif (substr($data, 0, 5) === "\x3C\x3F\x78\x6D\x6C") { + if ($pos = strpos($data, "\x3F\x3E")) { + $parser = $registry->create(Parser::class, [substr($data, 5, $pos - 5)]); + if ($parser->parse()) { + $encoding[] = $parser->encoding; + } + } + $encoding[] = 'UTF-8'; + } + // Fallback to UTF-8 + else { + $encoding[] = 'UTF-8'; + } + return $encoding; + } + + /** + * @return void + */ + public static function output_javascript() + { + if (function_exists('ob_gzhandler')) { + ob_start('ob_gzhandler'); + } + header('Content-type: text/javascript; charset: UTF-8'); + header('Cache-Control: must-revalidate'); + header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 604800) . ' GMT'); // 7 days + + $body = <<<JS +function embed_quicktime(type, bgcolor, width, height, link, placeholder, loop) { + if (placeholder != '') { + document.writeln('<embed type="'+type+'" style="cursor:hand; cursor:pointer;" href="'+link+'" src="'+placeholder+'" width="'+width+'" height="'+height+'" autoplay="false" target="myself" controller="false" loop="'+loop+'" scale="aspect" bgcolor="'+bgcolor+'" pluginspage="http://www.apple.com/quicktime/download/"></embed>'); + } + else { + document.writeln('<embed type="'+type+'" style="cursor:hand; cursor:pointer;" src="'+link+'" width="'+width+'" height="'+height+'" autoplay="false" target="myself" controller="true" loop="'+loop+'" scale="aspect" bgcolor="'+bgcolor+'" pluginspage="http://www.apple.com/quicktime/download/"></embed>'); + } +} + +function embed_flash(bgcolor, width, height, link, loop, type) { + document.writeln('<embed src="'+link+'" pluginspage="http://www.macromedia.com/go/getflashplayer" type="'+type+'" quality="high" width="'+width+'" height="'+height+'" bgcolor="'+bgcolor+'" loop="'+loop+'"></embed>'); +} + +function embed_flv(width, height, link, placeholder, loop, player) { + document.writeln('<embed src="'+player+'" pluginspage="http://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash" quality="high" width="'+width+'" height="'+height+'" wmode="transparent" flashvars="file='+link+'&autostart=false&repeat='+loop+'&showdigits=true&showfsbutton=false"></embed>'); +} + +function embed_wmedia(width, height, link) { + document.writeln('<embed type="application/x-mplayer2" src="'+link+'" autosize="1" width="'+width+'" height="'+height+'" showcontrols="1" showstatusbar="0" showdisplay="0" autostart="0"></embed>'); +} +JS; + echo $body; + } + + /** + * Get the SimplePie build timestamp + * + * Uses the git index if it exists, otherwise uses the modification time + * of the newest file. + * + * @return int + */ + public static function get_build() + { + if (self::$SIMPLEPIE_BUILD !== null) { + return self::$SIMPLEPIE_BUILD; + } + + $root = dirname(__FILE__, 2); + if (file_exists($root . '/.git/index')) { + self::$SIMPLEPIE_BUILD = (int) filemtime($root . '/.git/index'); + + return self::$SIMPLEPIE_BUILD; + } elseif (file_exists($root . '/src')) { + $time = 0; + foreach (glob($root . '/src/*.php') ?: [] as $file) { + if (($mtime = filemtime($file)) > $time) { + $time = $mtime; + } + } + + self::$SIMPLEPIE_BUILD = $time; + + return self::$SIMPLEPIE_BUILD; + } + + self::$SIMPLEPIE_BUILD = (int) filemtime(__FILE__); + + return self::$SIMPLEPIE_BUILD; + } + + /** + * Get the default user agent string + * + * @return string + */ + public static function get_default_useragent() + { + return \SimplePie\SimplePie::NAME . '/' . \SimplePie\SimplePie::VERSION . ' (Feed Parser; ' . \SimplePie\SimplePie::URL . '; Allow like Gecko) Build/' . static::get_build(); + } + + /** + * Format debugging information + * + * @return string + */ + public static function debug(SimplePie &$sp) + { + $info = 'SimplePie ' . \SimplePie\SimplePie::VERSION . ' Build ' . static::get_build() . "\n"; + $info .= 'PHP ' . PHP_VERSION . "\n"; + if ($sp->error() !== null) { + // TODO: Remove cast with multifeeds. + $info .= 'Error occurred: ' . implode(', ', (array) $sp->error()) . "\n"; + } else { + $info .= "No error found.\n"; + } + $info .= "Extensions:\n"; + $extensions = ['pcre', 'curl', 'zlib', 'mbstring', 'iconv', 'xmlreader', 'xml']; + foreach ($extensions as $ext) { + if (extension_loaded($ext)) { + $info .= " $ext loaded\n"; + switch ($ext) { + case 'pcre': + $info .= ' Version ' . PCRE_VERSION . "\n"; + break; + case 'curl': + $version = (array) curl_version(); + $info .= ' Version ' . $version['version'] . "\n"; + break; + case 'iconv': + $info .= ' Version ' . ICONV_VERSION . "\n"; + break; + case 'xml': + $info .= ' Version ' . LIBXML_DOTTED_VERSION . "\n"; + break; + } + } else { + $info .= " $ext not loaded\n"; + } + } + return $info; + } + + /** + * @return bool + */ + public static function silence_errors(int $num, string $str) + { + // No-op + return true; + } + + /** + * Sanitize a URL by removing HTTP credentials. + * @param string $url the URL to sanitize. + * @return string the same URL without HTTP credentials. + */ + public static function url_remove_credentials(string $url) + { + // Cast for PHPStan: I do not think this can fail. + // The regex is valid and there should be no backtracking. + // https://github.com/phpstan/phpstan/issues/11547 + return (string) preg_replace('#^(https?://)[^/:@]+:[^/:@]+@#i', '$1', $url); + } +} + +class_alias('SimplePie\Misc', 'SimplePie_Misc', false); + +} + +namespace SimplePie\Net { + +/** + * Class to validate and to work with IPv6 addresses. + * + * @copyright 2003-2005 The PHP Group + * @license http://www.opensource.org/licenses/bsd-license.php + * @link http://pear.php.net/package/Net_IPv6 + * @author Alexander Merz <alexander.merz@web.de> + * @author elfrink at introweb dot nl + * @author Josh Peck <jmp at joshpeck dot org> + * @author Sam Sneddon <geoffers@gmail.com> + */ +class IPv6 +{ + /** + * Uncompresses an IPv6 address + * + * RFC 4291 allows you to compress consecutive zero pieces in an address to + * '::'. This method expects a valid IPv6 address and expands the '::' to + * the required number of zero pieces. + * + * Example: FF01::101 -> FF01:0:0:0:0:0:0:101 + * ::1 -> 0:0:0:0:0:0:0:1 + * + * @author Alexander Merz <alexander.merz@web.de> + * @author elfrink at introweb dot nl + * @author Josh Peck <jmp at joshpeck dot org> + * @copyright 2003-2005 The PHP Group + * @license http://www.opensource.org/licenses/bsd-license.php + * @param string $ip An IPv6 address + * @return string The uncompressed IPv6 address + */ + public static function uncompress(string $ip) + { + $c1 = -1; + $c2 = -1; + if (substr_count($ip, '::') === 1) { + [$ip1, $ip2] = explode('::', $ip); + if ($ip1 === '') { + $c1 = -1; + } else { + $c1 = substr_count($ip1, ':'); + } + if ($ip2 === '') { + $c2 = -1; + } else { + $c2 = substr_count($ip2, ':'); + } + if (strpos($ip2, '.') !== false) { + $c2++; + } + // :: + if ($c1 === -1 && $c2 === -1) { + $ip = '0:0:0:0:0:0:0:0'; + } + // ::xxx + elseif ($c1 === -1) { + $fill = str_repeat('0:', 7 - $c2); + $ip = str_replace('::', $fill, $ip); + } + // xxx:: + elseif ($c2 === -1) { + $fill = str_repeat(':0', 7 - $c1); + $ip = str_replace('::', $fill, $ip); + } + // xxx::xxx + else { + $fill = ':' . str_repeat('0:', 6 - $c2 - $c1); + $ip = str_replace('::', $fill, $ip); + } + } + return $ip; + } + + /** + * Compresses an IPv6 address + * + * RFC 4291 allows you to compress consecutive zero pieces in an address to + * '::'. This method expects a valid IPv6 address and compresses consecutive + * zero pieces to '::'. + * + * Example: FF01:0:0:0:0:0:0:101 -> FF01::101 + * 0:0:0:0:0:0:0:1 -> ::1 + * + * @see uncompress() + * @param string $ip An IPv6 address + * @return string The compressed IPv6 address + */ + public static function compress(string $ip) + { + // Prepare the IP to be compressed + $ip = self::uncompress($ip); + $ip_parts = self::split_v6_v4($ip); + + // Replace all leading zeros + $ip_parts[0] = (string) preg_replace('/(^|:)0+([0-9])/', '\1\2', $ip_parts[0]); + + // Find bunches of zeros + if (preg_match_all('/(?:^|:)(?:0(?::|$))+/', $ip_parts[0], $matches, PREG_OFFSET_CAPTURE)) { + $max = 0; + $pos = null; + foreach ($matches[0] as $match) { + if (strlen($match[0]) > $max) { + $max = strlen($match[0]); + $pos = $match[1]; + } + } + + assert($pos !== null, 'For PHPStan: Since the regex matched, there is at least one match. And because the pattern is non-empty, the loop will always end with $pos ≥ 1.'); + $ip_parts[0] = substr_replace($ip_parts[0], '::', $pos, $max); + } + + if ($ip_parts[1] !== '') { + return implode(':', $ip_parts); + } + + return $ip_parts[0]; + } + + /** + * Splits an IPv6 address into the IPv6 and IPv4 representation parts + * + * RFC 4291 allows you to represent the last two parts of an IPv6 address + * using the standard IPv4 representation + * + * Example: 0:0:0:0:0:0:13.1.68.3 + * 0:0:0:0:0:FFFF:129.144.52.38 + * + * @param string $ip An IPv6 address + * @return array{string, string} [0] contains the IPv6 represented part, and [1] the IPv4 represented part + */ + private static function split_v6_v4(string $ip): array + { + if (strpos($ip, '.') !== false) { + $pos = strrpos($ip, ':'); + assert($pos !== false, 'For PHPStan: IPv6 address must contain colon, since split_v6_v4 is only ever called after uncompress.'); + $ipv6_part = substr($ip, 0, $pos); + $ipv4_part = substr($ip, $pos + 1); + return [$ipv6_part, $ipv4_part]; + } + + return [$ip, '']; + } + + /** + * Checks an IPv6 address + * + * Checks if the given IP is a valid IPv6 address + * + * @param string $ip An IPv6 address + * @return bool true if $ip is a valid IPv6 address + */ + public static function check_ipv6(string $ip) + { + $ip = self::uncompress($ip); + [$ipv6, $ipv4] = self::split_v6_v4($ip); + $ipv6 = explode(':', $ipv6); + $ipv4 = explode('.', $ipv4); + if (count($ipv6) === 8 && count($ipv4) === 1 || count($ipv6) === 6 && count($ipv4) === 4) { + foreach ($ipv6 as $ipv6_part) { + // The section can't be empty + if ($ipv6_part === '') { + return false; + } + + // Nor can it be over four characters + if (strlen($ipv6_part) > 4) { + return false; + } + + // Remove leading zeros (this is safe because of the above) + $ipv6_part = ltrim($ipv6_part, '0'); + if ($ipv6_part === '') { + $ipv6_part = '0'; + } + + // Check the value is valid + $value = hexdec($ipv6_part); + if ($value < 0 || $value > 0xFFFF) { + return false; + } + assert(is_int($value), 'For PHPStan: $value is only float when $ipv6_part > PHP_INT_MAX'); + if (dechex($value) !== strtolower($ipv6_part)) { + return false; + } + } + if (count($ipv4) === 4) { + foreach ($ipv4 as $ipv4_part) { + $value = (int) $ipv4_part; + if ((string) $value !== $ipv4_part || $value < 0 || $value > 0xFF) { + return false; + } + } + } + return true; + } + + return false; + } + + /** + * Checks if the given IP is a valid IPv6 address + * + * @codeCoverageIgnore + * @deprecated Use {@see IPv6::check_ipv6()} instead + * @see check_ipv6 + * @param string $ip An IPv6 address + * @return bool true if $ip is a valid IPv6 address + */ + public static function checkIPv6(string $ip) + { + return self::check_ipv6($ip); + } +} + +class_alias('SimplePie\Net\IPv6', 'SimplePie_Net_IPv6'); + +} + +namespace SimplePie\Parse { + +/** + * Date Parser + */ +class Date +{ + /** + * Input data + * + * @access protected + * @var string + */ + public $date; + + /** + * List of days, calendar day name => ordinal day number in the week + * + * @access protected + * @var array<string, int<1,7>> + */ + public $day = [ + // English + 'mon' => 1, + 'monday' => 1, + 'tue' => 2, + 'tuesday' => 2, + 'wed' => 3, + 'wednesday' => 3, + 'thu' => 4, + 'thursday' => 4, + 'fri' => 5, + 'friday' => 5, + 'sat' => 6, + 'saturday' => 6, + 'sun' => 7, + 'sunday' => 7, + // Dutch + 'maandag' => 1, + 'dinsdag' => 2, + 'woensdag' => 3, + 'donderdag' => 4, + 'vrijdag' => 5, + 'zaterdag' => 6, + 'zondag' => 7, + // French + 'lundi' => 1, + 'mardi' => 2, + 'mercredi' => 3, + 'jeudi' => 4, + 'vendredi' => 5, + 'samedi' => 6, + 'dimanche' => 7, + // German + 'montag' => 1, + 'mo' => 1, + 'dienstag' => 2, + 'di' => 2, + 'mittwoch' => 3, + 'mi' => 3, + 'donnerstag' => 4, + 'do' => 4, + 'freitag' => 5, + 'fr' => 5, + 'samstag' => 6, + 'sa' => 6, + 'sonnabend' => 6, + // AFAIK no short form for sonnabend + 'so' => 7, + 'sonntag' => 7, + // Italian + 'lunedì' => 1, + 'martedì' => 2, + 'mercoledì' => 3, + 'giovedì' => 4, + 'venerdì' => 5, + 'sabato' => 6, + 'domenica' => 7, + // Spanish + 'lunes' => 1, + 'martes' => 2, + 'miércoles' => 3, + 'jueves' => 4, + 'viernes' => 5, + 'sábado' => 6, + 'domingo' => 7, + // Finnish + 'maanantai' => 1, + 'tiistai' => 2, + 'keskiviikko' => 3, + 'torstai' => 4, + 'perjantai' => 5, + 'lauantai' => 6, + 'sunnuntai' => 7, + // Hungarian + 'hétfő' => 1, + 'kedd' => 2, + 'szerda' => 3, + 'csütörtok' => 4, + 'péntek' => 5, + 'szombat' => 6, + 'vasárnap' => 7, + // Greek + 'Δευ' => 1, + 'Τρι' => 2, + 'Τετ' => 3, + 'Πεμ' => 4, + 'Παρ' => 5, + 'Σαβ' => 6, + 'Κυρ' => 7, + // Russian + 'Пн.' => 1, + 'Вт.' => 2, + 'Ср.' => 3, + 'Чт.' => 4, + 'Пт.' => 5, + 'Сб.' => 6, + 'Вс.' => 7, + ]; + + /** + * List of months, calendar month name => calendar month number + * + * @access protected + * @var array<string, int<1,12>> + */ + public $month = [ + // English + 'jan' => 1, + 'january' => 1, + 'feb' => 2, + 'february' => 2, + 'mar' => 3, + 'march' => 3, + 'apr' => 4, + 'april' => 4, + 'may' => 5, + // No long form of May + 'jun' => 6, + 'june' => 6, + 'jul' => 7, + 'july' => 7, + 'aug' => 8, + 'august' => 8, + 'sep' => 9, + 'september' => 9, + 'oct' => 10, + 'october' => 10, + 'nov' => 11, + 'november' => 11, + 'dec' => 12, + 'december' => 12, + // Dutch + 'januari' => 1, + 'februari' => 2, + 'maart' => 3, + // 'april' => 4, + 'mei' => 5, + 'juni' => 6, + 'juli' => 7, + 'augustus' => 8, + // 'september' => 9, + 'oktober' => 10, + // 'november' => 11, + // 'december' => 12, + // French + 'janvier' => 1, + 'février' => 2, + 'mars' => 3, + 'avril' => 4, + 'mai' => 5, + 'juin' => 6, + 'juillet' => 7, + 'août' => 8, + 'septembre' => 9, + 'octobre' => 10, + 'novembre' => 11, + 'décembre' => 12, + // German + 'januar' => 1, + // 'jan' => 1, + 'februar' => 2, + // 'feb' => 2, + 'märz' => 3, + 'mär' => 3, + // 'april' => 4, + // 'apr' => 4, + // 'mai' => 5, // no short form for may + // 'juni' => 6, + // 'jun' => 6, + // 'juli' => 7, + // 'jul' => 7, + // 'august' => 8, + // 'aug' => 8, + // 'september' => 9, + // 'sep' => 9, + // 'oktober' => 10, + 'okt' => 10, + // 'november' => 11, + // 'nov' => 11, + 'dezember' => 12, + 'dez' => 12, + // Italian + 'gennaio' => 1, + 'febbraio' => 2, + 'marzo' => 3, + 'aprile' => 4, + 'maggio' => 5, + 'giugno' => 6, + 'luglio' => 7, + 'agosto' => 8, + 'settembre' => 9, + 'ottobre' => 10, + // 'novembre' => 11, + 'dicembre' => 12, + // Spanish + 'enero' => 1, + 'febrero' => 2, + // 'marzo' => 3, + 'abril' => 4, + 'mayo' => 5, + 'junio' => 6, + 'julio' => 7, + // 'agosto' => 8, + 'septiembre' => 9, + 'setiembre' => 9, + 'octubre' => 10, + 'noviembre' => 11, + 'diciembre' => 12, + // Finnish + 'tammikuu' => 1, + 'helmikuu' => 2, + 'maaliskuu' => 3, + 'huhtikuu' => 4, + 'toukokuu' => 5, + 'kesäkuu' => 6, + 'heinäkuu' => 7, + 'elokuu' => 8, + 'suuskuu' => 9, + 'lokakuu' => 10, + 'marras' => 11, + 'joulukuu' => 12, + // Hungarian + 'január' => 1, + 'február' => 2, + 'március' => 3, + 'április' => 4, + 'május' => 5, + 'június' => 6, + 'július' => 7, + 'augusztus' => 8, + 'szeptember' => 9, + 'október' => 10, + // 'november' => 11, + // 'december' => 12, + // Greek + 'Ιαν' => 1, + 'Φεβ' => 2, + 'Μάώ' => 3, + 'Μαώ' => 3, + 'Απρ' => 4, + 'Μάι' => 5, + 'Μαϊ' => 5, + 'Μαι' => 5, + 'Ιούν' => 6, + 'Ιον' => 6, + 'Ιούλ' => 7, + 'Ιολ' => 7, + 'Αύγ' => 8, + 'Αυγ' => 8, + 'Σεπ' => 9, + 'Οκτ' => 10, + 'Νοέ' => 11, + 'Δεκ' => 12, + // Russian + 'Янв' => 1, + 'января' => 1, + 'Фев' => 2, + 'февраля' => 2, + 'Мар' => 3, + 'марта' => 3, + 'Апр' => 4, + 'апреля' => 4, + 'Май' => 5, + 'мая' => 5, + 'Июн' => 6, + 'июня' => 6, + 'Июл' => 7, + 'июля' => 7, + 'Авг' => 8, + 'августа' => 8, + 'Сен' => 9, + 'сентября' => 9, + 'Окт' => 10, + 'октября' => 10, + 'Ноя' => 11, + 'ноября' => 11, + 'Дек' => 12, + 'декабря' => 12, + + ]; + + /** + * List of timezones, abbreviation => offset from UTC + * + * @access protected + * @var array<string, int> + */ + public $timezone = [ + 'ACDT' => 37800, + 'ACIT' => 28800, + 'ACST' => 34200, + 'ACT' => -18000, + 'ACWDT' => 35100, + 'ACWST' => 31500, + 'AEDT' => 39600, + 'AEST' => 36000, + 'AFT' => 16200, + 'AKDT' => -28800, + 'AKST' => -32400, + 'AMDT' => 18000, + 'AMT' => -14400, + 'ANAST' => 46800, + 'ANAT' => 43200, + 'ART' => -10800, + 'AZOST' => -3600, + 'AZST' => 18000, + 'AZT' => 14400, + 'BIOT' => 21600, + 'BIT' => -43200, + 'BOT' => -14400, + 'BRST' => -7200, + 'BRT' => -10800, + 'BST' => 3600, + 'BTT' => 21600, + 'CAST' => 18000, + 'CAT' => 7200, + 'CCT' => 23400, + 'CDT' => -18000, + 'CEDT' => 7200, + 'CEST' => 7200, + 'CET' => 3600, + 'CGST' => -7200, + 'CGT' => -10800, + 'CHADT' => 49500, + 'CHAST' => 45900, + 'CIST' => -28800, + 'CKT' => -36000, + 'CLDT' => -10800, + 'CLST' => -14400, + 'COT' => -18000, + 'CST' => -21600, + 'CVT' => -3600, + 'CXT' => 25200, + 'DAVT' => 25200, + 'DTAT' => 36000, + 'EADT' => -18000, + 'EAST' => -21600, + 'EAT' => 10800, + 'ECT' => -18000, + 'EDT' => -14400, + 'EEST' => 10800, + 'EET' => 7200, + 'EGT' => -3600, + 'EKST' => 21600, + 'EST' => -18000, + 'FJT' => 43200, + 'FKDT' => -10800, + 'FKST' => -14400, + 'FNT' => -7200, + 'GALT' => -21600, + 'GEDT' => 14400, + 'GEST' => 10800, + 'GFT' => -10800, + 'GILT' => 43200, + 'GIT' => -32400, + 'GST' => 14400, + // 'GST' => -7200, + 'GYT' => -14400, + 'HAA' => -10800, + 'HAC' => -18000, + 'HADT' => -32400, + 'HAE' => -14400, + 'HAP' => -25200, + 'HAR' => -21600, + 'HAST' => -36000, + 'HAT' => -9000, + 'HAY' => -28800, + 'HKST' => 28800, + 'HMT' => 18000, + 'HNA' => -14400, + 'HNC' => -21600, + 'HNE' => -18000, + 'HNP' => -28800, + 'HNR' => -25200, + 'HNT' => -12600, + 'HNY' => -32400, + 'IRDT' => 16200, + 'IRKST' => 32400, + 'IRKT' => 28800, + 'IRST' => 12600, + 'JFDT' => -10800, + 'JFST' => -14400, + 'JST' => 32400, + 'KGST' => 21600, + 'KGT' => 18000, + 'KOST' => 39600, + 'KOVST' => 28800, + 'KOVT' => 25200, + 'KRAST' => 28800, + 'KRAT' => 25200, + 'KST' => 32400, + 'LHDT' => 39600, + 'LHST' => 37800, + 'LINT' => 50400, + 'LKT' => 21600, + 'MAGST' => 43200, + 'MAGT' => 39600, + 'MAWT' => 21600, + 'MDT' => -21600, + 'MESZ' => 7200, + 'MEZ' => 3600, + 'MHT' => 43200, + 'MIT' => -34200, + 'MNST' => 32400, + 'MSDT' => 14400, + 'MSST' => 10800, + 'MST' => -25200, + 'MUT' => 14400, + 'MVT' => 18000, + 'MYT' => 28800, + 'NCT' => 39600, + 'NDT' => -9000, + 'NFT' => 41400, + 'NMIT' => 36000, + 'NOVST' => 25200, + 'NOVT' => 21600, + 'NPT' => 20700, + 'NRT' => 43200, + 'NST' => -12600, + 'NUT' => -39600, + 'NZDT' => 46800, + 'NZST' => 43200, + 'OMSST' => 25200, + 'OMST' => 21600, + 'PDT' => -25200, + 'PET' => -18000, + 'PETST' => 46800, + 'PETT' => 43200, + 'PGT' => 36000, + 'PHOT' => 46800, + 'PHT' => 28800, + 'PKT' => 18000, + 'PMDT' => -7200, + 'PMST' => -10800, + 'PONT' => 39600, + 'PST' => -28800, + 'PWT' => 32400, + 'PYST' => -10800, + 'PYT' => -14400, + 'RET' => 14400, + 'ROTT' => -10800, + 'SAMST' => 18000, + 'SAMT' => 14400, + 'SAST' => 7200, + 'SBT' => 39600, + 'SCDT' => 46800, + 'SCST' => 43200, + 'SCT' => 14400, + 'SEST' => 3600, + 'SGT' => 28800, + 'SIT' => 28800, + 'SRT' => -10800, + 'SST' => -39600, + 'SYST' => 10800, + 'SYT' => 7200, + 'TFT' => 18000, + 'THAT' => -36000, + 'TJT' => 18000, + 'TKT' => -36000, + 'TMT' => 18000, + 'TOT' => 46800, + 'TPT' => 32400, + 'TRUT' => 36000, + 'TVT' => 43200, + 'TWT' => 28800, + 'UYST' => -7200, + 'UYT' => -10800, + 'UZT' => 18000, + 'VET' => -14400, + 'VLAST' => 39600, + 'VLAT' => 36000, + 'VOST' => 21600, + 'VUT' => 39600, + 'WAST' => 7200, + 'WAT' => 3600, + 'WDT' => 32400, + 'WEST' => 3600, + 'WFT' => 43200, + 'WIB' => 25200, + 'WIT' => 32400, + 'WITA' => 28800, + 'WKST' => 18000, + 'WST' => 28800, + 'YAKST' => 36000, + 'YAKT' => 32400, + 'YAPT' => 36000, + 'YEKST' => 21600, + 'YEKT' => 18000, + ]; + + /** + * Cached PCRE for Date::$day + * + * @access protected + * @var string + */ + public $day_pcre; + + /** + * Cached PCRE for Date::$month + * + * @access protected + * @var string + */ + public $month_pcre; + + /** + * Array of user-added callback methods + * + * @access private + * @var array<string> + */ + public $built_in = []; + + /** + * Array of user-added callback methods + * + * @access private + * @var array<callable(string): (int|false)> + */ + public $user = []; + + /** + * Create new Date object, and set self::day_pcre, + * self::month_pcre, and self::built_in + * + * @access private + */ + public function __construct() + { + $this->day_pcre = '(' . implode('|', array_keys($this->day)) . ')'; + $this->month_pcre = '(' . implode('|', array_keys($this->month)) . ')'; + + static $cache; + if (!isset($cache[get_class($this)])) { + $all_methods = get_class_methods($this); + + foreach ($all_methods as $method) { + if (strtolower(substr($method, 0, 5)) === 'date_') { + $cache[get_class($this)][] = $method; + } + } + } + + foreach ($cache[get_class($this)] as $method) { + $this->built_in[] = $method; + } + } + + /** + * Get the object + * + * @access public + * @return Date + */ + public static function get() + { + static $object; + if (!$object) { + $object = new Date(); + } + return $object; + } + + /** + * Parse a date + * + * @final + * @access public + * @param string $date Date to parse + * @return int|false Timestamp corresponding to date string, or false on failure + */ + public function parse(string $date) + { + foreach ($this->user as $method) { + if (($returned = call_user_func($method, $date)) !== false) { + return (int) $returned; + } + } + + foreach ($this->built_in as $method) { + // TODO: we should really check this in constructor but that would require private properties. + /** @var callable(string): (int|false) */ + $callable = [$this, $method]; + if (($returned = call_user_func($callable, $date)) !== false) { + return $returned; + } + } + + return false; + } + + /** + * Add a callback method to parse a date + * + * @final + * @access public + * @param callable $callback + * @return void + */ + public function add_callback(callable $callback) + { + $this->user[] = $callback; + } + + /** + * Parse a superset of W3C-DTF (allows hyphens and colons to be omitted, as + * well as allowing any of upper or lower case "T", horizontal tabs, or + * spaces to be used as the time separator (including more than one)) + * + * @access protected + * @param string $date + * @return int|false Timestamp + */ + public function date_w3cdtf(string $date) + { + $pcre = <<<'PCRE' + / + ^ + (?P<year>[0-9]{4}) + (?: + -? + (?P<month>[0-9]{2}) + (?: + -? + (?P<day>[0-9]{2}) + (?: + [Tt\x09\x20]+ + (?P<hour>[0-9]{2}) + (?: + :? + (?P<minute>[0-9]{2}) + (?: + :? + (?P<second>[0-9]{2}) + (?: + . + (?P<second_fraction>[0-9]*) + )? + )? + )? + (?: + (?P<zulu>Z) + | (?P<tz_sign>[+\-]) + (?P<tz_hour>[0-9]{1,2}) + :? + (?P<tz_minute>[0-9]{1,2}) + ) + )? + )? + )? + $ + /x +PCRE; + if (preg_match($pcre, $date, $match)) { + // Fill in empty matches and convert to proper types. + $year = (int) $match['year']; + $month = isset($match['month']) ? (int) $match['month'] : 1; + $day = isset($match['day']) ? (int) $match['day'] : 1; + $hour = isset($match['hour']) ? (int) $match['hour'] : 0; + $minute = isset($match['minute']) ? (int) $match['minute'] : 0; + $second = isset($match['second']) ? (int) $match['second'] : 0; + $second_fraction = isset($match['second_fraction']) ? ((int) $match['second_fraction']) / (10 ** strlen($match['second_fraction'])) : 0; + $tz_sign = ($match['tz_sign'] ?? '') === '-' ? -1 : 1; + $tz_hour = isset($match['tz_hour']) ? (int) $match['tz_hour'] : 0; + $tz_minute = isset($match['tz_minute']) ? (int) $match['tz_minute'] : 0; + + // Numeric timezone + $timezone = $tz_hour * 3600; + $timezone += $tz_minute * 60; + $timezone *= $tz_sign; + + // Convert the number of seconds to an integer, taking decimals into account + $second = (int) round($second + $second_fraction); + + return gmmktime($hour, $minute, $second, $month, $day, $year) - $timezone; + } + + return false; + } + + /** + * Remove RFC822 comments + * + * @access protected + * @param string $string Data to strip comments from + * @return string Comment stripped string + */ + public function remove_rfc2822_comments(string $string) + { + $position = 0; + $length = strlen($string); + $depth = 0; + + $output = ''; + + while ($position < $length && ($pos = strpos($string, '(', $position)) !== false) { + $output .= substr($string, $position, $pos - $position); + $position = $pos + 1; + if ($pos === 0 || $string[$pos - 1] !== '\\') { + $depth++; + while ($depth && $position < $length) { + $position += strcspn($string, '()', $position); + if ($string[$position - 1] === '\\') { + $position++; + continue; + } elseif (isset($string[$position])) { + switch ($string[$position]) { + case '(': + $depth++; + break; + + case ')': + $depth--; + break; + } + $position++; + } else { + break; + } + } + } else { + $output .= '('; + } + } + $output .= substr($string, $position); + + return $output; + } + + /** + * Parse RFC2822's date format + * + * @access protected + * @param string $date + * @return int|false Timestamp + */ + public function date_rfc2822(string $date) + { + static $pcre; + if (!$pcre) { + $wsp = '[\x09\x20]'; + $fws = '(?:' . $wsp . '+|' . $wsp . '*(?:\x0D\x0A' . $wsp . '+)+)'; + $optional_fws = $fws . '?'; + $day_name = $this->day_pcre; + $month = $this->month_pcre; + $day = '([0-9]{1,2})'; + $hour = $minute = $second = '([0-9]{2})'; + $year = '([0-9]{2,4})'; + $num_zone = '([+\-])([0-9]{2})([0-9]{2})'; + $character_zone = '([A-Z]{1,5})'; + $zone = '(?:' . $num_zone . '|' . $character_zone . ')'; + $pcre = '/(?:' . $optional_fws . $day_name . $optional_fws . ',)?' . $optional_fws . $day . $fws . $month . $fws . $year . $fws . $hour . $optional_fws . ':' . $optional_fws . $minute . '(?:' . $optional_fws . ':' . $optional_fws . $second . ')?' . $fws . $zone . '/i'; + } + if (preg_match($pcre, $this->remove_rfc2822_comments($date), $match)) { + /* + Capturing subpatterns: + 1: Day name + 2: Day + 3: Month + 4: Year + 5: Hour + 6: Minute + 7: Second + 8: Timezone ± + 9: Timezone hours + 10: Timezone minutes + 11: Alphabetic timezone + */ + + $day = (int) $match[2]; + // Find the month number + $month = $this->month[strtolower($match[3])]; + $year = (int) $match[4]; + $hour = (int) $match[5]; + $minute = (int) $match[6]; + // Second is optional, if it is empty set it to zero + $second = (int) $match[7]; + + $tz_sign = $match[8]; + $tz_hour = (int) $match[9]; + $tz_minute = (int) $match[10]; + $tz_code = isset($match[11]) ? strtoupper($match[11]) : ''; + + // Numeric timezone + if ($tz_sign !== '') { + $timezone = $tz_hour * 3600; + $timezone += $tz_minute * 60; + if ($tz_sign === '-') { + $timezone = 0 - $timezone; + } + } + // Character timezone + elseif (isset($this->timezone[$tz_code])) { + $timezone = $this->timezone[$tz_code]; + } + // Assume everything else to be -0000 + else { + $timezone = 0; + } + + // Deal with 2/3 digit years + if ($year < 50) { + $year += 2000; + } elseif ($year < 1000) { + $year += 1900; + } + + return gmmktime($hour, $minute, $second, $month, $day, $year) - $timezone; + } + + return false; + } + + /** + * Parse RFC850's date format + * + * @access protected + * @param string $date + * @return int|false Timestamp + */ + public function date_rfc850(string $date) + { + static $pcre; + if (!$pcre) { + $space = '[\x09\x20]+'; + $day_name = $this->day_pcre; + $month = $this->month_pcre; + $day = '([0-9]{1,2})'; + $year = $hour = $minute = $second = '([0-9]{2})'; + $zone = '([A-Z]{1,5})'; + $pcre = '/^' . $day_name . ',' . $space . $day . '-' . $month . '-' . $year . $space . $hour . ':' . $minute . ':' . $second . $space . $zone . '$/i'; + } + if (preg_match($pcre, $date, $match)) { + /* + Capturing subpatterns: + 1: Day name + 2: Day + 3: Month + 4: Year + 5: Hour + 6: Minute + 7: Second + 8: Timezone + */ + + $day = (int) $match[2]; + // Month + $month = $this->month[strtolower($match[3])]; + $year = (int) $match[4]; + $hour = (int) $match[5]; + $minute = (int) $match[6]; + // Second is optional, if it is empty set it to zero + $second = (int) $match[7]; + + $tz_code = strtoupper($match[8]); + + // Character timezone + if (isset($this->timezone[$tz_code])) { + $timezone = $this->timezone[$tz_code]; + } + // Assume everything else to be -0000 + else { + $timezone = 0; + } + + // Deal with 2 digit year + if ($year < 50) { + $year += 2000; + } else { + $year += 1900; + } + + return gmmktime($hour, $minute, $second, $month, $day, $year) - $timezone; + } + + return false; + } + + /** + * Parse C99's asctime()'s date format + * + * @access protected + * @param string $date + * @return int|false Timestamp + */ + public function date_asctime(string $date) + { + static $pcre; + if (!$pcre) { + $space = '[\x09\x20]+'; + $wday_name = $this->day_pcre; + $mon_name = $this->month_pcre; + $day = '([0-9]{1,2})'; + $hour = $sec = $min = '([0-9]{2})'; + $year = '([0-9]{4})'; + $terminator = '\x0A?\x00?'; + $pcre = '/^' . $wday_name . $space . $mon_name . $space . $day . $space . $hour . ':' . $min . ':' . $sec . $space . $year . $terminator . '$/i'; + } + if (preg_match($pcre, $date, $match)) { + /* + Capturing subpatterns: + 1: Day name + 2: Month + 3: Day + 4: Hour + 5: Minute + 6: Second + 7: Year + */ + + $month = $this->month[strtolower($match[2])]; + return gmmktime((int) $match[4], (int) $match[5], (int) $match[6], $month, (int) $match[3], (int) $match[7]); + } + + return false; + } + + /** + * Parse dates using strtotime() + * + * @access protected + * @param string $date + * @return int|false Timestamp + */ + public function date_strtotime(string $date) + { + $strtotime = strtotime($date); + if ($strtotime === -1 || $strtotime === false) { + return false; + } + + return $strtotime; + } +} + +class_alias('SimplePie\Parse\Date', 'SimplePie_Parse_Date'); + +} + +namespace SimplePie { + +use SimplePie\XML\Declaration\Parser as DeclarationParser; +use XMLParser; + +/** + * Parses XML into something sane + * + * + * This class can be overloaded with {@see \SimplePie\SimplePie::set_parser_class()} + */ +class Parser implements RegistryAware +{ + /** @var int */ + public $error_code; + /** @var string */ + public $error_string; + /** @var int */ + public $current_line; + /** @var int */ + public $current_column; + /** @var int */ + public $current_byte; + /** @var string */ + public $separator = ' '; + /** @var string[] */ + public $namespace = ['']; + /** @var string[] */ + public $element = ['']; + /** @var string[] */ + public $xml_base = ['']; + /** @var bool[] */ + public $xml_base_explicit = [false]; + /** @var string[] */ + public $xml_lang = ['']; + /** @var array<string, mixed> */ + public $data = []; + /** @var array<array<string, mixed>> */ + public $datas = [[]]; + /** @var int */ + public $current_xhtml_construct = -1; + /** @var string */ + public $encoding; + /** @var Registry */ + protected $registry; + + /** + * @return void + */ + public function set_registry(\SimplePie\Registry $registry) + { + $this->registry = $registry; + } + + /** + * @return bool + */ + public function parse(string &$data, string $encoding, string $url = '') + { + if (class_exists('DOMXpath') && function_exists('Mf2\parse')) { + $doc = new \DOMDocument(); + @$doc->loadHTML($data); + $xpath = new \DOMXpath($doc); + // Check for both h-feed and h-entry, as both a feed with no entries + // and a list of entries without an h-feed wrapper are both valid. + $query = '//*[contains(concat(" ", @class, " "), " h-feed ") or '. + 'contains(concat(" ", @class, " "), " h-entry ")]'; + /** @var \DOMNodeList<\DOMElement> $result */ + $result = $xpath->query($query); + if ($result->length !== 0) { + return $this->parse_microformats($data, $url); + } + } + + // Use UTF-8 if we get passed US-ASCII, as every US-ASCII character is a UTF-8 character + if (strtoupper($encoding) === 'US-ASCII') { + $this->encoding = 'UTF-8'; + } else { + $this->encoding = $encoding; + } + + // Strip BOM: + // UTF-32 Big Endian BOM + if (substr($data, 0, 4) === "\x00\x00\xFE\xFF") { + $data = substr($data, 4); + } + // UTF-32 Little Endian BOM + elseif (substr($data, 0, 4) === "\xFF\xFE\x00\x00") { + $data = substr($data, 4); + } + // UTF-16 Big Endian BOM + elseif (substr($data, 0, 2) === "\xFE\xFF") { + $data = substr($data, 2); + } + // UTF-16 Little Endian BOM + elseif (substr($data, 0, 2) === "\xFF\xFE") { + $data = substr($data, 2); + } + // UTF-8 BOM + elseif (substr($data, 0, 3) === "\xEF\xBB\xBF") { + $data = substr($data, 3); + } + + if (substr($data, 0, 5) === '<?xml' && strspn(substr($data, 5, 1), "\x09\x0A\x0D\x20") && ($pos = strpos($data, '?>')) !== false) { + $declaration = $this->registry->create(DeclarationParser::class, [substr($data, 5, $pos - 5)]); + if ($declaration->parse()) { + $data = substr($data, $pos + 2); + $data = '<?xml version="' . $declaration->version . '" encoding="' . $encoding . '" standalone="' . (($declaration->standalone) ? 'yes' : 'no') . '"?>' . "\n" . + self::set_doctype($data); + } else { + $this->error_string = 'SimplePie bug! Please report this!'; + return false; + } + } else { + $data = self::set_doctype($data); + } + + $return = true; + + static $xml_is_sane = null; + if ($xml_is_sane === null) { + $parser_check = xml_parser_create(); + xml_parse_into_struct($parser_check, '<foo>&</foo>', $values); + if (\PHP_VERSION_ID < 80000) { + xml_parser_free($parser_check); + } + $xml_is_sane = isset($values[0]['value']); + } + + // Create the parser + if ($xml_is_sane) { + $xml = xml_parser_create_ns($this->encoding, $this->separator); + xml_parser_set_option($xml, XML_OPTION_SKIP_WHITE, 1); + xml_parser_set_option($xml, XML_OPTION_CASE_FOLDING, 0); + xml_set_character_data_handler($xml, [$this, 'cdata']); + xml_set_element_handler($xml, [$this, 'tag_open'], [$this, 'tag_close']); + + // Parse! + $wrapper = @is_writable(sys_get_temp_dir()) ? 'php://temp' : 'php://memory'; + if (($stream = fopen($wrapper, 'r+')) && + fwrite($stream, $data) && + rewind($stream)) { + //Parse by chunks not to use too much memory + do { + $stream_data = (string) fread($stream, 1048576); + + if (!xml_parse($xml, $stream_data, feof($stream))) { + $this->error_code = xml_get_error_code($xml); + $this->error_string = xml_error_string($this->error_code) ?: "Unknown"; + $return = false; + break; + } + } while (!feof($stream)); + fclose($stream); + } else { + $return = false; + } + + $this->current_line = xml_get_current_line_number($xml); + $this->current_column = xml_get_current_column_number($xml); + $this->current_byte = xml_get_current_byte_index($xml); + if (\PHP_VERSION_ID < 80000) { + xml_parser_free($xml); + } + return $return; + } + + libxml_clear_errors(); + $xml = new \XMLReader(); + $xml->xml($data); + while (@$xml->read()) { + switch ($xml->nodeType) { + case \XMLReader::END_ELEMENT: + if ($xml->namespaceURI !== '') { + $tagName = $xml->namespaceURI . $this->separator . $xml->localName; + } else { + $tagName = $xml->localName; + } + $this->tag_close(null, $tagName); + break; + case \XMLReader::ELEMENT: + $empty = $xml->isEmptyElement; + if ($xml->namespaceURI !== '') { + $tagName = $xml->namespaceURI . $this->separator . $xml->localName; + } else { + $tagName = $xml->localName; + } + $attributes = []; + while ($xml->moveToNextAttribute()) { + if ($xml->namespaceURI !== '') { + $attrName = $xml->namespaceURI . $this->separator . $xml->localName; + } else { + $attrName = $xml->localName; + } + $attributes[$attrName] = $xml->value; + } + $this->tag_open(null, $tagName, $attributes); + if ($empty) { + $this->tag_close(null, $tagName); + } + break; + case \XMLReader::TEXT: + + case \XMLReader::CDATA: + $this->cdata(null, $xml->value); + break; + } + } + if ($error = libxml_get_last_error()) { + $this->error_code = $error->code; + $this->error_string = $error->message; + $this->current_line = $error->line; + $this->current_column = $error->column; + return false; + } + + return true; + } + + /** + * @return int + */ + public function get_error_code() + { + return $this->error_code; + } + + /** + * @return string + */ + public function get_error_string() + { + return $this->error_string; + } + + /** + * @return int + */ + public function get_current_line() + { + return $this->current_line; + } + + /** + * @return int + */ + public function get_current_column() + { + return $this->current_column; + } + + /** + * @return int + */ + public function get_current_byte() + { + return $this->current_byte; + } + + /** + * @return array<string, mixed> + */ + public function get_data() + { + return $this->data; + } + + /** + * @param XMLParser|resource|null $parser + * @param array<string, string> $attributes + * @return void + */ + public function tag_open($parser, string $tag, array $attributes) + { + [$this->namespace[], $this->element[]] = $this->split_ns($tag); + + $attribs = []; + foreach ($attributes as $name => $value) { + [$attrib_namespace, $attribute] = $this->split_ns($name); + $attribs[$attrib_namespace][$attribute] = $value; + } + + if (isset($attribs[\SimplePie\SimplePie::NAMESPACE_XML]['base'])) { + $base = $this->registry->call(Misc::class, 'absolutize_url', [$attribs[\SimplePie\SimplePie::NAMESPACE_XML]['base'], end($this->xml_base)]); + if ($base !== false) { + $this->xml_base[] = $base; + $this->xml_base_explicit[] = true; + } + } else { + $this->xml_base[] = end($this->xml_base) ?: ''; + $this->xml_base_explicit[] = end($this->xml_base_explicit); + } + + if (isset($attribs[\SimplePie\SimplePie::NAMESPACE_XML]['lang'])) { + $this->xml_lang[] = $attribs[\SimplePie\SimplePie::NAMESPACE_XML]['lang']; + } else { + $this->xml_lang[] = end($this->xml_lang) ?: ''; + } + + if ($this->current_xhtml_construct >= 0) { + $this->current_xhtml_construct++; + if (end($this->namespace) === \SimplePie\SimplePie::NAMESPACE_XHTML) { + $this->data['data'] .= '<' . end($this->element); + if (isset($attribs[''])) { + foreach ($attribs[''] as $name => $value) { + $this->data['data'] .= ' ' . $name . '="' . htmlspecialchars($value, ENT_COMPAT, $this->encoding) . '"'; + } + } + $this->data['data'] .= '>'; + } + } else { + $this->datas[] = &$this->data; + $this->data = &$this->data['child'][end($this->namespace)][end($this->element)][]; + $this->data = ['data' => '', 'attribs' => $attribs, 'xml_base' => end($this->xml_base), 'xml_base_explicit' => end($this->xml_base_explicit), 'xml_lang' => end($this->xml_lang)]; + if ((end($this->namespace) === \SimplePie\SimplePie::NAMESPACE_ATOM_03 && in_array(end($this->element), ['title', 'tagline', 'copyright', 'info', 'summary', 'content']) && isset($attribs['']['mode']) && $attribs['']['mode'] === 'xml') + || (end($this->namespace) === \SimplePie\SimplePie::NAMESPACE_ATOM_10 && in_array(end($this->element), ['rights', 'subtitle', 'summary', 'info', 'title', 'content']) && isset($attribs['']['type']) && $attribs['']['type'] === 'xhtml') + || (end($this->namespace) === \SimplePie\SimplePie::NAMESPACE_RSS_20 && in_array(end($this->element), ['title'])) + || (end($this->namespace) === \SimplePie\SimplePie::NAMESPACE_RSS_090 && in_array(end($this->element), ['title'])) + || (end($this->namespace) === \SimplePie\SimplePie::NAMESPACE_RSS_10 && in_array(end($this->element), ['title']))) { + $this->current_xhtml_construct = 0; + } + } + } + + /** + * @param XMLParser|resource|null $parser + * @return void + */ + public function cdata($parser, string $cdata) + { + if ($this->current_xhtml_construct >= 0) { + $this->data['data'] .= htmlspecialchars($cdata, ENT_QUOTES, $this->encoding); + } else { + $this->data['data'] .= $cdata; + } + } + + /** + * @param XMLParser|resource|null $parser + * @return void + */ + public function tag_close($parser, string $tag) + { + if ($this->current_xhtml_construct >= 0) { + $this->current_xhtml_construct--; + if (end($this->namespace) === \SimplePie\SimplePie::NAMESPACE_XHTML && !in_array(end($this->element), ['area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param'])) { + $this->data['data'] .= '</' . end($this->element) . '>'; + } + } + if ($this->current_xhtml_construct === -1) { + $this->data = &$this->datas[count($this->datas) - 1]; + array_pop($this->datas); + } + + array_pop($this->element); + array_pop($this->namespace); + array_pop($this->xml_base); + array_pop($this->xml_base_explicit); + array_pop($this->xml_lang); + } + + /** + * @return array{string, string} + */ + public function split_ns(string $string) + { + static $cache = []; + if (!isset($cache[$string])) { + if ($pos = strpos($string, $this->separator)) { + static $separator_length; + if (!$separator_length) { + $separator_length = strlen($this->separator); + } + $namespace = substr($string, 0, $pos); + $local_name = substr($string, $pos + $separator_length); + if (strtolower($namespace) === \SimplePie\SimplePie::NAMESPACE_ITUNES) { + $namespace = \SimplePie\SimplePie::NAMESPACE_ITUNES; + } + + // Normalize the Media RSS namespaces + if ($namespace === \SimplePie\SimplePie::NAMESPACE_MEDIARSS_WRONG || + $namespace === \SimplePie\SimplePie::NAMESPACE_MEDIARSS_WRONG2 || + $namespace === \SimplePie\SimplePie::NAMESPACE_MEDIARSS_WRONG3 || + $namespace === \SimplePie\SimplePie::NAMESPACE_MEDIARSS_WRONG4 || + $namespace === \SimplePie\SimplePie::NAMESPACE_MEDIARSS_WRONG5) { + $namespace = \SimplePie\SimplePie::NAMESPACE_MEDIARSS; + } + $cache[$string] = [$namespace, $local_name]; + } else { + $cache[$string] = ['', $string]; + } + } + return $cache[$string]; + } + + /** + * @param array<string, mixed> $data + */ + private function parse_hcard(array $data, bool $category = false): string + { + $name = ''; + $link = ''; + // Check if h-card is set and pass that information on in the link. + if (isset($data['type']) && in_array('h-card', $data['type'])) { + if (isset($data['properties']['name'][0])) { + $name = $data['properties']['name'][0]; + } + if (isset($data['properties']['url'][0])) { + $link = $data['properties']['url'][0]; + if ($name === '') { + $name = $link; + } else { + // can't have commas in categories. + $name = str_replace(',', '', $name); + } + $person_tag = $category ? '<span class="person-tag"></span>' : ''; + return '<a class="h-card" href="'.$link.'">'.$person_tag.$name.'</a>'; + } + } + return $data['value'] ?? ''; + } + + /** + * @return true + */ + private function parse_microformats(string &$data, string $url): bool + { + // For PHPStan, we already check that in call site. + \assert(function_exists('Mf2\parse')); + \assert(function_exists('Mf2\fetch')); + $feed_title = ''; + $feed_author = null; + $author_cache = []; + $items = []; + $entries = []; + $mf = \Mf2\parse($data, $url); + // First look for an h-feed. + $h_feed = []; + foreach ($mf['items'] as $mf_item) { + if (in_array('h-feed', $mf_item['type'])) { + $h_feed = $mf_item; + break; + } + // Also look for h-feed or h-entry in the children of each top level item. + if (!isset($mf_item['children'][0]['type'])) { + continue; + } + if (in_array('h-feed', $mf_item['children'][0]['type'])) { + $h_feed = $mf_item['children'][0]; + // In this case the parent of the h-feed may be an h-card, so use it as + // the feed_author. + if (in_array('h-card', $mf_item['type'])) { + $feed_author = $mf_item; + } + break; + } elseif (in_array('h-entry', $mf_item['children'][0]['type'])) { + $entries = $mf_item['children']; + // In this case the parent of the h-entry list may be an h-card, so use + // it as the feed_author. + if (in_array('h-card', $mf_item['type'])) { + $feed_author = $mf_item; + } + break; + } + } + if (isset($h_feed['children'])) { + $entries = $h_feed['children']; + // Also set the feed title and store author from the h-feed if available. + if (isset($mf['items'][0]['properties']['name'][0])) { + $feed_title = $mf['items'][0]['properties']['name'][0]; + } + if (isset($mf['items'][0]['properties']['author'][0])) { + $feed_author = $mf['items'][0]['properties']['author'][0]; + } + } elseif (count($entries) === 0) { + $entries = $mf['items']; + } + for ($i = 0; $i < count($entries); $i++) { + $entry = $entries[$i]; + if (in_array('h-entry', $entry['type'])) { + $item = []; + $title = ''; + $description = ''; + if (isset($entry['properties']['url'][0])) { + $link = $entry['properties']['url'][0]; + if (isset($link['value'])) { + $link = $link['value']; + } + $item['link'] = [['data' => $link]]; + } + if (isset($entry['properties']['uid'][0])) { + $guid = $entry['properties']['uid'][0]; + if (isset($guid['value'])) { + $guid = $guid['value']; + } + $item['guid'] = [['data' => $guid]]; + } + if (isset($entry['properties']['name'][0])) { + $title = $entry['properties']['name'][0]; + if (isset($title['value'])) { + $title = $title['value']; + } + $item['title'] = [['data' => $title]]; + } + if (isset($entry['properties']['author'][0]) || isset($feed_author)) { + // author is a special case, it can be plain text or an h-card array. + // If it's plain text it can also be a url that should be followed to + // get the actual h-card. + $author = $entry['properties']['author'][0] ?? $feed_author; + if (!is_string($author)) { + $author = $this->parse_hcard($author); + } elseif (strpos($author, 'http') === 0) { + if (isset($author_cache[$author])) { + $author = $author_cache[$author]; + } else { + if ($mf = \Mf2\fetch($author)) { + foreach ($mf['items'] as $hcard) { + // Only interested in an h-card by itself in this case. + if (!in_array('h-card', $hcard['type'])) { + continue; + } + // It must have a url property matching what we fetched. + if (!isset($hcard['properties']['url']) || + !(in_array($author, $hcard['properties']['url']))) { + continue; + } + // Save parse_hcard the trouble of finding the correct url. + $hcard['properties']['url'][0] = $author; + // Cache this h-card for the next h-entry to check. + $author_cache[$author] = $this->parse_hcard($hcard); + $author = $author_cache[$author]; + break; + } + } + } + } + $item['author'] = [['data' => $author]]; + } + if (isset($entry['properties']['photo'][0])) { + // If a photo is also in content, don't need to add it again here. + $content = ''; + if (isset($entry['properties']['content'][0]['html'])) { + $content = $entry['properties']['content'][0]['html']; + } + $photo_list = []; + for ($j = 0; $j < count($entry['properties']['photo']); $j++) { + $photo = $entry['properties']['photo'][$j]; + if (!empty($photo) && strpos($content, $photo) === false) { + $photo_list[] = $photo; + } + } + // When there's more than one photo show the first and use a lightbox. + // Need a permanent, unique name for the image set, but don't have + // anything unique except for the content itself, so use that. + $count = count($photo_list); + if ($count > 1) { + $image_set_id = preg_replace('/[[:^alnum:]]/', '', $photo_list[0]); + $description = '<p>'; + for ($j = 0; $j < $count; $j++) { + $hidden = $j === 0 ? '' : 'class="hidden" '; + $description .= '<a href="'.$photo_list[$j].'" '.$hidden. + 'data-lightbox="image-set-'.$image_set_id.'">'. + '<img src="'.$photo_list[$j].'"></a>'; + } + $description .= '<br><b>'.$count.' photos</b></p>'; + } elseif ($count == 1) { + $description = '<p><img src="'.$photo_list[0].'"></p>'; + } + } + if (isset($entry['properties']['content'][0]['html'])) { + // e-content['value'] is the same as p-name when they are on the same + // element. Use this to replace title with a strip_tags version so + // that alt text from images is not included in the title. + if ($entry['properties']['content'][0]['value'] === $title) { + $title = strip_tags($entry['properties']['content'][0]['html']); + $item['title'] = [['data' => $title]]; + } + $description .= $entry['properties']['content'][0]['html']; + if (isset($entry['properties']['in-reply-to'][0])) { + $in_reply_to = ''; + if (is_string($entry['properties']['in-reply-to'][0])) { + $in_reply_to = $entry['properties']['in-reply-to'][0]; + } elseif (isset($entry['properties']['in-reply-to'][0]['value'])) { + $in_reply_to = $entry['properties']['in-reply-to'][0]['value']; + } + if ($in_reply_to !== '') { + $description .= '<p><span class="in-reply-to"></span> '. + '<a href="'.$in_reply_to.'">'.$in_reply_to.'</a><p>'; + } + } + $item['description'] = [['data' => $description]]; + } + if (isset($entry['properties']['category'])) { + $category_csv = ''; + // Categories can also contain h-cards. + foreach ($entry['properties']['category'] as $category) { + if ($category_csv !== '') { + $category_csv .= ', '; + } + if (is_string($category)) { + // Can't have commas in categories. + $category_csv .= str_replace(',', '', $category); + } else { + $category_csv .= $this->parse_hcard($category, true); + } + } + $item['category'] = [['data' => $category_csv]]; + } + if (isset($entry['properties']['published'][0])) { + $timestamp = strtotime($entry['properties']['published'][0]); + $pub_date = date('F j Y g:ia', $timestamp).' GMT'; + $item['pubDate'] = [['data' => $pub_date]]; + } + // The title and description are set to the empty string to represent + // a deleted item (which also makes it an invalid rss item). + if (isset($entry['properties']['deleted'][0])) { + $item['title'] = [['data' => '']]; + $item['description'] = [['data' => '']]; + } + $items[] = ['child' => ['' => $item]]; + } + } + // Mimic RSS data format when storing microformats. + $link = [['data' => $url]]; + $image = ''; + if (!is_string($feed_author) && + isset($feed_author['properties']['photo'][0])) { + $image = [['child' => ['' => ['url' => + [['data' => $feed_author['properties']['photo'][0]]]]]]]; + } + // Use the name given for the h-feed, or get the title from the html. + if ($feed_title !== '') { + $feed_title = [['data' => htmlspecialchars($feed_title)]]; + } elseif ($position = strpos($data, '<title>')) { + $start = $position < 200 ? 0 : $position - 200; + $check = substr($data, $start, 400); + $matches = []; + if (preg_match('/<title>(.+)<\/title>/', $check, $matches)) { + $feed_title = [['data' => htmlspecialchars($matches[1])]]; + } + } + $channel = ['channel' => [['child' => ['' => + ['link' => $link, 'image' => $image, 'title' => $feed_title, + 'item' => $items]]]]]; + $rss = [['attribs' => ['' => ['version' => '2.0']], + 'child' => ['' => $channel]]]; + $this->data = ['child' => ['' => ['rss' => $rss]]]; + return true; + } + + private static function set_doctype(string $data): string + { + // Strip DOCTYPE except if containing an [internal subset] + $data = preg_replace('/^\\s*<!DOCTYPE\\s[^>\\[\\]]*>\s*/', '', $data) ?? $data; + // Declare HTML entities only if no remaining DOCTYPE + $doctype = preg_match('/^\\s*<!DOCTYPE\\s/', $data) ? '' : self::declare_html_entities(); + return $doctype . $data; + } + + private static function declare_html_entities(): string + { + // This is required because the RSS specification says that entity-encoded + // html is allowed, but the xml specification says they must be declared. + return '<!DOCTYPE rss [ <!ENTITY nbsp " "> <!ENTITY iexcl "¡"> <!ENTITY cent "¢"> <!ENTITY pound "£"> <!ENTITY curren "¤"> <!ENTITY yen "¥"> <!ENTITY brvbar "¦"> <!ENTITY sect "§"> <!ENTITY uml "¨"> <!ENTITY copy "©"> <!ENTITY ordf "ª"> <!ENTITY laquo "«"> <!ENTITY not "¬"> <!ENTITY shy "­"> <!ENTITY reg "®"> <!ENTITY macr "¯"> <!ENTITY deg "°"> <!ENTITY plusmn "±"> <!ENTITY sup2 "²"> <!ENTITY sup3 "³"> <!ENTITY acute "´"> <!ENTITY micro "µ"> <!ENTITY para "¶"> <!ENTITY middot "·"> <!ENTITY cedil "¸"> <!ENTITY sup1 "¹"> <!ENTITY ordm "º"> <!ENTITY raquo "»"> <!ENTITY frac14 "¼"> <!ENTITY frac12 "½"> <!ENTITY frac34 "¾"> <!ENTITY iquest "¿"> <!ENTITY Agrave "À"> <!ENTITY Aacute "Á"> <!ENTITY Acirc "Â"> <!ENTITY Atilde "Ã"> <!ENTITY Auml "Ä"> <!ENTITY Aring "Å"> <!ENTITY AElig "Æ"> <!ENTITY Ccedil "Ç"> <!ENTITY Egrave "È"> <!ENTITY Eacute "É"> <!ENTITY Ecirc "Ê"> <!ENTITY Euml "Ë"> <!ENTITY Igrave "Ì"> <!ENTITY Iacute "Í"> <!ENTITY Icirc "Î"> <!ENTITY Iuml "Ï"> <!ENTITY ETH "Ð"> <!ENTITY Ntilde "Ñ"> <!ENTITY Ograve "Ò"> <!ENTITY Oacute "Ó"> <!ENTITY Ocirc "Ô"> <!ENTITY Otilde "Õ"> <!ENTITY Ouml "Ö"> <!ENTITY times "×"> <!ENTITY Oslash "Ø"> <!ENTITY Ugrave "Ù"> <!ENTITY Uacute "Ú"> <!ENTITY Ucirc "Û"> <!ENTITY Uuml "Ü"> <!ENTITY Yacute "Ý"> <!ENTITY THORN "Þ"> <!ENTITY szlig "ß"> <!ENTITY agrave "à"> <!ENTITY aacute "á"> <!ENTITY acirc "â"> <!ENTITY atilde "ã"> <!ENTITY auml "ä"> <!ENTITY aring "å"> <!ENTITY aelig "æ"> <!ENTITY ccedil "ç"> <!ENTITY egrave "è"> <!ENTITY eacute "é"> <!ENTITY ecirc "ê"> <!ENTITY euml "ë"> <!ENTITY igrave "ì"> <!ENTITY iacute "í"> <!ENTITY icirc "î"> <!ENTITY iuml "ï"> <!ENTITY eth "ð"> <!ENTITY ntilde "ñ"> <!ENTITY ograve "ò"> <!ENTITY oacute "ó"> <!ENTITY ocirc "ô"> <!ENTITY otilde "õ"> <!ENTITY ouml "ö"> <!ENTITY divide "÷"> <!ENTITY oslash "ø"> <!ENTITY ugrave "ù"> <!ENTITY uacute "ú"> <!ENTITY ucirc "û"> <!ENTITY uuml "ü"> <!ENTITY yacute "ý"> <!ENTITY thorn "þ"> <!ENTITY yuml "ÿ"> <!ENTITY OElig "Œ"> <!ENTITY oelig "œ"> <!ENTITY Scaron "Š"> <!ENTITY scaron "š"> <!ENTITY Yuml "Ÿ"> <!ENTITY fnof "ƒ"> <!ENTITY circ "ˆ"> <!ENTITY tilde "˜"> <!ENTITY Alpha "Α"> <!ENTITY Beta "Β"> <!ENTITY Gamma "Γ"> <!ENTITY Epsilon "Ε"> <!ENTITY Zeta "Ζ"> <!ENTITY Eta "Η"> <!ENTITY Theta "Θ"> <!ENTITY Iota "Ι"> <!ENTITY Kappa "Κ"> <!ENTITY Lambda "Λ"> <!ENTITY Mu "Μ"> <!ENTITY Nu "Ν"> <!ENTITY Xi "Ξ"> <!ENTITY Omicron "Ο"> <!ENTITY Pi "Π"> <!ENTITY Rho "Ρ"> <!ENTITY Sigma "Σ"> <!ENTITY Tau "Τ"> <!ENTITY Upsilon "Υ"> <!ENTITY Phi "Φ"> <!ENTITY Chi "Χ"> <!ENTITY Psi "Ψ"> <!ENTITY Omega "Ω"> <!ENTITY alpha "α"> <!ENTITY beta "β"> <!ENTITY gamma "γ"> <!ENTITY delta "δ"> <!ENTITY epsilon "ε"> <!ENTITY zeta "ζ"> <!ENTITY eta "η"> <!ENTITY theta "θ"> <!ENTITY iota "ι"> <!ENTITY kappa "κ"> <!ENTITY lambda "λ"> <!ENTITY mu "μ"> <!ENTITY nu "ν"> <!ENTITY xi "ξ"> <!ENTITY omicron "ο"> <!ENTITY pi "π"> <!ENTITY rho "ρ"> <!ENTITY sigmaf "ς"> <!ENTITY sigma "σ"> <!ENTITY tau "τ"> <!ENTITY upsilon "υ"> <!ENTITY phi "φ"> <!ENTITY chi "χ"> <!ENTITY psi "ψ"> <!ENTITY omega "ω"> <!ENTITY thetasym "ϑ"> <!ENTITY upsih "ϒ"> <!ENTITY piv "ϖ"> <!ENTITY ensp " "> <!ENTITY emsp " "> <!ENTITY thinsp " "> <!ENTITY zwnj "‌"> <!ENTITY zwj "‍"> <!ENTITY lrm "‎"> <!ENTITY rlm "‏"> <!ENTITY ndash "–"> <!ENTITY mdash "—"> <!ENTITY lsquo "‘"> <!ENTITY rsquo "’"> <!ENTITY sbquo "‚"> <!ENTITY ldquo "“"> <!ENTITY rdquo "”"> <!ENTITY bdquo "„"> <!ENTITY dagger "†"> <!ENTITY Dagger "‡"> <!ENTITY bull "•"> <!ENTITY hellip "…"> <!ENTITY permil "‰"> <!ENTITY prime "′"> <!ENTITY Prime "″"> <!ENTITY lsaquo "‹"> <!ENTITY rsaquo "›"> <!ENTITY oline "‾"> <!ENTITY frasl "⁄"> <!ENTITY euro "€"> <!ENTITY image "ℑ"> <!ENTITY weierp "℘"> <!ENTITY real "ℜ"> <!ENTITY trade "™"> <!ENTITY alefsym "ℵ"> <!ENTITY larr "←"> <!ENTITY uarr "↑"> <!ENTITY rarr "→"> <!ENTITY darr "↓"> <!ENTITY harr "↔"> <!ENTITY crarr "↵"> <!ENTITY lArr "⇐"> <!ENTITY uArr "⇑"> <!ENTITY rArr "⇒"> <!ENTITY dArr "⇓"> <!ENTITY hArr "⇔"> <!ENTITY forall "∀"> <!ENTITY part "∂"> <!ENTITY exist "∃"> <!ENTITY empty "∅"> <!ENTITY nabla "∇"> <!ENTITY isin "∈"> <!ENTITY notin "∉"> <!ENTITY ni "∋"> <!ENTITY prod "∏"> <!ENTITY sum "∑"> <!ENTITY minus "−"> <!ENTITY lowast "∗"> <!ENTITY radic "√"> <!ENTITY prop "∝"> <!ENTITY infin "∞"> <!ENTITY ang "∠"> <!ENTITY and "∧"> <!ENTITY or "∨"> <!ENTITY cap "∩"> <!ENTITY cup "∪"> <!ENTITY int "∫"> <!ENTITY there4 "∴"> <!ENTITY sim "∼"> <!ENTITY cong "≅"> <!ENTITY asymp "≈"> <!ENTITY ne "≠"> <!ENTITY equiv "≡"> <!ENTITY le "≤"> <!ENTITY ge "≥"> <!ENTITY sub "⊂"> <!ENTITY sup "⊃"> <!ENTITY nsub "⊄"> <!ENTITY sube "⊆"> <!ENTITY supe "⊇"> <!ENTITY oplus "⊕"> <!ENTITY otimes "⊗"> <!ENTITY perp "⊥"> <!ENTITY sdot "⋅"> <!ENTITY lceil "⌈"> <!ENTITY rceil "⌉"> <!ENTITY lfloor "⌊"> <!ENTITY rfloor "⌋"> <!ENTITY lang "〈"> <!ENTITY rang "〉"> <!ENTITY loz "◊"> <!ENTITY spades "♠"> <!ENTITY clubs "♣"> <!ENTITY hearts "♥"> <!ENTITY diams "♦"> ]>'; + } +} + +class_alias('SimplePie\Parser', 'SimplePie_Parser'); + +} + +namespace SimplePie { + +/** + * Handles `<media:rating>` or `<itunes:explicit>` tags as defined in Media RSS and iTunes RSS respectively + * + * Used by {@see \SimplePie\Enclosure::get_rating()} and {@see \SimplePie\Enclosure::get_ratings()} + * + * This class can be overloaded with {@see \SimplePie\SimplePie::set_rating_class()} + */ +class Rating +{ + /** + * Rating scheme + * + * @var ?string + * @see get_scheme() + */ + public $scheme; + + /** + * Rating value + * + * @var ?string + * @see get_value() + */ + public $value; + + /** + * Constructor, used to input the data + * + * For documentation on all the parameters, see the corresponding + * properties and their accessors + */ + public function __construct( + ?string $scheme = null, + ?string $value = null + ) { + $this->scheme = $scheme; + $this->value = $value; + } + + /** + * String-ified version + * + * @return string + */ + public function __toString() + { + // There is no $this->data here + return md5(serialize($this)); + } + + /** + * Get the organizational scheme for the rating + * + * @return string|null + */ + public function get_scheme() + { + if ($this->scheme !== null) { + return $this->scheme; + } + + return null; + } + + /** + * Get the value of the rating + * + * @return string|null + */ + public function get_value() + { + if ($this->value !== null) { + return $this->value; + } + + return null; + } +} + +class_alias('SimplePie\Rating', 'SimplePie_Rating'); + +} + +namespace SimplePie { + +use InvalidArgumentException; +use SimplePie\Content\Type\Sniffer; +use SimplePie\Parse\Date; +use SimplePie\XML\Declaration\Parser as DeclarationParser; + +/** + * Handles creating objects and calling methods + * + * Access this via {@see \SimplePie\SimplePie::get_registry()} + */ +class Registry +{ + /** + * Default class mapping + * + * Overriding classes *must* subclass these. + * + * @var array<class-string, class-string> + */ + protected $default = [ + Cache::class => Cache::class, + Locator::class => Locator::class, + Parser::class => Parser::class, + File::class => File::class, + Sanitize::class => Sanitize::class, + Item::class => Item::class, + Author::class => Author::class, + Category::class => Category::class, + Enclosure::class => Enclosure::class, + Caption::class => Caption::class, + Copyright::class => Copyright::class, + Credit::class => Credit::class, + Rating::class => Rating::class, + Restriction::class => Restriction::class, + Sniffer::class => Sniffer::class, + Source::class => Source::class, + Misc::class => Misc::class, + DeclarationParser::class => DeclarationParser::class, + Date::class => Date::class, + ]; + + /** + * Class mapping + * + * @see register() + * @var array<string, class-string> + */ + protected $classes = []; + + /** + * Legacy classes + * + * @see register() + * @var array<class-string> + */ + protected $legacy = []; + + /** + * Legacy types + * + * @see register() + * @var array<string, class-string> + */ + private $legacyTypes = [ + 'Cache' => Cache::class, + 'Locator' => Locator::class, + 'Parser' => Parser::class, + 'File' => File::class, + 'Sanitize' => Sanitize::class, + 'Item' => Item::class, + 'Author' => Author::class, + 'Category' => Category::class, + 'Enclosure' => Enclosure::class, + 'Caption' => Caption::class, + 'Copyright' => Copyright::class, + 'Credit' => Credit::class, + 'Rating' => Rating::class, + 'Restriction' => Restriction::class, + 'Content_Type_Sniffer' => Sniffer::class, + 'Source' => Source::class, + 'Misc' => Misc::class, + 'XML_Declaration_Parser' => DeclarationParser::class, + 'Parse_Date' => Date::class, + ]; + + /** + * Constructor + * + * No-op + */ + public function __construct() + { + } + + /** + * Register a class + * + * @param string $type See {@see $default} for names + * @param class-string $class Class name, must subclass the corresponding default + * @param bool $legacy Whether to enable legacy support for this class + * @return bool Successfulness + */ + public function register(string $type, $class, bool $legacy = false) + { + if (array_key_exists($type, $this->legacyTypes)) { + // trigger_error(sprintf('"%s"(): Using argument #1 ($type) with value "%s" is deprecated since SimplePie 1.8.0, use class-string "%s" instead.', __METHOD__, $type, $this->legacyTypes[$type]), \E_USER_DEPRECATED); + + $type = $this->legacyTypes[$type]; + } + + if (!array_key_exists($type, $this->default)) { + return false; + } + + if (!class_exists($class)) { + return false; + } + + /** @var string */ + $base_class = $this->default[$type]; + + if (!is_subclass_of($class, $base_class)) { + return false; + } + + $this->classes[$type] = $class; + + if ($legacy) { + $this->legacy[] = $class; + } + + return true; + } + + /** + * Get the class registered for a type + * + * Where possible, use {@see create()} or {@see call()} instead + * + * @template T + * @param class-string<T> $type + * @return class-string<T>|null + */ + public function get_class($type) + { + if (array_key_exists($type, $this->legacyTypes)) { + // trigger_error(sprintf('"%s"(): Using argument #1 ($type) with value "%s" is deprecated since SimplePie 1.8.0, use class-string "%s" instead.', __METHOD__, $type, $this->legacyTypes[$type]), \E_USER_DEPRECATED); + + $type = $this->legacyTypes[$type]; + } + + if (!array_key_exists($type, $this->default)) { + return null; + } + + // For PHPStan: values in $default should be subtypes of keys. + /** @var class-string<T> */ + $class = $this->default[$type]; + + if (array_key_exists($type, $this->classes)) { + // For PHPStan: values in $classes should be subtypes of keys. + /** @var class-string<T> */ + $class = $this->classes[$type]; + } + + return $class; + } + + /** + * Create a new instance of a given type + * + * @template T class-string $type + * @param class-string<T> $type + * @param array<mixed> $parameters Parameters to pass to the constructor + * @return T Instance of class + */ + public function &create($type, array $parameters = []) + { + $class = $this->get_class($type); + if ($class === null) { + throw new InvalidArgumentException(sprintf( + '%s(): Argument #1 ($type) "%s" not found in class list.', + __METHOD__, + $type + ), 1); + } + + if (!method_exists($class, '__construct')) { + $instance = new $class(); + } else { + $reflector = new \ReflectionClass($class); + // For PHPStan: $class is T. + /** @var T */ + $instance = $reflector->newInstanceArgs($parameters); + } + + if ($instance instanceof RegistryAware) { + $instance->set_registry($this); + } elseif (method_exists($instance, 'set_registry')) { + trigger_error(sprintf('Using the method "set_registry()" without implementing "%s" is deprecated since SimplePie 1.8.0, implement "%s" in "%s".', RegistryAware::class, RegistryAware::class, $class), \E_USER_DEPRECATED); + $instance->set_registry($this); + } + + return $instance; + } + + /** + * Call a static method for a type + * + * @param class-string $type + * @param string $method + * @param array<mixed> $parameters + * @return mixed + */ + public function &call($type, string $method, array $parameters = []) + { + $class = $this->get_class($type); + if ($class === null) { + throw new InvalidArgumentException(sprintf( + '%s(): Argument #1 ($type) "%s" not found in class list.', + __METHOD__, + $type + ), 1); + } + + if (in_array($class, $this->legacy)) { + switch ($type) { + case Cache::class: + // For backwards compatibility with old non-static + // Cache::create() methods in PHP < 8.0. + // No longer supported as of PHP 8.0. + if ($method === 'get_handler') { + // Fixing this PHPStan error breaks CacheTest::testDirectOverrideLegacy() + /** @phpstan-ignore argument.type */ + $result = @call_user_func_array([$class, 'create'], $parameters); + return $result; + } + break; + } + } + + $callable = [$class, $method]; + assert(is_callable($callable), 'For PHPstan'); + $result = call_user_func_array($callable, $parameters); + return $result; + } +} + +class_alias('SimplePie\Registry', 'SimplePie_Registry'); + +} + +namespace SimplePie { + +/** + * Handles the injection of Registry into other class + * + * {@see \SimplePie\SimplePie::get_registry()} + */ +interface RegistryAware +{ + /** + * Set the Registry into the class + * + * @return void + */ + public function set_registry(Registry $registry); +} + +} + +namespace SimplePie { + +/** + * Handles `<media:restriction>` as defined in Media RSS + * + * Used by {@see \SimplePie\Enclosure::get_restriction()} and {@see \SimplePie\Enclosure::get_restrictions()} + * + * This class can be overloaded with {@see \SimplePie\SimplePie::set_restriction_class()} + */ +class Restriction +{ + public const RELATIONSHIP_ALLOW = 'allow'; + public const RELATIONSHIP_DENY = 'deny'; + + /** + * Relationship ('allow'/'deny') + * + * @var self::RELATIONSHIP_*|null + * @see get_relationship() + */ + public $relationship; + + /** + * Type of restriction + * + * @var string|null + * @see get_type() + */ + public $type; + + /** + * Restricted values + * + * @var string|null + * @see get_value() + */ + public $value; + + /** + * Constructor, used to input the data + * + * For documentation on all the parameters, see the corresponding + * properties and their accessors + * + * @param ?self::RELATIONSHIP_* $relationship + */ + public function __construct(?string $relationship = null, ?string $type = null, ?string $value = null) + { + $this->relationship = $relationship; + $this->type = $type; + $this->value = $value; + } + + /** + * String-ified version + * + * @return string + */ + public function __toString() + { + // There is no $this->data here + return md5(serialize($this)); + } + + /** + * Get the relationship + * + * @return ?self::RELATIONSHIP_* + */ + public function get_relationship() + { + if ($this->relationship !== null) { + return $this->relationship; + } + + return null; + } + + /** + * Get the type + * + * @return string|null + */ + public function get_type() + { + if ($this->type !== null) { + return $this->type; + } + + return null; + } + + /** + * Get the list of restricted things + * + * @return string|null + */ + public function get_value() + { + if ($this->value !== null) { + return $this->value; + } + + return null; + } +} + +class_alias('SimplePie\Restriction', 'SimplePie_Restriction'); + +} + +namespace SimplePie { + +use DOMDocument; +use DOMXPath; +use InvalidArgumentException; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\UriFactoryInterface; +use SimplePie\Cache\Base; +use SimplePie\Cache\BaseDataCache; +use SimplePie\Cache\CallableNameFilter; +use SimplePie\Cache\DataCache; +use SimplePie\Cache\NameFilter; +use SimplePie\HTTP\Client; +use SimplePie\HTTP\ClientException; +use SimplePie\HTTP\FileClient; +use SimplePie\HTTP\Psr18Client; + +/** + * Used for data cleanup and post-processing + * + * + * This class can be overloaded with {@see \SimplePie\SimplePie::set_sanitize_class()} + * + * @todo Move to using an actual HTML parser (this will allow tags to be properly stripped, and to switch between HTML and XHTML), this will also make it easier to shorten a string while preserving HTML tags + */ +class Sanitize implements RegistryAware +{ + // Private vars + /** @var string */ + public $base = ''; + + // Options + /** @var bool */ + public $remove_div = true; + /** @var string */ + public $image_handler = ''; + /** @var string[] */ + public $strip_htmltags = ['base', 'blink', 'body', 'doctype', 'embed', 'font', 'form', 'frame', 'frameset', 'html', 'iframe', 'input', 'marquee', 'meta', 'noscript', 'object', 'param', 'script', 'style']; + /** @var bool */ + public $encode_instead_of_strip = false; + /** @var string[] */ + public $strip_attributes = ['bgsound', 'expr', 'id', 'style', 'onclick', 'onerror', 'onfinish', 'onmouseover', 'onmouseout', 'onfocus', 'onblur', 'lowsrc', 'dynsrc']; + /** @var string[] */ + public $rename_attributes = []; + /** @var array<string, array<string, string>> */ + public $add_attributes = ['audio' => ['preload' => 'none'], 'iframe' => ['sandbox' => 'allow-scripts allow-same-origin'], 'video' => ['preload' => 'none']]; + /** @var bool */ + public $strip_comments = false; + /** @var string */ + public $output_encoding = 'UTF-8'; + /** @var bool */ + public $enable_cache = true; + /** @var string */ + public $cache_location = './cache'; + /** @var string&(callable(string): string) */ + public $cache_name_function = 'md5'; + + /** + * @var NameFilter + */ + private $cache_namefilter; + /** @var int */ + public $timeout = 10; + /** @var string */ + public $useragent = ''; + /** @var bool */ + public $force_fsockopen = false; + /** @var array<string, string|string[]> */ + public $replace_url_attributes = []; + /** + * @var array<int, mixed> Custom curl options + * @see SimplePie::set_curl_options() + */ + private $curl_options = []; + + /** @var Registry */ + public $registry; + + /** + * @var DataCache|null + */ + private $cache = null; + + /** + * @var int Cache duration (in seconds) + */ + private $cache_duration = 3600; + + /** + * List of domains for which to force HTTPS. + * @see \SimplePie\Sanitize::set_https_domains() + * Array is a tree split at DNS levels. Example: + * array('biz' => true, 'com' => array('example' => true), 'net' => array('example' => array('www' => true))) + * @var true|array<string, true|array<string, true|array<string, array<string, true|array<string, true|array<string, true>>>>>> + */ + public $https_domains = []; + + /** + * @var Client|null + */ + private $http_client = null; + + public function __construct() + { + // Set defaults + $this->set_url_replacements(null); + } + + /** + * @return void + */ + public function remove_div(bool $enable = true) + { + $this->remove_div = (bool) $enable; + } + + /** + * @param string|false $page + * @return void + */ + public function set_image_handler($page = false) + { + if ($page) { + $this->image_handler = (string) $page; + } else { + $this->image_handler = ''; + } + } + + /** + * @return void + */ + public function set_registry(\SimplePie\Registry $registry) + { + $this->registry = $registry; + } + + /** + * @param (string&(callable(string): string))|NameFilter $cache_name_function + * @param class-string<Cache> $cache_class + * @return void + */ + public function pass_cache_data(bool $enable_cache = true, string $cache_location = './cache', $cache_name_function = 'md5', string $cache_class = Cache::class, ?DataCache $cache = null) + { + $this->enable_cache = $enable_cache; + + if ($cache_location) { + $this->cache_location = $cache_location; + } + + // @phpstan-ignore-next-line Enforce PHPDoc type. + if (!is_string($cache_name_function) && !$cache_name_function instanceof NameFilter) { + throw new InvalidArgumentException(sprintf( + '%s(): Argument #3 ($cache_name_function) must be of type %s', + __METHOD__, + NameFilter::class + ), 1); + } + + // BC: $cache_name_function could be a callable as string + if (is_string($cache_name_function)) { + // trigger_error(sprintf('Providing $cache_name_function as string in "%s()" is deprecated since SimplePie 1.8.0, provide as "%s" instead.', __METHOD__, NameFilter::class), \E_USER_DEPRECATED); + $this->cache_name_function = $cache_name_function; + + $cache_name_function = new CallableNameFilter($cache_name_function); + } + + $this->cache_namefilter = $cache_name_function; + + if ($cache !== null) { + $this->cache = $cache; + } + } + + /** + * Set a PSR-18 client and PSR-17 factories + * + * Allows you to use your own HTTP client implementations. + */ + final public function set_http_client( + ClientInterface $http_client, + RequestFactoryInterface $request_factory, + UriFactoryInterface $uri_factory + ): void { + $this->http_client = new Psr18Client($http_client, $request_factory, $uri_factory); + } + + /** + * @deprecated since SimplePie 1.9.0, use \SimplePie\Sanitize::set_http_client() instead. + * @param class-string<File> $file_class + * @param array<int, mixed> $curl_options + * @return void + */ + public function pass_file_data(string $file_class = File::class, int $timeout = 10, string $useragent = '', bool $force_fsockopen = false, array $curl_options = []) + { + // trigger_error(sprintf('SimplePie\Sanitize::pass_file_data() is deprecated since SimplePie 1.9.0, please use "SimplePie\Sanitize::set_http_client()" instead.'), \E_USER_DEPRECATED); + if ($timeout) { + $this->timeout = $timeout; + } + + if ($useragent) { + $this->useragent = $useragent; + } + + if ($force_fsockopen) { + $this->force_fsockopen = $force_fsockopen; + } + + $this->curl_options = $curl_options; + // Invalidate the registered client. + $this->http_client = null; + } + + /** + * @param string[]|string|false $tags Set a list of tags to strip, or set empty string to use default tags, or false to strip nothing. + * @return void + */ + public function strip_htmltags($tags = ['base', 'blink', 'body', 'doctype', 'embed', 'font', 'form', 'frame', 'frameset', 'html', 'iframe', 'input', 'marquee', 'meta', 'noscript', 'object', 'param', 'script', 'style']) + { + if ($tags) { + if (is_array($tags)) { + $this->strip_htmltags = $tags; + } else { + $this->strip_htmltags = explode(',', $tags); + } + } else { + $this->strip_htmltags = []; + } + } + + /** + * @return void + */ + public function encode_instead_of_strip(bool $encode = false) + { + $this->encode_instead_of_strip = $encode; + } + + /** + * @param string[]|string $attribs + * @return void + */ + public function rename_attributes($attribs = []) + { + if ($attribs) { + if (is_array($attribs)) { + $this->rename_attributes = $attribs; + } else { + $this->rename_attributes = explode(',', $attribs); + } + } else { + $this->rename_attributes = []; + } + } + + /** + * @param string[]|string $attribs + * @return void + */ + public function strip_attributes($attribs = ['bgsound', 'expr', 'id', 'style', 'onclick', 'onerror', 'onfinish', 'onmouseover', 'onmouseout', 'onfocus', 'onblur', 'lowsrc', 'dynsrc']) + { + if ($attribs) { + if (is_array($attribs)) { + $this->strip_attributes = $attribs; + } else { + $this->strip_attributes = explode(',', $attribs); + } + } else { + $this->strip_attributes = []; + } + } + + /** + * @param array<string, array<string, string>> $attribs + * @return void + */ + public function add_attributes(array $attribs = ['audio' => ['preload' => 'none'], 'iframe' => ['sandbox' => 'allow-scripts allow-same-origin'], 'video' => ['preload' => 'none']]) + { + $this->add_attributes = $attribs; + } + + /** + * @return void + */ + public function strip_comments(bool $strip = false) + { + $this->strip_comments = $strip; + } + + /** + * @return void + */ + public function set_output_encoding(string $encoding = 'UTF-8') + { + $this->output_encoding = $encoding; + } + + /** + * Set element/attribute key/value pairs of HTML attributes + * containing URLs that need to be resolved relative to the feed + * + * Defaults to |a|@href, |area|@href, |audio|@src, |blockquote|@cite, + * |del|@cite, |form|@action, |img|@longdesc, |img|@src, |input|@src, + * |ins|@cite, |q|@cite, |source|@src, |video|@src + * + * @since 1.0 + * @param array<string, string|string[]>|null $element_attribute Element/attribute key/value pairs, null for default + * @return void + */ + public function set_url_replacements(?array $element_attribute = null) + { + if ($element_attribute === null) { + $element_attribute = [ + 'a' => 'href', + 'area' => 'href', + 'audio' => 'src', + 'blockquote' => 'cite', + 'del' => 'cite', + 'form' => 'action', + 'img' => [ + 'longdesc', + 'src' + ], + 'input' => 'src', + 'ins' => 'cite', + 'q' => 'cite', + 'source' => 'src', + 'video' => [ + 'poster', + 'src' + ] + ]; + } + $this->replace_url_attributes = $element_attribute; + } + + /** + * Set the list of domains for which to force HTTPS. + * @see \SimplePie\Misc::https_url() + * Example array('biz', 'example.com', 'example.org', 'www.example.net'); + * + * @param string[] $domains list of domain names ['biz', 'example.com', 'example.org', 'www.example.net'] + * + * @return void + */ + public function set_https_domains(array $domains) + { + $this->https_domains = []; + foreach ($domains as $domain) { + $domain = trim($domain, ". \t\n\r\0\x0B"); + $segments = array_reverse(explode('.', $domain)); + /** @var true|array<string, true|array<string, true|array<string, array<string, true|array<string, true|array<string, true>>>>>> */ // Needed for PHPStan. + $node = &$this->https_domains; + foreach ($segments as $segment) {//Build a tree + if ($node === true) { + break; + } + if (!isset($node[$segment])) { + $node[$segment] = []; + } + $node = &$node[$segment]; + } + $node = true; + } + } + + /** + * Check if the domain is in the list of forced HTTPS. + * + * @return bool + */ + protected function is_https_domain(string $domain) + { + $domain = trim($domain, '. '); + $segments = array_reverse(explode('.', $domain)); + $node = &$this->https_domains; + foreach ($segments as $segment) {//Explore the tree + if (isset($node[$segment])) { + $node = &$node[$segment]; + } else { + break; + } + } + return $node === true; + } + + /** + * Force HTTPS for selected Web sites. + * + * @return string + */ + public function https_url(string $url) + { + return ( + strtolower(substr($url, 0, 7)) === 'http://' + && ($parsed = parse_url($url, PHP_URL_HOST)) !== false // Malformed URL + && $parsed !== null // Missing host + && $this->is_https_domain($parsed) // Should be forced? + ) ? substr_replace($url, 's', 4, 0) // Add the 's' to HTTPS + : $url; + } + + /** + * @param int-mask-of<SimplePie::CONSTRUCT_*> $type + * @param string $base + * @return string Sanitized data; false if output encoding is changed to something other than UTF-8 and conversion fails + */ + public function sanitize(string $data, int $type, string $base = '') + { + $data = trim($data); + if ($data !== '' || $type & \SimplePie\SimplePie::CONSTRUCT_IRI) { + if ($type & \SimplePie\SimplePie::CONSTRUCT_MAYBE_HTML) { + if (preg_match('/(&(#(x[0-9a-fA-F]+|[0-9]+)|[a-zA-Z0-9]+)|<\/[A-Za-z][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E]*' . \SimplePie\SimplePie::PCRE_HTML_ATTRIBUTE . '>)/', $data)) { + $type |= \SimplePie\SimplePie::CONSTRUCT_HTML; + } else { + $type |= \SimplePie\SimplePie::CONSTRUCT_TEXT; + } + } + + if ($type & \SimplePie\SimplePie::CONSTRUCT_BASE64) { + $data = base64_decode($data); + } + + if ($type & (\SimplePie\SimplePie::CONSTRUCT_HTML | \SimplePie\SimplePie::CONSTRUCT_XHTML)) { + if (!class_exists('DOMDocument')) { + throw new \SimplePie\Exception('DOMDocument not found, unable to use sanitizer'); + } + $document = new \DOMDocument(); + $document->encoding = 'UTF-8'; + + // PHPStan seems to have trouble resolving int-mask because bitwise + // operators are used when operators are used when passing this parameter. + // https://github.com/phpstan/phpstan/issues/9384 + /** @var int-mask-of<SimplePie::CONSTRUCT_*> $type */ + $data = $this->preprocess($data, $type); + + set_error_handler([Misc::class, 'silence_errors']); + $document->loadHTML($data); + restore_error_handler(); + + $xpath = new \DOMXPath($document); + + // Strip comments + if ($this->strip_comments) { + /** @var \DOMNodeList<\DOMComment> */ + $comments = $xpath->query('//comment()'); + + foreach ($comments as $comment) { + $parentNode = $comment->parentNode; + assert($parentNode !== null, 'For PHPStan, comment must have a parent'); + $parentNode->removeChild($comment); + } + } + + // Strip out HTML tags and attributes that might cause various security problems. + // Based on recommendations by Mark Pilgrim at: + // https://web.archive.org/web/20110902041826/http://diveintomark.org:80/archives/2003/06/12/how_to_consume_rss_safely + if ($this->strip_htmltags) { + foreach ($this->strip_htmltags as $tag) { + $this->strip_tag($tag, $document, $xpath, $type); + } + } + + if ($this->rename_attributes) { + foreach ($this->rename_attributes as $attrib) { + $this->rename_attr($attrib, $xpath); + } + } + + if ($this->strip_attributes) { + foreach ($this->strip_attributes as $attrib) { + $this->strip_attr($attrib, $xpath); + } + } + + if ($this->add_attributes) { + foreach ($this->add_attributes as $tag => $valuePairs) { + $this->add_attr($tag, $valuePairs, $document); + } + } + + // Replace relative URLs + $this->base = $base; + foreach ($this->replace_url_attributes as $element => $attributes) { + $this->replace_urls($document, $element, $attributes); + } + + // If image handling (caching, etc.) is enabled, cache and rewrite all the image tags. + if ($this->image_handler !== '' && $this->enable_cache) { + $images = $document->getElementsByTagName('img'); + + foreach ($images as $img) { + if ($img->hasAttribute('src')) { + $image_url = $this->cache_namefilter->filter($img->getAttribute('src')); + $cache = $this->get_cache($image_url); + + if ($cache->get_data($image_url, false)) { + $img->setAttribute('src', $this->image_handler . $image_url); + } else { + try { + $file = $this->get_http_client()->request( + Client::METHOD_GET, + $img->getAttribute('src'), + ['X-FORWARDED-FOR' => $_SERVER['REMOTE_ADDR']] + ); + } catch (ClientException $th) { + continue; + } + + if ((!Misc::is_remote_uri($file->get_final_requested_uri()) || ($file->get_status_code() === 200 || $file->get_status_code() > 206 && $file->get_status_code() < 300))) { + if ($cache->set_data($image_url, ['headers' => $file->get_headers(), 'body' => $file->get_body_content()], $this->cache_duration)) { + $img->setAttribute('src', $this->image_handler . $image_url); + } else { + trigger_error("$this->cache_location is not writable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING); + } + } + } + } + } + } + + // Get content node + $div = null; + if (($item = $document->getElementsByTagName('body')->item(0)) !== null) { + $div = $item->firstChild; + } + // Finally, convert to a HTML string + $data = trim((string) $document->saveHTML($div)); + + if ($this->remove_div) { + $data = preg_replace('/^<div' . \SimplePie\SimplePie::PCRE_XML_ATTRIBUTE . '>/', '', $data); + // Cast for PHPStan, it is unable to validate a non-literal regex above. + $data = preg_replace('/<\/div>$/', '', (string) $data); + } else { + $data = preg_replace('/^<div' . \SimplePie\SimplePie::PCRE_XML_ATTRIBUTE . '>/', '<div>', $data); + } + + // Cast for PHPStan, it is unable to validate a non-literal regex above. + $data = str_replace('</source>', '', (string) $data); + } + + if ($type & \SimplePie\SimplePie::CONSTRUCT_IRI) { + $absolute = $this->registry->call(Misc::class, 'absolutize_url', [$data, $base]); + if ($absolute !== false) { + $data = $absolute; + } + } + + if ($type & (\SimplePie\SimplePie::CONSTRUCT_TEXT | \SimplePie\SimplePie::CONSTRUCT_IRI)) { + $data = htmlspecialchars($data, ENT_COMPAT, 'UTF-8'); + } + + if ($this->output_encoding !== 'UTF-8') { + // This really returns string|false but changing encoding is uncommon and we are going to deprecate it, so let’s just lie to PHPStan in the interest of cleaner annotations. + /** @var string */ + $data = $this->registry->call(Misc::class, 'change_encoding', [$data, 'UTF-8', $this->output_encoding]); + } + } + return $data; + } + + /** + * @param int-mask-of<SimplePie::CONSTRUCT_*> $type + * @return string + */ + protected function preprocess(string $html, int $type) + { + $ret = ''; + $html = preg_replace('%</?(?:html|body)[^>]*?'.'>%is', '', $html); + if ($type & ~\SimplePie\SimplePie::CONSTRUCT_XHTML) { + // Atom XHTML constructs are wrapped with a div by default + // Note: No protection if $html contains a stray </div>! + $html = '<div>' . $html . '</div>'; + $ret .= '<!DOCTYPE html>'; + $content_type = 'text/html'; + } else { + $ret .= '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'; + $content_type = 'application/xhtml+xml'; + } + + $ret .= '<html><head>'; + $ret .= '<meta http-equiv="Content-Type" content="' . $content_type . '; charset=utf-8" />'; + $ret .= '</head><body>' . $html . '</body></html>'; + return $ret; + } + + /** + * @param array<string>|string $attributes + * @return void + */ + public function replace_urls(DOMDocument $document, string $tag, $attributes) + { + if (!is_array($attributes)) { + $attributes = [$attributes]; + } + + if (!is_array($this->strip_htmltags) || !in_array($tag, $this->strip_htmltags)) { + $elements = $document->getElementsByTagName($tag); + foreach ($elements as $element) { + foreach ($attributes as $attribute) { + if ($element->hasAttribute($attribute)) { + $value = $this->registry->call(Misc::class, 'absolutize_url', [$element->getAttribute($attribute), $this->base]); + if ($value !== false) { + $value = $this->https_url($value); + $element->setAttribute($attribute, $value); + } + } + } + } + } + } + + /** + * @param array<int, string> $match + * @return string + */ + public function do_strip_htmltags(array $match) + { + if ($this->encode_instead_of_strip) { + if (isset($match[4]) && !in_array(strtolower($match[1]), ['script', 'style'])) { + $match[1] = htmlspecialchars($match[1], ENT_COMPAT, 'UTF-8'); + $match[2] = htmlspecialchars($match[2], ENT_COMPAT, 'UTF-8'); + return "<$match[1]$match[2]>$match[3]</$match[1]>"; + } else { + return htmlspecialchars($match[0], ENT_COMPAT, 'UTF-8'); + } + } elseif (isset($match[4]) && !in_array(strtolower($match[1]), ['script', 'style'])) { + return $match[4]; + } else { + return ''; + } + } + + /** + * @param int-mask-of<SimplePie::CONSTRUCT_*> $type + * @return void + */ + protected function strip_tag(string $tag, DOMDocument $document, DOMXPath $xpath, int $type) + { + $elements = $xpath->query('body//' . $tag); + + if ($elements === false) { + throw new \SimplePie\Exception(sprintf( + '%s(): Possibly malformed expression, check argument #1 ($tag)', + __METHOD__ + ), 1); + } + + if ($this->encode_instead_of_strip) { + foreach ($elements as $element) { + $fragment = $document->createDocumentFragment(); + + // For elements which aren't script or style, include the tag itself + if (!in_array($tag, ['script', 'style'])) { + $text = '<' . $tag; + if ($element->attributes !== null) { + $attrs = []; + foreach ($element->attributes as $name => $attr) { + $value = $attr->value; + + // In XHTML, empty values should never exist, so we repeat the value + if (empty($value) && ($type & \SimplePie\SimplePie::CONSTRUCT_XHTML)) { + $value = $name; + } + // For HTML, empty is fine + elseif (empty($value) && ($type & \SimplePie\SimplePie::CONSTRUCT_HTML)) { + $attrs[] = $name; + continue; + } + + // Standard attribute text + $attrs[] = $name . '="' . $attr->value . '"'; + } + $text .= ' ' . implode(' ', $attrs); + } + $text .= '>'; + $fragment->appendChild(new \DOMText($text)); + } + + $number = $element->childNodes->length; + for ($i = $number; $i > 0; $i--) { + if (($child = $element->childNodes->item(0)) !== null) { + $fragment->appendChild($child); + } + } + + if (!in_array($tag, ['script', 'style'])) { + $fragment->appendChild(new \DOMText('</' . $tag . '>')); + } + + if (($parentNode = $element->parentNode) !== null) { + $parentNode->replaceChild($fragment, $element); + } + } + + return; + } elseif (in_array($tag, ['script', 'style'])) { + foreach ($elements as $element) { + if (($parentNode = $element->parentNode) !== null) { + $parentNode->removeChild($element); + } + } + + return; + } else { + foreach ($elements as $element) { + $fragment = $document->createDocumentFragment(); + $number = $element->childNodes->length; + for ($i = $number; $i > 0; $i--) { + if (($child = $element->childNodes->item(0)) !== null) { + $fragment->appendChild($child); + } + } + + if (($parentNode = $element->parentNode) !== null) { + $parentNode->replaceChild($fragment, $element); + } + } + } + } + + /** + * @return void + */ + protected function strip_attr(string $attrib, DOMXPath $xpath) + { + $elements = $xpath->query('//*[@' . $attrib . ']'); + + if ($elements === false) { + throw new \SimplePie\Exception(sprintf( + '%s(): Possibly malformed expression, check argument #1 ($attrib)', + __METHOD__ + ), 1); + } + + /** @var \DOMElement $element */ + foreach ($elements as $element) { + $element->removeAttribute($attrib); + } + } + + /** + * @return void + */ + protected function rename_attr(string $attrib, DOMXPath $xpath) + { + $elements = $xpath->query('//*[@' . $attrib . ']'); + + if ($elements === false) { + throw new \SimplePie\Exception(sprintf( + '%s(): Possibly malformed expression, check argument #1 ($attrib)', + __METHOD__ + ), 1); + } + + /** @var \DOMElement $element */ + foreach ($elements as $element) { + $element->setAttribute('data-sanitized-' . $attrib, $element->getAttribute($attrib)); + $element->removeAttribute($attrib); + } + } + + /** + * @param array<string, string> $valuePairs + * @return void + */ + protected function add_attr(string $tag, array $valuePairs, DOMDocument $document) + { + $elements = $document->getElementsByTagName($tag); + /** @var \DOMElement $element */ + foreach ($elements as $element) { + foreach ($valuePairs as $attrib => $value) { + $element->setAttribute($attrib, $value); + } + } + } + + /** + * Get a DataCache + * + * @param string $image_url Only needed for BC, can be removed in SimplePie 2.0.0 + * + * @return DataCache + */ + private function get_cache(string $image_url = ''): DataCache + { + if ($this->cache === null) { + // @trigger_error(sprintf('Not providing as PSR-16 cache implementation is deprecated since SimplePie 1.8.0, please use "SimplePie\SimplePie::set_cache()".'), \E_USER_DEPRECATED); + $cache = $this->registry->call(Cache::class, 'get_handler', [ + $this->cache_location, + $image_url, + Base::TYPE_IMAGE + ]); + + return new BaseDataCache($cache); + } + + return $this->cache; + } + + /** + * Get a HTTP client + */ + private function get_http_client(): Client + { + if ($this->http_client === null) { + $this->http_client = new FileClient( + $this->registry, + [ + 'timeout' => $this->timeout, + 'redirects' => 5, + 'useragent' => $this->useragent, + 'force_fsockopen' => $this->force_fsockopen, + 'curl_options' => $this->curl_options, + ] + ); + } + + return $this->http_client; + } +} + +class_alias('SimplePie\Sanitize', 'SimplePie_Sanitize'); + +} + +namespace SimplePie { + +/** + * Handles `<atom:source>` + * + * Used by {@see \SimplePie\Item::get_source()} + * + * This class can be overloaded with {@see \SimplePie::set_source_class()} + */ +class Source implements RegistryAware +{ + /** @var Item */ + public $item; + /** @var array<string, mixed> */ + public $data = []; + /** @var Registry */ + protected $registry; + + /** + * @param array<string, mixed> $data + */ + public function __construct(Item $item, array $data) + { + $this->item = $item; + $this->data = $data; + } + + /** + * @return void + */ + public function set_registry(\SimplePie\Registry $registry) + { + $this->registry = $registry; + } + + /** + * @return string + */ + public function __toString() + { + return md5(serialize($this->data)); + } + + /** + * @param string $namespace + * @param string $tag + * @return array<array<string, mixed>>|null + */ + public function get_source_tags(string $namespace, string $tag) + { + if (isset($this->data['child'][$namespace][$tag])) { + return $this->data['child'][$namespace][$tag]; + } + + return null; + } + + /** + * @param array<string, mixed> $element + * @return string + */ + public function get_base(array $element = []) + { + return $this->item->get_base($element); + } + + /** + * @param string $data + * @param int-mask-of<SimplePie::CONSTRUCT_*> $type + * @param string $base + * @return string + */ + public function sanitize(string $data, $type, string $base = '') + { + return $this->item->sanitize($data, $type, $base); + } + + /** + * @return Item + */ + public function get_item() + { + return $this->item; + } + + /** + * @return string|null + */ + public function get_title() + { + if ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'title')) { + return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'title')) { + return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_RSS_10, 'title')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_RSS_090, 'title')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, 'title')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_DC_11, 'title')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_DC_10, 'title')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + + return null; + } + + /** + * @param int $key + * @return Category|null + */ + public function get_category(int $key = 0) + { + $categories = $this->get_categories(); + if (isset($categories[$key])) { + return $categories[$key]; + } + + return null; + } + + /** + * @return array<Category>|null + */ + public function get_categories() + { + $categories = []; + + foreach ((array) $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'category') as $category) { + $term = null; + $scheme = null; + $label = null; + if (isset($category['attribs']['']['term'])) { + $term = $this->sanitize($category['attribs']['']['term'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['scheme'])) { + $scheme = $this->sanitize($category['attribs']['']['scheme'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['label'])) { + $label = $this->sanitize($category['attribs']['']['label'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + $categories[] = $this->registry->create(Category::class, [$term, $scheme, $label]); + } + foreach ((array) $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, 'category') as $category) { + // This is really the label, but keep this as the term also for BC. + // Label will also work on retrieving because that falls back to term. + $term = $this->sanitize($category['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + if (isset($category['attribs']['']['domain'])) { + $scheme = $this->sanitize($category['attribs']['']['domain'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } else { + $scheme = null; + } + $categories[] = $this->registry->create(Category::class, [$term, $scheme, null]); + } + foreach ((array) $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_DC_11, 'subject') as $category) { + $categories[] = $this->registry->create(Category::class, [$this->sanitize($category['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT), null, null]); + } + foreach ((array) $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_DC_10, 'subject') as $category) { + $categories[] = $this->registry->create(Category::class, [$this->sanitize($category['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT), null, null]); + } + + if (!empty($categories)) { + return array_unique($categories); + } + + return null; + } + + /** + * @param int $key + * @return Author|null + */ + public function get_author(int $key = 0) + { + $authors = $this->get_authors(); + if (isset($authors[$key])) { + return $authors[$key]; + } + + return null; + } + + /** + * @return array<Author>|null + */ + public function get_authors() + { + $authors = []; + foreach ((array) $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'author') as $author) { + $name = null; + $uri = null; + $email = null; + if (isset($author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['name'][0]['data'])) { + $name = $this->sanitize($author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['name'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['uri'][0]['data'])) { + $uri = $author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['uri'][0]; + $uri = $this->sanitize($uri['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($uri)); + } + if (isset($author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['email'][0]['data'])) { + $email = $this->sanitize($author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['email'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $uri !== null) { + $authors[] = $this->registry->create(Author::class, [$name, $uri, $email]); + } + } + if ($author = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'author')) { + $name = null; + $url = null; + $email = null; + if (isset($author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['name'][0]['data'])) { + $name = $this->sanitize($author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['name'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['url'][0]['data'])) { + $url = $author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['url'][0]; + $url = $this->sanitize($url['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($url)); + } + if (isset($author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['email'][0]['data'])) { + $email = $this->sanitize($author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['email'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $url !== null) { + $authors[] = $this->registry->create(Author::class, [$name, $url, $email]); + } + } + foreach ((array) $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_DC_11, 'creator') as $author) { + $authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT), null, null]); + } + foreach ((array) $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_DC_10, 'creator') as $author) { + $authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT), null, null]); + } + foreach ((array) $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'author') as $author) { + $authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT), null, null]); + } + + if (!empty($authors)) { + return array_unique($authors); + } + + return null; + } + + /** + * @param int $key + * @return Author|null + */ + public function get_contributor(int $key = 0) + { + $contributors = $this->get_contributors(); + if (isset($contributors[$key])) { + return $contributors[$key]; + } + + return null; + } + + /** + * @return array<Author>|null + */ + public function get_contributors() + { + $contributors = []; + foreach ((array) $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'contributor') as $contributor) { + $name = null; + $uri = null; + $email = null; + if (isset($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['name'][0]['data'])) { + $name = $this->sanitize($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['name'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['uri'][0]['data'])) { + $uri = $contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['uri'][0]; + $uri = $this->sanitize($uri['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($uri)); + } + if (isset($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['email'][0]['data'])) { + $email = $this->sanitize($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['email'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $uri !== null) { + $contributors[] = $this->registry->create(Author::class, [$name, $uri, $email]); + } + } + foreach ((array) $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'contributor') as $contributor) { + $name = null; + $url = null; + $email = null; + if (isset($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['name'][0]['data'])) { + $name = $this->sanitize($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['name'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if (isset($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['url'][0]['data'])) { + $url = $contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['url'][0]; + $url = $this->sanitize($url['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($url)); + } + if (isset($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['email'][0]['data'])) { + $email = $this->sanitize($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['email'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $url !== null) { + $contributors[] = $this->registry->create(Author::class, [$name, $url, $email]); + } + } + + if (!empty($contributors)) { + return array_unique($contributors); + } + + return null; + } + + /** + * @param int $key + * @param string $rel + * @return string|null + */ + public function get_link(int $key = 0, string $rel = 'alternate') + { + $links = $this->get_links($rel); + if (isset($links[$key])) { + return $links[$key]; + } + + return null; + } + + /** + * Added for parity between the parent-level and the item/entry-level. + * + * @return string|null + */ + public function get_permalink() + { + return $this->get_link(0); + } + + /** + * @param string $rel + * @return array<string>|null + */ + public function get_links(string $rel = 'alternate') + { + if (!isset($this->data['links'])) { + $this->data['links'] = []; + if ($links = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'link')) { + foreach ($links as $link) { + if (isset($link['attribs']['']['href'])) { + $link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate'; + $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($link)); + } + } + } + if ($links = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'link')) { + foreach ($links as $link) { + if (isset($link['attribs']['']['href'])) { + $link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate'; + $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($link)); + } + } + } + if ($links = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_RSS_10, 'link')) { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($links[0])); + } + if ($links = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_RSS_090, 'link')) { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($links[0])); + } + if ($links = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, 'link')) { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($links[0])); + } + + $keys = array_keys($this->data['links']); + foreach ($keys as $key) { + $key = (string) $key; + + if ($this->registry->call(Misc::class, 'is_isegment_nz_nc', [$key])) { + if (isset($this->data['links'][\SimplePie\SimplePie::IANA_LINK_RELATIONS_REGISTRY . $key])) { + $this->data['links'][\SimplePie\SimplePie::IANA_LINK_RELATIONS_REGISTRY . $key] = array_merge($this->data['links'][$key], $this->data['links'][\SimplePie\SimplePie::IANA_LINK_RELATIONS_REGISTRY . $key]); + $this->data['links'][$key] = &$this->data['links'][\SimplePie\SimplePie::IANA_LINK_RELATIONS_REGISTRY . $key]; + } else { + $this->data['links'][\SimplePie\SimplePie::IANA_LINK_RELATIONS_REGISTRY . $key] = &$this->data['links'][$key]; + } + } elseif (substr($key, 0, 41) === \SimplePie\SimplePie::IANA_LINK_RELATIONS_REGISTRY) { + $this->data['links'][substr($key, 41)] = &$this->data['links'][$key]; + } + $this->data['links'][$key] = array_unique($this->data['links'][$key]); + } + } + + if (isset($this->data['links'][$rel])) { + return $this->data['links'][$rel]; + } + + return null; + } + + /** + * @return string|null + */ + public function get_description() + { + if ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'subtitle')) { + return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'tagline')) { + return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_RSS_10, 'description')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_RSS_090, 'description')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, 'description')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_DC_11, 'description')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_DC_10, 'description')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'summary')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_HTML, $this->get_base($return[0])); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'subtitle')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_HTML, $this->get_base($return[0])); + } + + return null; + } + + /** + * @return string|null + */ + public function get_copyright() + { + if ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'rights')) { + return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'copyright')) { + return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, 'copyright')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_DC_11, 'rights')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_DC_10, 'rights')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + + return null; + } + + /** + * @return string|null + */ + public function get_language() + { + if ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, 'language')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_DC_11, 'language')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_DC_10, 'language')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } elseif (isset($this->data['xml_lang'])) { + return $this->sanitize($this->data['xml_lang'], \SimplePie\SimplePie::CONSTRUCT_TEXT); + } + + return null; + } + + /** + * @return float|null + */ + public function get_latitude() + { + if ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_W3C_BASIC_GEO, 'lat')) { + return (float) $return[0]['data']; + } elseif (($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) { + return (float) $match[1]; + } + + return null; + } + + /** + * @return float|null + */ + public function get_longitude() + { + if ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_W3C_BASIC_GEO, 'long')) { + return (float) $return[0]['data']; + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_W3C_BASIC_GEO, 'lon')) { + return (float) $return[0]['data']; + } elseif (($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) { + return (float) $match[2]; + } + + return null; + } + + /** + * @return string|null + */ + public function get_image_url() + { + if ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'image')) { + return $this->sanitize($return[0]['attribs']['']['href'], \SimplePie\SimplePie::CONSTRUCT_IRI); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'logo')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($return[0])); + } elseif ($return = $this->get_source_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'icon')) { + return $this->sanitize($return[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($return[0])); + } + + return null; + } +} + +class_alias('SimplePie\Source', 'SimplePie_Source'); + +} + +namespace SimplePie\XML\Declaration { + +/** + * Parses the XML Declaration + */ +class Parser +{ + /** + * XML Version + * + * @access public + * @var string + */ + public $version = '1.0'; + + /** + * Encoding + * + * @access public + * @var string + */ + public $encoding = 'UTF-8'; + + /** + * Standalone + * + * @access public + * @var bool + */ + public $standalone = false; + + private const STATE_BEFORE_VERSION_NAME = 'before_version_name'; + + private const STATE_VERSION_NAME = 'version_name'; + + private const STATE_VERSION_EQUALS = 'version_equals'; + + private const STATE_VERSION_VALUE = 'version_value'; + + private const STATE_ENCODING_NAME = 'encoding_name'; + + private const STATE_EMIT = 'emit'; + + private const STATE_ENCODING_EQUALS = 'encoding_equals'; + + private const STATE_STANDALONE_NAME = 'standalone_name'; + + private const STATE_ENCODING_VALUE = 'encoding_value'; + + private const STATE_STANDALONE_EQUALS = 'standalone_equals'; + + private const STATE_STANDALONE_VALUE = 'standalone_value'; + + private const STATE_ERROR = false; + + /** + * Current state of the state machine + * + * @access private + * @var self::STATE_* + */ + public $state = self::STATE_BEFORE_VERSION_NAME; + + /** + * Input data + * + * @access private + * @var string + */ + public $data = ''; + + /** + * Input data length (to avoid calling strlen() everytime this is needed) + * + * @access private + * @var int + */ + public $data_length = 0; + + /** + * Current position of the pointer + * + * @var int + * @access private + */ + public $position = 0; + + /** + * Create an instance of the class with the input data + * + * @access public + * @param string $data Input data + */ + public function __construct(string $data) + { + $this->data = $data; + $this->data_length = strlen($this->data); + } + + /** + * Parse the input data + * + * @access public + * @return bool true on success, false on failure + */ + public function parse(): bool + { + while ($this->state && $this->state !== self::STATE_EMIT && $this->has_data()) { + $state = $this->state; + $this->$state(); + } + $this->data = ''; + if ($this->state === self::STATE_EMIT) { + return true; + } + + // Reset the parser state. + $this->version = '1.0'; + $this->encoding = 'UTF-8'; + $this->standalone = false; + return false; + } + + /** + * Check whether there is data beyond the pointer + * + * @access private + * @return bool true if there is further data, false if not + */ + public function has_data(): bool + { + return (bool) ($this->position < $this->data_length); + } + + /** + * Advance past any whitespace + * + * @return int Number of whitespace characters passed + */ + public function skip_whitespace() + { + $whitespace = strspn($this->data, "\x09\x0A\x0D\x20", $this->position); + $this->position += $whitespace; + return $whitespace; + } + + /** + * Read value + * + * @return string|false + */ + public function get_value() + { + $quote = substr($this->data, $this->position, 1); + if ($quote === '"' || $quote === "'") { + $this->position++; + $len = strcspn($this->data, $quote, $this->position); + if ($this->has_data()) { + $value = substr($this->data, $this->position, $len); + $this->position += $len + 1; + return $value; + } + } + return false; + } + + public function before_version_name(): void + { + if ($this->skip_whitespace()) { + $this->state = self::STATE_VERSION_NAME; + } else { + $this->state = self::STATE_ERROR; + } + } + + public function version_name(): void + { + if (substr($this->data, $this->position, 7) === 'version') { + $this->position += 7; + $this->skip_whitespace(); + $this->state = self::STATE_VERSION_EQUALS; + } else { + $this->state = self::STATE_ERROR; + } + } + + public function version_equals(): void + { + if (substr($this->data, $this->position, 1) === '=') { + $this->position++; + $this->skip_whitespace(); + $this->state = self::STATE_VERSION_VALUE; + } else { + $this->state = self::STATE_ERROR; + } + } + + public function version_value(): void + { + if ($version = $this->get_value()) { + $this->version = $version; + $this->skip_whitespace(); + if ($this->has_data()) { + $this->state = self::STATE_ENCODING_NAME; + } else { + $this->state = self::STATE_EMIT; + } + } else { + $this->state = self::STATE_ERROR; + } + } + + public function encoding_name(): void + { + if (substr($this->data, $this->position, 8) === 'encoding') { + $this->position += 8; + $this->skip_whitespace(); + $this->state = self::STATE_ENCODING_EQUALS; + } else { + $this->state = self::STATE_STANDALONE_NAME; + } + } + + public function encoding_equals(): void + { + if (substr($this->data, $this->position, 1) === '=') { + $this->position++; + $this->skip_whitespace(); + $this->state = self::STATE_ENCODING_VALUE; + } else { + $this->state = self::STATE_ERROR; + } + } + + public function encoding_value(): void + { + if ($encoding = $this->get_value()) { + $this->encoding = strtoupper($encoding); + $this->skip_whitespace(); + if ($this->has_data()) { + $this->state = self::STATE_STANDALONE_NAME; + } else { + $this->state = self::STATE_EMIT; + } + } else { + $this->state = self::STATE_ERROR; + } + } + + public function standalone_name(): void + { + if (substr($this->data, $this->position, 10) === 'standalone') { + $this->position += 10; + $this->skip_whitespace(); + $this->state = self::STATE_STANDALONE_EQUALS; + } else { + $this->state = self::STATE_ERROR; + } + } + + public function standalone_equals(): void + { + if (substr($this->data, $this->position, 1) === '=') { + $this->position++; + $this->skip_whitespace(); + $this->state = self::STATE_STANDALONE_VALUE; + } else { + $this->state = self::STATE_ERROR; + } + } + + public function standalone_value(): void + { + if ($standalone = $this->get_value()) { + switch ($standalone) { + case 'yes': + $this->standalone = true; + break; + + case 'no': + $this->standalone = false; + break; + + default: + $this->state = self::STATE_ERROR; + return; + } + + $this->skip_whitespace(); + if ($this->has_data()) { + $this->state = self::STATE_ERROR; + } else { + $this->state = self::STATE_EMIT; + } + } else { + $this->state = self::STATE_ERROR; + } + } +} + +class_alias('SimplePie\XML\Declaration\Parser', 'SimplePie_XML_Declaration_Parser'); + +} + +namespace { + +use SimplePie\Misc; + +class_exists('SimplePie\Misc'); + +// @trigger_error(sprintf('Using the "SimplePie_Misc" class is deprecated since SimplePie 1.7.0, use "SimplePie\Misc" instead.'), \E_USER_DEPRECATED); + +/** @phpstan-ignore-next-line */ +if (\false) { + /** @deprecated since SimplePie 1.7.0, use "SimplePie\Misc" instead */ + class SimplePie_Misc extends Misc + { + } +} + +} + +namespace { + +/** + * SimplePie class. + * + * Class for backward compatibility. + * + * @deprecated Use {@see SimplePie} directly + */ +class SimplePie_Core extends SimplePie +{ +} + +} + +namespace { + +/** + * Decode HTML Entities + * + * This implements HTML5 as of revision 967 (2007-06-28) + * + * @deprecated Use DOMDocument instead! + */ +class SimplePie_Decode_HTML_Entities +{ + /** + * Data to be parsed + * + * @access private + * @var string + */ + public $data = ''; + + /** + * Currently consumed bytes + * + * @access private + * @var string + */ + public $consumed = ''; + + /** + * Position of the current byte being parsed + * + * @access private + * @var int + */ + public $position = 0; + + /** + * Create an instance of the class with the input data + * + * @access public + * @param string $data Input data + */ + public function __construct(string $data) + { + $this->data = $data; + } + + /** + * Parse the input data + * + * @access public + * @return string Output data + */ + public function parse() + { + while (($position = strpos($this->data, '&', $this->position)) !== false) { + $this->position = $position; + $this->consume(); + $this->entity(); + $this->consumed = ''; + } + return $this->data; + } + + /** + * Consume the next byte + * + * @access private + * @return string|false The next byte, or false, if there is no more data + */ + public function consume() + { + if (isset($this->data[$this->position])) { + $this->consumed .= $this->data[$this->position]; + return $this->data[$this->position++]; + } + + return false; + } + + /** + * Consume a range of characters + * + * @access private + * @param string $chars Characters to consume + * @return string|false A series of characters that match the range, or false + */ + public function consume_range(string $chars) + { + if ($len = strspn($this->data, $chars, $this->position)) { + $data = substr($this->data, $this->position, $len); + $this->consumed .= $data; + $this->position += $len; + return $data; + } + + return false; + } + + /** + * Unconsume one byte + * + * @access private + * @return void + */ + public function unconsume() + { + $this->consumed = substr($this->consumed, 0, -1); + $this->position--; + } + + /** + * Decode an entity + * + * @access private + * @return void + */ + public function entity() + { + switch ($this->consume()) { + case "\x09": + case "\x0A": + case "\x0B": + case "\x0C": + case "\x20": + case "\x3C": + case "\x26": + case false: + break; + + case "\x23": + switch ($this->consume()) { + case "\x78": + case "\x58": + $range = '0123456789ABCDEFabcdef'; + $hex = true; + break; + + default: + $range = '0123456789'; + $hex = false; + $this->unconsume(); + break; + } + + if ($codepoint = $this->consume_range($range)) { + static $windows_1252_specials = [0x0D => "\x0A", 0x80 => "\xE2\x82\xAC", 0x81 => "\xEF\xBF\xBD", 0x82 => "\xE2\x80\x9A", 0x83 => "\xC6\x92", 0x84 => "\xE2\x80\x9E", 0x85 => "\xE2\x80\xA6", 0x86 => "\xE2\x80\xA0", 0x87 => "\xE2\x80\xA1", 0x88 => "\xCB\x86", 0x89 => "\xE2\x80\xB0", 0x8A => "\xC5\xA0", 0x8B => "\xE2\x80\xB9", 0x8C => "\xC5\x92", 0x8D => "\xEF\xBF\xBD", 0x8E => "\xC5\xBD", 0x8F => "\xEF\xBF\xBD", 0x90 => "\xEF\xBF\xBD", 0x91 => "\xE2\x80\x98", 0x92 => "\xE2\x80\x99", 0x93 => "\xE2\x80\x9C", 0x94 => "\xE2\x80\x9D", 0x95 => "\xE2\x80\xA2", 0x96 => "\xE2\x80\x93", 0x97 => "\xE2\x80\x94", 0x98 => "\xCB\x9C", 0x99 => "\xE2\x84\xA2", 0x9A => "\xC5\xA1", 0x9B => "\xE2\x80\xBA", 0x9C => "\xC5\x93", 0x9D => "\xEF\xBF\xBD", 0x9E => "\xC5\xBE", 0x9F => "\xC5\xB8"]; + + if ($hex) { + // Cap to PHP_INT_MAX to ensure consistent behaviour if $codepoint is so large + // it cannot fit into int – just casting float to int might return junk (e.g. a negative number). + // If it is so large, `Misc::codepoint_to_utf8` will just return a replacement character. + $codepoint = (int) min(hexdec($codepoint), \PHP_INT_MAX); + } else { + // Casting string to int caps at PHP_INT_MAX automatically. + $codepoint = (int) $codepoint; + } + + if (isset($windows_1252_specials[$codepoint])) { + $replacement = $windows_1252_specials[$codepoint]; + } else { + $replacement = SimplePie_Misc::codepoint_to_utf8($codepoint); + } + + if (!in_array($this->consume(), [';', false], true)) { + $this->unconsume(); + } + + $consumed_length = strlen($this->consumed); + $this->data = substr_replace($this->data, $replacement, $this->position - $consumed_length, $consumed_length); + $this->position += strlen($replacement) - $consumed_length; + } + break; + + default: + static $entities = [ + 'Aacute' => "\xC3\x81", + 'aacute' => "\xC3\xA1", + 'Aacute;' => "\xC3\x81", + 'aacute;' => "\xC3\xA1", + 'Acirc' => "\xC3\x82", + 'acirc' => "\xC3\xA2", + 'Acirc;' => "\xC3\x82", + 'acirc;' => "\xC3\xA2", + 'acute' => "\xC2\xB4", + 'acute;' => "\xC2\xB4", + 'AElig' => "\xC3\x86", + 'aelig' => "\xC3\xA6", + 'AElig;' => "\xC3\x86", + 'aelig;' => "\xC3\xA6", + 'Agrave' => "\xC3\x80", + 'agrave' => "\xC3\xA0", + 'Agrave;' => "\xC3\x80", + 'agrave;' => "\xC3\xA0", + 'alefsym;' => "\xE2\x84\xB5", + 'Alpha;' => "\xCE\x91", + 'alpha;' => "\xCE\xB1", + 'AMP' => "\x26", + 'amp' => "\x26", + 'AMP;' => "\x26", + 'amp;' => "\x26", + 'and;' => "\xE2\x88\xA7", + 'ang;' => "\xE2\x88\xA0", + 'apos;' => "\x27", + 'Aring' => "\xC3\x85", + 'aring' => "\xC3\xA5", + 'Aring;' => "\xC3\x85", + 'aring;' => "\xC3\xA5", + 'asymp;' => "\xE2\x89\x88", + 'Atilde' => "\xC3\x83", + 'atilde' => "\xC3\xA3", + 'Atilde;' => "\xC3\x83", + 'atilde;' => "\xC3\xA3", + 'Auml' => "\xC3\x84", + 'auml' => "\xC3\xA4", + 'Auml;' => "\xC3\x84", + 'auml;' => "\xC3\xA4", + 'bdquo;' => "\xE2\x80\x9E", + 'Beta;' => "\xCE\x92", + 'beta;' => "\xCE\xB2", + 'brvbar' => "\xC2\xA6", + 'brvbar;' => "\xC2\xA6", + 'bull;' => "\xE2\x80\xA2", + 'cap;' => "\xE2\x88\xA9", + 'Ccedil' => "\xC3\x87", + 'ccedil' => "\xC3\xA7", + 'Ccedil;' => "\xC3\x87", + 'ccedil;' => "\xC3\xA7", + 'cedil' => "\xC2\xB8", + 'cedil;' => "\xC2\xB8", + 'cent' => "\xC2\xA2", + 'cent;' => "\xC2\xA2", + 'Chi;' => "\xCE\xA7", + 'chi;' => "\xCF\x87", + 'circ;' => "\xCB\x86", + 'clubs;' => "\xE2\x99\xA3", + 'cong;' => "\xE2\x89\x85", + 'COPY' => "\xC2\xA9", + 'copy' => "\xC2\xA9", + 'COPY;' => "\xC2\xA9", + 'copy;' => "\xC2\xA9", + 'crarr;' => "\xE2\x86\xB5", + 'cup;' => "\xE2\x88\xAA", + 'curren' => "\xC2\xA4", + 'curren;' => "\xC2\xA4", + 'Dagger;' => "\xE2\x80\xA1", + 'dagger;' => "\xE2\x80\xA0", + 'dArr;' => "\xE2\x87\x93", + 'darr;' => "\xE2\x86\x93", + 'deg' => "\xC2\xB0", + 'deg;' => "\xC2\xB0", + 'Delta;' => "\xCE\x94", + 'delta;' => "\xCE\xB4", + 'diams;' => "\xE2\x99\xA6", + 'divide' => "\xC3\xB7", + 'divide;' => "\xC3\xB7", + 'Eacute' => "\xC3\x89", + 'eacute' => "\xC3\xA9", + 'Eacute;' => "\xC3\x89", + 'eacute;' => "\xC3\xA9", + 'Ecirc' => "\xC3\x8A", + 'ecirc' => "\xC3\xAA", + 'Ecirc;' => "\xC3\x8A", + 'ecirc;' => "\xC3\xAA", + 'Egrave' => "\xC3\x88", + 'egrave' => "\xC3\xA8", + 'Egrave;' => "\xC3\x88", + 'egrave;' => "\xC3\xA8", + 'empty;' => "\xE2\x88\x85", + 'emsp;' => "\xE2\x80\x83", + 'ensp;' => "\xE2\x80\x82", + 'Epsilon;' => "\xCE\x95", + 'epsilon;' => "\xCE\xB5", + 'equiv;' => "\xE2\x89\xA1", + 'Eta;' => "\xCE\x97", + 'eta;' => "\xCE\xB7", + 'ETH' => "\xC3\x90", + 'eth' => "\xC3\xB0", + 'ETH;' => "\xC3\x90", + 'eth;' => "\xC3\xB0", + 'Euml' => "\xC3\x8B", + 'euml' => "\xC3\xAB", + 'Euml;' => "\xC3\x8B", + 'euml;' => "\xC3\xAB", + 'euro;' => "\xE2\x82\xAC", + 'exist;' => "\xE2\x88\x83", + 'fnof;' => "\xC6\x92", + 'forall;' => "\xE2\x88\x80", + 'frac12' => "\xC2\xBD", + 'frac12;' => "\xC2\xBD", + 'frac14' => "\xC2\xBC", + 'frac14;' => "\xC2\xBC", + 'frac34' => "\xC2\xBE", + 'frac34;' => "\xC2\xBE", + 'frasl;' => "\xE2\x81\x84", + 'Gamma;' => "\xCE\x93", + 'gamma;' => "\xCE\xB3", + 'ge;' => "\xE2\x89\xA5", + 'GT' => "\x3E", + 'gt' => "\x3E", + 'GT;' => "\x3E", + 'gt;' => "\x3E", + 'hArr;' => "\xE2\x87\x94", + 'harr;' => "\xE2\x86\x94", + 'hearts;' => "\xE2\x99\xA5", + 'hellip;' => "\xE2\x80\xA6", + 'Iacute' => "\xC3\x8D", + 'iacute' => "\xC3\xAD", + 'Iacute;' => "\xC3\x8D", + 'iacute;' => "\xC3\xAD", + 'Icirc' => "\xC3\x8E", + 'icirc' => "\xC3\xAE", + 'Icirc;' => "\xC3\x8E", + 'icirc;' => "\xC3\xAE", + 'iexcl' => "\xC2\xA1", + 'iexcl;' => "\xC2\xA1", + 'Igrave' => "\xC3\x8C", + 'igrave' => "\xC3\xAC", + 'Igrave;' => "\xC3\x8C", + 'igrave;' => "\xC3\xAC", + 'image;' => "\xE2\x84\x91", + 'infin;' => "\xE2\x88\x9E", + 'int;' => "\xE2\x88\xAB", + 'Iota;' => "\xCE\x99", + 'iota;' => "\xCE\xB9", + 'iquest' => "\xC2\xBF", + 'iquest;' => "\xC2\xBF", + 'isin;' => "\xE2\x88\x88", + 'Iuml' => "\xC3\x8F", + 'iuml' => "\xC3\xAF", + 'Iuml;' => "\xC3\x8F", + 'iuml;' => "\xC3\xAF", + 'Kappa;' => "\xCE\x9A", + 'kappa;' => "\xCE\xBA", + 'Lambda;' => "\xCE\x9B", + 'lambda;' => "\xCE\xBB", + 'lang;' => "\xE3\x80\x88", + 'laquo' => "\xC2\xAB", + 'laquo;' => "\xC2\xAB", + 'lArr;' => "\xE2\x87\x90", + 'larr;' => "\xE2\x86\x90", + 'lceil;' => "\xE2\x8C\x88", + 'ldquo;' => "\xE2\x80\x9C", + 'le;' => "\xE2\x89\xA4", + 'lfloor;' => "\xE2\x8C\x8A", + 'lowast;' => "\xE2\x88\x97", + 'loz;' => "\xE2\x97\x8A", + 'lrm;' => "\xE2\x80\x8E", + 'lsaquo;' => "\xE2\x80\xB9", + 'lsquo;' => "\xE2\x80\x98", + 'LT' => "\x3C", + 'lt' => "\x3C", + 'LT;' => "\x3C", + 'lt;' => "\x3C", + 'macr' => "\xC2\xAF", + 'macr;' => "\xC2\xAF", + 'mdash;' => "\xE2\x80\x94", + 'micro' => "\xC2\xB5", + 'micro;' => "\xC2\xB5", + 'middot' => "\xC2\xB7", + 'middot;' => "\xC2\xB7", + 'minus;' => "\xE2\x88\x92", + 'Mu;' => "\xCE\x9C", + 'mu;' => "\xCE\xBC", + 'nabla;' => "\xE2\x88\x87", + 'nbsp' => "\xC2\xA0", + 'nbsp;' => "\xC2\xA0", + 'ndash;' => "\xE2\x80\x93", + 'ne;' => "\xE2\x89\xA0", + 'ni;' => "\xE2\x88\x8B", + 'not' => "\xC2\xAC", + 'not;' => "\xC2\xAC", + 'notin;' => "\xE2\x88\x89", + 'nsub;' => "\xE2\x8A\x84", + 'Ntilde' => "\xC3\x91", + 'ntilde' => "\xC3\xB1", + 'Ntilde;' => "\xC3\x91", + 'ntilde;' => "\xC3\xB1", + 'Nu;' => "\xCE\x9D", + 'nu;' => "\xCE\xBD", + 'Oacute' => "\xC3\x93", + 'oacute' => "\xC3\xB3", + 'Oacute;' => "\xC3\x93", + 'oacute;' => "\xC3\xB3", + 'Ocirc' => "\xC3\x94", + 'ocirc' => "\xC3\xB4", + 'Ocirc;' => "\xC3\x94", + 'ocirc;' => "\xC3\xB4", + 'OElig;' => "\xC5\x92", + 'oelig;' => "\xC5\x93", + 'Ograve' => "\xC3\x92", + 'ograve' => "\xC3\xB2", + 'Ograve;' => "\xC3\x92", + 'ograve;' => "\xC3\xB2", + 'oline;' => "\xE2\x80\xBE", + 'Omega;' => "\xCE\xA9", + 'omega;' => "\xCF\x89", + 'Omicron;' => "\xCE\x9F", + 'omicron;' => "\xCE\xBF", + 'oplus;' => "\xE2\x8A\x95", + 'or;' => "\xE2\x88\xA8", + 'ordf' => "\xC2\xAA", + 'ordf;' => "\xC2\xAA", + 'ordm' => "\xC2\xBA", + 'ordm;' => "\xC2\xBA", + 'Oslash' => "\xC3\x98", + 'oslash' => "\xC3\xB8", + 'Oslash;' => "\xC3\x98", + 'oslash;' => "\xC3\xB8", + 'Otilde' => "\xC3\x95", + 'otilde' => "\xC3\xB5", + 'Otilde;' => "\xC3\x95", + 'otilde;' => "\xC3\xB5", + 'otimes;' => "\xE2\x8A\x97", + 'Ouml' => "\xC3\x96", + 'ouml' => "\xC3\xB6", + 'Ouml;' => "\xC3\x96", + 'ouml;' => "\xC3\xB6", + 'para' => "\xC2\xB6", + 'para;' => "\xC2\xB6", + 'part;' => "\xE2\x88\x82", + 'permil;' => "\xE2\x80\xB0", + 'perp;' => "\xE2\x8A\xA5", + 'Phi;' => "\xCE\xA6", + 'phi;' => "\xCF\x86", + 'Pi;' => "\xCE\xA0", + 'pi;' => "\xCF\x80", + 'piv;' => "\xCF\x96", + 'plusmn' => "\xC2\xB1", + 'plusmn;' => "\xC2\xB1", + 'pound' => "\xC2\xA3", + 'pound;' => "\xC2\xA3", + 'Prime;' => "\xE2\x80\xB3", + 'prime;' => "\xE2\x80\xB2", + 'prod;' => "\xE2\x88\x8F", + 'prop;' => "\xE2\x88\x9D", + 'Psi;' => "\xCE\xA8", + 'psi;' => "\xCF\x88", + 'QUOT' => "\x22", + 'quot' => "\x22", + 'QUOT;' => "\x22", + 'quot;' => "\x22", + 'radic;' => "\xE2\x88\x9A", + 'rang;' => "\xE3\x80\x89", + 'raquo' => "\xC2\xBB", + 'raquo;' => "\xC2\xBB", + 'rArr;' => "\xE2\x87\x92", + 'rarr;' => "\xE2\x86\x92", + 'rceil;' => "\xE2\x8C\x89", + 'rdquo;' => "\xE2\x80\x9D", + 'real;' => "\xE2\x84\x9C", + 'REG' => "\xC2\xAE", + 'reg' => "\xC2\xAE", + 'REG;' => "\xC2\xAE", + 'reg;' => "\xC2\xAE", + 'rfloor;' => "\xE2\x8C\x8B", + 'Rho;' => "\xCE\xA1", + 'rho;' => "\xCF\x81", + 'rlm;' => "\xE2\x80\x8F", + 'rsaquo;' => "\xE2\x80\xBA", + 'rsquo;' => "\xE2\x80\x99", + 'sbquo;' => "\xE2\x80\x9A", + 'Scaron;' => "\xC5\xA0", + 'scaron;' => "\xC5\xA1", + 'sdot;' => "\xE2\x8B\x85", + 'sect' => "\xC2\xA7", + 'sect;' => "\xC2\xA7", + 'shy' => "\xC2\xAD", + 'shy;' => "\xC2\xAD", + 'Sigma;' => "\xCE\xA3", + 'sigma;' => "\xCF\x83", + 'sigmaf;' => "\xCF\x82", + 'sim;' => "\xE2\x88\xBC", + 'spades;' => "\xE2\x99\xA0", + 'sub;' => "\xE2\x8A\x82", + 'sube;' => "\xE2\x8A\x86", + 'sum;' => "\xE2\x88\x91", + 'sup;' => "\xE2\x8A\x83", + 'sup1' => "\xC2\xB9", + 'sup1;' => "\xC2\xB9", + 'sup2' => "\xC2\xB2", + 'sup2;' => "\xC2\xB2", + 'sup3' => "\xC2\xB3", + 'sup3;' => "\xC2\xB3", + 'supe;' => "\xE2\x8A\x87", + 'szlig' => "\xC3\x9F", + 'szlig;' => "\xC3\x9F", + 'Tau;' => "\xCE\xA4", + 'tau;' => "\xCF\x84", + 'there4;' => "\xE2\x88\xB4", + 'Theta;' => "\xCE\x98", + 'theta;' => "\xCE\xB8", + 'thetasym;' => "\xCF\x91", + 'thinsp;' => "\xE2\x80\x89", + 'THORN' => "\xC3\x9E", + 'thorn' => "\xC3\xBE", + 'THORN;' => "\xC3\x9E", + 'thorn;' => "\xC3\xBE", + 'tilde;' => "\xCB\x9C", + 'times' => "\xC3\x97", + 'times;' => "\xC3\x97", + 'TRADE;' => "\xE2\x84\xA2", + 'trade;' => "\xE2\x84\xA2", + 'Uacute' => "\xC3\x9A", + 'uacute' => "\xC3\xBA", + 'Uacute;' => "\xC3\x9A", + 'uacute;' => "\xC3\xBA", + 'uArr;' => "\xE2\x87\x91", + 'uarr;' => "\xE2\x86\x91", + 'Ucirc' => "\xC3\x9B", + 'ucirc' => "\xC3\xBB", + 'Ucirc;' => "\xC3\x9B", + 'ucirc;' => "\xC3\xBB", + 'Ugrave' => "\xC3\x99", + 'ugrave' => "\xC3\xB9", + 'Ugrave;' => "\xC3\x99", + 'ugrave;' => "\xC3\xB9", + 'uml' => "\xC2\xA8", + 'uml;' => "\xC2\xA8", + 'upsih;' => "\xCF\x92", + 'Upsilon;' => "\xCE\xA5", + 'upsilon;' => "\xCF\x85", + 'Uuml' => "\xC3\x9C", + 'uuml' => "\xC3\xBC", + 'Uuml;' => "\xC3\x9C", + 'uuml;' => "\xC3\xBC", + 'weierp;' => "\xE2\x84\x98", + 'Xi;' => "\xCE\x9E", + 'xi;' => "\xCE\xBE", + 'Yacute' => "\xC3\x9D", + 'yacute' => "\xC3\xBD", + 'Yacute;' => "\xC3\x9D", + 'yacute;' => "\xC3\xBD", + 'yen' => "\xC2\xA5", + 'yen;' => "\xC2\xA5", + 'yuml' => "\xC3\xBF", + 'Yuml;' => "\xC5\xB8", + 'yuml;' => "\xC3\xBF", + 'Zeta;' => "\xCE\x96", + 'zeta;' => "\xCE\xB6", + 'zwj;' => "\xE2\x80\x8D", + 'zwnj;' => "\xE2\x80\x8C" + ]; + + for ($i = 0, $match = null; $i < 9 && $this->consume() !== false; $i++) { + // Cast for PHPStan on PHP < 8.0: We consumed as per the loop condition, + // so `$this->consumed` is non-empty and the substr offset is valid. + $consumed = (string) substr($this->consumed, 1); + if (isset($entities[$consumed])) { + $match = $consumed; + } + } + + if ($match !== null) { + $this->data = substr_replace($this->data, $entities[$match], $this->position - strlen($consumed) - 1, strlen($match) + 1); + $this->position += strlen($entities[$match]) - strlen($consumed) - 1; + } + break; + } + } +} + +} + +namespace { + +use SimplePie\SimplePie as NamespacedSimplePie; + +class_exists('SimplePie\SimplePie'); + +// @trigger_error(sprintf('Using the "SimplePie" class is deprecated since SimplePie 1.7.0, use "SimplePie\SimplePie" instead.'), \E_USER_DEPRECATED); + +/** @phpstan-ignore-next-line */ +if (\false) { + /** @deprecated since SimplePie 1.7.0, use "SimplePie\SimplePie" instead */ + class SimplePie extends NamespacedSimplePie + { + } +} + +/** + * SimplePie Name + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAME instead. + */ +define('SIMPLEPIE_NAME', NamespacedSimplePie::NAME); + +/** + * SimplePie Version + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::VERSION instead. + */ +define('SIMPLEPIE_VERSION', NamespacedSimplePie::VERSION); + +/** + * SimplePie Build + * @todo Hardcode for release (there's no need to have to call SimplePie_Misc::get_build() only every load of simplepie.inc) + * @deprecated since SimplePie 1.7.0, use \SimplePie\Misc::get_build() instead. + */ +define('SIMPLEPIE_BUILD', '20250930132948'); + +/** + * SimplePie Website URL + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::URL instead. + */ +define('SIMPLEPIE_URL', NamespacedSimplePie::URL); + +/** + * SimplePie Useragent + * @see SimplePie::set_useragent() + * @deprecated since SimplePie 1.7.0, use \SimplePie\Misc::get_default_useragent() instead. + */ +define('SIMPLEPIE_USERAGENT', \SimplePie\Misc::get_default_useragent()); + +/** + * SimplePie Linkback + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::LINKBACK instead. + */ +define('SIMPLEPIE_LINKBACK', NamespacedSimplePie::LINKBACK); + +/** + * No Autodiscovery + * @see SimplePie::set_autodiscovery_level() + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::LOCATOR_NONE instead. + */ +define('SIMPLEPIE_LOCATOR_NONE', NamespacedSimplePie::LOCATOR_NONE); + +/** + * Feed Link Element Autodiscovery + * @see SimplePie::set_autodiscovery_level() + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::LOCATOR_AUTODISCOVERY instead. + */ +define('SIMPLEPIE_LOCATOR_AUTODISCOVERY', NamespacedSimplePie::LOCATOR_AUTODISCOVERY); + +/** + * Local Feed Extension Autodiscovery + * @see SimplePie::set_autodiscovery_level() + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::LOCATOR_LOCAL_EXTENSION instead. + */ +define('SIMPLEPIE_LOCATOR_LOCAL_EXTENSION', NamespacedSimplePie::LOCATOR_LOCAL_EXTENSION); + +/** + * Local Feed Body Autodiscovery + * @see SimplePie::set_autodiscovery_level() + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::LOCATOR_LOCAL_BODY instead. + */ +define('SIMPLEPIE_LOCATOR_LOCAL_BODY', NamespacedSimplePie::LOCATOR_LOCAL_BODY); + +/** + * Remote Feed Extension Autodiscovery + * @see SimplePie::set_autodiscovery_level() + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::LOCATOR_REMOTE_EXTENSION instead. + */ +define('SIMPLEPIE_LOCATOR_REMOTE_EXTENSION', NamespacedSimplePie::LOCATOR_REMOTE_EXTENSION); + +/** + * Remote Feed Body Autodiscovery + * @see SimplePie::set_autodiscovery_level() + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::LOCATOR_REMOTE_BODY instead. + */ +define('SIMPLEPIE_LOCATOR_REMOTE_BODY', NamespacedSimplePie::LOCATOR_REMOTE_BODY); + +/** + * All Feed Autodiscovery + * @see SimplePie::set_autodiscovery_level() + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::LOCATOR_ALL instead. + */ +define('SIMPLEPIE_LOCATOR_ALL', NamespacedSimplePie::LOCATOR_ALL); + +/** + * No known feed type + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_NONE instead. + */ +define('SIMPLEPIE_TYPE_NONE', NamespacedSimplePie::TYPE_NONE); + +/** + * RSS 0.90 + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_RSS_090 instead. + */ +define('SIMPLEPIE_TYPE_RSS_090', NamespacedSimplePie::TYPE_RSS_090); + +/** + * RSS 0.91 (Netscape) + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_RSS_091_NETSCAPE instead. + */ +define('SIMPLEPIE_TYPE_RSS_091_NETSCAPE', NamespacedSimplePie::TYPE_RSS_091_NETSCAPE); + +/** + * RSS 0.91 (Userland) + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_RSS_091_USERLAND instead. + */ +define('SIMPLEPIE_TYPE_RSS_091_USERLAND', NamespacedSimplePie::TYPE_RSS_091_USERLAND); + +/** + * RSS 0.91 (both Netscape and Userland) + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_RSS_091 instead. + */ +define('SIMPLEPIE_TYPE_RSS_091', NamespacedSimplePie::TYPE_RSS_091); + +/** + * RSS 0.92 + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_RSS_092 instead. + */ +define('SIMPLEPIE_TYPE_RSS_092', NamespacedSimplePie::TYPE_RSS_092); + +/** + * RSS 0.93 + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_RSS_093 instead. + */ +define('SIMPLEPIE_TYPE_RSS_093', NamespacedSimplePie::TYPE_RSS_093); + +/** + * RSS 0.94 + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_RSS_094 instead. + */ +define('SIMPLEPIE_TYPE_RSS_094', NamespacedSimplePie::TYPE_RSS_094); + +/** + * RSS 1.0 + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_RSS_10 instead. + */ +define('SIMPLEPIE_TYPE_RSS_10', NamespacedSimplePie::TYPE_RSS_10); + +/** + * RSS 2.0 + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_RSS_20 instead. + */ +define('SIMPLEPIE_TYPE_RSS_20', NamespacedSimplePie::TYPE_RSS_20); + +/** + * RDF-based RSS + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_RSS_RDF instead. + */ +define('SIMPLEPIE_TYPE_RSS_RDF', NamespacedSimplePie::TYPE_RSS_RDF); + +/** + * Non-RDF-based RSS (truly intended as syndication format) + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_RSS_SYNDICATION instead. + */ +define('SIMPLEPIE_TYPE_RSS_SYNDICATION', NamespacedSimplePie::TYPE_RSS_SYNDICATION); + +/** + * All RSS + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_RSS_ALL instead. + */ +define('SIMPLEPIE_TYPE_RSS_ALL', NamespacedSimplePie::TYPE_RSS_ALL); + +/** + * Atom 0.3 + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_ATOM_03 instead. + */ +define('SIMPLEPIE_TYPE_ATOM_03', NamespacedSimplePie::TYPE_ATOM_03); + +/** + * Atom 1.0 + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_ATOM_10 instead. + */ +define('SIMPLEPIE_TYPE_ATOM_10', NamespacedSimplePie::TYPE_ATOM_10); + +/** + * All Atom + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_ATOM_ALL instead. + */ +define('SIMPLEPIE_TYPE_ATOM_ALL', NamespacedSimplePie::TYPE_ATOM_ALL); + +/** + * All feed types + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::TYPE_ALL instead. + */ +define('SIMPLEPIE_TYPE_ALL', NamespacedSimplePie::TYPE_ALL); + +/** + * No construct + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::CONSTRUCT_NONE instead. + */ +define('SIMPLEPIE_CONSTRUCT_NONE', NamespacedSimplePie::CONSTRUCT_NONE); + +/** + * Text construct + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::CONSTRUCT_TEXT instead. + */ +define('SIMPLEPIE_CONSTRUCT_TEXT', NamespacedSimplePie::CONSTRUCT_TEXT); + +/** + * HTML construct + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::CONSTRUCT_HTML instead. + */ +define('SIMPLEPIE_CONSTRUCT_HTML', NamespacedSimplePie::CONSTRUCT_HTML); + +/** + * XHTML construct + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::CONSTRUCT_XHTML instead. + */ +define('SIMPLEPIE_CONSTRUCT_XHTML', NamespacedSimplePie::CONSTRUCT_XHTML); + +/** + * base64-encoded construct + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::CONSTRUCT_BASE64 instead. + */ +define('SIMPLEPIE_CONSTRUCT_BASE64', NamespacedSimplePie::CONSTRUCT_BASE64); + +/** + * IRI construct + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::CONSTRUCT_IRI instead. + */ +define('SIMPLEPIE_CONSTRUCT_IRI', NamespacedSimplePie::CONSTRUCT_IRI); + +/** + * A construct that might be HTML + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::CONSTRUCT_MAYBE_HTML instead. + */ +define('SIMPLEPIE_CONSTRUCT_MAYBE_HTML', NamespacedSimplePie::CONSTRUCT_MAYBE_HTML); + +/** + * All constructs + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::CONSTRUCT_ALL instead. + */ +define('SIMPLEPIE_CONSTRUCT_ALL', NamespacedSimplePie::CONSTRUCT_ALL); + +/** + * Don't change case + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::SAME_CASE instead. + */ +define('SIMPLEPIE_SAME_CASE', NamespacedSimplePie::SAME_CASE); + +/** + * Change to lowercase + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::LOWERCASE instead. + */ +define('SIMPLEPIE_LOWERCASE', NamespacedSimplePie::LOWERCASE); + +/** + * Change to uppercase + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::UPPERCASE instead. + */ +define('SIMPLEPIE_UPPERCASE', NamespacedSimplePie::UPPERCASE); + +/** + * PCRE for HTML attributes + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::PCRE_HTML_ATTRIBUTE instead. + */ +define('SIMPLEPIE_PCRE_HTML_ATTRIBUTE', NamespacedSimplePie::PCRE_HTML_ATTRIBUTE); + +/** + * PCRE for XML attributes + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::PCRE_XML_ATTRIBUTE instead. + */ +define('SIMPLEPIE_PCRE_XML_ATTRIBUTE', NamespacedSimplePie::PCRE_XML_ATTRIBUTE); + +/** + * XML Namespace + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_XML instead. + */ +define('SIMPLEPIE_NAMESPACE_XML', NamespacedSimplePie::NAMESPACE_XML); + +/** + * Atom 1.0 Namespace + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_ATOM_10 instead. + */ +define('SIMPLEPIE_NAMESPACE_ATOM_10', NamespacedSimplePie::NAMESPACE_ATOM_10); + +/** + * Atom 0.3 Namespace + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_ATOM_03 instead. + */ +define('SIMPLEPIE_NAMESPACE_ATOM_03', NamespacedSimplePie::NAMESPACE_ATOM_03); + +/** + * RDF Namespace + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_RDF instead. + */ +define('SIMPLEPIE_NAMESPACE_RDF', NamespacedSimplePie::NAMESPACE_RDF); + +/** + * RSS 0.90 Namespace + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_RSS_090 instead. + */ +define('SIMPLEPIE_NAMESPACE_RSS_090', NamespacedSimplePie::NAMESPACE_RSS_090); + +/** + * RSS 1.0 Namespace + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_RSS_10 instead. + */ +define('SIMPLEPIE_NAMESPACE_RSS_10', NamespacedSimplePie::NAMESPACE_RSS_10); + +/** + * RSS 1.0 Content Module Namespace + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_RSS_10_MODULES_CONTENT instead. + */ +define('SIMPLEPIE_NAMESPACE_RSS_10_MODULES_CONTENT', NamespacedSimplePie::NAMESPACE_RSS_10_MODULES_CONTENT); + +/** + * RSS 2.0 Namespace + * (Stupid, I know, but I'm certain it will confuse people less with support.) + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_RSS_20 instead. + */ +define('SIMPLEPIE_NAMESPACE_RSS_20', NamespacedSimplePie::NAMESPACE_RSS_20); + +/** + * DC 1.0 Namespace + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_DC_10 instead. + */ +define('SIMPLEPIE_NAMESPACE_DC_10', NamespacedSimplePie::NAMESPACE_DC_10); + +/** + * DC 1.1 Namespace + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_DC_11 instead. + */ +define('SIMPLEPIE_NAMESPACE_DC_11', NamespacedSimplePie::NAMESPACE_DC_11); + +/** + * W3C Basic Geo (WGS84 lat/long) Vocabulary Namespace + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_W3C_BASIC_GEO instead. + */ +define('SIMPLEPIE_NAMESPACE_W3C_BASIC_GEO', NamespacedSimplePie::NAMESPACE_W3C_BASIC_GEO); + +/** + * GeoRSS Namespace + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_GEORSS instead. + */ +define('SIMPLEPIE_NAMESPACE_GEORSS', NamespacedSimplePie::NAMESPACE_GEORSS); + +/** + * Media RSS Namespace + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_MEDIARSS instead. + */ +define('SIMPLEPIE_NAMESPACE_MEDIARSS', NamespacedSimplePie::NAMESPACE_MEDIARSS); + +/** + * Wrong Media RSS Namespace. Caused by a long-standing typo in the spec. + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_MEDIARSS_WRONG instead. + */ +define('SIMPLEPIE_NAMESPACE_MEDIARSS_WRONG', NamespacedSimplePie::NAMESPACE_MEDIARSS_WRONG); + +/** + * Wrong Media RSS Namespace #2. New namespace introduced in Media RSS 1.5. + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_MEDIARSS_WRONG2 instead. + */ +define('SIMPLEPIE_NAMESPACE_MEDIARSS_WRONG2', NamespacedSimplePie::NAMESPACE_MEDIARSS_WRONG2); + +/** + * Wrong Media RSS Namespace #3. A possible typo of the Media RSS 1.5 namespace. + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_MEDIARSS_WRONG3 instead. + */ +define('SIMPLEPIE_NAMESPACE_MEDIARSS_WRONG3', NamespacedSimplePie::NAMESPACE_MEDIARSS_WRONG3); + +/** + * Wrong Media RSS Namespace #4. New spec location after the RSS Advisory Board takes it over, but not a valid namespace. + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_MEDIARSS_WRONG4 instead. + */ +define('SIMPLEPIE_NAMESPACE_MEDIARSS_WRONG4', NamespacedSimplePie::NAMESPACE_MEDIARSS_WRONG4); + +/** + * Wrong Media RSS Namespace #5. A possible typo of the RSS Advisory Board URL. + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_MEDIARSS_WRONG5 instead. + */ +define('SIMPLEPIE_NAMESPACE_MEDIARSS_WRONG5', NamespacedSimplePie::NAMESPACE_MEDIARSS_WRONG5); + +/** + * iTunes RSS Namespace + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_ITUNES instead. + */ +define('SIMPLEPIE_NAMESPACE_ITUNES', NamespacedSimplePie::NAMESPACE_ITUNES); + +/** + * XHTML Namespace + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::NAMESPACE_XHTML instead. + */ +define('SIMPLEPIE_NAMESPACE_XHTML', NamespacedSimplePie::NAMESPACE_XHTML); + +/** + * IANA Link Relations Registry + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::IANA_LINK_RELATIONS_REGISTRY instead. + */ +define('SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY', NamespacedSimplePie::IANA_LINK_RELATIONS_REGISTRY); + +/** + * No file source + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::FILE_SOURCE_NONE instead. + */ +define('SIMPLEPIE_FILE_SOURCE_NONE', NamespacedSimplePie::FILE_SOURCE_NONE); + +/** + * Remote file source + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::FILE_SOURCE_REMOTE instead. + */ +define('SIMPLEPIE_FILE_SOURCE_REMOTE', NamespacedSimplePie::FILE_SOURCE_REMOTE); + +/** + * Local file source + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::FILE_SOURCE_LOCAL instead. + */ +define('SIMPLEPIE_FILE_SOURCE_LOCAL', NamespacedSimplePie::FILE_SOURCE_LOCAL); + +/** + * fsockopen() file source + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::FILE_SOURCE_FSOCKOPEN instead. + */ +define('SIMPLEPIE_FILE_SOURCE_FSOCKOPEN', NamespacedSimplePie::FILE_SOURCE_FSOCKOPEN); + +/** + * cURL file source + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::FILE_SOURCE_CURL instead. + */ +define('SIMPLEPIE_FILE_SOURCE_CURL', NamespacedSimplePie::FILE_SOURCE_CURL); + +/** + * file_get_contents() file source + * @deprecated since SimplePie 1.7.0, use \SimplePie\SimplePie::FILE_SOURCE_FILE_GET_CONTENTS instead. + */ +define('SIMPLEPIE_FILE_SOURCE_FILE_GET_CONTENTS', NamespacedSimplePie::FILE_SOURCE_FILE_GET_CONTENTS); + +} + diff --git a/scripts/getfeeds_http.php b/scripts/getfeeds_http.php new file mode 100644 index 0000000..eaa03eb --- /dev/null +++ b/scripts/getfeeds_http.php @@ -0,0 +1,167 @@ +<?php +/* + Build rss feed pages + creates the necessary index.phps using + /feeds/feedname/feed and /feeds/feedname/title + feed contains the feed url, title is whatever title you edit + requires simplepie https://www.simplepie.org/ +*/ + +require_once('/srv/http/wittamore.fr/scripts/SimplePie.compiled.php'); + +$root = "/srv/http/wittamore.fr/feeds"; +$cache = "/srv/http/wittamore.fr/.cache"; +$fs = "feed"; +$ts = "title"; +$dir = glob($root . '/*' , GLOB_ONLYDIR); + +$dt = new DateTime("now", new DateTimeZone('Europe/Paris')); +$curdir = str_replace($_SERVER["DOCUMENT_ROOT"],"",__DIR__); + +// build header +$header = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n"; +$header .= "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" >\n"; +$header .= "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" >\n"; +$header .= "<meta name=\"description\" content=\"my rss feeds\" >\n"; +$header .= "<link href=\"/style.css\" rel=\"stylesheet\" type=\"text/css\" >\n"; +$header .= "<link rel=\"icon\" href=\"/favicon.ico\" type=\"image/x-icon\" >\n"; +$header .= "<title>My feeds\n\n\n