| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376 |
- 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.
- BEVESTIGINGSMAIL — waarom die er voor de app NIET is
- -----------------------------------------------------
- De drie nieuwsbrieven staan op opt-in/out-methode **Double**. Simplenews
- omschrijft die stand zelf zo:
- "Double: When (un)subscribing at a subscription form, anonymous users
- receive an (un)subscription confirmation email. Authenticated users
- are (un)subscribed immediately."
- Bevestiging geldt dus alleen voor ANONIEME bezoekers. Een app-gebruiker is
- per definitie ingelogd en wordt direct (un)subscribed — net als op
- /user/<uid>/simplenews.
- ⚠️ Simplenews past die regel NIET zelf toe. simplenews_subscribe_user()
- bevat geen enkele check op de ingelogde gebruiker; hij doet blind wat de
- $confirm-parameter zegt. De formulieren bepalen die waarde, met
- simplenews_require_double_opt_in($tid, $account) — FALSE zodra het
- mailadres van de ingelogde gebruiker zelf is, anders de opt-in-methode.
- Deze resource roept precies diezelfde functie aan, dus de app volgt de
- website automatisch, ook als de opt-in-instelling ooit wijzigt. Je hoeft
- er geen variabele voor te zetten.
- Dit verklaart ook het cijfer dat eerder verdacht leek. Gemeten 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
- Precies de tweedeling die de Double-stand voorschrijft: anoniem gaat naar
- status 2 (wacht op de bevestigingslink), ingelogd gaat meteen naar 1. Geen
- kapotte mailroute. Wel blijft staan dat van die 450 anonieme inschrijvingen
- er in tien jaar nul zijn bevestigd — dat is een lage conversie op de
- publieke formulieren en misschien ooit een eigen kijkje waard, maar het
- raakt de app niet.
- NOODREM (normaal niet nodig): custom_nieuwsbrieven_confirm overrulet de
- functie hierboven. Niet gezet = automatisch, 1 = altijd bevestigingsmail,
- 0 = nooit.
- drush @uitgaanskrant.com vset custom_nieuwsbrieven_confirm 1
- drush @uitgaanskrant.com vdel custom_nieuwsbrieven_confirm
- GEDRAG IN HET KORT
- -------------------
- ABONNEREN -> direct actief (status 1), geen mail. Zie hierboven.
- UITSCHRIJVEN -> altijd direct, zonder mail. Ook met bevestiging aan:
- anders drukt de gebruiker in de app op "uit" en blijft
- hij abonnee tot hij een mail opent. Afmelden hoort
- makkelijker te zijn dan aanmelden, en "Double" schrijft
- voor ingelogde gebruikers sowieso direct uitschrijven voor.
- ⚠️ Deze instelling raakt ALLEEN de app. De website-pagina
- (/user/<uid>/simplenews) heeft confirm=FALSE hardcoded in simplenews zelf
- (includes/simplenews.subscription.inc regel 97); daar verandert niets aan.
- =====================================================================
- 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) {
- // Wel of geen bevestigingsmail? Niet zelf beslissen, maar exact
- // dezelfde regel volgen als het websiteformulier: dat roept
- // simplenews_require_double_opt_in() aan. Die geeft FALSE zodra het
- // mailadres van de ingelogde gebruiker zelf is, en valt anders terug
- // op de opt-in/out-methode van de nieuwsbrief. Een app-gebruiker is
- // per definitie ingelogd, dus dit levert hier altijd FALSE op:
- // meteen actief, geen mail. Verandert de opt-in-instelling ooit, dan
- // volgt de app vanzelf mee.
- //
- // Let op: simplenews_subscribe_user() kijkt NIET zelf of iemand
- // ingelogd is -- hij doet blind wat deze parameter zegt. De
- // "Double"-stand wordt dus door de aanroeper toegepast, niet door
- // simplenews.
- $confirm = variable_get('custom_nieuwsbrieven_confirm', NULL);
- if ($confirm === NULL) {
- $confirm = simplenews_require_double_opt_in($tid, $account);
- }
- simplenews_subscribe_user($account->mail, $tid, (bool) $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.
|