PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 All 128 releases
extendify / app / Mcp / Jobs.php

Jobs.php in Extendify 3.2.2, at app/Mcp/Jobs.php

232 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Background jobs for the tool calls that outrun a request.
5 */
6
7 namespace Extendify\Mcp;
8
9 defined('ABSPATH') || die('No direct access.');
10
11 use Extendify\PartnerData;
12
13 /**
14 * A job runs on WP-Cron as the user who started it, one item at a time, and
15 * reschedules itself when a host will not extend the request.
16 */
17 class Jobs
18 {
19 // phpcs:disable PSR12.Properties.ConstantVisibility.NotFound
20 const HOOK = 'extendify_mcp_job';
21
22 const PREFIX = 'extendify_mcp_job_';
23
24 const TTL = DAY_IN_SECONDS;
25
26 /**
27 * What a chunk may take when set_time_limit() is refused.
28 */
29 const BUDGET = 20;
30
31 /**
32 * A fatal error mid-step reaches no catch; a job silent this long is failed on read.
33 */
34 const STALE = 10 * MINUTE_IN_SECONDS;
35 // phpcs:enable PSR12.Properties.ConstantVisibility.NotFound
36
37 /**
38 * @return void
39 */
40 public static function register()
41 {
42 \add_action(self::HOOK, [self::class, 'run']);
43 }
44
45 /**
46 * @param string $tool - The tool the job runs for.
47 * @param array $payload - What each step needs.
48 * @param integer $total - How many steps there are.
49 * @return array - What the tool answers the client with.
50 */
51 public static function start($tool, array $payload, $total)
52 {
53 $id = \wp_generate_password(12, false, false);
54 self::save([
55 'id' => $id,
56 'tool' => $tool,
57 'userId' => \get_current_user_id(),
58 'payload' => $payload,
59 'status' => 'queued',
60 'total' => (int) $total,
61 'done' => 0,
62 'results' => [],
63 'error' => '',
64 'updated' => time(),
65 ]);
66 \wp_schedule_single_event(time(), self::HOOK, [$id]);
67 \spawn_cron();
68
69 return [
70 'job_id' => $id,
71 'status' => 'queued',
72 'total' => (int) $total,
73 'note' => 'Runs in the background on WP-Cron. Follow it with get_task_status; a job that stays queued'
74 . ' means the site\'s cron is not firing, which get_site_health reports.',
75 ];
76 }
77
78 /**
79 * Answers only for the caller's own jobs, so the id needs no secrecy.
80 *
81 * @param string $id - The job id a tool handed out.
82 * @return array|null
83 */
84 public static function status($id)
85 {
86 $job = self::record($id);
87 if (!$job || $job['userId'] !== \get_current_user_id()) {
88 return null;
89 }
90
91 return [
92 'job_id' => $job['id'],
93 'tool' => $job['tool'],
94 'status' => $job['status'],
95 'total' => $job['total'],
96 'done' => $job['done'],
97 'results' => $job['results'],
98 'error' => $job['error'],
99 'updated' => gmdate('c', $job['updated']),
100 ];
101 }
102
103 /**
104 * @param string $id - The job to run or continue.
105 * @return void
106 */
107 public static function run($id)
108 {
109 $job = self::record($id);
110 if (!$job || in_array($job['status'], ['done', 'failed'], true)) {
111 return;
112 }
113
114 // Without this, a job queued by a demoted administrator would still run with their old rights.
115 if (!\user_can($job['userId'], 'manage_options')) {
116 self::fail($job, 'The user who started this job can no longer manage the site.');
117 return;
118 }
119
120 PartnerData::refreshIfStale();
121 if (!Availability::live()) {
122 self::fail($job, 'Connections are turned off for this site.');
123 return;
124 }
125
126 \wp_set_current_user($job['userId']);
127 $job['status'] = 'running';
128 self::save($job);
129 Guard::mark();
130
131 try {
132 Guard::during(function () use (&$job) {
133 self::work($job);
134 });
135 } catch (Refused $refused) {
136 self::fail($job, $refused->getMessage());
137 return;
138 } catch (\Throwable $failed) {
139 self::fail($job, $failed->getMessage());
140 return;
141 } finally {
142 Guard::unmark();
143 }
144
145 if ($job['done'] < $job['total']) {
146 \wp_schedule_single_event(time(), self::HOOK, [$job['id']]);
147 \spawn_cron();
148 return;
149 }
150
151 $failed = array_filter($job['results'], function ($result) {
152 return empty($result['ok']);
153 });
154 if ($job['results'] && count($failed) === count($job['results'])) {
155 self::fail($job, (string) ((array) end($failed))['error'] ?: 'Every step failed.');
156 return;
157 }
158
159 $job['status'] = 'done';
160 self::save($job);
161 }
162
163 /**
164 * @param array $job - The job, updated in place after every step.
165 * @return void
166 */
167 private static function work(array &$job)
168 {
169 // An upgrader writes options of its own, and refusing them would abort it half-run.
170 Guard::lift();
171 $deadline = time() + self::BUDGET;
172 while ($job['done'] < $job['total']) {
173 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- A host that forbids it answers with a warning.
174 $extended = @set_time_limit(60);
175 if (!$extended && time() >= $deadline) {
176 return;
177 }
178
179 $job['results'][] = Maintenance::step($job['tool'], $job['payload'], $job['done']);
180 $job['done']++;
181 $job['updated'] = time();
182 self::save($job);
183 }
184 }
185
186 /**
187 * @param array $job - The job that cannot go on.
188 * @param string $message - Why, for the model to read.
189 * @return array - The job as saved.
190 */
191 private static function fail(array $job, $message)
192 {
193 $job['status'] = 'failed';
194 $job['error'] = $message;
195 $job['updated'] = time();
196 self::save($job);
197
198 return $job;
199 }
200
201 /**
202 * @param string $id - The job id.
203 * @return array|null
204 */
205 private static function record($id)
206 {
207 if (!is_string($id) || $id === '') {
208 return null;
209 }
210
211 $job = \get_transient(self::PREFIX . $id);
212 if (!is_array($job)) {
213 return null;
214 }
215
216 if ($job['status'] === 'running' && $job['updated'] < time() - self::STALE) {
217 return self::fail($job, 'The job stopped without finishing. The site\'s error log may say why.');
218 }
219
220 return $job;
221 }
222
223 /**
224 * @param array $job - The job to keep.
225 * @return void
226 */
227 private static function save(array $job)
228 {
229 \set_transient(self::PREFIX . $job['id'], $job, self::TTL);
230 }
231 }
232