| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- Taak 128-A — Drupal: nieuwsbrief-abonnementen voor de app
- =========================================================
- DRIE INGREPEN op de server, daarna `drush @uitgaanskrant.com cc all`:
- 1) NIEUW BESTAND sites/all/modules/custom/custom.nieuwsbrieven.inc
- (zie blok A hieronder — het hele bestand)
- 2) custom.module regel 15, ONDER de bestaande module_load_include-regels:
- module_load_include('inc', 'custom', 'custom.nieuwsbrieven');
- 3) custom_services_resources(), vlak vóór 'favorieten' => array(
- (regel ~3398): het blok B hieronder invoegen.
- DAARNA — en dit is de stap die het vaakst vergeten wordt:
- /admin/structure/services/list/flutterdrup/resources
- → 'nieuwsbrieven' aanvinken ÉN de drie operaties eronder
- (index, subscribe, unsubscribe) → Save.
- Zonder die vinkjes wordt er geen route geregistreerd en krijg je 404,
- precies zoals mijn_aanmeldingen maandenlang deed.
- CONTROLE NA DEPLOY (anoniem, dus zonder sessie):
- curl -s -o /dev/null -w '%{http_code}\n' \
- 'https://uitgaanskrant.com/nl/flutterdrup/nieuwsbrieven.json'
- → moet 403 geven ("route bestaat, sessie ontbreekt"), NIET 404.
- ⚠️ NOOIT EDGE-CACHEN. Deze respons is per gebruiker verschillend. Het pad
- valt buiten de bestaande Cloudflare Cache Rules (die matchen op
- /flutterdrup/views/… en plaatsen), dus dat gaat vanzelf goed — voeg het
- er alleen nooit aan toe.
- ⚠️⚠️ TEST EERST OF DIE BEVESTIGINGSMAIL AANKOMT — er is reden tot twijfel
- ------------------------------------------------------------------------
- Gemeten 2026-09-21 over de hele database (tid 1, een oude restterm, niet
- meegeteld):
- anoniem (uid 0) met account (uid > 0)
- status 1 0 45
- status 0 0 3
- status 2 450 0
- Dus: van de 450 anonieme inschrijvingen via de publieke formulieren is er
- in TIEN JAAR (2015-12 t/m 2025-12, gestaag verspreid — geen eenmalige
- import) GEEN ENKELE ooit bevestigd. Bij een werkende bevestigingsmail zou
- je 30-70% verwachten, niet 0%.
- De inschrijvingen mét account staan allemaal meteen goed, en dat is ook
- logisch: die liepen via /user/<uid>/simplenews, en dat pad gebruikt
- confirm=FALSE en stuurt dus nooit een mail. Met andere woorden: het enige
- pad dat wél een bevestigingsmail stuurt, is ook precies het pad waar nooit
- iemand doorheen komt.
- Dat kan twee dingen betekenen: de mail wordt niet bezorgd, of de
- bevestigingslink werkt niet. De mailconfiguratie zelf oogt in orde
- (smtp_on = 1, mailsystem delegeert naar mimemail, geen reroute_email,
- afzender contact@uitgaanskrant.com) en de watchdog bevat geen mailfouten —
- maar die is grotendeels leeg, dus dat bewijst niets.
- WAAROM DIT ERTOE DOET: zet je bevestiging aan voor de app zonder dit te
- testen, dan zet de gebruiker de switch aan, gebeurt er verder niets, en
- blijft het abonnement voor eeuwig op status 2 staan. Precies wat die 450
- nu doen.
- TEST (schrijft wel, en verstuurt een echte mail — daarom niet door Claude
- gedraaid):
- # 1. abonneer jezelf MET bevestiging op de bezoekersnieuwsbrief
- drush @uitgaanskrant.com php-eval \
- "simplenews_subscribe_user('JOUW@MAIL.NL', 36667, TRUE, 'test');"
- # 2. check de status — hoort 2 te zijn
- drush @uitgaanskrant.com sql-query \
- "SELECT s.tid, s.status FROM simplenews_subscription s
- INNER JOIN simplenews_subscriber sub ON sub.snid=s.snid
- WHERE sub.mail='JOUW@MAIL.NL';"
- # 3. komt de mail aan? klik de link. Daarna moet stap 2 status 1 geven.
- # 4. opruimen
- drush @uitgaanskrant.com php-eval \
- "simplenews_unsubscribe_user('JOUW@MAIL.NL', 36667, FALSE, 'test');"
- Komt de mail NIET aan, dan is dat eerst een mailprobleem om op te lossen —
- en zolang dat niet opgelost is, is `custom_nieuwsbrieven_confirm` op 0
- (direct abonneren, geen mail) de enige variant die voor de gebruiker
- werkt. De code hieronder ondersteunt beide zonder wijziging.
- BEVESTIGINGSMAIL — hoe je dit aanpast
- --------------------------------------
- Staat AAN (Bob 2026-09-21). Concreet gedrag:
- ABONNEREN -> simplenews zet het abonnement op status 2 (onbevestigd)
- en mailt een link. Pas na het klikken wordt het status 1
- en ontvangt de gebruiker de nieuwsbrief.
- UITSCHRIJVEN-> gaat ALTIJD direct, zonder mail. Bewust: met bevestiging
- zou de gebruiker in de app op 'uit' drukken en tóch
- abonnee blijven tot hij een mail opent. Dat is slechte UX
- en juridisch precies de verkeerde kant op — afmelden moet
- makkelijker zijn dan aanmelden.
- Omzetten naar 'direct abonneren, geen mail' kan zonder codewijziging:
- drush @uitgaanskrant.com vset custom_nieuwsbrieven_confirm 0
- Terug naar bevestiging:
- drush @uitgaanskrant.com vdel custom_nieuwsbrieven_confirm
- De TEKST van die bevestigingsmail is al Nederlands en staat in variabelen,
- te bewerken op /admin/config/services/simplenews (tab Subscription):
- simplenews_confirm_subscribe_subject
- "Bevestiging voor [simplenews-category:name] van [site:name]"
- simplenews_confirm_subscribe_unsubscribed
- "Wij hebben een verzoek ontvangen om [simplenews-subscriber:mail] in
- te schrijven op de [simplenews-category:name] nieuwsbrief …
- [simplenews-subscriber:subscribe-url]"
- De tekst noemt nu "de website op [site:url]" — dat klopt straks niet meer
- voor een aanmelding vanuit de app. Overweeg een neutralere formulering.
- ⚠️ Deze instelling raakt ALLEEN de app. De website-pagina uit je screenshot
- (/user/<uid>/simplenews) heeft confirm=FALSE HARDCODED in simplenews zelf
- (includes/simplenews.subscription.inc regel 97) en stuurt dus nog steeds
- geen mail. Wil je dat gelijktrekken, dan is dat een aparte ingreep
- (hook_form_alter die dat submit-handler vervangt) — niet in deze patch.
- =====================================================================
- BLOK A — het complete bestand custom.nieuwsbrieven.inc
- =====================================================================
- ⬇ PLAK VANAF HIER ⬇
- <?php
- /**
- * @file
- * Nieuwsbrief-abonnementen (simplenews) voor de Flutter-app.
- *
- * Drie Services-resources onder het endpoint 'flutterdrup':
- * GET nieuwsbrieven.json - lijst + status van de ingelogde gebruiker
- * POST nieuwsbrieven/subscribe.json - {"tid": 36676}
- * POST nieuwsbrieven/unsubscribe.json - {"tid": 36676}
- *
- * Simplenews 7.x-1.1: nieuwsbrieven zijn taxonomy-termen in de vocabulary
- * 'newsletter'. Let op: variable_get('simplenews_vid') geeft op deze site 0,
- * dus leid de vocabulary daar NOOIT uit af — simplenews_category_get_visible()
- * is de betrouwbare ingang (en laat 'hidden' nieuwsbrieven automatisch weg).
- *
- * Statuscodes in {simplenews_subscription}.status:
- * 1 = geabonneerd, 0 = uitgeschreven, 2 = wacht op bevestiging.
- */
- /**
- * Rol die de ondernemersnieuwsbrief te zien krijgt.
- */
- define('CUSTOM_NIEUWSBRIEVEN_ROL_ONDERNEMER', 'Horeca-owner');
- /**
- * De nieuwsbrieven die deze gebruiker mag zien.
- *
- * De ondernemersnieuwsbrief is alleen zichtbaar voor de rol Horeca-owner.
- * Dit filter is met opzet server-side: de app heeft er geen conditie voor
- * nodig, en subscribe() gebruikt dezelfde lijst als whitelist, zodat een
- * handmatig verzoek zich er ook niet op kan abonneren.
- *
- * @param object $account
- * Volledig geladen user-object (user_load), want de globale $user hoeft
- * zijn rollen niet gevuld te hebben.
- *
- * @return array
- * Categorie-objecten, keyed op tid.
- */
- function _custom_nieuwsbrieven_zichtbaar($account) {
- $lijst = simplenews_category_get_visible();
- $ondernemers_tid = (int) variable_get('custom_nieuwsbrieven_ondernemers_tid', 18017);
- if ($ondernemers_tid && isset($lijst[$ondernemers_tid])) {
- $rollen = (is_object($account) && !empty($account->roles)) ? $account->roles : array();
- if (!in_array(CUSTOM_NIEUWSBRIEVEN_ROL_ONDERNEMER, $rollen, TRUE)) {
- unset($lijst[$ondernemers_tid]);
- }
- }
- return $lijst;
- }
- /**
- * Abonnementsstatus per tid, in één query.
- *
- * Bewust rechtstreeks op de database in plaats van via
- * simplenews_subscriber_load_by_mail(): die kent een static cache, en deze
- * functie wordt ook aangeroepen direct NA een wijziging.
- *
- * @return array
- * tid => status.
- */
- function _custom_nieuwsbrieven_statussen($mail) {
- if (!is_string($mail) || $mail === '') {
- return array();
- }
- return db_query('SELECT s.tid, s.status
- FROM {simplenews_subscription} s
- INNER JOIN {simplenews_subscriber} sub ON sub.snid = s.snid
- WHERE sub.mail = :mail', array(':mail' => $mail))->fetchAllKeyed();
- }
- /**
- * Eén rij van de respons, in dezelfde vorm voor index en subscribe.
- */
- function _custom_nieuwsbrieven_rij($tid, $category, $statussen) {
- $tid = (int) $tid;
- $status = isset($statussen[$tid]) ? (int) $statussen[$tid] : 0;
- $omschrijving = '';
- if (is_object($category) && property_exists($category, 'description')) {
- $omschrijving = trim(strip_tags((string) $category->description));
- }
- return array(
- 'tid' => (string) $tid,
- 'naam' => is_object($category) ? $category->name : '',
- 'omschrijving' => $omschrijving,
- // 1 = aan, 0 = uit, 2 = wacht op bevestiging. De app toont bij 2 een
- // regel "check je mail"; zonder dit onderscheid zou de switch bij de
- // volgende paginaload terugspringen naar uit en lijkt de app kapot.
- 'status' => $status,
- 'geabonneerd' => ($status === 1),
- );
- }
- /**
- * GET nieuwsbrieven.json
- */
- function _custom_nieuwsbrieven_index() {
- global $user;
- if (empty($user->uid)) {
- return services_error('Niet ingelogd', 401);
- }
- $account = user_load($user->uid);
- if (!$account || empty($account->mail)) {
- return services_error('Account zonder e-mailadres', 400);
- }
- $statussen = _custom_nieuwsbrieven_statussen($account->mail);
- $out = array();
- foreach (_custom_nieuwsbrieven_zichtbaar($account) as $tid => $category) {
- $out[] = _custom_nieuwsbrieven_rij($tid, $category, $statussen);
- }
- return $out;
- }
- /**
- * POST nieuwsbrieven/subscribe.json
- */
- function _custom_nieuwsbrieven_subscribe($tid) {
- return _custom_nieuwsbrieven_wijzig($tid, TRUE);
- }
- /**
- * POST nieuwsbrieven/unsubscribe.json
- */
- function _custom_nieuwsbrieven_unsubscribe($tid) {
- return _custom_nieuwsbrieven_wijzig($tid, FALSE);
- }
- /**
- * Gedeelde afhandeling van (un)subscribe.
- */
- function _custom_nieuwsbrieven_wijzig($tid, $aan) {
- global $user;
- if (empty($user->uid)) {
- return services_error('Niet ingelogd', 401);
- }
- $account = user_load($user->uid);
- if (!$account || empty($account->mail)) {
- return services_error('Account zonder e-mailadres', 400);
- }
- $tid = (int) $tid;
- $lijst = _custom_nieuwsbrieven_zichtbaar($account);
- // Whitelist. Zonder deze check kan een handmatig verzoek zich abonneren op
- // een 'hidden' nieuwsbrief of op de ondernemersnieuwsbrief zonder de rol.
- if (!isset($lijst[$tid])) {
- return services_error('Onbekende nieuwsbrief', 400);
- }
- $statussen = _custom_nieuwsbrieven_statussen($account->mail);
- $huidig = isset($statussen[$tid]) ? (int) $statussen[$tid] : 0;
- if ($aan) {
- // Al actief? Niets doen. Anders stuurt simplenews bij confirm=TRUE tóch
- // een mail ("je bent al ingeschreven") bij elke dubbele tik.
- if ($huidig !== 1) {
- // TRUE = bevestigingsmail, abonnement blijft op status 2 tot de
- // gebruiker op de link klikt.
- // FALSE = meteen actief, geen mail (wat de website zelf doet voor
- // ingelogde gebruikers).
- $confirm = (bool) variable_get('custom_nieuwsbrieven_confirm', TRUE);
- simplenews_subscribe_user($account->mail, $tid, $confirm, 'app');
- }
- }
- else {
- // Uitschrijven gaat altijd direct: met een bevestigingsmail zou de
- // gebruiker in de app op 'uit' drukken en toch abonnee blijven.
- if ($huidig !== 0) {
- simplenews_unsubscribe_user($account->mail, $tid, FALSE, 'app');
- }
- }
- // Verse status ophalen, zodat de app meteen de echte stand toont.
- $statussen = _custom_nieuwsbrieven_statussen($account->mail);
- return _custom_nieuwsbrieven_rij($tid, $lijst[$tid], $statussen);
- }
- ⬆ TOT HIER ⬆
- =====================================================================
- BLOK B — invoegen in custom_services_resources() in custom.module
- =====================================================================
- Vlak vóór de regel 'favorieten' => array( (regel ~3398).
- ⬇ PLAK VANAF HIER ⬇
- 'nieuwsbrieven' => array(
- 'operations' => array(
- 'index' => array(
- 'help' => 'Nieuwsbrieven met de abonnementsstatus van de ingelogde gebruiker.',
- 'callback' => '_custom_nieuwsbrieven_index',
- 'access callback' => 'user_is_logged_in',
- 'access arguments' => array(),
- 'access arguments append' => FALSE,
- 'args' => array(),
- ),
- ),
- 'actions' => array(
- 'subscribe' => array(
- 'help' => 'Abonneer de ingelogde gebruiker op een nieuwsbrief.',
- 'callback' => '_custom_nieuwsbrieven_subscribe',
- 'access callback' => 'user_is_logged_in',
- 'access arguments' => array(),
- 'access arguments append' => FALSE,
- 'args' => array(
- array(
- 'name' => 'tid',
- 'type' => 'int',
- 'description' => 'Term-id van de nieuwsbrief.',
- 'source' => array('data' => 'tid'),
- 'optional' => FALSE,
- ),
- ),
- ),
- 'unsubscribe' => array(
- 'help' => 'Schrijf de ingelogde gebruiker uit voor een nieuwsbrief.',
- 'callback' => '_custom_nieuwsbrieven_unsubscribe',
- 'access callback' => 'user_is_logged_in',
- 'access arguments' => array(),
- 'access arguments append' => FALSE,
- 'args' => array(
- array(
- 'name' => 'tid',
- 'type' => 'int',
- 'description' => 'Term-id van de nieuwsbrief.',
- 'source' => array('data' => 'tid'),
- 'optional' => FALSE,
- ),
- ),
- ),
- ),
- ),
- ⬆ TOT HIER ⬆
- =====================================================================
- Voorbeeldrespons
- =====================================================================
- GET nieuwsbrieven.json — gewone gebruiker (geen Horeca-owner):
- [
- {"tid":"36667","naam":"Uitgaanskrant voor bezoekers van de horeca",
- "omschrijving":"Uitgaanskrant voor bezoekers van de horeca",
- "status":0,"geabonneerd":false},
- {"tid":"36676","naam":"Uitgaanskrant.com wekelijkse uitgaansagenda",
- "omschrijving":"Wekelijkse agenda op basis van je favoriete gemeenten. …",
- "status":1,"geabonneerd":true}
- ]
- Een Horeca-owner krijgt daar tid 18017 bij.
- POST subscribe/unsubscribe geven exact één zo'n rij terug, met de nieuwe
- status — de app kan de switch daar direct op zetten.
- ⚠️ Bind in FlutterFlow pas op $.status nadat "Test API Call" een gevulde
- respons heeft opgeleverd; anders krijgt het pad type "Anything" en plakt de
- builder er .toString() achter.
|