nieuwsbrieven-drupal.inc.txt 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. Taak 128-A — Drupal: nieuwsbrief-abonnementen voor de app
  2. =========================================================
  3. DRIE INGREPEN op de server, daarna `drush @uitgaanskrant.com cc all`:
  4. 1) NIEUW BESTAND sites/all/modules/custom/custom.nieuwsbrieven.inc
  5. (zie blok A hieronder — het hele bestand)
  6. 2) custom.module regel 15, ONDER de bestaande module_load_include-regels:
  7. module_load_include('inc', 'custom', 'custom.nieuwsbrieven');
  8. 3) custom_services_resources(), vlak vóór 'favorieten' => array(
  9. (regel ~3398): het blok B hieronder invoegen.
  10. DAARNA — en dit is de stap die het vaakst vergeten wordt:
  11. /admin/structure/services/list/flutterdrup/resources
  12. → 'nieuwsbrieven' aanvinken ÉN de drie operaties eronder
  13. (index, subscribe, unsubscribe) → Save.
  14. Zonder die vinkjes wordt er geen route geregistreerd en krijg je 404,
  15. precies zoals mijn_aanmeldingen maandenlang deed.
  16. CONTROLE NA DEPLOY (anoniem, dus zonder sessie):
  17. curl -s -o /dev/null -w '%{http_code}\n' \
  18. 'https://uitgaanskrant.com/nl/flutterdrup/nieuwsbrieven.json'
  19. → moet 403 geven ("route bestaat, sessie ontbreekt"), NIET 404.
  20. ⚠️ NOOIT EDGE-CACHEN. Deze respons is per gebruiker verschillend. Het pad
  21. valt buiten de bestaande Cloudflare Cache Rules (die matchen op
  22. /flutterdrup/views/… en plaatsen), dus dat gaat vanzelf goed — voeg het
  23. er alleen nooit aan toe.
  24. BEVESTIGINGSMAIL — waarom die er voor de app NIET is
  25. -----------------------------------------------------
  26. De drie nieuwsbrieven staan op opt-in/out-methode **Double**. Simplenews
  27. omschrijft die stand zelf zo:
  28. "Double: When (un)subscribing at a subscription form, anonymous users
  29. receive an (un)subscription confirmation email. Authenticated users
  30. are (un)subscribed immediately."
  31. Bevestiging geldt dus alleen voor ANONIEME bezoekers. Een app-gebruiker is
  32. per definitie ingelogd en wordt direct (un)subscribed — net als op
  33. /user/<uid>/simplenews. Daarom staat `custom_nieuwsbrieven_confirm` op
  34. FALSE: zo doet de app exact hetzelfde als de website.
  35. Dit verklaart ook het cijfer dat eerder verdacht leek. Gemeten over de hele
  36. database (tid 1, een oude restterm, niet meegeteld):
  37. anoniem (uid 0) met account (uid > 0)
  38. status 1 0 45
  39. status 0 0 3
  40. status 2 450 0
  41. Precies de tweedeling die de Double-stand voorschrijft: anoniem gaat naar
  42. status 2 (wacht op de bevestigingslink), ingelogd gaat meteen naar 1. Geen
  43. kapotte mailroute. Wel blijft staan dat van die 450 anonieme inschrijvingen
  44. er in tien jaar nul zijn bevestigd — dat is een lage conversie op de
  45. publieke formulieren en misschien ooit een eigen kijkje waard, maar het
  46. raakt de app niet.
  47. WIL JE HET TOCH MET BEVESTIGING (dan blijft het abonnement op status 2 tot
  48. de gebruiker op de link klikt — de app toont die stand al):
  49. drush @uitgaanskrant.com vset custom_nieuwsbrieven_confirm 1
  50. Terug naar direct abonneren:
  51. drush @uitgaanskrant.com vdel custom_nieuwsbrieven_confirm
  52. GEDRAG IN HET KORT
  53. -------------------
  54. ABONNEREN -> direct actief (status 1), geen mail. Zie hierboven.
  55. UITSCHRIJVEN -> altijd direct, zonder mail. Ook met bevestiging aan:
  56. anders drukt de gebruiker in de app op "uit" en blijft
  57. hij abonnee tot hij een mail opent. Afmelden hoort
  58. makkelijker te zijn dan aanmelden, en "Double" schrijft
  59. voor ingelogde gebruikers sowieso direct uitschrijven voor.
  60. ⚠️ Deze instelling raakt ALLEEN de app. De website-pagina
  61. (/user/<uid>/simplenews) heeft confirm=FALSE hardcoded in simplenews zelf
  62. (includes/simplenews.subscription.inc regel 97); daar verandert niets aan.
  63. =====================================================================
  64. BLOK A — het complete bestand custom.nieuwsbrieven.inc
  65. =====================================================================
  66. ⬇ PLAK VANAF HIER ⬇
  67. <?php
  68. /**
  69. * @file
  70. * Nieuwsbrief-abonnementen (simplenews) voor de Flutter-app.
  71. *
  72. * Drie Services-resources onder het endpoint 'flutterdrup':
  73. * GET nieuwsbrieven.json - lijst + status van de ingelogde gebruiker
  74. * POST nieuwsbrieven/subscribe.json - {"tid": 36676}
  75. * POST nieuwsbrieven/unsubscribe.json - {"tid": 36676}
  76. *
  77. * Simplenews 7.x-1.1: nieuwsbrieven zijn taxonomy-termen in de vocabulary
  78. * 'newsletter'. Let op: variable_get('simplenews_vid') geeft op deze site 0,
  79. * dus leid de vocabulary daar NOOIT uit af — simplenews_category_get_visible()
  80. * is de betrouwbare ingang (en laat 'hidden' nieuwsbrieven automatisch weg).
  81. *
  82. * Statuscodes in {simplenews_subscription}.status:
  83. * 1 = geabonneerd, 0 = uitgeschreven, 2 = wacht op bevestiging.
  84. */
  85. /**
  86. * Rol die de ondernemersnieuwsbrief te zien krijgt.
  87. */
  88. define('CUSTOM_NIEUWSBRIEVEN_ROL_ONDERNEMER', 'Horeca-owner');
  89. /**
  90. * De nieuwsbrieven die deze gebruiker mag zien.
  91. *
  92. * De ondernemersnieuwsbrief is alleen zichtbaar voor de rol Horeca-owner.
  93. * Dit filter is met opzet server-side: de app heeft er geen conditie voor
  94. * nodig, en subscribe() gebruikt dezelfde lijst als whitelist, zodat een
  95. * handmatig verzoek zich er ook niet op kan abonneren.
  96. *
  97. * @param object $account
  98. * Volledig geladen user-object (user_load), want de globale $user hoeft
  99. * zijn rollen niet gevuld te hebben.
  100. *
  101. * @return array
  102. * Categorie-objecten, keyed op tid.
  103. */
  104. function _custom_nieuwsbrieven_zichtbaar($account) {
  105. $lijst = simplenews_category_get_visible();
  106. $ondernemers_tid = (int) variable_get('custom_nieuwsbrieven_ondernemers_tid', 18017);
  107. if ($ondernemers_tid && isset($lijst[$ondernemers_tid])) {
  108. $rollen = (is_object($account) && !empty($account->roles)) ? $account->roles : array();
  109. if (!in_array(CUSTOM_NIEUWSBRIEVEN_ROL_ONDERNEMER, $rollen, TRUE)) {
  110. unset($lijst[$ondernemers_tid]);
  111. }
  112. }
  113. return $lijst;
  114. }
  115. /**
  116. * Abonnementsstatus per tid, in één query.
  117. *
  118. * Bewust rechtstreeks op de database in plaats van via
  119. * simplenews_subscriber_load_by_mail(): die kent een static cache, en deze
  120. * functie wordt ook aangeroepen direct NA een wijziging.
  121. *
  122. * @return array
  123. * tid => status.
  124. */
  125. function _custom_nieuwsbrieven_statussen($mail) {
  126. if (!is_string($mail) || $mail === '') {
  127. return array();
  128. }
  129. return db_query('SELECT s.tid, s.status
  130. FROM {simplenews_subscription} s
  131. INNER JOIN {simplenews_subscriber} sub ON sub.snid = s.snid
  132. WHERE sub.mail = :mail', array(':mail' => $mail))->fetchAllKeyed();
  133. }
  134. /**
  135. * Eén rij van de respons, in dezelfde vorm voor index en subscribe.
  136. */
  137. function _custom_nieuwsbrieven_rij($tid, $category, $statussen) {
  138. $tid = (int) $tid;
  139. $status = isset($statussen[$tid]) ? (int) $statussen[$tid] : 0;
  140. $omschrijving = '';
  141. if (is_object($category) && property_exists($category, 'description')) {
  142. $omschrijving = trim(strip_tags((string) $category->description));
  143. }
  144. return array(
  145. 'tid' => (string) $tid,
  146. 'naam' => is_object($category) ? $category->name : '',
  147. 'omschrijving' => $omschrijving,
  148. // 1 = aan, 0 = uit, 2 = wacht op bevestiging. De app toont bij 2 een
  149. // regel "check je mail"; zonder dit onderscheid zou de switch bij de
  150. // volgende paginaload terugspringen naar uit en lijkt de app kapot.
  151. 'status' => $status,
  152. 'geabonneerd' => ($status === 1),
  153. );
  154. }
  155. /**
  156. * GET nieuwsbrieven.json
  157. */
  158. function _custom_nieuwsbrieven_index() {
  159. global $user;
  160. if (empty($user->uid)) {
  161. return services_error('Niet ingelogd', 401);
  162. }
  163. $account = user_load($user->uid);
  164. if (!$account || empty($account->mail)) {
  165. return services_error('Account zonder e-mailadres', 400);
  166. }
  167. $statussen = _custom_nieuwsbrieven_statussen($account->mail);
  168. $out = array();
  169. foreach (_custom_nieuwsbrieven_zichtbaar($account) as $tid => $category) {
  170. $out[] = _custom_nieuwsbrieven_rij($tid, $category, $statussen);
  171. }
  172. return $out;
  173. }
  174. /**
  175. * POST nieuwsbrieven/subscribe.json
  176. */
  177. function _custom_nieuwsbrieven_subscribe($tid) {
  178. return _custom_nieuwsbrieven_wijzig($tid, TRUE);
  179. }
  180. /**
  181. * POST nieuwsbrieven/unsubscribe.json
  182. */
  183. function _custom_nieuwsbrieven_unsubscribe($tid) {
  184. return _custom_nieuwsbrieven_wijzig($tid, FALSE);
  185. }
  186. /**
  187. * Gedeelde afhandeling van (un)subscribe.
  188. */
  189. function _custom_nieuwsbrieven_wijzig($tid, $aan) {
  190. global $user;
  191. if (empty($user->uid)) {
  192. return services_error('Niet ingelogd', 401);
  193. }
  194. $account = user_load($user->uid);
  195. if (!$account || empty($account->mail)) {
  196. return services_error('Account zonder e-mailadres', 400);
  197. }
  198. $tid = (int) $tid;
  199. $lijst = _custom_nieuwsbrieven_zichtbaar($account);
  200. // Whitelist. Zonder deze check kan een handmatig verzoek zich abonneren op
  201. // een 'hidden' nieuwsbrief of op de ondernemersnieuwsbrief zonder de rol.
  202. if (!isset($lijst[$tid])) {
  203. return services_error('Onbekende nieuwsbrief', 400);
  204. }
  205. $statussen = _custom_nieuwsbrieven_statussen($account->mail);
  206. $huidig = isset($statussen[$tid]) ? (int) $statussen[$tid] : 0;
  207. if ($aan) {
  208. // Al actief? Niets doen. Anders stuurt simplenews bij confirm=TRUE tóch
  209. // een mail ("je bent al ingeschreven") bij elke dubbele tik.
  210. if ($huidig !== 1) {
  211. // FALSE (default) = meteen actief, geen mail. Dit is wat de
  212. // opt-in/out-methode "Double" voorschrijft: anonieme bezoekers
  213. // krijgen een bevestigingsmail, ingelogde gebruikers worden
  214. // direct (un)subscribed. Een app-gebruiker is altijd ingelogd.
  215. // TRUE = bevestigingsmail; het abonnement blijft dan op status 2
  216. // tot de gebruiker op de link klikt.
  217. $confirm = (bool) variable_get('custom_nieuwsbrieven_confirm', FALSE);
  218. simplenews_subscribe_user($account->mail, $tid, $confirm, 'app');
  219. }
  220. }
  221. else {
  222. // Uitschrijven gaat altijd direct: met een bevestigingsmail zou de
  223. // gebruiker in de app op 'uit' drukken en toch abonnee blijven.
  224. if ($huidig !== 0) {
  225. simplenews_unsubscribe_user($account->mail, $tid, FALSE, 'app');
  226. }
  227. }
  228. // Verse status ophalen, zodat de app meteen de echte stand toont.
  229. $statussen = _custom_nieuwsbrieven_statussen($account->mail);
  230. return _custom_nieuwsbrieven_rij($tid, $lijst[$tid], $statussen);
  231. }
  232. ⬆ TOT HIER ⬆
  233. =====================================================================
  234. BLOK B — invoegen in custom_services_resources() in custom.module
  235. =====================================================================
  236. Vlak vóór de regel 'favorieten' => array( (regel ~3398).
  237. ⬇ PLAK VANAF HIER ⬇
  238. 'nieuwsbrieven' => array(
  239. 'operations' => array(
  240. 'index' => array(
  241. 'help' => 'Nieuwsbrieven met de abonnementsstatus van de ingelogde gebruiker.',
  242. 'callback' => '_custom_nieuwsbrieven_index',
  243. 'access callback' => 'user_is_logged_in',
  244. 'access arguments' => array(),
  245. 'access arguments append' => FALSE,
  246. 'args' => array(),
  247. ),
  248. ),
  249. 'actions' => array(
  250. 'subscribe' => array(
  251. 'help' => 'Abonneer de ingelogde gebruiker op een nieuwsbrief.',
  252. 'callback' => '_custom_nieuwsbrieven_subscribe',
  253. 'access callback' => 'user_is_logged_in',
  254. 'access arguments' => array(),
  255. 'access arguments append' => FALSE,
  256. 'args' => array(
  257. array(
  258. 'name' => 'tid',
  259. 'type' => 'int',
  260. 'description' => 'Term-id van de nieuwsbrief.',
  261. 'source' => array('data' => 'tid'),
  262. 'optional' => FALSE,
  263. ),
  264. ),
  265. ),
  266. 'unsubscribe' => array(
  267. 'help' => 'Schrijf de ingelogde gebruiker uit voor een nieuwsbrief.',
  268. 'callback' => '_custom_nieuwsbrieven_unsubscribe',
  269. 'access callback' => 'user_is_logged_in',
  270. 'access arguments' => array(),
  271. 'access arguments append' => FALSE,
  272. 'args' => array(
  273. array(
  274. 'name' => 'tid',
  275. 'type' => 'int',
  276. 'description' => 'Term-id van de nieuwsbrief.',
  277. 'source' => array('data' => 'tid'),
  278. 'optional' => FALSE,
  279. ),
  280. ),
  281. ),
  282. ),
  283. ),
  284. ⬆ TOT HIER ⬆
  285. =====================================================================
  286. Voorbeeldrespons
  287. =====================================================================
  288. GET nieuwsbrieven.json — gewone gebruiker (geen Horeca-owner):
  289. [
  290. {"tid":"36667","naam":"Uitgaanskrant voor bezoekers van de horeca",
  291. "omschrijving":"Uitgaanskrant voor bezoekers van de horeca",
  292. "status":0,"geabonneerd":false},
  293. {"tid":"36676","naam":"Uitgaanskrant.com wekelijkse uitgaansagenda",
  294. "omschrijving":"Wekelijkse agenda op basis van je favoriete gemeenten. …",
  295. "status":1,"geabonneerd":true}
  296. ]
  297. Een Horeca-owner krijgt daar tid 18017 bij.
  298. POST subscribe/unsubscribe geven exact één zo'n rij terug, met de nieuwe
  299. status — de app kan de switch daar direct op zetten.
  300. ⚠️ Bind in FlutterFlow pas op $.status nadat "Test API Call" een gevulde
  301. respons heeft opgeleverd; anders krijgt het pad type "Anything" en plakt de
  302. builder er .toString() achter.