|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* @package org.openpsa.directmarketing |
|
4
|
|
|
* @author CONTENT CONTROL http://www.contentcontrol-berlin.de/ |
|
5
|
|
|
* @copyright CONTENT CONTROL http://www.contentcontrol-berlin.de/ |
|
6
|
|
|
* @license http://www.gnu.org/licenses/gpl.html GNU General Public License |
|
7
|
|
|
*/ |
|
8
|
|
|
|
|
9
|
|
|
/** |
|
10
|
|
|
* Campaign message sender |
|
11
|
|
|
* |
|
12
|
|
|
* @package org.openpsa.directmarketing |
|
13
|
|
|
*/ |
|
14
|
|
|
class org_openpsa_directmarketing_sender extends midcom_baseclasses_components_purecode |
|
15
|
|
|
{ |
|
16
|
|
|
/** |
|
17
|
|
|
* Are we running in test mode |
|
18
|
|
|
* |
|
19
|
|
|
* @var boolean |
|
20
|
|
|
*/ |
|
21
|
|
|
private $test_mode = false; |
|
22
|
|
|
|
|
23
|
|
|
/** |
|
24
|
|
|
* How many messages to send in one go |
|
25
|
|
|
* |
|
26
|
|
|
* @var integer |
|
27
|
|
|
*/ |
|
28
|
|
|
public $chunk_size = 50; |
|
29
|
|
|
|
|
30
|
|
|
/** |
|
31
|
|
|
* Length of the message token |
|
32
|
|
|
* |
|
33
|
|
|
* @var integer |
|
34
|
|
|
*/ |
|
35
|
|
|
public $token_size = 15; |
|
36
|
|
|
|
|
37
|
|
|
/** |
|
38
|
|
|
* The message we're working on |
|
39
|
|
|
* |
|
40
|
|
|
* @var org_openpsa_directmarketing_campaign_message_dba |
|
41
|
|
|
*/ |
|
42
|
|
|
private $_message; |
|
43
|
|
|
|
|
44
|
|
|
/** |
|
45
|
|
|
* The backend to use |
|
46
|
|
|
* |
|
47
|
|
|
* @var org_openpsa_directmarketing_sender_backend |
|
48
|
|
|
*/ |
|
49
|
|
|
private $_backend; |
|
50
|
|
|
|
|
51
|
|
|
/** |
|
52
|
|
|
* Tracks total number of sent messages |
|
53
|
|
|
* |
|
54
|
|
|
* @var integer |
|
55
|
|
|
*/ |
|
56
|
|
|
private static $_messages_sent = 0; |
|
57
|
|
|
|
|
58
|
|
|
private $_chunk_num = 0; |
|
59
|
|
|
|
|
60
|
|
|
/** |
|
61
|
|
|
* How many times to recurse if all results are filtered (speed vs memory [and risk on crashing], higher is faster) |
|
62
|
|
|
* |
|
63
|
|
|
* @var integer |
|
64
|
|
|
*/ |
|
65
|
|
|
private $_chunk_max_recurse = 15; |
|
66
|
|
|
|
|
67
|
|
|
private $from; |
|
68
|
|
|
|
|
69
|
|
|
private $subject; |
|
70
|
|
|
|
|
71
|
|
|
/** |
|
72
|
|
|
* @param org_openpsa_directmarketing_campaign_message_dba $message The message we're working on |
|
73
|
|
|
* @param array $config Configuration that gets handed to the backend |
|
74
|
|
|
*/ |
|
75
|
3 |
|
public function __construct(org_openpsa_directmarketing_campaign_message_dba $message, array $config = [], $from = '', $subject = '') |
|
76
|
|
|
{ |
|
77
|
3 |
|
parent::__construct(); |
|
78
|
3 |
|
$this->_message = $message; |
|
79
|
3 |
|
$this->from = $from; |
|
80
|
3 |
|
$this->subject = $subject; |
|
81
|
|
|
|
|
82
|
3 |
|
if ( $this->_message->orgOpenpsaObtype != org_openpsa_directmarketing_campaign_message_dba::EMAIL_TEXT |
|
83
|
3 |
|
&& $this->_message->orgOpenpsaObtype != org_openpsa_directmarketing_campaign_message_dba::EMAIL_HTML) { |
|
84
|
|
|
throw new midcom_error('unsupported message type'); |
|
85
|
|
|
} |
|
86
|
3 |
|
$this->_backend = new org_openpsa_directmarketing_sender_backend_email($config, $this->_message); |
|
87
|
3 |
|
$this->chunk_size = $this->_config->get('chunk_size'); |
|
|
|
|
|
|
88
|
3 |
|
} |
|
89
|
|
|
|
|
90
|
|
|
/** |
|
91
|
|
|
* Sends a message to all testers |
|
92
|
|
|
*/ |
|
93
|
1 |
|
public function send_test($content) |
|
94
|
|
|
{ |
|
95
|
1 |
|
$this->test_mode = true; |
|
96
|
1 |
|
midcom::get()->disable_limits(); |
|
97
|
|
|
|
|
98
|
1 |
|
while ($results = $this->_qb_send_loop()) { |
|
99
|
|
|
if (!$this->process_results($results, $content)) { |
|
100
|
|
|
return false; |
|
101
|
|
|
} |
|
102
|
|
|
} |
|
103
|
|
|
|
|
104
|
1 |
|
return true; |
|
105
|
|
|
} |
|
106
|
|
|
|
|
107
|
|
|
/** |
|
108
|
|
|
* Sends $content to all members of the campaign |
|
109
|
|
|
*/ |
|
110
|
1 |
|
public function send_bg($url_base, $batch, $content) |
|
111
|
|
|
{ |
|
112
|
|
|
//TODO: Figure out how to recognize errors and pass the info on |
|
113
|
|
|
|
|
114
|
1 |
|
$this->_chunk_num = $batch - 1; |
|
115
|
|
|
|
|
116
|
1 |
|
midcom::get()->disable_limits(); |
|
117
|
|
|
//For first batch (they start from 1 instead of 0) make sure we have smart campaign members up to date |
|
118
|
1 |
|
if ($batch == 1) { |
|
119
|
|
|
$this->_check_campaign_up_to_date(); |
|
120
|
|
|
} |
|
121
|
|
|
// Register sendStarted if not already set |
|
122
|
1 |
|
if (!$this->_message->sendStarted) { |
|
123
|
1 |
|
$this->_message->sendStarted = time(); |
|
124
|
1 |
|
$this->_message->update(); |
|
125
|
|
|
} |
|
126
|
1 |
|
$results = $this->_qb_single_chunk(); |
|
127
|
|
|
//The method above might have incremented the counter for internal reasons |
|
128
|
1 |
|
$batch = $this->_chunk_num + 1; |
|
129
|
1 |
|
if ($results === false) { |
|
130
|
1 |
|
$status = true; //All should be ok |
|
131
|
|
|
} elseif ($status = $this->process_results($results, $content)) { |
|
132
|
|
|
//register next batch |
|
133
|
|
|
return $this->register_send_job($batch + 1, $url_base); |
|
134
|
|
|
} |
|
135
|
|
|
|
|
136
|
|
|
// Last batch done, register sendCompleted if we're not in test mode |
|
137
|
1 |
|
$this->_message->sendCompleted = time(); |
|
138
|
1 |
|
$this->_message->update(); |
|
139
|
|
|
|
|
140
|
1 |
|
return $status; |
|
141
|
|
|
} |
|
142
|
|
|
|
|
143
|
|
|
private function process_results(array $results, $content) |
|
144
|
|
|
{ |
|
145
|
|
|
if (!$this->_backend->check_results($results)) { |
|
146
|
|
|
return false; //Backend refuses delivery |
|
147
|
|
|
} |
|
148
|
|
|
foreach ($results as $member) { |
|
149
|
|
|
$this->_send_member($member, $content); |
|
150
|
|
|
} |
|
151
|
|
|
return true; |
|
152
|
|
|
} |
|
153
|
|
|
|
|
154
|
|
|
public function register_send_job($batch, $url_base, $time = null) |
|
155
|
|
|
{ |
|
156
|
|
|
$time = $time ?: time() + 30; |
|
157
|
|
|
$args = [ |
|
158
|
|
|
'batch' => $batch, |
|
159
|
|
|
'url_base' => $url_base, |
|
160
|
|
|
]; |
|
161
|
|
|
debug_add("Registering batch #{$args['batch']} for {$args['url_base']} to start on: " . date('Y-m-d H:i:s', $time)); |
|
162
|
|
|
midcom::get()->auth->request_sudo('org.openpsa.directmarketing'); |
|
163
|
|
|
$atstat = midcom_services_at_interface::register($time, 'org.openpsa.directmarketing', 'background_send_message', $args); |
|
164
|
|
|
midcom::get()->auth->drop_sudo(); |
|
165
|
|
|
if (!$atstat) { |
|
166
|
|
|
debug_add("FAILED to register batch #{$args['batch']} for {$args['url_base']}, errstr: " . midcom_connection::get_error_string(), MIDCOM_LOG_ERROR); |
|
167
|
|
|
} |
|
168
|
|
|
return $atstat; |
|
169
|
|
|
} |
|
170
|
|
|
|
|
171
|
|
|
private function _send_member(org_openpsa_directmarketing_campaign_member_dba $member, $content) |
|
172
|
|
|
{ |
|
173
|
|
|
if (!$person = $this->_get_person($member)) { |
|
174
|
|
|
return; |
|
175
|
|
|
} |
|
176
|
|
|
|
|
177
|
|
|
$from = $this->from ?: '[email protected]'; |
|
178
|
|
|
$subject = $this->subject ?: '[no subject]'; |
|
179
|
|
|
if ($this->test_mode) { |
|
180
|
|
|
$subject = "[TEST] {$subject}"; |
|
181
|
|
|
} |
|
182
|
|
|
$content = $member->personalize_message($content, $this->_message->orgOpenpsaObtype, $person); |
|
183
|
|
|
$token = $this->_create_token(); |
|
184
|
|
|
$subject = $member->personalize_message($subject, org_openpsa_directmarketing_campaign_message_dba::EMAIL_TEXT, $person); |
|
185
|
|
|
$params = []; |
|
186
|
|
|
|
|
187
|
|
|
try { |
|
188
|
|
|
$this->_backend->send($person, $member, $token, $subject, $content, $from); |
|
189
|
|
|
self::$_messages_sent++; |
|
190
|
|
|
$status = org_openpsa_directmarketing_campaign_messagereceipt_dba::SENT; |
|
191
|
|
|
} catch (midcom_error $e) { |
|
192
|
|
|
$status = org_openpsa_directmarketing_campaign_messagereceipt_dba::FAILURE; |
|
193
|
|
|
if (!$this->test_mode) { |
|
194
|
|
|
$params[] = [ |
|
195
|
|
|
'domain' => 'org.openpsa.directmarketing', |
|
196
|
|
|
'name' => 'send_error_message', |
|
197
|
|
|
'value' => $e->getMessage(), |
|
198
|
|
|
]; |
|
199
|
|
|
} else { |
|
200
|
|
|
midcom::get()->uimessages->add($this->_l10n->get($this->_component), $e->getMessage(), 'error'); |
|
201
|
|
|
} |
|
202
|
|
|
} |
|
203
|
|
|
if (!$this->test_mode) { |
|
204
|
|
|
$member->create_receipt($this->_message->id, $status, $token, $params); |
|
205
|
|
|
} |
|
206
|
|
|
} |
|
207
|
|
|
|
|
208
|
|
|
/** |
|
209
|
|
|
* Creates a random token string that can be used to track a single |
|
210
|
|
|
* delivery. The returned token string will only contain |
|
211
|
|
|
* lowercase alphanumeric characters and will start with a lowercase |
|
212
|
|
|
* letter to avoid problems with special processing being triggered |
|
213
|
|
|
* by special characters in the token string. |
|
214
|
|
|
* |
|
215
|
|
|
* @return string random token string |
|
216
|
|
|
*/ |
|
217
|
|
|
private function _create_token() |
|
218
|
|
|
{ |
|
219
|
|
|
//Testers need dummy token |
|
220
|
|
|
if ($this->test_mode) { |
|
221
|
|
|
return 'dummy'; |
|
222
|
|
|
} |
|
223
|
|
|
|
|
224
|
|
|
$token = midcom_helper_misc::random_string(1, 'abcdefghijklmnopqrstuvwxyz'); |
|
225
|
|
|
$token .= midcom_helper_misc::random_string($this->token_size - 1, 'abcdefghijklmnopqrstuvwxyz0123456789'); |
|
226
|
|
|
|
|
227
|
|
|
//If token is not free or (very, very unlikely) matches our dummy token, recurse. |
|
228
|
|
|
if ( $token === 'dummy' |
|
229
|
|
|
|| !org_openpsa_directmarketing_campaign_messagereceipt_dba::token_is_free($token)) { |
|
230
|
|
|
return $this->_create_token(); |
|
231
|
|
|
} |
|
232
|
|
|
return $token; |
|
233
|
|
|
} |
|
234
|
|
|
|
|
235
|
|
|
/** |
|
236
|
|
|
* Check is given member has denied contacts of given type |
|
237
|
|
|
* |
|
238
|
|
|
* @param org_openpsa_directmarketing_campaign_member_dba $member campaign_member object related to the person |
|
239
|
|
|
* @return mixed org_openpsa_contacts_person_dba person on success, false if denied |
|
240
|
|
|
*/ |
|
241
|
|
|
private function _get_person(org_openpsa_directmarketing_campaign_member_dba $member) |
|
242
|
|
|
{ |
|
243
|
|
|
try { |
|
244
|
|
|
$person = org_openpsa_contacts_person_dba::get_cached($member->person); |
|
245
|
|
|
} catch (midcom_error $e) { |
|
246
|
|
|
debug_add("Person #{$member->person} deleted or missing, removing member (member #{$member->id})"); |
|
247
|
|
|
$member->orgOpenpsaObtype = org_openpsa_directmarketing_campaign_member_dba::UNSUBSCRIBED; |
|
248
|
|
|
$member->delete(); |
|
249
|
|
|
return false; |
|
250
|
|
|
} |
|
251
|
|
|
$type = $this->_backend->get_type(); |
|
252
|
|
|
if ( $person->get_parameter('org.openpsa.directmarketing', "send_all_denied") |
|
253
|
|
|
|| $person->get_parameter('org.openpsa.directmarketing', "send_{$type}_denied")) { |
|
254
|
|
|
debug_add("Sending {$type} messages to person {$person->rname} is denied, unsubscribing member (member #{$member->id})"); |
|
|
|
|
|
|
255
|
|
|
$member->orgOpenpsaObtype = org_openpsa_directmarketing_campaign_member_dba::UNSUBSCRIBED; |
|
256
|
|
|
$member->update(); |
|
257
|
|
|
return false; |
|
258
|
|
|
} |
|
259
|
|
|
return $person; |
|
260
|
|
|
} |
|
261
|
|
|
|
|
262
|
|
|
/** |
|
263
|
|
|
* Loops trough send filter in chunks, adds some common constraints and checks for send-receipts. |
|
264
|
|
|
*/ |
|
265
|
1 |
|
private function _qb_send_loop() |
|
266
|
|
|
{ |
|
267
|
1 |
|
$ret = $this->_qb_single_chunk(); |
|
268
|
1 |
|
$this->_chunk_num++; |
|
269
|
|
|
//Trivial rate limiting |
|
270
|
1 |
|
sleep(1); |
|
271
|
1 |
|
return $ret; |
|
272
|
|
|
} |
|
273
|
|
|
|
|
274
|
2 |
|
private function _qb_single_chunk($level = 0) |
|
275
|
|
|
{ |
|
276
|
2 |
|
$qb = org_openpsa_directmarketing_campaign_member_dba::new_query_builder(); |
|
277
|
2 |
|
$this->_backend->add_member_constraints($qb); |
|
278
|
2 |
|
$this->_qb_common_constraints($qb); |
|
279
|
2 |
|
$this->_qb_chunk_limits($qb); |
|
280
|
|
|
|
|
281
|
2 |
|
$results = $qb->execute_unchecked(); |
|
282
|
2 |
|
if (empty($results)) { |
|
283
|
2 |
|
debug_add('Got failure or empty resultset, aborting'); |
|
284
|
2 |
|
return false; |
|
285
|
|
|
} |
|
286
|
|
|
|
|
287
|
|
|
if ($this->test_mode) { |
|
288
|
|
|
debug_add('TEST mode, no receipt filtering will be done'); |
|
289
|
|
|
return $results; |
|
290
|
|
|
} |
|
291
|
|
|
debug_add('Got ' . count($results) . ' initial results'); |
|
292
|
|
|
|
|
293
|
|
|
$results = $this->_qb_filter_results($results); |
|
294
|
|
|
|
|
295
|
|
|
debug_add('Have ' . count($results) . ' results left after filtering'); |
|
296
|
|
|
debug_add("Recursion level is {$level}, limit is {$this->_chunk_max_recurse}"); |
|
297
|
|
|
/* Make sure we still have results left, if not just recurse... |
|
298
|
|
|
(basically this is to avoid returning an empty array when everything is otherwise ok) */ |
|
299
|
|
|
if (empty($results) && ($level < $this->_chunk_max_recurse)) { |
|
300
|
|
|
debug_add('All our results got filtered, recursing for another round'); |
|
301
|
|
|
$this->_chunk_num++; |
|
302
|
|
|
return $this->_qb_single_chunk($level + 1); |
|
303
|
|
|
} |
|
304
|
|
|
|
|
305
|
|
|
reset($results); |
|
306
|
|
|
return $results; |
|
307
|
|
|
} |
|
308
|
|
|
|
|
309
|
2 |
|
private function _qb_chunk_limits($qb) |
|
310
|
|
|
{ |
|
311
|
2 |
|
debug_add("Processing chunk {$this->_chunk_num}"); |
|
312
|
2 |
|
$offset = $this->_chunk_num * $this->chunk_size; |
|
313
|
2 |
|
if ($offset > 0) { |
|
314
|
1 |
|
debug_add("Setting offset to {$offset}"); |
|
315
|
1 |
|
$qb->set_offset($offset); |
|
316
|
|
|
} |
|
317
|
2 |
|
debug_add("Setting limit to {$this->chunk_size}"); |
|
318
|
2 |
|
$qb->set_limit($this->chunk_size); |
|
319
|
2 |
|
} |
|
320
|
|
|
|
|
321
|
|
|
private function _qb_filter_results($results) |
|
322
|
|
|
{ |
|
323
|
|
|
if (empty($results)) { |
|
324
|
|
|
return $results; |
|
325
|
|
|
} |
|
326
|
|
|
//Make a map for receipt filtering |
|
327
|
|
|
$results_person_map = []; |
|
328
|
|
|
foreach ($results as $k => $member) { |
|
329
|
|
|
$results_person_map[$member->person] = $k; |
|
330
|
|
|
} |
|
331
|
|
|
$mc = org_openpsa_directmarketing_campaign_messagereceipt_dba::new_collector('message', $this->_message->id); |
|
332
|
|
|
$mc->add_constraint('message', '=', $this->_message->id); |
|
333
|
|
|
$mc->add_constraint('orgOpenpsaObtype', '=', org_openpsa_directmarketing_campaign_messagereceipt_dba::SENT); |
|
334
|
|
|
$mc->add_constraint('person', 'IN', array_keys($results_person_map)); |
|
335
|
|
|
|
|
336
|
|
|
$receipts = $mc->get_values('person'); |
|
337
|
|
|
|
|
338
|
|
|
if (empty($receipts)) { |
|
339
|
|
|
return $results; |
|
340
|
|
|
} |
|
341
|
|
|
debug_add('Found ' . count($receipts) . ' send receipts for this chunk'); |
|
342
|
|
|
|
|
343
|
|
|
//Filter results array by receipt |
|
344
|
|
|
$receipts = array_flip($receipts); |
|
345
|
|
|
$persons_to_remove = array_intersect_key($results_person_map, $receipts); |
|
346
|
|
|
$results = array_diff_key($results, array_flip($persons_to_remove)); |
|
347
|
|
|
return $results; |
|
348
|
|
|
} |
|
349
|
|
|
|
|
350
|
|
|
/** |
|
351
|
|
|
* Get send status |
|
352
|
|
|
* |
|
353
|
|
|
* @return array Number of valid members at index 0 and number of send receipts at 1 |
|
354
|
|
|
*/ |
|
355
|
1 |
|
public function get_status() |
|
356
|
|
|
{ |
|
357
|
1 |
|
$qb_mem = org_openpsa_directmarketing_campaign_member_dba::new_query_builder(); |
|
358
|
1 |
|
$this->_backend->add_member_constraints($qb_mem); |
|
359
|
|
|
|
|
360
|
1 |
|
$this->_qb_common_constraints($qb_mem); |
|
361
|
1 |
|
$valid_members = $qb_mem->count_unchecked(); |
|
362
|
|
|
|
|
363
|
1 |
|
$qb_receipts = org_openpsa_directmarketing_campaign_messagereceipt_dba::new_query_builder(); |
|
364
|
1 |
|
$qb_receipts->add_constraint('message', '=', $this->_message->id); |
|
365
|
1 |
|
$qb_receipts->add_constraint('orgOpenpsaObtype', '=', org_openpsa_directmarketing_campaign_messagereceipt_dba::SENT); |
|
366
|
1 |
|
$send_receipts = $qb_receipts->count_unchecked(); |
|
367
|
|
|
|
|
368
|
1 |
|
return [$valid_members, $send_receipts]; |
|
369
|
|
|
} |
|
370
|
|
|
|
|
371
|
|
|
/** |
|
372
|
|
|
* Check if this message is attached to a smart campaign, if so update the campaign members |
|
373
|
|
|
*/ |
|
374
|
|
|
private function _check_campaign_up_to_date() |
|
375
|
|
|
{ |
|
376
|
|
|
midcom::get()->auth->request_sudo('org.openpsa.directmarketing'); |
|
377
|
|
|
$campaign = new org_openpsa_directmarketing_campaign_dba($this->_message->campaign); |
|
378
|
|
|
midcom::get()->auth->drop_sudo(); |
|
379
|
|
|
if ($campaign->orgOpenpsaObtype == org_openpsa_directmarketing_campaign_dba::TYPE_SMART) { |
|
380
|
|
|
$campaign->update_smart_campaign_members(); |
|
381
|
|
|
} |
|
382
|
|
|
} |
|
383
|
|
|
|
|
384
|
|
|
/** |
|
385
|
|
|
* Sets the common constrains for campaign members queries |
|
386
|
|
|
*/ |
|
387
|
3 |
|
protected function _qb_common_constraints($qb) |
|
388
|
|
|
{ |
|
389
|
3 |
|
debug_add("Setting constraint campaign = {$this->_message->campaign}"); |
|
390
|
3 |
|
$qb->add_constraint('campaign', '=', $this->_message->campaign); |
|
391
|
3 |
|
$qb->add_constraint('suspended', '<', time()); |
|
392
|
3 |
|
if ($this->test_mode) { |
|
393
|
1 |
|
debug_add('TEST mode, adding constraints'); |
|
394
|
1 |
|
$qb->add_constraint('orgOpenpsaObtype', '=', org_openpsa_directmarketing_campaign_member_dba::TESTER); |
|
395
|
|
|
} else { |
|
396
|
2 |
|
debug_add('REAL mode, adding constraints'); |
|
397
|
|
|
//Fail safe way, exclude those we know we do not want, in case some wanted members have incorrect type... |
|
398
|
2 |
|
$qb->add_constraint('orgOpenpsaObtype', '<>', org_openpsa_directmarketing_campaign_member_dba::TESTER); |
|
399
|
2 |
|
$qb->add_constraint('orgOpenpsaObtype', '<>', org_openpsa_directmarketing_campaign_member_dba::UNSUBSCRIBED); |
|
400
|
2 |
|
$qb->add_constraint('orgOpenpsaObtype', '<>', org_openpsa_directmarketing_campaign_member_dba::BOUNCED); |
|
401
|
|
|
} |
|
402
|
3 |
|
$qb->add_order('person.lastname', 'ASC'); |
|
403
|
3 |
|
$qb->add_order('person.firstname', 'ASC'); |
|
404
|
3 |
|
$qb->add_order('person.id', 'ASC'); |
|
405
|
3 |
|
} |
|
406
|
|
|
} |
|
407
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountIdthat can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theidproperty of an instance of theAccountclass. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.