SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Controllers/ContentSubscribeController.php

  1. <?php
  2. /*
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  */
  13.  
  14. namespace Sprout\Controllers;
  15.  
  16. use Exception;
  17. use InvalidArgumentException;
  18.  
  19. use karmabunny\pdb\Exceptions\RowMissingException;
  20. use Sprout\Helpers\AdminAuth;
  21. use Sprout\Helpers\Cron;
  22. use Sprout\Helpers\Email;
  23. use Sprout\Helpers\Enc;
  24. use Sprout\Helpers\Notification;
  25. use Sprout\Helpers\Pdb;
  26. use Sprout\Helpers\Sprout;
  27. use Sprout\Helpers\SubsiteSelector;
  28. use Sprout\Helpers\Subsites;
  29. use Sprout\Helpers\Url;
  30. use Sprout\Helpers\View;
  31.  
  32.  
  33. /**
  34.  * Handles subscriptions to various types of content in a centralised manner
  35.  *
  36.  * Subscribers are emailed regarding new/updated content as various subscription handlers see fit
  37.  */
  38. class ContentSubscribeController extends Controller
  39. {
  40.  
  41. public function __construct()
  42. {
  43. parent::__construct();
  44. }
  45.  
  46.  
  47. /**
  48.   * Unsubscribe a user from a subscription (form)
  49.   **/
  50. public function unsub($id, $code)
  51. {
  52. $id = (int) $id;
  53. $code = trim($code);
  54.  
  55. // Check the id and code match
  56. $q = "SELECT email
  57. FROM ~content_subscriptions
  58. WHERE id = ? AND code = ?
  59. LIMIT 1";
  60. try {
  61. $email = Pdb::q($q, [$id, $code], 'val');
  62. } catch (RowMissingException $ex) {
  63. Notification::error('Invalid id or unsubscription code');
  64. Url::redirect('result/error');
  65. }
  66.  
  67. // Fetch all of the subscriptions for this email address.
  68. // Codes are derived from the email address so they'll all be the same.
  69. $q = "SELECT id, handler_class, handler_settings
  70. FROM ~content_subscriptions
  71. WHERE code = ? AND email = ?
  72. ORDER BY id";
  73. $res = Pdb::q($q, [$code, $email], 'arr');
  74.  
  75. $subs = array();
  76. foreach ($res as $row) {
  77. // Create instance
  78. $inst = Sprout::instance($row['handler_class'], 'Sprout\\Helpers\\Subscribe');
  79.  
  80. // Load settings
  81. $settings = json_decode($row['handler_settings'], true);
  82. if (!is_array($settings)) $settings = [];
  83.  
  84. // Get the name
  85. $result = $inst->getName($settings);
  86. if (! $result) continue;
  87.  
  88. $subs[$row['id']] = $result;
  89. }
  90.  
  91. $view = new View('sprout/content_unsubscribe_form');
  92. $view->subscriptions = $subs;
  93. $view->id = $id;
  94. $view->code = $code;
  95.  
  96. $page_view = new View('skin/inner');
  97. $page_view->page_title = 'Unsubscribe';
  98. $page_view->main_content = $view;
  99. $page_view->controller_name = $this->getCssClassName();
  100. echo $page_view->render();
  101. }
  102.  
  103.  
  104. /**
  105.   * Unsubscribe a user from a subscription (action)
  106.   **/
  107. public function unsubAction($id, $code)
  108. {
  109. $id = (int) $id;
  110. $code = trim($code);
  111.  
  112. // Check the id and code match
  113. $q = "SELECT email
  114. FROM ~content_subscriptions
  115. WHERE id = ? AND code = ?
  116. LIMIT 1";
  117. try {
  118. $email = Pdb::q($q, [$id, $code], 'row');
  119. } catch (RowMissingException $ex) {
  120. Notification::error('Invalid id or unsubscription code');
  121. Url::redirect('result/error');
  122. }
  123.  
  124. // Did they actually choose anything?
  125. if (empty($_POST['unsubscribe'])) {
  126. Notification::error('You didn\'t select anything');
  127. Url::redirect("content_subscribe/unsub/{$id}/{$code}");
  128. }
  129.  
  130. // Fetch the IDs of all of the subscriptions for this email address.
  131. // Codes are derived from the email address so they'll all be the same.
  132. $q = "SELECT id
  133. FROM ~content_subscriptions
  134. WHERE code = ? AND email = ?";
  135. $ids = Pdb::q($q, [$code, $email], 'col');
  136.  
  137. // Now we can iterate over the unsubscriptions, and remove them
  138. foreach ($_POST['unsubscribe'] as $id) {
  139. if (! in_array($id, $ids)) continue;
  140.  
  141. Pdb::delete('content_subscriptions', ['id' => $id]);
  142. }
  143.  
  144. Notification::confirm('You have been unsubscribed');
  145. Url::redirect('result/error');
  146. }
  147.  
  148.  
  149. /**
  150.   * Send the content subscription emails to the users who have registered
  151.   **/
  152. public function cronSendSubscriptions()
  153. {
  154. Cron::start("Content subscribe");
  155.  
  156. Cron::message('Fetching subscriptions');
  157.  
  158. $q = 'SELECT id, content_id, name, code, mobile FROM ~subsites';
  159. $subsites = Pdb::query($q, [], 'arr');
  160.  
  161. foreach ($subsites as $subsite) {
  162. Cron::message('');
  163. Cron::message('');
  164. Cron::message('Subscriptions for subsite: ' . $subsite['name']);
  165.  
  166. // Fake the subsite selection so that the subscription handlers and email sending all behaves correctly
  167. SubsiteSelector::$subsite_id = $subsite['id'];
  168. SubsiteSelector::$content_id = $subsite['content_id'] ?: $subsite['id'];
  169. SubsiteSelector::$subsite_code = $subsite['code'];
  170. SubsiteSelector::$mobile = $subsite['mobile'];
  171.  
  172. // Get the records
  173. $q = "SELECT id, code, handler_class, handler_settings, name, email, subsite_id
  174. FROM ~content_subscriptions
  175. WHERE subsite_id = ?
  176. ORDER BY id";
  177. $res = Pdb::q($q, [$subsite['id']], 'pdo');
  178.  
  179. // TODO: should this be per subscription or should be store when it last ran?
  180. $since = time() - 86400;
  181.  
  182. Cron::message('Loading lists');
  183.  
  184. // Get a unique list of class/settings
  185. $lists = array();
  186. $users = array();
  187. foreach ($res as $row) {
  188.  
  189. if (!isset($users[$row['email']])) {
  190. $users[$row['email']] = array(
  191. 'id' => $row['id'],
  192. 'code' => $row['code'],
  193. 'name' => $row['name'],
  194. 'subs' => array()
  195. );
  196. }
  197.  
  198. $users[$row['email']]['subs'][] = md5($row['handler_class'] . '.' . $row['handler_settings']);
  199.  
  200. $key = md5($row['handler_class'] . '.' . $row['handler_settings']);
  201. if (isset($lists[$key])) continue;
  202.  
  203. $class = $row['handler_class'];
  204.  
  205. // Create class instance
  206. try {
  207. $inst = Sprout::instance($class, 'Sprout\\Helpers\\Subscribe');
  208. } catch (InvalidArgumentException $ex) {
  209. Cron::message(" Loading '{$class}' failed: {$ex->getMessage()}");
  210. continue;
  211. }
  212.  
  213. // Load settings
  214. $settings = json_decode($row['handler_settings'], true);
  215. if (!is_array($settings)) $settings = [];
  216.  
  217. // Run method to fetch items
  218. try {
  219. $result = $inst->getList($settings, $since);
  220. if (! is_array($result)) throw new Exception("Returned result is not an array");
  221.  
  222. } catch (Exception $ex) {
  223. Cron::message(" Loading '{$class}' failed: {$ex->getMessage()}");
  224. continue;
  225. }
  226.  
  227. Cron::message(" Loaded '{$class}'; Key '{$key}'; Num rows: " . count($result));
  228.  
  229. // Set the URL as the key for each item
  230. // This prevents duplicates when the items are sent out
  231. $uniq_result = array();
  232. foreach ($result as $row) {
  233. $uniq_result[$row['url']] = $row;
  234. }
  235.  
  236. // Save result
  237. $lists[$key] = $uniq_result;
  238. }
  239.  
  240. Cron::message('');
  241. Cron::message('Building user lists');
  242.  
  243. $res->closeCursor();
  244.  
  245. Cron::message('Num users: ' . count($users));
  246. Cron::message('');
  247.  
  248. // For each user, build, sort and send out the lists
  249. $none = 0;
  250. $success = 0;
  251. $failure = 0;
  252. foreach ($users as $email => $deets) {
  253. $items = array();
  254. foreach ($deets['subs'] as $listkey) {
  255. if ($lists[$listkey]) {
  256. $items = array_merge($items, $lists[$listkey]);
  257. }
  258. }
  259.  
  260. if (count($items) == 0) {
  261. $none++;
  262. continue;
  263. }
  264.  
  265. usort($items, 'Sprout\\Helpers\\ContentSubscribe::tsSort');
  266.  
  267. foreach ($items as &$row) {
  268. if ($row['url'][0] == '/') {
  269. $row['url'] = Subsites::getAbsRoot($subsite['id']) . ltrim($row['url'], '/');
  270. }
  271. }
  272.  
  273. $subsite_title = Subsites::getConfig('site_title', $subsite['id']);
  274.  
  275. $view = new View('sprout/email/content_subscribe');
  276. $view->unsubscribe_url = Subsites::getAbsRoot($subsite['id']) . "content_subscribe/unsub/{$deets['id']}/{$deets['code']}";
  277. $view->name = $deets['name'];
  278. $view->email = $email;
  279. $view->items = $items;
  280. $view->subsite_title = $subsite_title;
  281.  
  282. $mail = new Email();
  283. $mail->AddAddress($email);
  284. $mail->Subject = 'Updates on the ' . $subsite_title . ' website';
  285. $mail->SkinnedHTML($view->render());
  286. $result = $mail->Send();
  287.  
  288. Cron::message($email . ($result ? '; success' : '; failure'));
  289.  
  290. if ($result) { $success++; } else { $failure++; }
  291. }
  292. }
  293.  
  294. if (!$success and $failure) {
  295. Cron::failure('All emails we tried to send failed; is there a server config error?');
  296. }
  297.  
  298. if ($success or $failure) {
  299. Cron::message('');
  300. }
  301.  
  302. Cron::message('');
  303. Cron::message('No items: ' . $none);
  304. Cron::message('Success: ' . $success);
  305. Cron::message('Failed: ' . $failure);
  306.  
  307. Cron::success();
  308. }
  309.  
  310.  
  311. /**
  312.   * Tool to clean up subscriptions which refer to classes that don't exist.
  313.   **/
  314. public function cleanupInvalidClasses()
  315. {
  316. AdminAuth::checkLogin();
  317.  
  318. // Get the records
  319. $q = "SELECT handler_class
  320. FROM ~content_subscriptions
  321. GROUP BY handler_class
  322. ORDER BY handler_class";
  323. $res = Pdb::q($q, [], 'arr');
  324.  
  325. echo '<pre>';
  326.  
  327. if (! $_GET['delete']) {
  328. echo 'Not deleting, use GET param delete=1 to delete categories.', PHP_EOL, PHP_EOL;
  329. } else {
  330. echo 'Deleting invalid subscriptions, if found.', PHP_EOL, PHP_EOL;
  331. }
  332.  
  333. foreach ($res as $row) {
  334. $delete = false;
  335.  
  336. if (class_exists($row['handler_class'])) {
  337. echo '<span style="color: #090;">[Found ] ', Enc::html($row['handler_class']), '</span>', PHP_EOL;
  338. } else {
  339. echo '<span style="color: #900;">[MISSING] ', Enc::html($row['handler_class']), '</span>', PHP_EOL;
  340. $delete = true;
  341. }
  342.  
  343. if ($_GET['delete'] and $delete) {
  344. Pdb::delete('content_subscriptions', ['handler_class' => $row['handler_class']]);
  345. echo '<span style="color: #090;">[Deleted] ', Enc::html($row['handler_class']), '</span>', PHP_EOL;
  346. }
  347. }
  348.  
  349. echo '</pre>';
  350. }
  351.  
  352. }
  353.