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
fluent-support / app / Hooks / Handlers / CleanupHandler.php

CleanupHandler.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.4.5, at app/Hooks/Handlers/CleanupHandler.php

300 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentSupport\App\Hooks\Handlers;
4
5 use FluentSupport\App\Models\Activity;
6 use FluentSupport\App\Models\AIActivityLogs;
7 use FluentSupport\App\Models\Attachment;
8 use FluentSupport\App\Models\Meta;
9 use FluentSupport\App\Services\EmailNotification\Settings;
10 use FluentSupport\App\Services\Helper;
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;
15
16 class CleanupHandler
17 {
18 public function initHourlyTasks()
19 {
20 $this->cleanLiveActivities();
21 $this->maybeDeleteOldTempFiles();
22 $this->cleanAbandonedTempAttachments();
23 $this->cleanExpiredAuthChallenges();
24 }
25
26 public function initDailyTasks()
27 {
28 $this->cleanActivityLogs();
29
30 $this->cleanAIActivityLogs();
31
32 $this->cleanInternalNotifications();
33 }
34
35 protected function cleanLiveActivities()
36 {
37 $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - 86400);
38
39 Meta::where('key', '_live_activity')
40 ->where('object_type', 'ticket_meta')
41 ->where('updated_at', '<', $oldDateTime)
42 ->delete();
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
60 protected function cleanActivityLogs()
61 {
62 $settings = Helper::getOption('_activity_settings', []);
63
64 if (empty($settings['delete_days'])) {
65 $settings['delete_days'] = 14;
66 }
67
68 $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - ($settings['delete_days'] * 86400));
69
70 $this->deleteInBatches(function () use ($oldDateTime) {
71 return Activity::where('created_at', '<', $oldDateTime);
72 });
73 }
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
155 public function maybeDeleteAttachmentsOnClose($ticket)
156 {
157 $settings = (new Settings())->globalBusinessSettings();
158 if ($settings['del_files_on_close'] == 'yes') {
159 $this->deleteTicketAttachments($ticket);
160 }
161 }
162
163 public function deleteTicketAttachments($ticket)
164 {
165 $uploadDir = wp_upload_dir();
166 $dir = $uploadDir['basedir'] . '/' . FLUENT_SUPPORT_UPLOAD_DIR;
167
168 $attachments = Attachment::where('ticket_id', $ticket->id)->get();
169
170 if (!$attachments->isEmpty()) {
171 $ticketDir = $dir . '/ticket_' . $ticket->id;
172 if (is_dir($ticketDir)) {
173 $this->deleteDir($ticketDir);
174 }
175
176 foreach ($attachments as $attachment) {
177 if ($attachment->driver != 'local') {
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);
181 }
182 }
183
184 Attachment::where('ticket_id', $ticket->id)->delete();
185 }
186
187 }
188
189 private function deleteDir($dir)
190 {
191 if (!class_exists('\WP_Filesystem_Direct')) {
192 require_once(ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php');
193 require_once(ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php');
194 }
195
196 $fileSystemDirect = new \WP_Filesystem_Direct(false);
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;
298 }
299 }
300