PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.5
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.5
2.4.5 2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 All 69 releases
← All changes | app/Hooks/Handlers/CleanupHandler.php +217 -10 1.5.4 → 2.4.5 View file →
@@ -2,13 +2,17 @@
2 2
3 3 namespace FluentSupport\App\Hooks\Handlers;
4 4
5 5 use FluentSupport\App\Models\Activity;
6 +use FluentSupport\App\Models\AIActivityLogs;
6 7 use FluentSupport\App\Models\Attachment;
7 8 use FluentSupport\App\Models\Meta;
8 9 use FluentSupport\App\Services\EmailNotification\Settings;
9 10 use FluentSupport\App\Services\Helper;
10 11 use FluentSupport\App\Services\Includes\FileSystem;
12 +use FluentSupport\App\Services\Integrations\Maintenance;
13 +use FluentSupport\Database\Migrations\NotificationsMigrator;
14 +use FluentSupport\Database\Migrations\NotificationUsersMigrator;
11 15
12 16 class CleanupHandler
13 17 {
14 18 public function initHourlyTasks()
@@ -13,19 +17,25 @@
13 17 {
14 18 public function initHourlyTasks()
15 19 {
16 20 $this->cleanLiveActivities();
21 + $this->maybeDeleteOldTempFiles();
22 + $this->cleanAbandonedTempAttachments();
23 + $this->cleanExpiredAuthChallenges();
17 24 }
18 25
19 26 public function initDailyTasks()
20 27 {
21 28 $this->cleanActivityLogs();
29 +
30 + $this->cleanAIActivityLogs();
31 +
32 + $this->cleanInternalNotifications();
22 33 }
23 34
24 35 protected function cleanLiveActivities()
25 36 {
26 - // Delete All Live Activity older than 24 hours
27 - $oldDateTime = date('Y-m-d H:i:s', strtotime(current_time('mysql')) - 86400);
37 + $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - 86400);
28 38
29 39 Meta::where('key', '_live_activity')
30 40 ->where('object_type', 'ticket_meta')
31 41 ->where('updated_at', '<', $oldDateTime)
@@ -31,21 +41,118 @@
31 41 ->where('updated_at', '<', $oldDateTime)
32 42 ->delete();
33 43 }
34 44
45 + protected function cleanExpiredAuthChallenges()
46 + {
47 + // Codes live for minutes (see the CODE_TTL_SECONDS constants), so an hour
48 + // never races a still-valid code.
49 + $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - HOUR_IN_SECONDS);
50 +
51 + // Older versions wrote these rows without created_at. They are long expired.
52 + Meta::whereIn('object_type', ['fs_login_hashes', 'fs_2fa'])
53 + ->where(function ($query) use ($oldDateTime) {
54 + $query->where('created_at', '<', $oldDateTime)
55 + ->orWhereNull('created_at');
56 + })
57 + ->delete();
58 + }
59 +
35 60 protected function cleanActivityLogs()
36 61 {
37 62 $settings = Helper::getOption('_activity_settings', []);
38 63
39 - if (!$settings && empty($settings['delete_days'])) {
64 + if (empty($settings['delete_days'])) {
40 65 $settings['delete_days'] = 14;
41 66 }
42 67
43 - $oldDateTime = date('Y-m-d H:i:s', strtotime(current_time('mysql')) - ($settings['delete_days'] * 86400));
68 + $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - ($settings['delete_days'] * 86400));
44 69
45 - Activity::where('created_at', '<', $oldDateTime)->delete();
70 + $this->deleteInBatches(function () use ($oldDateTime) {
71 + return Activity::where('created_at', '<', $oldDateTime);
72 + });
46 73 }
47 74
75 + /**
76 + * Delete the rows $query() matches a batch at a time, so a large backlog
77 + * (say, after shortening the retention) never locks the table in one
78 + * statement. Stops when the time budget is spent; the next run continues.
79 + *
80 + * @param callable $query Returns a fresh model query for the rows to delete.
81 + * @param int $batchSize
82 + * @param int $timeBudget Seconds.
83 + * @return int Rows deleted.
84 + */
85 + public function deleteInBatches(callable $query, $batchSize = 1000, $timeBudget = 20)
86 + {
87 + $started = time();
88 + $deleted = 0;
89 +
90 + do {
91 + $ids = $query()->orderBy('id', 'ASC')->limit($batchSize)->pluck('id')->all();
92 + if (!$ids) {
93 + break;
94 + }
95 +
96 + $deleted += $query()->whereIn('id', $ids)->delete();
97 + } while (count($ids) === $batchSize && (time() - $started) < $timeBudget);
98 +
99 + return $deleted;
100 + }
101 +
102 + protected function cleanAIActivityLogs()
103 + {
104 + if ((!defined('FLUENT_SUPPORT_PRO_DIR_FILE') || !Helper::openAIIntegrationStatus())
105 + && !Helper::fluentBotIntegrationStatus()) {
106 + return;
107 + }
108 +
109 + $settings = Helper::getOption('_ai_activity_settings', []);
110 +
111 + $defaultDays = 14;
112 +
113 + if (!empty($settings['delete_days'])) {
114 + $defaultDays = (int)$settings['delete_days'];
115 + }
116 +
117 + $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - ($defaultDays * 86400));
118 +
119 + $this->deleteInBatches(function () use ($oldDateTime) {
120 + return AIActivityLogs::where('created_at', '<', $oldDateTime);
121 + });
122 + }
123 +
124 + protected function cleanInternalNotifications()
125 + {
126 + global $wpdb;
127 +
128 + $notificationsTable = $wpdb->prefix . NotificationsMigrator::$tableName;
129 + $notificationUsersTable = $wpdb->prefix . NotificationUsersMigrator::$tableName;
130 +
131 + if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $notificationsTable)) !== $notificationsTable
132 + || $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $notificationUsersTable)) !== $notificationUsersTable) {
133 + return;
134 + }
135 +
136 + $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - (15 * 86400));
137 +
138 + // A batch of old notifications at a time, recipients first.
139 + $started = time();
140 + do {
141 + $ids = array_map('intval', $wpdb->get_col($wpdb->prepare(
142 + "SELECT id FROM {$notificationsTable} WHERE created_at < %s ORDER BY id ASC LIMIT 1000",
143 + $oldDateTime
144 + )));
145 + if (!$ids) {
146 + break;
147 + }
148 +
149 + $idList = implode(',', $ids);
150 + $wpdb->query("DELETE FROM {$notificationUsersTable} WHERE notification_id IN ({$idList})"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- integer ids
151 + $wpdb->query("DELETE FROM {$notificationsTable} WHERE id IN ({$idList})"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- integer ids
152 + } while (count($ids) === 1000 && (time() - $started) < 20);
153 + }
154 +
48 155 public function maybeDeleteAttachmentsOnClose($ticket)
49 156 {
50 157 $settings = (new Settings())->globalBusinessSettings();
51 158 if ($settings['del_files_on_close'] == 'yes') {
@@ -55,9 +162,9 @@
55 162
56 163 public function deleteTicketAttachments($ticket)
57 164 {
58 165 $uploadDir = wp_upload_dir();
59 - $dir = $uploadDir['basedir'] . FLUENT_SUPPORT_UPLOAD_DIR;
166 + $dir = $uploadDir['basedir'] . '/' . FLUENT_SUPPORT_UPLOAD_DIR;
60 167
61 168 $attachments = Attachment::where('ticket_id', $ticket->id)->get();
62 169
63 170 if (!$attachments->isEmpty()) {
@@ -67,13 +174,12 @@
67 174 }
68 175
69 176 foreach ($attachments as $attachment) {
70 177 if ($attachment->driver != 'local') {
71 - continue;
178 + do_action('fluent_support/delete_remote_attachment_' . $attachment->driver, $attachment, $ticket->id);
179 + } else if (file_exists($attachment->file_path)) {
180 + wp_delete_file($attachment->file_path);
72 181 }
73 - if (file_exists($attachment->file_path)) {
74 - @unlink($attachment->file_path);
75 - }
76 182 }
77 183
78 184 Attachment::where('ticket_id', $ticket->id)->delete();
79 185 }
@@ -85,8 +191,109 @@
85 191 if (!class_exists('\WP_Filesystem_Direct')) {
86 192 require_once(ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php');
87 193 require_once(ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php');
88 194 }
195 +
89 196 $fileSystemDirect = new \WP_Filesystem_Direct(false);
90 197 $fileSystemDirect->rmdir($dir, true);
198 + }
199 +
200 + public function maybeMaintanceTask()
201 + {
202 + (new Maintenance())->maybeProcessData();
203 +
204 + $this->maybeRemoveOldScheuledActionLogs();
205 + }
206 +
207 + private function maybeDeleteOldTempFiles()
208 + {
209 + $dir = FileSystem::getDir();
210 +
211 + foreach (glob($dir . '/temp_files/*') as $filename) {
212 + if (time() - filectime($filename) >= 7200) { // 2 hours
213 + wp_delete_file($filename);
214 + }
215 + }
216 + }
217 +
218 + /**
219 + * Uploads stay 'in-active' until submitted with a ticket or reply. Abandoned ones
220 + * count against the upload quota, so drop them after the same 2 hours that
221 + * maybeDeleteOldTempFiles() uses for the files.
222 + *
223 + * NULL created_at rows are left alone: bulk reply writes them without a timestamp
224 + * and activates them within the same request.
225 + */
226 + protected function cleanAbandonedTempAttachments()
227 + {
228 + $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - 7200); // 2 hours
229 +
230 + // Each row hits the filesystem or remote storage, so drain a backlog over several runs.
231 + $attachments = Attachment::where('status', 'in-active')
232 + ->where('created_at', '<', $oldDateTime)
233 + ->limit(500)
234 + ->get();
235 +
236 + if ($attachments->isEmpty()) {
237 + return;
238 + }
239 +
240 + Attachment::purgeAttachments($attachments);
241 +
242 + Attachment::whereIn('id', $attachments->pluck('id')->toArray())->delete();
243 + }
244 +
245 +
246 + function maybeRemoveOldScheuledActionLogs($group_slug = 'fluent-support', $days_old = 7)
247 + {
248 + global $wpdb;
249 +
250 + $cutoff_date = gmdate('Y-m-d H:i:s', strtotime("-{$days_old} days"));
251 +
252 + $group_id = $wpdb->get_var($wpdb->prepare(
253 + "SELECT group_id FROM {$wpdb->prefix}actionscheduler_groups WHERE slug = %s",
254 + $group_slug
255 + ));
256 +
257 + if (!$group_id) {
258 + return false;
259 + }
260 +
261 + $actionsTable = $wpdb->prefix . 'actionscheduler_actions';
262 + $logsTable = $wpdb->prefix . 'actionscheduler_logs';
263 + $claimsTable = $wpdb->prefix . 'actionscheduler_claims';
264 +
265 + $deleted = 0;
266 + $started = time();
267 +
268 + // A batch at a time, and only our own actions, logs and claims.
269 + do {
270 + $rows = $wpdb->get_results($wpdb->prepare(
271 + "SELECT action_id, claim_id FROM {$actionsTable}
272 + WHERE group_id = %d AND status IN ('complete', 'failed') AND scheduled_date_gmt < %s
273 + ORDER BY action_id ASC LIMIT 1000",
274 + $group_id, $cutoff_date
275 + ));
276 + if (!$rows) {
277 + break;
278 + }
279 +
280 + $actionIds = implode(',', array_map('intval', wp_list_pluck($rows, 'action_id')));
281 + $claimIds = array_filter(array_map('intval', wp_list_pluck($rows, 'claim_id')));
282 +
283 + $wpdb->query("DELETE FROM {$logsTable} WHERE action_id IN ({$actionIds})"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- integer ids
284 + $deleted += (int) $wpdb->query("DELETE FROM {$actionsTable} WHERE action_id IN ({$actionIds})"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- integer ids
285 +
286 + if ($claimIds) {
287 + // Claims those actions held, once no action uses them.
288 + $claimList = implode(',', array_unique($claimIds));
289 + $wpdb->query(
290 + "DELETE c FROM {$claimsTable} c
291 + LEFT JOIN {$actionsTable} a ON a.claim_id = c.claim_id
292 + WHERE c.claim_id IN ({$claimList}) AND a.action_id IS NULL"
293 + ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- integer ids
294 + }
295 + } while (count($rows) === 1000 && (time() - $started) < 20);
296 +
297 + return $deleted;
91 298 }
92 299 }