PluginProbe
MyRewards / trunk
MyRewards vtrunk
5.7.8 5.7.7 5.7.6 trunk 1.2.2 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.1.1 2.2.0 2.3.0 2.4.0 2.4.1 2.5.0 2.6.4 2.6.6 3.0.0.0 3.1.0 3.1.2 3.1.2.1 3.10.4 3.10.9 All 165 releases
woorewards / include / core / pointstack.php

pointstack.php in MyRewards trunk, at include/core/pointstack.php

808 lines 27.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace LWS\WOOREWARDS\Core;
3
4 // don't call the file directly
5 if( !defined( 'ABSPATH' ) ) exit();
6
7 /** Manage user points (and point history in pro version).
8 * Few functions have a force argument to reset the buffered amount by reading the database again. */
9 class PointStack
10 {
11 const MetaPrefix = 'lws_wre_points_';
12 public $lastLogId = 0;
13
14 public $name = '';
15 public $userId = false;
16 public $amount = false;
17
18 function __construct($name, $userId)
19 {
20 $this->name = $name;
21 $this->userId = $userId;
22 }
23
24 /** relevant when several pages opened at the same time.
25 * WP load meta very soon, then all is read from cache if possible,
26 * no way to know other thread changed something, db never called again. */
27 static function cleanCache($userId)
28 {
29 if (\function_exists('\wp_cache_flush_group')) {
30 // require at least WP 6.1.0
31 if (\wp_cache_supports('flush_group')) {
32 \wp_cache_flush_group('user_meta');
33 } // else means another cache system exists that does not support flush_group
34 }
35 }
36
37 /** $force bypass any cache and directly call DB. */
38 function get($force = false)
39 {
40 if( false === $this->amount || $force )
41 {
42 if ($force) {
43 global $wpdb;
44 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
45 $val = $wpdb->get_var($wpdb->prepare(
46 "SELECT `meta_value` FROM {$wpdb->usermeta} WHERE `user_id`=%d AND `meta_key`=%s",
47 $this->userId, $this->metaKey()
48 ));
49 } else {
50 $val = \get_user_meta($this->userId, $this->metaKey(), true);
51 }
52 $this->amount = ($val && \is_numeric($val)) ? (int)$val : 0;
53 }
54 return $this->amount;
55 }
56
57 function &set($points, $reason='', $origin='', $origin2=false)
58 {
59 $this->amount = intval(round($points));
60 \update_user_meta($this->userId, $this->metaKey(), $this->amount);
61 $this->trace($points, null, $reason, $origin, $origin2);
62 return $this;
63 }
64
65 function &add($points, $reason='', $force = false, $origin='', $origin2=false)
66 {
67 if( !empty($points = intval(round($points))) )
68 {
69 $amount = $this->get($force);
70 $this->amount = $amount + $points;
71 \update_user_meta($this->userId, $this->metaKey(), $this->amount);
72 $this->trace($this->amount, $points, $reason, $origin, $origin2);
73 }
74 return $this;
75 }
76
77 function &sub($points, $reason='', $force = false, $origin='', $origin2=false)
78 {
79 if( !empty($points = intval(round($points))) )
80 {
81 $amount = $this->get($force);
82 $this->amount = $amount - $points;
83 \update_user_meta($this->userId, $this->metaKey(), $this->amount);
84 $this->trace($this->amount, -$points, $reason, $origin, $origin2);
85 }
86 return $this;
87 }
88
89 /** That action is performed for all users.
90 *
91 * Reset any point amount in this stack unchanged since $threshold.
92 * If option 'lws_woorewards_pointstack_timeout_delete' is 'on', delete all record before that date.
93 * @param $threshold (false|\DateTime) reset points if last change is before that date.
94 * @param $getAffectedUserIds (bool) if true, return an array with affected user IDs. default is false.
95 * @param $reason (string) the cleanup reason to set in user history.
96 * @param $resetTo (int) reset points to this value, default is zero.
97 * @return null|array depends on $getAffectedUserIds */
98 public function reset($threshold, $getAffectedUserIds=false, $reason=false, $resetTo=0)
99 {
100 $affected = null;
101 global $wpdb;
102 $table = self::table();
103 $resetTo = intval($resetTo);
104
105 // reset point values for customers without recent activity but with points (note we set '' and not zero)
106 $update = "UPDATE {$wpdb->usermeta} as raz SET raz.meta_value='' WHERE raz.meta_key=%s AND raz.meta_value>%d";
107 $args = array(
108 $this->metaKey(),
109 $resetTo
110 );
111 if( \is_a($threshold, '\DateTime') )
112 {
113 $update .= " AND raz.user_id NOT IN (SELECT DISTINCT good.user_id FROM $table as good WHERE good.stack=%s AND date(good.mvt_date) >= date(%s))";
114 $args[] = $this->name;
115 $args[] = $threshold->format('Y-m-d');
116 }
117 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
118 $wpdb->query($wpdb->prepare($update, $args));
119
120 // insert reset line for customers with '' as point value
121 $reason = $reason ? $this->formatReason($reason) : \LWS\WOOREWARDS\Core\Trace::byReason("Lost due to inactivity", 'woorewards-lite');
122 $fields = array('new_total', 'stack', 'commentar', 'blog_id', 'origin', 'mvt_date');
123 $values = array('%d', '%s', '%s', '%d', '%s', \gmdate("'Y-m-d H:i:s'", \time()));
124 $args = array(
125 $resetTo,
126 $this->name,
127 $reason->reason,
128 $reason->getBlog(),
129 $reason->referral ? $reason->referral : 'stack_reset',
130 );
131 if( $reason->providerId ){ $fields[] = 'origin2'; $values[] = '%d'; $args[] = $reason->providerId; }
132 if( $reason->orderId ){ $fields[] = 'order_id'; $values[] = '%d'; $args[] = $reason->orderId; }
133
134 $fields = implode(', ', $fields);
135 $values = implode(', ', $values);
136 $args[] = $this->metaKey();
137 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
138 $wpdb->query($wpdb->prepare("INSERT INTO $table (user_id, {$fields}) SELECT DISTINCT pts.user_id, {$values} FROM {$wpdb->usermeta} as pts WHERE pts.meta_key=%s AND pts.meta_value=''", $args));
139
140 if( $getAffectedUserIds )
141 {
142 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
143 $affected = $wpdb->get_col($wpdb->prepare(
144 "SELECT user_id FROM {$wpdb->usermeta} as raz WHERE raz.meta_key=%s AND raz.meta_value=''",
145 $this->metaKey()
146 ));
147 }
148
149 // clean points amounts values (replace '' by zero)
150 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
151 $wpdb->query($wpdb->prepare(
152 "UPDATE {$wpdb->usermeta} as raz SET raz.meta_value=%d WHERE raz.meta_key=%s AND raz.meta_value=''",
153 $resetTo,
154 $this->metaKey()
155 ));
156
157 if( \is_a($threshold, '\DateTime') && !empty(\get_option('lws_woorewards_pointstack_timeout_delete', '')) )
158 {
159 $this->cleanup($threshold);
160 }
161
162 $this->amount = false;
163
164 \do_action('lws_woorewards_point_stack_reseted', $this, $threshold, $resetTo, $reason, $getAffectedUserIds, $affected);
165 return $affected;
166 }
167
168 /** That action is performed for all users.
169 *
170 * Remove from db any trace of that stack (usermeta and history) */
171 public function delete()
172 {
173 global $wpdb;
174 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
175 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->usermeta} WHERE meta_key=%s", $this->metaKey()));
176 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
177 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->lwsWooRewardsHistoric} WHERE stack=%s", $this->name));
178
179 $this->amount = false;
180 }
181
182 /** @return (bool) in usage by a pool */
183 public function isUsed()
184 {
185 global $wpdb;
186 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
187 $c = $wpdb->get_var($wpdb->prepare(
188 "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key='wre_pool_point_stack' AND meta_value=%s",
189 $this->name
190 ));
191 return is_null($c) ? false : !empty($c);
192 }
193
194 /** Merge points from another stack to this one.
195 * The other stack is NOT modified. */
196 public function merge($otherStackName)
197 {
198 global $wpdb;
199
200 // mark the merge in history, let stack empty for futur reference
201 $insert = "INSERT INTO {$wpdb->lwsWooRewardsHistoric} (user_id, new_total, points_moved, stack, commentar, origin, blog_id, mvt_date)"
202 . " SELECT m.user_id, SUM(m.meta_value), SUM(m.diff), '', %s, 'merge', %d, %s"
203 . " FROM ("
204 . " SELECT s.user_id, s.meta_value, 0 as diff FROM {$wpdb->usermeta} as s"
205 . " WHERE s.meta_key=%s"
206 . " UNION"
207 . " SELECT d.user_id, d.meta_value, d.meta_value as diff FROM {$wpdb->usermeta} as d"
208 . " WHERE d.meta_key=%s"
209 . " ) as m GROUP BY m.user_id";
210 $args = [
211 \LWS\WOOREWARDS\Core\Trace::serializeReason(array("Points merged from %s", $otherStackName), 'woorewards-lite'),
212 \get_current_blog_id(), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
213 \gmdate('Y-m-d H:i:s', \time()),
214 $this->metaKey(),
215 $this->metaKey($otherStackName),
216 ];
217 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
218 $wpdb->query($wpdb->prepare($insert, $args));
219
220 // copy points in history back to usermeta
221 $update = "UPDATE {$wpdb->usermeta} as d"
222 . " INNER JOIN {$wpdb->lwsWooRewardsHistoric} as s ON s.user_id=d.user_id AND s.stack=''"
223 . " SET d.meta_value=s.new_total"
224 . " WHERE d.meta_key=%s";
225 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
226 $wpdb->query($wpdb->prepare($update, $this->metaKey()));
227
228 // clean history, restore stack name
229 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
230 $wpdb->query($wpdb->prepare("UPDATE {$wpdb->lwsWooRewardsHistoric} SET stack=%s WHERE stack=''", $this->name));
231
232 $this->amount = false;
233 }
234
235 /** That action is performed for all users.
236 *
237 * delete history in database.
238 * @param $threshold (\DateTime) remove all entry before that date. */
239 protected function cleanup(\DateTime $threshold)
240 {
241 global $wpdb;
242 $table = self::table();
243 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $table from self::table()
244 $wpdb->query($wpdb->prepare("DELETE FROM $table WHERE date(mvt_date)<date(%s)", $threshold->format('Y-m-d')));
245 }
246
247 public function metaKey($name=false)
248 {
249 return self::MetaPrefix . ($name===false ? $this->name : $name);
250 }
251
252 public function getName()
253 {
254 return $this->name;
255 }
256
257 static function getUTCTimezone()
258 {
259 static $tz = null;
260 if (null === $tz)
261 $tz = new \DateTimeZone('UTC');
262 return $tz;
263 }
264
265 /** That usefull function exists since 5.3
266 * But we keep a 4.9 compatibility. */
267 static function getSiteTimezone()
268 {
269 if( function_exists('wp_timezone') )
270 return \wp_timezone();
271 else
272 return new \DateTimeZone(self::getSiteTimezoneString());
273 }
274
275 /** That usefull function exists since 5.3
276 * But we keep a 4.9 compatibility. */
277 static function getSiteTimezoneString()
278 {
279 if( function_exists('wp_timezone_string') )
280 return \wp_timezone_string();
281
282 $timezone_string = get_option( 'timezone_string' );
283
284 if ( $timezone_string ) {
285 return $timezone_string;
286 }
287
288 $offset = (float) get_option( 'gmt_offset' );
289 $hours = (int) $offset;
290 $minutes = ( $offset - $hours );
291
292 $sign = ( $offset < 0 ) ? '-' : '+';
293 $abs_hour = abs( $hours );
294 $abs_mins = abs( $minutes * 60 );
295 $tz_offset = sprintf( '%s%02d:%02d', $sign, $abs_hour, $abs_mins );
296
297 return $tz_offset;
298 }
299
300 /** Convert value and add timezone.
301 * @param $op_date (string) the date as read in DB
302 * @param $retDateTime (bool) choose to get a DateTime instance or the string representation.
303 * @param $withTime (bool) set false if your string does not have time.
304 * @return (string|\DateTime) depending on $retDateTime */
305 static function dateI18n($op_date, $retDateTime=false)
306 {
307 $date = \date_create($op_date);
308 if ($retDateTime)
309 return $date;
310 else
311 return \date_i18n(\get_option('date_format'), $date->getTimestamp() + self::getSiteTimezone()->getOffset($date));
312 }
313
314 /** Convert value and add timezone.
315 * @param $op_date (string) the date as read in DB
316 * @param $retDateTime (bool) choose to get a DateTime instance or the string representation.
317 * @param $withTime (bool) set false if your string does not have time.
318 * @return (string|\DateTime) depending on $retDateTime */
319 static function dateTimeI18n($op_date, $retDateTime=false)
320 {
321 $date = \date_create($op_date);
322 if ($retDateTime)
323 return $date;
324 else
325 return \date_i18n(\get_option('date_format') . ' ' . \get_option('time_format'), $date->getTimestamp() + self::getSiteTimezone()->getOffset($date));
326 }
327
328 /** @return array [op_date, op_value, op_result, op_reason] */
329 function getHistory($force = false, $translate=true, $offset=false, $limit=false)
330 {
331 global $wpdb;
332 $sql = "SELECT id, mvt_date as op_date, points_moved as op_value, new_total as op_result, commentar as op_reason, `origin`"
333 . " FROM $wpdb->lwsWooRewardsHistoric"
334 . " WHERE user_id=%d AND stack=%s"
335 . " ORDER BY mvt_date DESC, id DESC";
336 $args = array(
337 $this->userId,
338 $this->name
339 );
340 if( $offset !== false && $limit )
341 {
342 $sql .= " LIMIT %d, %d";
343 $args[] = \absint($offset);
344 $args[] = max(\intval($limit), 1);
345 }
346
347 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql built from wpdb table name
348 $history = $wpdb->get_results($wpdb->prepare($sql, $args), ARRAY_A);
349
350 if( $translate )
351 {
352 if( $history )
353 {
354 $pool = \apply_filters('lws_woorewards_get_pools_by_stack', false, $this->name);
355 $pool = $pool ? $pool->sort()->first() : false;
356
357 foreach($history as &$row)
358 {
359 if ($pool) {
360 if (\is_numeric($row['op_value'])) {
361 $row['op_value'] = $pool->formatPoints($row['op_value'], false);
362 }
363 if (\is_numeric($row['op_result'])) {
364 $row['op_result'] = $pool->formatPoints($row['op_result'], false);
365 }
366 }
367
368 if ($row['origin'] && \is_numeric($row['origin'])) {
369 $title = $this->getOriginTitle($row['origin']);
370 if ($title) {
371 $row['op_reason'] = $title;
372 continue; // skip translation of original reason
373 }
374 }
375 if( $row['op_reason'] && \is_serialized($row['op_reason']) )
376 {
377 $reason = @unserialize($row['op_reason']);
378 if( $reason && is_array($reason) )
379 $row['op_reason'] = \LWS\WOOREWARDS\Core\Trace::reasonToString($reason, true);
380 }
381 }
382 }
383 }
384 return $history;
385 }
386
387 /**
388 * Retrieve history for multiple stacks in a single query
389 * @param int $userId
390 * @param array $stackNames Array of stack names
391 * @param int $offset
392 * @param int $limit
393 * @param bool $translate
394 * @return array Grouped by stack name
395 */
396 public static function getHistoryBulk($userId, $stackNames, $offset = 0, $limit = 15, $translate = true)
397 {
398 global $wpdb;
399
400 if (empty($stackNames)) {
401 return array();
402 }
403
404 $placeholders = implode(',', array_fill(0, count($stackNames), '%s'));
405 $sql = "
406 SELECT
407 id,
408 mvt_date as op_date,
409 points_moved as op_value,
410 new_total as op_result,
411 commentar as op_reason,
412 origin,
413 stack
414 FROM {$wpdb->lwsWooRewardsHistoric}
415 WHERE user_id = %d AND stack IN ($placeholders)
416 ORDER BY mvt_date DESC, id DESC
417 LIMIT %d OFFSET %d
418 ";
419
420 $args = array_merge(
421 array($userId),
422 $stackNames,
423 array($limit, $offset)
424 );
425
426 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
427 $history = $wpdb->get_results($wpdb->prepare($sql, $args), ARRAY_A);
428
429 if ($translate && $history) {
430 $pools = array();
431 foreach ($stackNames as $stackName) {
432 $pool = \apply_filters('lws_woorewards_get_pools_by_stack', false, $stackName);
433 $pools[$stackName] = $pool ? $pool->sort()->first() : false;
434 }
435
436 $originIds = array_filter(
437 array_column($history, 'origin'),
438 function($val) { return $val && \is_numeric($val); }
439 );
440 $originTitles = !empty($originIds) ? self::bulkGetOriginTitles($originIds) : array();
441
442 foreach ($history as &$row) {
443 $pool = $pools[$row['stack']] ?? false;
444
445 if ($pool) {
446 if (\is_numeric($row['op_value'])) {
447 $row['op_value'] = $pool->formatPoints($row['op_value'], false);
448 }
449 if (\is_numeric($row['op_result'])) {
450 $row['op_result'] = $pool->formatPoints($row['op_result'], false);
451 }
452 }
453
454 if ($row['origin'] && ($originTitles[$row['origin']] ?? false)) {
455 $row['op_reason'] = $originTitles[$row['origin']];
456 } elseif ($row['op_reason'] && \is_serialized($row['op_reason'])) {
457 $reason = @unserialize($row['op_reason']);
458 if ($reason && is_array($reason)) {
459 $row['op_reason'] = \LWS\WOOREWARDS\Core\Trace::reasonToString($reason, true);
460 }
461 }
462 }
463 }
464
465 return $history;
466 }
467
468 protected static function bulkGetOriginTitles($originIds)
469 {
470 static $replaceReason = null;
471 if (null === $replaceReason) {
472 $replaceReason = \apply_filters('lws_woorewards_stack_history_prefers_origin_title', true);
473 }
474
475 $titles = array();
476 if (!$replaceReason) {
477 return $titles;
478 }
479
480 global $wpdb;
481 $placeholders = implode(',', array_fill(0, count($originIds), '%d'));
482 $sql = "SELECT ID, post_type FROM {$wpdb->posts} WHERE ID IN ($placeholders)";
483
484 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
485 $results = $wpdb->get_results($wpdb->prepare($sql, $originIds), OBJECT_K);
486
487 foreach ($results as $id => $row) {
488 $object = null;
489 $post = \get_post($id);
490 if (!$post) {
491 continue;
492 }
493
494 if ($row->post_type === \LWS\WOOREWARDS\Abstracts\Event::POST_TYPE) {
495 $object = \LWS\WOOREWARDS\Abstracts\Event::fromPost($post);
496 } elseif ($row->post_type === \LWS\WOOREWARDS\Abstracts\Unlockable::POST_TYPE) {
497 $object = \LWS\WOOREWARDS\Abstracts\Unlockable::fromPost($post);
498 }
499
500 if ($object) {
501 $title = $object->getTitleAsReason();
502 if ($title) {
503 $titles[$id] = $title;
504 }
505 }
506 }
507
508 return $titles;
509 }
510
511 /** overwrite \LWS\WOOREWARDS\Core\Trace reason with origin and origin2
512 * if given as arguments and not already in reason */
513 protected function formatReason($reason='', $origin='', $origin2=false)
514 {
515 if( is_a($reason, '\LWS\WOOREWARDS\Core\Trace') )
516 $trace = $reason;
517 else if( is_array($reason) )
518 $trace = new \LWS\WOOREWARDS\Core\Trace($reason);
519 else
520 $trace = \LWS\WOOREWARDS\Core\Trace::byReason($reason);
521
522 if( $origin && !$trace->referral )
523 $trace->setOrigin($origin);
524 if( $origin2 !== false && $trace->providerId === false )
525 $trace->setProvider($origin2);
526
527 return $trace;
528 }
529
530 protected function &trace($points, $move=null, $reason='', $origin='', $origin2=false)
531 {
532 $reason = $this->formatReason($reason, $origin, $origin2);
533 global $wpdb;
534 $values = array(
535 'user_id' => $this->userId,
536 'stack' => $this->name,
537 'points_moved' => $move,
538 'new_total' => $points,
539 'commentar' => $reason->reason,
540 'origin' => $reason->referral,
541 'blog_id' => $reason->getBlog(),
542 'mvt_date' => \gmdate('Y-m-d H:i:s', \time()),
543 );
544 $formats = array(
545 '%d',
546 '%s',
547 '%d',
548 '%d',
549 '%s',
550 '%s',
551 '%d',
552 '%s',
553 );
554 if( $reason->orderId )
555 {
556 $values['order_id'] = $reason->orderId;
557 $formats[] = '%d';
558 }
559 if( $reason->providerId )
560 {
561 $values['origin2'] = $reason->providerId;
562 $formats[] = '%d';
563 }
564
565 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
566 $wpdb->insert($wpdb->lwsWooRewardsHistoric, $values, $formats);
567 $this->lastLogId = $wpdb->insert_id;
568 return $this;
569 }
570
571 /** @return array history rows as array of object
572 * {trace_id, user_id, stack, date, move, total, origin, provider_id, order_id, blog_id, comments}
573 * @param $args (array) define values tested in where clause (equal).
574 * All returned field testable against a value or array of value, except comments.
575 * prefix field name by ! to neg the test.
576 * In addition, you can set:
577 * start (DateTime), end (DateTime) */
578 static function queryTrace($args)
579 {
580 global $wpdb;
581 $select = array(
582 'id as trace_id',
583 'user_id',
584 '`stack`',
585 'mvt_date as `date`',
586 'points_moved as `move`',
587 'new_total as `total`',
588 '`origin`',
589 '`origin2` as provider_id',
590 'order_id',
591 'blog_id',
592 'commentar as `comments`',
593 );
594 $where = array();
595 foreach( $args as $key => $value )
596 {
597 if( $neg = (substr($key, 0, 1) == '!') )
598 $key = substr($key, 1);
599
600 switch($key)
601 {
602 case 'trace_id' : $where[] = self::clause('id' , $value, $neg); break;
603 case 'user_id' : $where[] = self::clause('user_id' , $value, $neg); break;
604 case 'stack' : $where[] = self::clause('stack' , $value, $neg); break;
605 case 'date' : $where[] = self::clause('mvt_date' , $value, $neg); break;
606 case 'move' : $where[] = self::clause('points_moved', $value, $neg); break;
607 case 'total' : $where[] = self::clause('new_total' , $value, $neg); break;
608 case 'origin' : $where[] = self::clause('origin' , $value, $neg); break;
609 case 'provider_id': $where[] = self::clause('origin2' , $value, $neg); break;
610 case 'order_id' : $where[] = self::clause('order_id' , $value, $neg); break;
611 case 'blog_id' : $where[] = self::clause('blog_id' , $value, $neg); break;
612 case 'start':
613 $where[] = sprintf("mvt_date >= DATE('%s')", $value->format('Y-m-d H:i:s'));
614 break;
615 case 'end':
616 $where[] = sprintf("mvt_date <= DATE('%s')", $value->format('Y-m-d H:i:s'));
617 break;
618 }
619 }
620
621 $query = \LWS\Adminpanel\Tools\Request::from($wpdb->lwsWooRewardsHistoric);
622 $query->select($select);
623 $query->order(array('mvt_date DESC', 'id DESC'));
624 if( $where )
625 $query->where($where);
626 return $query->getResults();
627 }
628
629 static protected function clause($key, $value, $neg=false)
630 {
631 $value = \esc_sql($value);
632 if( is_array($value) )
633 return sprintf("`%s` %s ('%s')", $key, $neg ? 'NOT IN' : 'IN', implode("','", $value));
634 else
635 return sprintf("`%s` %s '%s'", $key, $neg ? '!=' : '=', $value);
636 }
637
638 /** Get point move history.
639 * @return (array) each value is an object with:
640 * * trace_id
641 * * user_id
642 * * stack (string) : the id of point stack
643 * * date (string) : the move date, can be used with \date_create().
644 * * move (int) : points moved
645 * * total (int) : point total after the move
646 * * origin (false|int|string|array) : source of move, any text, a event id or an unlockable id.
647 * * origin2 (null|int)
648 * * comments (string)
649 * @param $dateStart (false|\DateTime) only after the date if not false
650 * @param $dateEnd (false|\DateTime) only before the date if not false
651 * @param $origin (false|string|array) any origin if false (strict compare, empty string is not false)
652 * @param $origin2 (false|int|array) same but for origin2 with integers
653 * @param $userId (false|int|array) the stack the use was init for (if any) if false or override with a user id or an array of (int) user id.
654 */
655 function getTraces($dateStart, $dateEnd, $origin=false, $origin2=false, $userId=false, $withComments=false)
656 {
657 global $wpdb;
658 $sql = "SELECT id as trace_id, user_id, `stack`, mvt_date as `date`, points_moved as `move`, new_total as `total`, `origin`, `origin2`";
659 if( $withComments )
660 $sql .= ", commentar as `comments`";
661 $sql .= (' FROM ' . self::table());
662
663 $where = array();
664 $prepare = array();
665 if( $dateStart )
666 {
667 $where[] = 'mvt_date>=FROM_UNIXTIME(%d)';
668 $prepare[] = $dateStart->getTimestamp();
669 }
670 if( $dateEnd )
671 {
672 $where[] = 'mvt_date<=FROM_UNIXTIME(%d)';
673 $prepare[] = $dateEnd->getTimestamp();
674 }
675 if( false !== $origin )
676 {
677 if( is_array($origin) )
678 {
679 if( $origin )
680 {
681 $in = implode("','", array_map('\esc_sql', $origin));
682 $where[] = "origin IN ('{$in}')";
683 }
684 }
685 else
686 {
687 $where[] = 'origin=%s';
688 $prepare[] = $origin;
689 }
690 }
691 if( false !== $origin2 )
692 {
693 if( is_array($origin2) )
694 {
695 if( $origin2 )
696 {
697 $in = implode(',', array_map('\intval', $origin2));
698 $where[] = "origin2 IN ({$in})";
699 }
700 }
701 else
702 {
703 $where[] = 'origin2=%d';
704 $prepare[] = $origin2;
705 }
706 }
707 if( is_array($userId) )
708 {
709 if( $userId )
710 {
711 $in = implode(',', array_map('\intval', $userId));
712 $where[] = "user_id IN ({$in})";
713 }
714 }
715 else
716 {
717 if( false === $userId )
718 $userId = $this->userId;
719 if( $userId )
720 {
721 $where[] = 'user_id=%d';
722 $prepare[] = $userId;
723 }
724 }
725
726 if( !$where ) {
727 if (defined('WP_DEBUG') && WP_DEBUG) error_log("Read point history with any WHERE clause could lead to too many result."); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
728 }
729 else
730 $sql .= (' WHERE ' . implode(' AND ', $where));
731
732 $sql .= " ORDER BY mvt_date DESC, id DESC";
733 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
734 $traces = $wpdb->get_results($prepare ? $wpdb->prepare($sql, $prepare) : $sql);
735 if( false === $traces )
736 {
737 if (defined('WP_DEBUG') && WP_DEBUG) error_log("An error occured during point history table read."); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
738 return array();
739 }
740 return $traces;
741 }
742
743 /** @see getTraces but with just-in-time translated comments */
744 function getFormatedTraces($dateStart, $dateEnd, $origin=false, $origin2=false, $userId=false)
745 {
746 $traces = $this->getTraces($dateStart, $dateEnd, $origin, $origin2, $userId, true);
747 if( $traces )
748 {
749 foreach( $traces as &$row )
750 {
751 if ($row->origin && \is_numeric($row->origin)) {
752 $title = $this->getOriginTitle($row->origin);
753 if ($title) {
754 $row->comments = $title;
755 continue; // skip translation of original reason
756 }
757 }
758 if ($row->comments && \is_serialized($row->comments)) {
759 $reason = @unserialize($row->comments);
760 if( $reason && is_array($reason) )
761 $row->comments = \LWS\WOOREWARDS\Core\Trace::reasonToString($reason, true);
762 }
763 }
764 }
765 return $traces;
766 }
767
768 /** Read origin title from Event and Unlockable. */
769 protected function getOriginTitle($origin)
770 {
771 static $replaceReason = null;
772 static $origins = array();
773 if (null === $replaceReason) {
774 $replaceReason = \apply_filters('lws_woorewards_stack_history_prefers_origin_title', true);
775 if ($replaceReason) {
776 // load existant origins that implement `function getTitleAsReason()`
777 foreach (\get_posts(array('post_type' => \LWS\WOOREWARDS\Abstracts\Event::POST_TYPE, 'numberposts' => -1)) as $post) {
778 $origins[$post->ID] = \LWS\WOOREWARDS\Abstracts\Event::fromPost($post);
779 }
780 foreach (\get_posts(array('post_type' => \LWS\WOOREWARDS\Abstracts\Unlockable::POST_TYPE, 'numberposts' => -1)) as $post) {
781 $origins[$post->ID] = \LWS\WOOREWARDS\Abstracts\Unlockable::fromPost($post);
782 }
783 }
784 }
785 if ($replaceReason) {
786 if (isset($origins[$origin])) {
787 $title = $origins[$origin]->getTitleAsReason();
788 if ($title)
789 return $title;
790 }
791 }
792 return false;
793 }
794
795 static function table()
796 {
797 global $wpdb;
798 return $wpdb->lwsWooRewardsHistoric;
799 }
800
801 /** Never call, only to have poedit/wpml able to extract the sentance. */
802 private function poeditDeclare()
803 {
804 /* translators: %s: source stack name */
805 __("Points merged from %s", 'woorewards-lite');
806 __("Lost due to inactivity", 'woorewards-lite');
807 }
808 }