|
|
@@ -0,0 +1,399 @@
|
|
|
+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.
|