PluginProbe
Hash Form – Drag & Drop Form Builder / trunk
Hash Form – Drag & Drop Form Builder vtrunk
1.4.4 1.4.3 1.4.2 1.4.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.6.1 1.2.7 1.2.8 1.2.9 1.3.0 All 47 releases
hash-form / includes / HashFormCron.php

HashFormCron.php in Hash Form – Drag & Drop Form Builder trunk, at includes/HashFormCron.php

196 lines 6.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 defined('ABSPATH') || die();
4
5 /**
6 * The plugin's one scheduled event.
7 *
8 * Nothing was ever scheduled before this: there is no wp_schedule_event
9 * anywhere in either plugin. That is why abandoned checkouts sat pending
10 * forever, and why nothing that accumulates - analytics rows, generated pdfs,
11 * uploads whose entry is long gone - was ever cleared up.
12 *
13 * One daily event rather than one per job, and add-ons hook it. Pro attaches
14 * its own housekeeping to the same hook, so a site has a single cron entry to
15 * see, disable or reschedule.
16 *
17 * Everything that DELETES is off unless the site turns it on. A plugin update
18 * that quietly starts removing data on a timer is worse than the mess it
19 * cleans up, so the default is to do nothing destructive and let the owner
20 * decide. What runs unasked is limited to work that can be undone.
21 */
22 class HashFormCron {
23
24 const HOOK = 'hashform_daily_maintenance';
25
26 /** Never remove more than this in one run, so a huge backlog is paced. */
27 const BATCH = 500;
28
29 public function __construct() {
30 add_action('init', array(__CLASS__, 'ensure_scheduled'));
31 add_action(self::HOOK, array(__CLASS__, 'run'));
32 }
33
34 /* ---------------------------------------------------------------------
35 * Scheduling
36 * ------------------------------------------------------------------- */
37
38 /**
39 * Make sure the event exists.
40 *
41 * Checked on every load rather than only on activation: a site that
42 * updates the plugin without deactivating it never runs the activation
43 * hook, and would otherwise never get the event at all.
44 */
45 public static function ensure_scheduled() {
46 if (wp_next_scheduled(self::HOOK)) {
47 return;
48 }
49
50 /**
51 * How often maintenance runs.
52 *
53 * @param string $recurrence A registered cron schedule.
54 */
55 $recurrence = apply_filters('hashform_maintenance_recurrence', 'daily');
56
57 if (!array_key_exists($recurrence, (array) wp_get_schedules())) {
58 $recurrence = 'daily';
59 }
60
61 wp_schedule_event(self::first_run(), $recurrence, self::HOOK);
62 }
63
64 /**
65 * The first run, at a quiet hour in the site's own timezone rather than
66 * whenever the plugin happened to be activated.
67 *
68 * @return int
69 */
70 private static function first_run() {
71 $offset = (int) (get_option('gmt_offset') * HOUR_IN_SECONDS);
72 $local_now = time() + $offset;
73 $next_local = strtotime('tomorrow 03:00', $local_now);
74
75 return $next_local - $offset;
76 }
77
78 /**
79 * Remove the event. Called on deactivation and from uninstall.
80 */
81 public static function unschedule() {
82 wp_clear_scheduled_hook(self::HOOK);
83 }
84
85 /**
86 * What is scheduled and what is turned on, for the site health screen,
87 * for support, and for tests.
88 *
89 * @return array
90 */
91 public static function status() {
92 return array(
93 'hook' => self::HOOK,
94 'next_run' => wp_next_scheduled(self::HOOK),
95 'purge_orphaned_meta' => self::purging_orphaned_meta(),
96 );
97 }
98
99 /* ---------------------------------------------------------------------
100 * The run
101 * ------------------------------------------------------------------- */
102
103 /**
104 * @return array What each task did, for the log and for tests.
105 */
106 public static function run() {
107 $report = array(
108 'orphaned_meta' => self::purge_orphaned_meta(),
109 );
110
111 /**
112 * Daily maintenance.
113 *
114 * Pro hooks this for payments, analytics and generated files. Runs
115 * after the free plugin's own housekeeping.
116 */
117 do_action('hashform_maintenance', $report);
118
119 return $report;
120 }
121
122 /* ---------------------------------------------------------------------
123 * Tasks
124 * ------------------------------------------------------------------- */
125
126 private static function purging_orphaned_meta() {
127 /**
128 * Whether to delete answers whose entry no longer exists.
129 *
130 * Off by default. These rows are unreachable - nothing can display an
131 * answer with no entry behind it - but they are still somebody's data,
132 * and a site should choose to remove them rather than find out
133 * afterwards.
134 *
135 * @param bool $enabled
136 */
137 return (bool) apply_filters('hashform_purge_orphaned_meta', false);
138 }
139
140 /**
141 * Answers left behind by an entry that was deleted without going through
142 * destroy_entry() - a manual database edit, or a crash part way through.
143 *
144 * @return int Rows removed.
145 */
146 public static function purge_orphaned_meta() {
147 global $wpdb;
148
149 if (!self::purging_orphaned_meta()) {
150 return 0;
151 }
152
153 $meta = $wpdb->prefix . 'hashform_entry_meta';
154 $entries = $wpdb->prefix . 'hashform_entries';
155
156 /*
157 * Found first, then deleted by id. MySQL refuses LIMIT on a
158 * multi-table DELETE, so the join and the batch cap cannot be
159 * expressed in one statement - and a DELETE that fails on syntax
160 * returns false, which reads as "nothing to do" rather than as an
161 * error. Two statements, both valid, and the cap survives.
162 */
163 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $meta and $entries are $wpdb->prefix plus table name literals set above; the batch size is bound.
164 $ids = $wpdb->get_col($wpdb->prepare(
165 "SELECT m.id FROM {$meta} AS m
166 LEFT JOIN {$entries} AS e ON e.id = m.item_id
167 WHERE e.id IS NULL
168 LIMIT %d", self::BATCH));
169 // phpcs:enable
170
171 if (empty($ids)) {
172 return 0;
173 }
174
175 $in = implode(',', array_map('absint', $ids));
176
177 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $meta is a table name literal and $in is the id list mapped through absint() on the line above.
178 $removed = (int) $wpdb->query("DELETE FROM {$meta} WHERE id IN ({$in})");
179
180 if ($removed) {
181 self::log(sprintf('removed %d orphaned answer row(s)', $removed));
182 }
183
184 return $removed;
185 }
186
187 private static function log($message) {
188 if (class_exists('HashFormHelper') && method_exists('HashFormHelper', 'log')) {
189 HashFormHelper::log($message, 'hash-form/maintenance');
190 }
191 }
192
193 }
194
195 new HashFormCron();
196