PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / trunk
Fluent Support – Helpdesk & Customer Support Ticket System vtrunk
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 1.5.5 All 68 releases
fluent-support / app / Hooks / Handlers / CleanupHandler.php

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

269 lines 9.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 // Delete All Live Activity older than 24 hours
38 $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - 86400);
39
40 Meta::where('key', '_live_activity')
41 ->where('object_type', 'ticket_meta')
42 ->where('updated_at', '<', $oldDateTime)
43 ->delete();
44 }
45
46 protected function cleanExpiredAuthChallenges()
47 {
48 // Signup/2FA verification codes are only ever valid for a few minutes (see
49 // EmailVerificationHandler::CODE_TTL_SECONDS and TwoFaHandler::CODE_TTL_SECONDS);
50 // an hour is a generous margin so this never races a still-valid code.
51 $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - HOUR_IN_SECONDS);
52
53 // Challenges issued by earlier versions were written with the query builder,
54 // which does not stamp created_at, so those rows are NULL and can never match
55 // the range check. They pre-date this upgrade and are long past their TTL, so
56 // they are purged alongside the expired ones.
57 Meta::whereIn('object_type', ['fs_login_hashes', 'fs_2fa'])
58 ->where(function ($query) use ($oldDateTime) {
59 $query->where('created_at', '<', $oldDateTime)
60 ->orWhereNull('created_at');
61 })
62 ->delete();
63 }
64
65 protected function cleanActivityLogs()
66 {
67 $settings = Helper::getOption('_activity_settings', []);
68
69 if (!$settings && empty($settings['delete_days'])) {
70 $settings['delete_days'] = 14;
71 }
72
73 $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - ($settings['delete_days'] * 86400));
74
75 Activity::where('created_at', '<', $oldDateTime)->delete();
76 }
77
78 protected function cleanAIActivityLogs()
79 {
80 if ((!defined('FLUENT_SUPPORT_PRO_DIR_FILE') || !Helper::openAIIntegrationStatus())
81 && !Helper::fluentBotIntegrationStatus()) {
82 return;
83 }
84
85 $settings = Helper::getOption('_ai_activity_settings', []);
86
87 $defaultDays = 14;
88
89 if (!empty($settings['delete_days'])) {
90 $defaultDays = (int)$settings['delete_days'];
91 }
92
93 $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - ($defaultDays * 86400));
94
95 AIActivityLogs::where('created_at', '<', $oldDateTime)->delete();
96 }
97
98 protected function cleanInternalNotifications()
99 {
100 global $wpdb;
101
102 $notificationsTable = $wpdb->prefix . NotificationsMigrator::$tableName;
103 $notificationUsersTable = $wpdb->prefix . NotificationUsersMigrator::$tableName;
104
105 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $notificationsTable)) !== $notificationsTable
106 || $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $notificationUsersTable)) !== $notificationUsersTable) {
107 return;
108 }
109
110 $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - (15 * 86400));
111
112 $wpdb->query($wpdb->prepare(
113 "DELETE notification_users
114 FROM {$notificationUsersTable} AS notification_users
115 INNER JOIN {$notificationsTable} AS notifications
116 ON notification_users.notification_id = notifications.id
117 WHERE notifications.created_at < %s",
118 $oldDateTime
119 ));
120
121 $wpdb->query($wpdb->prepare(
122 "DELETE FROM {$notificationsTable} WHERE created_at < %s",
123 $oldDateTime
124 ));
125 }
126
127 public function maybeDeleteAttachmentsOnClose($ticket)
128 {
129 $settings = (new Settings())->globalBusinessSettings();
130 if ($settings['del_files_on_close'] == 'yes') {
131 $this->deleteTicketAttachments($ticket);
132 }
133 }
134
135 public function deleteTicketAttachments($ticket)
136 {
137 $uploadDir = wp_upload_dir();
138 $dir = $uploadDir['basedir'] . '/' . FLUENT_SUPPORT_UPLOAD_DIR;
139
140 $attachments = Attachment::where('ticket_id', $ticket->id)->get();
141
142 if (!$attachments->isEmpty()) {
143 $ticketDir = $dir . '/ticket_' . $ticket->id;
144 if (is_dir($ticketDir)) {
145 $this->deleteDir($ticketDir);
146 }
147
148 foreach ($attachments as $attachment) {
149 if ($attachment->driver != 'local') {
150 do_action('fluent_support/delete_remote_attachment_' . $attachment->driver, $attachment, $ticket->id);
151 } else if (file_exists($attachment->file_path)) {
152 wp_delete_file($attachment->file_path);
153 }
154 }
155
156 Attachment::where('ticket_id', $ticket->id)->delete();
157 }
158
159 }
160
161 private function deleteDir($dir)
162 {
163 if (!class_exists('\WP_Filesystem_Direct')) {
164 require_once(ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php');
165 require_once(ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php');
166 }
167
168 $fileSystemDirect = new \WP_Filesystem_Direct(false);
169 $fileSystemDirect->rmdir($dir, true);
170 }
171
172 public function maybeMaintanceTask()
173 {
174 (new Maintenance())->maybeProcessData();
175
176 $this->maybeRemoveOldScheuledActionLogs();
177 }
178
179 private function maybeDeleteOldTempFiles()
180 {
181 $dir = FileSystem::getDir();
182
183 // loop through files in directory
184 foreach (glob($dir . '/temp_files/*') as $filename) {
185 // check if file was created before last 2 hours
186 if (time() - filectime($filename) >= 7200) { // 2 hours
187 wp_delete_file($filename); // delete file
188 }
189 }
190 }
191
192 /**
193 * An upload stays 'in-active' until its hash is submitted with a ticket or reply,
194 * at which point TicketService/ResponseService flip it to 'active' (or 'inline' for
195 * pasted images). A hash that is never submitted keeps its row forever, and
196 * UploaderController::checkAttachmentQuota() counts those rows against the
197 * per-ticket upload limit — so abandoned uploads permanently consume the quota.
198 *
199 * maybeDeleteOldTempFiles() already deletes the underlying file at 2 hours, so
200 * these rows point at files that no longer exist. The same window is used here to
201 * keep the row lifetime aligned with the file lifetime.
202 *
203 * Rows with a NULL created_at are skipped rather than swept in: unlike the auth
204 * challenges above, they are not necessarily expired. TicketController's bulk reply
205 * writes clones with the query builder, which does not stamp created_at, and those
206 * rows are flipped to 'active' moments later in the same request. They also carry a
207 * ticket_id and no person_id, so they can never match the quota check anyway.
208 */
209 protected function cleanAbandonedTempAttachments()
210 {
211 $oldDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') - 7200); // 2 hours
212
213 // Bounded per run: purging touches the filesystem (and remote storage) per row,
214 // so a large first-run backlog drains over several hourly passes instead of
215 // stalling one cron tick.
216 $attachments = Attachment::where('status', 'in-active')
217 ->where('created_at', '<', $oldDateTime)
218 ->limit(500)
219 ->get();
220
221 if ($attachments->isEmpty()) {
222 return;
223 }
224
225 // Removes the local temp file, or fires the remote-driver hook when the
226 // abandoned upload was already pushed to Dropbox/GDrive/etc.
227 Attachment::purgeAttachments($attachments);
228
229 Attachment::whereIn('id', $attachments->pluck('id')->toArray())->delete();
230 }
231
232
233 function maybeRemoveOldScheuledActionLogs($group_slug = 'fluent-support', $days_old = 7)
234 {
235 global $wpdb;
236
237 // Get the timestamp for 7 days ago
238 $cutoff_date = gmdate('Y-m-d H:i:s', strtotime("-{$days_old} days"));
239
240 // Get the group ID
241 $group_id = $wpdb->get_var($wpdb->prepare(
242 "SELECT group_id FROM {$wpdb->prefix}actionscheduler_groups WHERE slug = %s",
243 $group_slug
244 ));
245
246 if (!$group_id) {
247 return false; // Group not found
248 }
249
250 // Delete old actions and their associated logs
251 $deleted = $wpdb->query($wpdb->prepare("
252 DELETE a, l
253 FROM {$wpdb->prefix}actionscheduler_actions a
254 LEFT JOIN {$wpdb->prefix}actionscheduler_logs l ON a.action_id = l.action_id
255 WHERE a.group_id = %d
256 AND a.status IN ('complete', 'failed')
257 AND a.scheduled_date_gmt < %s", $group_id, $cutoff_date));
258
259 // Clean up orphaned claims
260 $wpdb->query("
261 DELETE c
262 FROM {$wpdb->prefix}actionscheduler_claims c
263 LEFT JOIN {$wpdb->prefix}actionscheduler_actions a ON c.claim_id = a.claim_id
264 WHERE a.action_id IS NULL");
265
266 return $deleted;
267 }
268 }
269