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 +128 -36 2.2.0 → 2.4.5 View file →
@@ -18,8 +18,10 @@
18 18 public function initHourlyTasks()
19 19 {
20 20 $this->cleanLiveActivities();
21 21 $this->maybeDeleteOldTempFiles();
22 + $this->cleanAbandonedTempAttachments();
23 + $this->cleanExpiredAuthChallenges();
22 24 }
23 25
24 26 public function initDailyTasks()
25 27 {
@@ -31,9 +33,8 @@
31 33 }
32 34
33 35 protected function cleanLiveActivities()
34 36 {
35 - // Delete All Live Activity older than 24 hours
36 37 $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - 86400);
37 38
38 39 Meta::where('key', '_live_activity')
39 40 ->where('object_type', 'ticket_meta')
@@ -40,21 +41,65 @@
40 41 ->where('updated_at', '<', $oldDateTime)
41 42 ->delete();
42 43 }
43 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 +
44 60 protected function cleanActivityLogs()
45 61 {
46 62 $settings = Helper::getOption('_activity_settings', []);
47 63
48 - if (!$settings && empty($settings['delete_days'])) {
64 + if (empty($settings['delete_days'])) {
49 65 $settings['delete_days'] = 14;
50 66 }
51 67
52 68 $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - ($settings['delete_days'] * 86400));
53 69
54 - Activity::where('created_at', '<', $oldDateTime)->delete();
70 + $this->deleteInBatches(function () use ($oldDateTime) {
71 + return Activity::where('created_at', '<', $oldDateTime);
72 + });
55 73 }
56 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 +
57 102 protected function cleanAIActivityLogs()
58 103 {
59 104 if ((!defined('FLUENT_SUPPORT_PRO_DIR_FILE') || !Helper::openAIIntegrationStatus())
60 105 && !Helper::fluentBotIntegrationStatus()) {
@@ -70,9 +115,11 @@
70 115 }
71 116
72 117 $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - ($defaultDays * 86400));
73 118
74 - AIActivityLogs::where('created_at', '<', $oldDateTime)->delete();
119 + $this->deleteInBatches(function () use ($oldDateTime) {
120 + return AIActivityLogs::where('created_at', '<', $oldDateTime);
121 + });
75 122 }
76 123
77 124 protected function cleanInternalNotifications()
78 125 {
@@ -87,21 +134,23 @@
87 134 }
88 135
89 136 $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - (15 * 86400));
90 137
91 - $wpdb->query($wpdb->prepare(
92 - "DELETE notification_users
93 - FROM {$notificationUsersTable} AS notification_users
94 - INNER JOIN {$notificationsTable} AS notifications
95 - ON notification_users.notification_id = notifications.id
96 - WHERE notifications.created_at < %s",
97 - $oldDateTime
98 - ));
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 + }
99 148
100 - $wpdb->query($wpdb->prepare(
101 - "DELETE FROM {$notificationsTable} WHERE created_at < %s",
102 - $oldDateTime
103 - ));
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);
104 153 }
105 154
106 155 public function maybeDeleteAttachmentsOnClose($ticket)
107 156 {
@@ -158,26 +207,49 @@
158 207 private function maybeDeleteOldTempFiles()
159 208 {
160 209 $dir = FileSystem::getDir();
161 210
162 - // loop through files in directory
163 211 foreach (glob($dir . '/temp_files/*') as $filename) {
164 - // check if file was created before last 2 hours
165 212 if (time() - filectime($filename) >= 7200) { // 2 hours
166 - wp_delete_file($filename); // delete file
213 + wp_delete_file($filename);
167 214 }
168 215 }
169 216 }
170 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
171 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 +
172 246 function maybeRemoveOldScheuledActionLogs($group_slug = 'fluent-support', $days_old = 7)
173 247 {
174 248 global $wpdb;
175 249
176 - // Get the timestamp for 7 days ago
177 250 $cutoff_date = gmdate('Y-m-d H:i:s', strtotime("-{$days_old} days"));
178 251
179 - // Get the group ID
180 252 $group_id = $wpdb->get_var($wpdb->prepare(
181 253 "SELECT group_id FROM {$wpdb->prefix}actionscheduler_groups WHERE slug = %s",
182 254 $group_slug
183 255 ));
@@ -182,26 +254,46 @@
182 254 $group_slug
183 255 ));
184 256
185 257 if (!$group_id) {
186 - return false; // Group not found
258 + return false;
187 259 }
188 260
189 - // Delete old actions and their associated logs
190 - $deleted = $wpdb->query($wpdb->prepare("
191 - DELETE a, l
192 - FROM {$wpdb->prefix}actionscheduler_actions a
193 - LEFT JOIN {$wpdb->prefix}actionscheduler_logs l ON a.action_id = l.action_id
194 - WHERE a.group_id = %d
195 - AND a.status IN ('complete', 'failed')
196 - AND a.scheduled_date_gmt < %s", $group_id, $cutoff_date));
261 + $actionsTable = $wpdb->prefix . 'actionscheduler_actions';
262 + $logsTable = $wpdb->prefix . 'actionscheduler_logs';
263 + $claimsTable = $wpdb->prefix . 'actionscheduler_claims';
197 264
198 - // Clean up orphaned claims
199 - $wpdb->query("
200 - DELETE c
201 - FROM {$wpdb->prefix}actionscheduler_claims c
202 - LEFT JOIN {$wpdb->prefix}actionscheduler_actions a ON c.claim_id = a.claim_id
203 - WHERE a.action_id IS NULL");
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);
204 296
205 297 return $deleted;
206 298 }
207 299 }