WP Crontrol - Version 1.15.0

Version Description

Download this release

Release Info

Developer johnbillion
Plugin Icon 128x128 WP Crontrol
Version 1.15.0
Comparing to
See all releases

Code changes from version 1.14.0 to 1.15.0

composer.json ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "johnbillion/wp-crontrol",
3
+ "description": "WP Crontrol lets you view and control what's happening in the WP-Cron system.",
4
+ "homepage": "https://github.com/johnbillion/wp-crontrol/",
5
+ "license": "GPL-2.0-or-later",
6
+ "type": "wordpress-plugin",
7
+ "authors": [
8
+ {
9
+ "name": "John Blackbourn",
10
+ "homepage": "https://johnblackbourn.com/"
11
+ },
12
+ {
13
+ "name": "Edward Dale",
14
+ "homepage": "http://scompt.com/"
15
+ }
16
+ ],
17
+ "config": {
18
+ "sort-packages": true,
19
+ "preferred-install": "dist",
20
+ "prepend-autoloader": false,
21
+ "classmap-authoritative": true,
22
+ "allow-plugins": {
23
+ "composer/installers": true,
24
+ "dealerdirect/phpcodesniffer-composer-installer": true,
25
+ "roots/wordpress-core-installer": true
26
+ }
27
+ },
28
+ "require-dev": {
29
+ "codeception/module-asserts": "^1.0",
30
+ "codeception/module-filesystem": "^1.0",
31
+ "codeception/module-webdriver": "^1.0",
32
+ "codeception/util-universalframework": "^1.0",
33
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
34
+ "lucatume/wp-browser": "^3.0",
35
+ "phpcompatibility/phpcompatibility-wp": "2.1.2",
36
+ "phpstan/phpstan": "^1.7",
37
+ "phpunit/phpunit": "^9.0",
38
+ "roots/wordpress-core-installer": "^1.0.0",
39
+ "roots/wordpress-full": "*",
40
+ "szepeviktor/phpstan-wordpress": "^1.0",
41
+ "wp-coding-standards/wpcs": "2.3.0"
42
+ },
43
+ "require": {
44
+ "php": ">=5.6",
45
+ "composer/installers": "^1.0 || ^2.0"
46
+ },
47
+ "autoload": {
48
+ "classmap": [
49
+ "src"
50
+ ]
51
+ },
52
+ "extra": {
53
+ "wordpress-install-dir": "tests/wordpress"
54
+ },
55
+ "scripts": {
56
+ "test:cs": [
57
+ "phpcs -nps --colors --report-code --report-summary --report-width=80 --cache=tests/cache/phpcs --basepath=./ --standard=phpcs53.xml",
58
+ "phpcs -nps --colors --report-code --report-summary --report-width=80 --cache=tests/cache/phpcs --basepath=./ ."
59
+ ],
60
+ "test:cs2pr": [
61
+ "phpcs -nsq --report=checkstyle --cache=tests/cache/phpcs . | cs2pr"
62
+ ],
63
+ "test:analyze": [
64
+ "phpstan analyze --memory-limit=1024M"
65
+ ],
66
+ "test:start": [
67
+ "docker compose up -d"
68
+ ],
69
+ "test:acceptance": [
70
+ "bin/test.sh"
71
+ ],
72
+ "test:stop": [
73
+ "docker compose down"
74
+ ],
75
+ "test": [
76
+ "composer validate --strict",
77
+ "@test:cs",
78
+ "@test:analyze",
79
+ "@test:acceptance"
80
+ ]
81
+ },
82
+ "support": {
83
+ "issues": "https://github.com/johnbillion/wp-crontrol/issues",
84
+ "forum": "https://wordpress.org/support/plugin/wp-crontrol",
85
+ "source": "https://github.com/johnbillion/wp-crontrol"
86
+ },
87
+ "funding": [
88
+ {
89
+ "type": "github",
90
+ "url": "https://github.com/sponsors/johnbillion"
91
+ }
92
+ ]
93
+ }
css/wp-crontrol.css CHANGED
@@ -28,6 +28,7 @@ table.wp-list-table {
28
  .wp-list-table code {
29
  background: transparent;
30
  padding: 0;
 
31
  }
32
 
33
  .wp-list-table td.column-crontrol_status {
@@ -42,31 +43,43 @@ table.wp-list-table {
42
  .wp-list-table tr.crontrol-no-action th,
43
  .wp-list-table tr.crontrol-stalled th,
44
  .wp-list-table tr.crontrol-warning th {
45
- border-color: #ffb900;
46
  }
47
 
48
  .wp-list-table tr.crontrol-error th {
49
- border-color: #dc3232;
 
 
 
 
 
 
 
 
50
  }
51
 
52
  .wp-list-table .column-crontrol_icon .dashicons,
53
  .wp-list-table .check-column .dashicons {
54
  margin-left: 6px;
55
  font-size: 14px;
56
- color: #aaa;
57
  }
58
 
59
  .wp-list-table .column-crontrol_icon .dashicons {
60
  margin-top: 2px;
61
  }
62
 
 
 
 
 
63
  #crontrol-hash-message {
64
  display: none;
65
  }
66
 
67
  .status-crontrol-complete,
68
  .wp-list-table tr.crontrol-complete td.column-crontrol_status {
69
- color: #080;
70
  }
71
 
72
  .status-crontrol-no-action,
@@ -75,7 +88,7 @@ table.wp-list-table {
75
  .wp-list-table tr.crontrol-no-action td.column-crontrol_status,
76
  .wp-list-table tr.crontrol-stalled td.column-crontrol_status,
77
  .wp-list-table tr.crontrol-warning td.column-crontrol_status {
78
- color: #c60;
79
  }
80
 
81
  .status-crontrol-emergency,
@@ -83,7 +96,19 @@ table.wp-list-table {
83
  .status-crontrol-critical,
84
  .status-crontrol-error,
85
  .wp-list-table tr.crontrol-error td.column-crontrol_status {
86
- color: #a00;
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
88
 
89
  .wp-list-table .column-crontrol_https,
@@ -99,7 +124,7 @@ table.wp-list-table {
99
  }
100
 
101
  .row-actions .crontrol-in-use {
102
- color: #666;
103
  }
104
 
105
  .form-field input[type="number"] {
28
  .wp-list-table code {
29
  background: transparent;
30
  padding: 0;
31
+ font-size: 12px;
32
  }
33
 
34
  .wp-list-table td.column-crontrol_status {
43
  .wp-list-table tr.crontrol-no-action th,
44
  .wp-list-table tr.crontrol-stalled th,
45
  .wp-list-table tr.crontrol-warning th {
46
+ border-color: #dba617;
47
  }
48
 
49
  .wp-list-table tr.crontrol-error th {
50
+ border-color: #d63638;
51
+ }
52
+
53
+ .wp-list-table tr.crontrol-paused th {
54
+ border-color: #8c8f94;
55
+ }
56
+
57
+ .wp-list-table tr.crontrol-paused:not(.crontrol-no-action) .column-crontrol_actions {
58
+ text-decoration: line-through;
59
  }
60
 
61
  .wp-list-table .column-crontrol_icon .dashicons,
62
  .wp-list-table .check-column .dashicons {
63
  margin-left: 6px;
64
  font-size: 14px;
65
+ color: #a7aaad;
66
  }
67
 
68
  .wp-list-table .column-crontrol_icon .dashicons {
69
  margin-top: 2px;
70
  }
71
 
72
+ .wp-list-table .qm-icon-edit {
73
+ display: none;
74
+ }
75
+
76
  #crontrol-hash-message {
77
  display: none;
78
  }
79
 
80
  .status-crontrol-complete,
81
  .wp-list-table tr.crontrol-complete td.column-crontrol_status {
82
+ color: #00a32a;
83
  }
84
 
85
  .status-crontrol-no-action,
88
  .wp-list-table tr.crontrol-no-action td.column-crontrol_status,
89
  .wp-list-table tr.crontrol-stalled td.column-crontrol_status,
90
  .wp-list-table tr.crontrol-warning td.column-crontrol_status {
91
+ color: #bd8600;
92
  }
93
 
94
  .status-crontrol-emergency,
96
  .status-crontrol-critical,
97
  .status-crontrol-error,
98
  .wp-list-table tr.crontrol-error td.column-crontrol_status {
99
+ color: #d63638;
100
+ }
101
+
102
+ .status-crontrol-paused .dashicons {
103
+ background: #646970;
104
+ border-radius: 50%;
105
+ color: #fff;
106
+ font-size: 14px;
107
+ height: 14px;
108
+ line-height: 14px;
109
+ margin-top: 2px;
110
+ padding: 1px;
111
+ width: 14px;
112
  }
113
 
114
  .wp-list-table .column-crontrol_https,
124
  }
125
 
126
  .row-actions .crontrol-in-use {
127
+ color: #646970;
128
  }
129
 
130
  .form-field input[type="number"] {
readme.md CHANGED
@@ -3,9 +3,9 @@
3
  Contributors: johnbillion, scompt
4
  Tags: cron, wp-cron, crontrol, debug
5
  Requires at least: 4.2
6
- Tested up to: 6.0
7
- Stable tag: 1.14.0
8
- Requires PHP: 5.3
9
  Donate link: https://github.com/sponsors/johnbillion
10
 
11
  WP Crontrol enables you to view and control what's happening in the WP-Cron system.
@@ -15,13 +15,13 @@ WP Crontrol enables you to view and control what's happening in the WP-Cron syst
15
  WP Crontrol enables you to view and control what's happening in the WP-Cron system. From the admin screens you can:
16
 
17
  * View all cron events along with their arguments, recurrence, callback functions, and when they are next due.
18
- * Edit, delete, and immediately run any cron events.
19
  * Add new cron events.
20
  * Bulk delete cron events.
21
  * Add and remove custom cron schedules.
22
- * Export cron event lists as a CSV file.
23
 
24
- WP Crontrol is aware of timezones, will alert you to events that have no actions or that have missed their schedule, and will show you a warning message if your cron system doesn't appear to be working (for example if your server can't connect to itself to fire scheduled cron events).
25
 
26
  ### Usage
27
 
@@ -51,7 +51,7 @@ Yes, it's actively tested and working up to PHP 8.1.
51
 
52
  ### Why do some cron events reappear shortly after I delete them?
53
 
54
- If the event is added by a plugin then the plugin most likely rescheduled the event as soon as it saw that the event was missing. Unfortunately there's nothing that WP Crontrol can do about this - you should contact the author of the related plugin and ask for advice.
55
 
56
  ### Is it safe to delete cron events?
57
 
@@ -63,6 +63,18 @@ If the event shows "None" as its action then it's usually safe to delete. Please
63
 
64
  The WordPress core software uses cron events for some of its functionality and removing these events is not possible because WordPress would immediately reschedule them if you did delete them. For this reason, WP Crontrol doesn't let you delete these persistent events from WordPress core in the first place.
65
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  ### What does it mean when "None" is shown for the Action of a cron event?
67
 
68
  This means the cron event is scheduled to run at the specified time but there is no corresponding functionality that will be triggered when the event runs, therefore the event is useless.
@@ -87,7 +99,7 @@ Please see the "Which users can manage PHP cron events?" FAQ for information abo
87
 
88
  ### Can I export a list of cron events?
89
 
90
- Yes, a CSV file of the event list can be exported via the "Export" button on the cron event listing screen. This file can be opened in any spreadsheet application.
91
 
92
  ### Can I see a historical log of all the cron events that ran on my site?
93
 
@@ -141,7 +153,7 @@ Therefore, the user access level required to execute arbitrary PHP code does not
141
 
142
  ### Are any WP-CLI commands available?
143
 
144
- The cron commands which were previously included in WP Crontrol are now part of WP-CLI (since 0.16), so this plugin no longer provides any WP-CLI commands. See `wp help cron` for more info.
145
 
146
  ### Who took the photo in the plugin header image?
147
 
@@ -157,6 +169,13 @@ The photo was taken by <a href="https://www.flickr.com/photos/michaelpardo/21453
157
 
158
  ## Changelog ##
159
 
 
 
 
 
 
 
 
160
  ### 1.14.0 ###
161
 
162
  * Reverts the changes introduced in version 1.13 while I look into the problem with the deployment process for wordpress.org
@@ -171,35 +190,35 @@ The photo was taken by <a href="https://www.flickr.com/photos/michaelpardo/21453
171
 
172
  ### 1.13.0 ###
173
 
174
- * Introduces the ability to pause and resume cron events from the event listing screen; see the FAQ for full details
175
- * Implements an autoloader to reduce memory usage
176
  * Bumps the minimum supported version of PHP to 5.6
177
 
178
  ### 1.12.1 ###
179
 
180
- * Corrects an issue where an invalid hook callback isn't always identified
181
  * Various code quality improvements
182
 
183
  ### 1.12.0 ###
184
 
185
- * Fix the PHP cron event management.
186
  * More "namespacing" of query variables to avoid conflicts with other cron management plugins.
187
 
188
  ### 1.11.0 ###
189
 
190
- * Introduced an `Export` feature to the event listing screen for exporting the list of events as a CSV file.
191
- * Added the timezone offset to the date displayed for events that are due to run after the next DST change, for extra clarity.
192
- * Introduced the `crontrol/filter-types` and `crontrol/filtered-events` filters for adjusting the available event filters on the event listing screen.
193
- * Lots of code quality improvements (thanks, PHPStan!).
194
 
195
 
196
  ### 1.10.0 ###
197
 
198
- * Support for more granular cron-related error messages in WordPress 5.7
199
- * Several accessibility improvements
200
- * Warning for events that are attached to [a schedule that is too frequent](https://github.com/johnbillion/wp-crontrol/wiki/This-interval-is-less-than-the-WP_CRON_LOCK_TIMEOUT-constant)
201
- * More clarity around events and schedules that are built in to WordPress core
202
- * Add a Help tab with links to the wiki and FAQs
203
 
204
 
205
  ### 1.9.1 ###
@@ -208,12 +227,12 @@ The photo was taken by <a href="https://www.flickr.com/photos/michaelpardo/21453
208
 
209
  ### 1.9.0 ###
210
 
211
- * Add filters and sorting to the event listing screen. Props @yuriipavlov.
212
- * Replace the "Add New" tabs with a more standard "Add New" button on the cron event listing page.
213
- * Switch back to using browser-native controls for the date and time inputs.
214
- * Add an error message when trying to edit a non-existent event.
215
- * Introduce an informational message which appears when there are events that have missed their schedule.
216
- * Fire actions when cron events and schedules are added, updated, and deleted.
217
 
218
 
219
  ### 1.8.5 ###
@@ -222,7 +241,7 @@ The photo was taken by <a href="https://www.flickr.com/photos/michaelpardo/21453
222
 
223
  ### 1.8.4 ###
224
 
225
- * Add a warning message if the default timezone has been changed. <a href="https://github.com/johnbillion/wp-crontrol/wiki/PHP-default-timezone-is-not-set-to-UTC">More information</a>.
226
  * Fixed string being passed to `strtotime()` function when the `Now` option is chosen when adding or editing an event.
227
 
228
  ### 1.8.3 ###
@@ -231,27 +250,27 @@ The photo was taken by <a href="https://www.flickr.com/photos/michaelpardo/21453
231
 
232
  ### 1.8.2 ###
233
 
234
- * Bypass the duplicate event check when manually running an event. This allows an event to manually run even if it's due within ten minutes or if it's overdue.
235
- * Force only one event to fire when manually running a cron event.
236
- * Introduce polling of the events list in order to show a warning when the event listing screen is out of date.
237
- * Add a warning for cron schedules which are shorter than `WP_CRON_LOCK_TIMEOUT`.
238
- * Add the Site Health check event to the list of persistent core hooks.
239
 
240
 
241
  ### 1.8.1 ###
242
 
243
- * Fix the bottom bulk action menu on the event listing screen.
244
  * Make the timezone more prominent when adding or editing a cron event.
245
 
246
  ### 1.8.0 ###
247
 
248
- * Searching and pagination for cron events
249
- * Ability to delete all cron events with a given hook
250
- * More accurate response messages when managing events (in WordPress 5.1+)
251
- * Visual warnings for events without actions, and PHP events with syntax errors
252
- * Timezone-related clarifications and fixes
253
- * A more unified UI
254
- * Modernised codebase
255
 
256
 
257
  ### 1.7.1 ###
@@ -260,16 +279,16 @@ The photo was taken by <a href="https://www.flickr.com/photos/michaelpardo/21453
260
 
261
  ### 1.7.0 ###
262
 
263
- * Remove the `date` and `time` inputs and replace with a couple of preset options and a plain text field. Fixes #24 .
264
- * Ensure the schedule name is always correct when multiple schedules exist with the same interval. Add error handling. Fixes #25.
265
- * Re-introduce the display of the current site time.
266
- * Use a more appropriate HTTP response code for unauthorised request errors.
267
 
268
 
269
  ### 1.6.2 ###
270
 
271
- * Remove the ability to delete a PHP cron event if the user cannot edit files.
272
- * Remove the `Edit` link for PHP cron events when the user cannot edit the event.
273
  * Avoid a PHP notice due to an undefined variable when adding a new cron event.
274
 
275
  ### 1.6.1 ###
@@ -278,22 +297,22 @@ The photo was taken by <a href="https://www.flickr.com/photos/michaelpardo/21453
278
 
279
  ### 1.6 ###
280
 
281
- * Introduce bulk deletion of cron events. Yay!
282
- * Show the schedule name instead of the schedule interval next to each event.
283
- * Add core's new `delete_expired_transients` event to the list of core events.
284
- * Don't allow custom cron schedules to be deleted if they're in use.
285
- * Add links between the Events and Schedules admin screens.
286
- * Add syntax highlighting to the PHP code editor for a PHP cron event.
287
- * Styling fixes for events with many arguments or long arguments.
288
- * Improvements to help text.
289
- * Remove usage of `create_function()`.
290
  * Fix some translator comments, improve i18n, improve coding standards.
291
 
292
  ### 1.5.0 ###
293
 
294
- * Show the hooked actions for each cron event.
295
- * Don't show the `Delete` link for core's built-in cron events, as they get re-populated immediately.
296
- * Correct the success message after adding or editing PHP cron events.
297
  * Correct the translations directory name.
298
 
299
  ### 1.4 ###
@@ -331,10 +350,3 @@ The photo was taken by <a href="https://www.flickr.com/photos/michaelpardo/21453
331
  - Added `wp crontrol run-event` and `wp crontrol delete-event` WP-CLI commands
332
  - Clarify language regarding hooks/entries/events
333
 
334
-
335
- ### 1.2.1 ###
336
-
337
- - Correctly display the local time when listing cron events
338
- - Remove a PHP notice
339
- - Pass the WP-Cron spawn check through the same filter as the actual spawner
340
-
3
  Contributors: johnbillion, scompt
4
  Tags: cron, wp-cron, crontrol, debug
5
  Requires at least: 4.2
6
+ Tested up to: 6.1
7
+ Stable tag: 1.15.0
8
+ Requires PHP: 5.6
9
  Donate link: https://github.com/sponsors/johnbillion
10
 
11
  WP Crontrol enables you to view and control what's happening in the WP-Cron system.
15
  WP Crontrol enables you to view and control what's happening in the WP-Cron system. From the admin screens you can:
16
 
17
  * View all cron events along with their arguments, recurrence, callback functions, and when they are next due.
18
+ * Edit, delete, pause, resume, and immediately run cron events.
19
  * Add new cron events.
20
  * Bulk delete cron events.
21
  * Add and remove custom cron schedules.
22
+ * Export and download cron event lists as a CSV file.
23
 
24
+ WP Crontrol is aware of timezones, will alert you to events that have no actions or that have missed their schedule, and will show you a helpful warning message if it detects any problems with your cron system.
25
 
26
  ### Usage
27
 
51
 
52
  ### Why do some cron events reappear shortly after I delete them?
53
 
54
+ If the event is added by a plugin then the plugin most likely rescheduled the event as soon as it saw that the event was missing. To get around this you can instead use the "Pause" option for the event which means it'll remain in place but won't perform any action when it runs.
55
 
56
  ### Is it safe to delete cron events?
57
 
63
 
64
  The WordPress core software uses cron events for some of its functionality and removing these events is not possible because WordPress would immediately reschedule them if you did delete them. For this reason, WP Crontrol doesn't let you delete these persistent events from WordPress core in the first place.
65
 
66
+ If you don't want these events to run, you can "Pause" them instead.
67
+
68
+ ### What happens when I pause an event?
69
+
70
+ Pausing an event will disable all actions attached to the event's hook. The event itself will remain in place and will run according to its schedule, but all actions attached to its hook will be disabled. This renders the event inoperative but keeps it scheduled so as to remain fully compatible with events which would otherwise get automatically rescheduled when they're missing.
71
+
72
+ As pausing an event actually pauses its hook, all events that use the same hook will be paused or resumed when pausing and resuming an event. This is much more useful and reliable than pausing individual events separately.
73
+
74
+ ### What happens when I resume an event?
75
+
76
+ Resuming an event re-enables all actions attached to the event's hook. All events that use the same hook will be resumed.
77
+
78
  ### What does it mean when "None" is shown for the Action of a cron event?
79
 
80
  This means the cron event is scheduled to run at the specified time but there is no corresponding functionality that will be triggered when the event runs, therefore the event is useless.
99
 
100
  ### Can I export a list of cron events?
101
 
102
+ Yes, a CSV file of the event list can be exported and downloaded via the "Export" button on the cron event listing screen. This file can be opened in any spreadsheet application.
103
 
104
  ### Can I see a historical log of all the cron events that ran on my site?
105
 
153
 
154
  ### Are any WP-CLI commands available?
155
 
156
+ The cron commands which were previously included in WP Crontrol are now part of WP-CLI itself. See `wp help cron` for more info.
157
 
158
  ### Who took the photo in the plugin header image?
159
 
169
 
170
  ## Changelog ##
171
 
172
+ ### 1.15.0 ###
173
+
174
+ * Introduces the ability to pause and resume cron events from the event listing screen; see the FAQ for full details
175
+ * Adds the site time to the cron event editing screen
176
+ * Implements an autoloader to reduce memory usage
177
+ * Bumps the minimum supported version of PHP to 5.6
178
+
179
  ### 1.14.0 ###
180
 
181
  * Reverts the changes introduced in version 1.13 while I look into the problem with the deployment process for wordpress.org
190
 
191
  ### 1.13.0 ###
192
 
193
+ * Introduces the ability to pause and resume cron events from the event listing screen; see the FAQ for full details
194
+ * Implements an autoloader to reduce memory usage
195
  * Bumps the minimum supported version of PHP to 5.6
196
 
197
  ### 1.12.1 ###
198
 
199
+ * Corrects an issue where an invalid hook callback isn't always identified
200
  * Various code quality improvements
201
 
202
  ### 1.12.0 ###
203
 
204
+ * Fix the PHP cron event management.
205
  * More "namespacing" of query variables to avoid conflicts with other cron management plugins.
206
 
207
  ### 1.11.0 ###
208
 
209
+ * Introduced an `Export` feature to the event listing screen for exporting the list of events as a CSV file.
210
+ * Added the timezone offset to the date displayed for events that are due to run after the next DST change, for extra clarity.
211
+ * Introduced the `crontrol/filter-types` and `crontrol/filtered-events` filters for adjusting the available event filters on the event listing screen.
212
+ * Lots of code quality improvements (thanks, PHPStan!).
213
 
214
 
215
  ### 1.10.0 ###
216
 
217
+ * Support for more granular cron-related error messages in WordPress 5.7
218
+ * Several accessibility improvements
219
+ * Warning for events that are attached to [a schedule that is too frequent](https://github.com/johnbillion/wp-crontrol/wiki/This-interval-is-less-than-the-WP_CRON_LOCK_TIMEOUT-constant)
220
+ * More clarity around events and schedules that are built in to WordPress core
221
+ * Add a Help tab with links to the wiki and FAQs
222
 
223
 
224
  ### 1.9.1 ###
227
 
228
  ### 1.9.0 ###
229
 
230
+ * Add filters and sorting to the event listing screen. Props @yuriipavlov.
231
+ * Replace the "Add New" tabs with a more standard "Add New" button on the cron event listing page.
232
+ * Switch back to using browser-native controls for the date and time inputs.
233
+ * Add an error message when trying to edit a non-existent event.
234
+ * Introduce an informational message which appears when there are events that have missed their schedule.
235
+ * Fire actions when cron events and schedules are added, updated, and deleted.
236
 
237
 
238
  ### 1.8.5 ###
241
 
242
  ### 1.8.4 ###
243
 
244
+ * Add a warning message if the default timezone has been changed. <a href="https://github.com/johnbillion/wp-crontrol/wiki/PHP-default-timezone-is-not-set-to-UTC">More information</a>.
245
  * Fixed string being passed to `strtotime()` function when the `Now` option is chosen when adding or editing an event.
246
 
247
  ### 1.8.3 ###
250
 
251
  ### 1.8.2 ###
252
 
253
+ * Bypass the duplicate event check when manually running an event. This allows an event to manually run even if it's due within ten minutes or if it's overdue.
254
+ * Force only one event to fire when manually running a cron event.
255
+ * Introduce polling of the events list in order to show a warning when the event listing screen is out of date.
256
+ * Add a warning for cron schedules which are shorter than `WP_CRON_LOCK_TIMEOUT`.
257
+ * Add the Site Health check event to the list of persistent core hooks.
258
 
259
 
260
  ### 1.8.1 ###
261
 
262
+ * Fix the bottom bulk action menu on the event listing screen.
263
  * Make the timezone more prominent when adding or editing a cron event.
264
 
265
  ### 1.8.0 ###
266
 
267
+ * Searching and pagination for cron events
268
+ * Ability to delete all cron events with a given hook
269
+ * More accurate response messages when managing events (in WordPress 5.1+)
270
+ * Visual warnings for events without actions, and PHP events with syntax errors
271
+ * Timezone-related clarifications and fixes
272
+ * A more unified UI
273
+ * Modernised codebase
274
 
275
 
276
  ### 1.7.1 ###
279
 
280
  ### 1.7.0 ###
281
 
282
+ * Remove the `date` and `time` inputs and replace with a couple of preset options and a plain text field. Fixes #24 .
283
+ * Ensure the schedule name is always correct when multiple schedules exist with the same interval. Add error handling. Fixes #25.
284
+ * Re-introduce the display of the current site time.
285
+ * Use a more appropriate HTTP response code for unauthorised request errors.
286
 
287
 
288
  ### 1.6.2 ###
289
 
290
+ * Remove the ability to delete a PHP cron event if the user cannot edit files.
291
+ * Remove the `Edit` link for PHP cron events when the user cannot edit the event.
292
  * Avoid a PHP notice due to an undefined variable when adding a new cron event.
293
 
294
  ### 1.6.1 ###
297
 
298
  ### 1.6 ###
299
 
300
+ * Introduce bulk deletion of cron events. Yay!
301
+ * Show the schedule name instead of the schedule interval next to each event.
302
+ * Add core's new `delete_expired_transients` event to the list of core events.
303
+ * Don't allow custom cron schedules to be deleted if they're in use.
304
+ * Add links between the Events and Schedules admin screens.
305
+ * Add syntax highlighting to the PHP code editor for a PHP cron event.
306
+ * Styling fixes for events with many arguments or long arguments.
307
+ * Improvements to help text.
308
+ * Remove usage of `create_function()`.
309
  * Fix some translator comments, improve i18n, improve coding standards.
310
 
311
  ### 1.5.0 ###
312
 
313
+ * Show the hooked actions for each cron event.
314
+ * Don't show the `Delete` link for core's built-in cron events, as they get re-populated immediately.
315
+ * Correct the success message after adding or editing PHP cron events.
316
  * Correct the translations directory name.
317
 
318
  ### 1.4 ###
350
  - Added `wp crontrol run-event` and `wp crontrol delete-event` WP-CLI commands
351
  - Clarify language regarding hooks/entries/events
352
 
 
 
 
 
 
 
 
src/bootstrap.php ADDED
@@ -0,0 +1,2210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Functions related to bootstrapping WP Crontrol.
4
+ */
5
+
6
+ namespace Crontrol;
7
+
8
+ use Crontrol\Event\Table;
9
+ use stdClass;
10
+ use WP_Error;
11
+
12
+ const TRANSIENT = 'crontrol-message-%d';
13
+ const PAUSED_OPTION = 'wp_crontrol_paused';
14
+
15
+ /**
16
+ * Hook onto all of the actions and filters needed by the plugin.
17
+ *
18
+ * @return void
19
+ */
20
+ function init_hooks() {
21
+ $plugin_file = plugin_basename( PLUGIN_FILE );
22
+
23
+ add_action( 'init', __NAMESPACE__ . '\action_init' );
24
+ add_action( 'init', __NAMESPACE__ . '\action_handle_posts' );
25
+ add_action( 'admin_menu', __NAMESPACE__ . '\action_admin_menu' );
26
+ add_action( 'wp_ajax_crontrol_checkhash', __NAMESPACE__ . '\ajax_check_events_hash' );
27
+ add_filter( "plugin_action_links_{$plugin_file}", __NAMESPACE__ . '\plugin_action_links', 10, 4 );
28
+ add_filter( "network_admin_plugin_action_links_{$plugin_file}", __NAMESPACE__ . '\network_plugin_action_links' );
29
+ add_filter( 'removable_query_args', __NAMESPACE__ . '\filter_removable_query_args' );
30
+ add_filter( 'pre_unschedule_event', __NAMESPACE__ . '\maybe_clear_doing_cron' );
31
+ add_filter( 'plugin_row_meta', __NAMESPACE__ . '\filter_plugin_row_meta', 10, 2 );
32
+
33
+ add_action( 'load-tools_page_crontrol_admin_manage_page', __NAMESPACE__ . '\setup_manage_page' );
34
+
35
+ add_filter( 'cron_schedules', __NAMESPACE__ . '\filter_cron_schedules' );
36
+ add_action( 'crontrol_cron_job', __NAMESPACE__ . '\action_php_cron_event' );
37
+ add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\enqueue_assets' );
38
+ add_action( 'crontrol/tab-header', __NAMESPACE__ . '\show_cron_status', 20 );
39
+ }
40
+
41
+ /**
42
+ * Sets an error message to show to the current user after a redirect.
43
+ *
44
+ * @param string $message The error message text.
45
+ * @return bool Whether the message was saved.
46
+ */
47
+ function set_message( $message ) {
48
+ $key = sprintf(
49
+ TRANSIENT,
50
+ get_current_user_id()
51
+ );
52
+ return set_transient( $key, $message, 60 );
53
+ }
54
+
55
+ /**
56
+ * Gets the error message to show to the current user after a redirect.
57
+ *
58
+ * @return string The error message text.
59
+ */
60
+ function get_message() {
61
+ $key = sprintf(
62
+ TRANSIENT,
63
+ get_current_user_id()
64
+ );
65
+ return get_transient( $key );
66
+ }
67
+
68
+ /**
69
+ * Filters the array of row meta for each plugin in the Plugins list table.
70
+ *
71
+ * @param array<int,string> $plugin_meta An array of the plugin row's meta data.
72
+ * @param string $plugin_file Path to the plugin file relative to the plugins directory.
73
+ * @return array<int,string> An array of the plugin row's meta data.
74
+ */
75
+ function filter_plugin_row_meta( array $plugin_meta, $plugin_file ) {
76
+ if ( 'wp-crontrol/wp-crontrol.php' !== $plugin_file ) {
77
+ return $plugin_meta;
78
+ }
79
+
80
+ $plugin_meta[] = sprintf(
81
+ '<a href="%1$s"><span class="dashicons dashicons-star-filled" aria-hidden="true" style="font-size:14px;line-height:1.3"></span>%2$s</a>',
82
+ 'https://github.com/sponsors/johnbillion',
83
+ esc_html_x( 'Sponsor', 'verb', 'wp-crontrol' )
84
+ );
85
+
86
+ return $plugin_meta;
87
+ }
88
+
89
+ /**
90
+ * Run using the 'init' action.
91
+ *
92
+ * @return void
93
+ */
94
+ function action_init() {
95
+ load_plugin_textdomain( 'wp-crontrol', false, dirname( plugin_basename( PLUGIN_FILE ) ) . '/languages' );
96
+
97
+ /** @var array<string, true>|false $paused */
98
+ $paused = get_option( PAUSED_OPTION, array() );
99
+
100
+ if ( is_array( $paused ) ) {
101
+ foreach ( $paused as $hook => $value ) {
102
+ add_action( $hook, __NAMESPACE__ . '\\pauser', -99999 );
103
+ }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * @return void
109
+ */
110
+ function pauser() {
111
+ remove_all_actions( current_filter() );
112
+ }
113
+
114
+ /**
115
+ * Handles any POSTs and GETs made by the plugin. Run using the 'init' action.
116
+ *
117
+ * @return void
118
+ */
119
+ function action_handle_posts() {
120
+ $request = new Request();
121
+
122
+ if ( isset( $_POST['crontrol_action'] ) && ( 'new_cron' === $_POST['crontrol_action'] ) ) {
123
+ if ( ! current_user_can( 'manage_options' ) ) {
124
+ wp_die( esc_html__( 'You are not allowed to add new cron events.', 'wp-crontrol' ), 401 );
125
+ }
126
+ check_admin_referer( 'crontrol-new-cron' );
127
+
128
+ $cr = $request->init( wp_unslash( $_POST ) );
129
+
130
+ if ( 'crontrol_cron_job' === $cr->hookname && ! current_user_can( 'edit_files' ) ) {
131
+ wp_die( esc_html__( 'You are not allowed to add new PHP cron events.', 'wp-crontrol' ), 401 );
132
+ }
133
+ $args = json_decode( $cr->args, true );
134
+
135
+ if ( empty( $args ) || ! is_array( $args ) ) {
136
+ $args = array();
137
+ }
138
+
139
+ $next_run_local = ( 'custom' === $cr->next_run_date_local ) ? $cr->next_run_date_local_custom_date . ' ' . $cr->next_run_date_local_custom_time : $cr->next_run_date_local;
140
+
141
+ add_filter( 'schedule_event', function( $event ) {
142
+ if ( ! $event ) {
143
+ return $event;
144
+ }
145
+
146
+ /**
147
+ * Fires after a new cron event is added.
148
+ *
149
+ * @param stdClass $event {
150
+ * An object containing the event's data.
151
+ *
152
+ * @type string $hook Action hook to execute when the event is run.
153
+ * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
154
+ * @type string|false $schedule How often the event should subsequently recur.
155
+ * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
156
+ * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
157
+ * }
158
+ */
159
+ do_action( 'crontrol/added_new_event', $event );
160
+
161
+ return $event;
162
+ }, 99 );
163
+
164
+ $added = Event\add( $next_run_local, $cr->schedule, $cr->hookname, $args );
165
+
166
+ $redirect = array(
167
+ 'page' => 'crontrol_admin_manage_page',
168
+ 'crontrol_message' => '5',
169
+ 'crontrol_name' => rawurlencode( $cr->hookname ),
170
+ );
171
+
172
+ if ( is_wp_error( $added ) ) {
173
+ set_message( $added->get_error_message() );
174
+ $redirect['crontrol_message'] = 'error';
175
+ }
176
+
177
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
178
+ exit;
179
+
180
+ } elseif ( isset( $_POST['crontrol_action'] ) && ( 'new_php_cron' === $_POST['crontrol_action'] ) ) {
181
+ if ( ! current_user_can( 'edit_files' ) ) {
182
+ wp_die( esc_html__( 'You are not allowed to add new PHP cron events.', 'wp-crontrol' ), 401 );
183
+ }
184
+ check_admin_referer( 'crontrol-new-cron' );
185
+
186
+ $cr = $request->init( wp_unslash( $_POST ) );
187
+
188
+ $next_run_local = ( 'custom' === $cr->next_run_date_local ) ? $cr->next_run_date_local_custom_date . ' ' . $cr->next_run_date_local_custom_time : $cr->next_run_date_local;
189
+ $args = array(
190
+ 'code' => $cr->hookcode,
191
+ 'name' => $cr->eventname,
192
+ );
193
+
194
+ add_filter( 'schedule_event', function( $event ) {
195
+ if ( ! $event ) {
196
+ return $event;
197
+ }
198
+
199
+ /**
200
+ * Fires after a new PHP cron event is added.
201
+ *
202
+ * @param stdClass $event {
203
+ * An object containing the event's data.
204
+ *
205
+ * @type string $hook Action hook to execute when the event is run.
206
+ * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
207
+ * @type string|false $schedule How often the event should subsequently recur.
208
+ * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
209
+ * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
210
+ * }
211
+ */
212
+ do_action( 'crontrol/added_new_php_event', $event );
213
+
214
+ return $event;
215
+ }, 99 );
216
+
217
+ $added = Event\add( $next_run_local, $cr->schedule, 'crontrol_cron_job', $args );
218
+
219
+ $hookname = ( ! empty( $cr->eventname ) ) ? $cr->eventname : __( 'PHP Cron', 'wp-crontrol' );
220
+ $redirect = array(
221
+ 'page' => 'crontrol_admin_manage_page',
222
+ 'crontrol_message' => '5',
223
+ 'crontrol_name' => rawurlencode( $hookname ),
224
+ );
225
+
226
+ if ( is_wp_error( $added ) ) {
227
+ set_message( $added->get_error_message() );
228
+ $redirect['crontrol_message'] = 'error';
229
+ }
230
+
231
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
232
+ exit;
233
+
234
+ } elseif ( isset( $_POST['crontrol_action'] ) && ( 'edit_cron' === $_POST['crontrol_action'] ) ) {
235
+ if ( ! current_user_can( 'manage_options' ) ) {
236
+ wp_die( esc_html__( 'You are not allowed to edit cron events.', 'wp-crontrol' ), 401 );
237
+ }
238
+
239
+ $cr = $request->init( wp_unslash( $_POST ) );
240
+
241
+ check_admin_referer( "crontrol-edit-cron_{$cr->original_hookname}_{$cr->original_sig}_{$cr->original_next_run_utc}" );
242
+
243
+ if ( 'crontrol_cron_job' === $cr->hookname && ! current_user_can( 'edit_files' ) ) {
244
+ wp_die( esc_html__( 'You are not allowed to edit PHP cron events.', 'wp-crontrol' ), 401 );
245
+ }
246
+
247
+ $args = json_decode( $cr->args, true );
248
+
249
+ if ( empty( $args ) || ! is_array( $args ) ) {
250
+ $args = array();
251
+ }
252
+
253
+ $redirect = array(
254
+ 'page' => 'crontrol_admin_manage_page',
255
+ 'crontrol_message' => '4',
256
+ 'crontrol_name' => rawurlencode( $cr->hookname ),
257
+ );
258
+
259
+ $original = Event\get_single( $cr->original_hookname, $cr->original_sig, $cr->original_next_run_utc );
260
+
261
+ if ( is_wp_error( $original ) ) {
262
+ set_message( $original->get_error_message() );
263
+ $redirect['crontrol_message'] = 'error';
264
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
265
+ exit;
266
+ }
267
+
268
+ $deleted = Event\delete( $cr->original_hookname, $cr->original_sig, $cr->original_next_run_utc );
269
+
270
+ if ( is_wp_error( $deleted ) ) {
271
+ set_message( $deleted->get_error_message() );
272
+ $redirect['crontrol_message'] = 'error';
273
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
274
+ exit;
275
+ }
276
+
277
+ $next_run_local = ( 'custom' === $cr->next_run_date_local ) ? $cr->next_run_date_local_custom_date . ' ' . $cr->next_run_date_local_custom_time : $cr->next_run_date_local;
278
+
279
+ /**
280
+ * Modifies an event before it is scheduled.
281
+ *
282
+ * @param stdClass|false $event An object containing the new event's data, or boolean false.
283
+ */
284
+ add_filter( 'schedule_event', function( $event ) use ( $original ) {
285
+ if ( ! $event ) {
286
+ return $event;
287
+ }
288
+
289
+ /**
290
+ * Fires after a cron event is edited.
291
+ *
292
+ * @param stdClass $event {
293
+ * An object containing the new event's data.
294
+ *
295
+ * @type string $hook Action hook to execute when the event is run.
296
+ * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
297
+ * @type string|false $schedule How often the event should subsequently recur.
298
+ * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
299
+ * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
300
+ * }
301
+ * @param stdClass $original {
302
+ * An object containing the original event's data.
303
+ *
304
+ * @type string $hook Action hook to execute when the event is run.
305
+ * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
306
+ * @type string|false $schedule How often the event should subsequently recur.
307
+ * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
308
+ * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
309
+ * }
310
+ */
311
+ do_action( 'crontrol/edited_event', $event, $original );
312
+
313
+ return $event;
314
+ }, 99 );
315
+
316
+ $added = Event\add( $next_run_local, $cr->schedule, $cr->hookname, $args );
317
+
318
+ if ( is_wp_error( $added ) ) {
319
+ set_message( $added->get_error_message() );
320
+ $redirect['crontrol_message'] = 'error';
321
+ }
322
+
323
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
324
+ exit;
325
+
326
+ } elseif ( isset( $_POST['crontrol_action'] ) && ( 'edit_php_cron' === $_POST['crontrol_action'] ) ) {
327
+ if ( ! current_user_can( 'edit_files' ) ) {
328
+ wp_die( esc_html__( 'You are not allowed to edit PHP cron events.', 'wp-crontrol' ), 401 );
329
+ }
330
+
331
+ $cr = $request->init( wp_unslash( $_POST ) );
332
+
333
+ check_admin_referer( "crontrol-edit-cron_{$cr->original_hookname}_{$cr->original_sig}_{$cr->original_next_run_utc}" );
334
+ $args = array(
335
+ 'code' => $cr->hookcode,
336
+ 'name' => $cr->eventname,
337
+ );
338
+ $hookname = ( ! empty( $cr->eventname ) ) ? $cr->eventname : __( 'PHP Cron', 'wp-crontrol' );
339
+ $redirect = array(
340
+ 'page' => 'crontrol_admin_manage_page',
341
+ 'crontrol_message' => '4',
342
+ 'crontrol_name' => rawurlencode( $hookname ),
343
+ );
344
+
345
+ $original = Event\get_single( $cr->original_hookname, $cr->original_sig, $cr->original_next_run_utc );
346
+
347
+ if ( is_wp_error( $original ) ) {
348
+ set_message( $original->get_error_message() );
349
+ $redirect['crontrol_message'] = 'error';
350
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
351
+ exit;
352
+ }
353
+
354
+ $deleted = Event\delete( $cr->original_hookname, $cr->original_sig, $cr->original_next_run_utc );
355
+
356
+ if ( is_wp_error( $deleted ) ) {
357
+ set_message( $deleted->get_error_message() );
358
+ $redirect['crontrol_message'] = 'error';
359
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
360
+ exit;
361
+ }
362
+
363
+ $next_run_local = ( 'custom' === $cr->next_run_date_local ) ? $cr->next_run_date_local_custom_date . ' ' . $cr->next_run_date_local_custom_time : $cr->next_run_date_local;
364
+
365
+ /**
366
+ * Modifies an event before it is scheduled.
367
+ *
368
+ * @param stdClass|false $event An object containing the new event's data, or boolean false.
369
+ */
370
+ add_filter( 'schedule_event', function( $event ) use ( $original ) {
371
+ if ( ! $event ) {
372
+ return $event;
373
+ }
374
+
375
+ /**
376
+ * Fires after a PHP cron event is edited.
377
+ *
378
+ * @param stdClass $event {
379
+ * An object containing the new event's data.
380
+ *
381
+ * @type string $hook Action hook to execute when the event is run.
382
+ * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
383
+ * @type string|false $schedule How often the event should subsequently recur.
384
+ * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
385
+ * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
386
+ * }
387
+ * @param stdClass $original {
388
+ * An object containing the original event's data.
389
+ *
390
+ * @type string $hook Action hook to execute when the event is run.
391
+ * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
392
+ * @type string|false $schedule How often the event should subsequently recur.
393
+ * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
394
+ * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
395
+ * }
396
+ */
397
+ do_action( 'crontrol/edited_php_event', $event, $original );
398
+
399
+ return $event;
400
+ }, 99 );
401
+
402
+ $added = Event\add( $next_run_local, $cr->schedule, 'crontrol_cron_job', $args );
403
+
404
+ if ( is_wp_error( $added ) ) {
405
+ set_message( $added->get_error_message() );
406
+ $redirect['crontrol_message'] = 'error';
407
+ }
408
+
409
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
410
+ exit;
411
+
412
+ } elseif ( isset( $_POST['crontrol_new_schedule'] ) ) {
413
+ if ( ! current_user_can( 'manage_options' ) ) {
414
+ wp_die( esc_html__( 'You are not allowed to add new cron schedules.', 'wp-crontrol' ), 401 );
415
+ }
416
+ check_admin_referer( 'crontrol-new-schedule' );
417
+ $name = wp_unslash( $_POST['crontrol_schedule_internal_name'] );
418
+ $interval = absint( $_POST['crontrol_schedule_interval'] );
419
+ $display = wp_unslash( $_POST['crontrol_schedule_display_name'] );
420
+
421
+ Schedule\add( $name, $interval, $display );
422
+ $redirect = array(
423
+ 'page' => 'crontrol_admin_options_page',
424
+ 'crontrol_message' => '3',
425
+ 'crontrol_name' => rawurlencode( $name ),
426
+ );
427
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'options-general.php' ) ) );
428
+ exit;
429
+
430
+ } elseif ( isset( $_GET['crontrol_action'] ) && 'delete-schedule' === $_GET['crontrol_action'] ) {
431
+ if ( ! current_user_can( 'manage_options' ) ) {
432
+ wp_die( esc_html__( 'You are not allowed to delete cron schedules.', 'wp-crontrol' ), 401 );
433
+ }
434
+ $schedule = wp_unslash( $_GET['crontrol_id'] );
435
+ check_admin_referer( "crontrol-delete-schedule_{$schedule}" );
436
+ Schedule\delete( $schedule );
437
+ $redirect = array(
438
+ 'page' => 'crontrol_admin_options_page',
439
+ 'crontrol_message' => '2',
440
+ 'crontrol_name' => rawurlencode( $schedule ),
441
+ );
442
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'options-general.php' ) ) );
443
+ exit;
444
+
445
+ } elseif ( ( isset( $_POST['action'] ) && 'crontrol_delete_crons' === $_POST['action'] ) || ( isset( $_POST['action2'] ) && 'crontrol_delete_crons' === $_POST['action2'] ) ) {
446
+ if ( ! current_user_can( 'manage_options' ) ) {
447
+ wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
448
+ }
449
+ check_admin_referer( 'bulk-crontrol-events' );
450
+
451
+ if ( empty( $_POST['crontrol_delete'] ) ) {
452
+ return;
453
+ }
454
+
455
+ /**
456
+ * @var array<string,array<string,string>>
457
+ */
458
+ $delete = (array) wp_unslash( $_POST['crontrol_delete'] );
459
+ $deleted = 0;
460
+
461
+ foreach ( $delete as $next_run_utc => $events ) {
462
+ foreach ( (array) $events as $hook => $sig ) {
463
+ if ( 'crontrol_cron_job' === $hook && ! current_user_can( 'edit_files' ) ) {
464
+ continue;
465
+ }
466
+
467
+ $event = Event\get_single( urldecode( $hook ), $sig, $next_run_utc );
468
+ $deleted = Event\delete( urldecode( $hook ), $sig, $next_run_utc );
469
+
470
+ if ( ! is_wp_error( $deleted ) ) {
471
+ $deleted++;
472
+
473
+ /** This action is documented in wp-crontrol.php */
474
+ do_action( 'crontrol/deleted_event', $event );
475
+ }
476
+ }
477
+ }
478
+
479
+ $redirect = array(
480
+ 'page' => 'crontrol_admin_manage_page',
481
+ 'crontrol_name' => $deleted,
482
+ 'crontrol_message' => '9',
483
+ );
484
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
485
+ exit;
486
+
487
+ } elseif ( isset( $_GET['crontrol_action'] ) && 'delete-cron' === $_GET['crontrol_action'] ) {
488
+ if ( ! current_user_can( 'manage_options' ) ) {
489
+ wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
490
+ }
491
+ $hook = wp_unslash( $_GET['crontrol_id'] );
492
+ $sig = wp_unslash( $_GET['crontrol_sig'] );
493
+ $next_run_utc = wp_unslash( $_GET['crontrol_next_run_utc'] );
494
+ check_admin_referer( "crontrol-delete-cron_{$hook}_{$sig}_{$next_run_utc}" );
495
+
496
+ if ( 'crontrol_cron_job' === $hook && ! current_user_can( 'edit_files' ) ) {
497
+ wp_die( esc_html__( 'You are not allowed to delete PHP cron events.', 'wp-crontrol' ), 401 );
498
+ }
499
+
500
+ $redirect = array(
501
+ 'page' => 'crontrol_admin_manage_page',
502
+ 'crontrol_message' => '6',
503
+ 'crontrol_name' => rawurlencode( $hook ),
504
+ );
505
+
506
+ $event = Event\get_single( $hook, $sig, $next_run_utc );
507
+
508
+ if ( is_wp_error( $event ) ) {
509
+ set_message( $event->get_error_message() );
510
+ $redirect['crontrol_message'] = 'error';
511
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
512
+ exit;
513
+ }
514
+
515
+ $deleted = Event\delete( $hook, $sig, $next_run_utc );
516
+
517
+ if ( is_wp_error( $deleted ) ) {
518
+ set_message( $deleted->get_error_message() );
519
+ $redirect['crontrol_message'] = 'error';
520
+ } else {
521
+ /**
522
+ * Fires after a cron event is deleted.
523
+ *
524
+ * @param stdClass $event {
525
+ * An object containing the event's data.
526
+ *
527
+ * @type string $hook Action hook to execute when the event is run.
528
+ * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
529
+ * @type string|false $schedule How often the event should subsequently recur.
530
+ * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
531
+ * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
532
+ * }
533
+ */
534
+ do_action( 'crontrol/deleted_event', $event );
535
+ }
536
+
537
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
538
+ exit;
539
+
540
+ } elseif ( isset( $_GET['crontrol_action'] ) && 'delete-hook' === $_GET['crontrol_action'] ) {
541
+ if ( ! current_user_can( 'manage_options' ) ) {
542
+ wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
543
+ }
544
+ $hook = wp_unslash( $_GET['crontrol_id'] );
545
+ $deleted = false;
546
+ check_admin_referer( "crontrol-delete-hook_{$hook}" );
547
+
548
+ if ( 'crontrol_cron_job' === $hook ) {
549
+ wp_die( esc_html__( 'You are not allowed to delete PHP cron events.', 'wp-crontrol' ), 401 );
550
+ }
551
+
552
+ if ( function_exists( 'wp_unschedule_hook' ) ) {
553
+ /** @var int|false */
554
+ $deleted = wp_unschedule_hook( $hook );
555
+ }
556
+
557
+ if ( 0 === $deleted ) {
558
+ $redirect = array(
559
+ 'page' => 'crontrol_admin_manage_page',
560
+ 'crontrol_message' => '3',
561
+ 'crontrol_name' => rawurlencode( $hook ),
562
+ );
563
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
564
+ exit;
565
+ } elseif ( $deleted ) {
566
+ /**
567
+ * Fires after all cron events with the given hook are deleted.
568
+ *
569
+ * @param string $hook The hook name.
570
+ * @param int $deleted The number of events that were deleted.
571
+ */
572
+ do_action( 'crontrol/deleted_all_with_hook', $hook, $deleted );
573
+
574
+ $redirect = array(
575
+ 'page' => 'crontrol_admin_manage_page',
576
+ 'crontrol_message' => '2',
577
+ 'crontrol_name' => rawurlencode( $hook ),
578
+ );
579
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
580
+ exit;
581
+ } else {
582
+ $redirect = array(
583
+ 'page' => 'crontrol_admin_manage_page',
584
+ 'crontrol_message' => '7',
585
+ 'crontrol_name' => rawurlencode( $hook ),
586
+ );
587
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
588
+ exit;
589
+ }
590
+ } elseif ( isset( $_GET['crontrol_action'] ) && 'run-cron' === $_GET['crontrol_action'] ) {
591
+ if ( ! current_user_can( 'manage_options' ) ) {
592
+ wp_die( esc_html__( 'You are not allowed to run cron events.', 'wp-crontrol' ), 401 );
593
+ }
594
+ $hook = wp_unslash( $_GET['crontrol_id'] );
595
+ $sig = wp_unslash( $_GET['crontrol_sig'] );
596
+ check_admin_referer( "crontrol-run-cron_{$hook}_{$sig}" );
597
+
598
+ $ran = Event\run( $hook, $sig );
599
+
600
+ $redirect = array(
601
+ 'page' => 'crontrol_admin_manage_page',
602
+ 'crontrol_message' => '1',
603
+ 'crontrol_name' => rawurlencode( $hook ),
604
+ );
605
+
606
+ if ( is_wp_error( $ran ) ) {
607
+ $set = set_message( $ran->get_error_message() );
608
+
609
+ // If we can't store the error message in a transient, just display it.
610
+ if ( ! $set ) {
611
+ wp_die(
612
+ esc_html( $ran->get_error_message() ),
613
+ '',
614
+ array(
615
+ 'response' => 500,
616
+ 'back_link' => true,
617
+ )
618
+ );
619
+ }
620
+ $redirect['crontrol_message'] = 'error';
621
+ }
622
+
623
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
624
+ exit;
625
+ } elseif ( isset( $_GET['crontrol_action'] ) && 'pause-hook' === $_GET['crontrol_action'] ) {
626
+ if ( ! current_user_can( 'manage_options' ) ) {
627
+ wp_die( esc_html__( 'You are not allowed to pause or resume cron events.', 'wp-crontrol' ), 401 );
628
+ }
629
+
630
+ $hook = wp_unslash( $_GET['crontrol_id'] );
631
+ check_admin_referer( "crontrol-pause-hook_{$hook}" );
632
+
633
+ $paused = Event\pause( $hook );
634
+
635
+ $redirect = array(
636
+ 'page' => 'crontrol_admin_manage_page',
637
+ 'crontrol_message' => '11',
638
+ 'crontrol_name' => rawurlencode( $hook ),
639
+ );
640
+
641
+ if ( is_wp_error( $paused ) ) {
642
+ $set = set_message( $paused->get_error_message() );
643
+
644
+ // If we can't store the error message in a transient, just display it.
645
+ if ( ! $set ) {
646
+ wp_die(
647
+ esc_html( $paused->get_error_message() ),
648
+ '',
649
+ array(
650
+ 'response' => 500,
651
+ 'back_link' => true,
652
+ )
653
+ );
654
+ }
655
+ $redirect['crontrol_message'] = 'error';
656
+ } else {
657
+ /**
658
+ * Fires after a cron event hook is paused.
659
+ *
660
+ * @param string $hook The event hook name.
661
+ */
662
+ do_action( 'crontrol/paused_hook', $hook );
663
+ }
664
+
665
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
666
+ exit;
667
+ } elseif ( isset( $_GET['crontrol_action'] ) && 'resume-hook' === $_GET['crontrol_action'] ) {
668
+ if ( ! current_user_can( 'manage_options' ) ) {
669
+ wp_die( esc_html__( 'You are not allowed to pause or resume cron events.', 'wp-crontrol' ), 401 );
670
+ }
671
+
672
+ $hook = wp_unslash( $_GET['crontrol_id'] );
673
+ check_admin_referer( "crontrol-resume-hook_{$hook}" );
674
+
675
+ $resumed = Event\resume( $hook );
676
+
677
+ $redirect = array(
678
+ 'page' => 'crontrol_admin_manage_page',
679
+ 'crontrol_message' => '12',
680
+ 'crontrol_name' => rawurlencode( $hook ),
681
+ );
682
+
683
+ if ( is_wp_error( $resumed ) ) {
684
+ $set = set_message( $resumed->get_error_message() );
685
+
686
+ // If we can't store the error message in a transient, just display it.
687
+ if ( ! $set ) {
688
+ wp_die(
689
+ esc_html( $resumed->get_error_message() ),
690
+ '',
691
+ array(
692
+ 'response' => 500,
693
+ 'back_link' => true,
694
+ )
695
+ );
696
+ }
697
+ $redirect['crontrol_message'] = 'error';
698
+ } else {
699
+ /**
700
+ * Fires after a paused cron event hook is resumed.
701
+ *
702
+ * @param string $hook The event hook name.
703
+ */
704
+ do_action( 'crontrol/resumed_hook', $hook );
705
+ }
706
+
707
+ wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
708
+ exit;
709
+ } elseif ( isset( $_POST['crontrol_action'] ) && 'export-event-csv' === $_POST['crontrol_action'] ) {
710
+ check_admin_referer( 'crontrol-export-event-csv', 'crontrol_nonce' );
711
+
712
+ $type = isset( $_POST['crontrol_hooks_type'] ) ? $_POST['crontrol_hooks_type'] : 'all';
713
+ $headers = array(
714
+ 'hook',
715
+ 'arguments',
716
+ 'next_run',
717
+ 'next_run_gmt',
718
+ 'action',
719
+ 'recurrence',
720
+ 'interval',
721
+ );
722
+ $filename = sprintf(
723
+ 'cron-events-%s-%s.csv',
724
+ $type,
725
+ gmdate( 'Y-m-d-H.i.s' )
726
+ );
727
+ $csv = fopen( 'php://output', 'w' );
728
+
729
+ if ( false === $csv ) {
730
+ wp_die( esc_html__( 'Could not save CSV file.', 'wp-crontrol' ) );
731
+ }
732
+
733
+ $events = Table::get_filtered_events( Event\get() );
734
+
735
+ header( 'Content-Type: text/csv; charset=utf-8' );
736
+ header(
737
+ sprintf(
738
+ 'Content-Disposition: attachment; filename="%s"',
739
+ esc_attr( $filename )
740
+ )
741
+ );
742
+
743
+ fputcsv( $csv, $headers );
744
+
745
+ if ( isset( $events[ $type ] ) ) {
746
+ foreach ( $events[ $type ] as $event ) {
747
+ $next_run_local = get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $event->timestamp ), 'c' );
748
+ $next_run_utc = gmdate( 'c', $event->timestamp );
749
+ $hook_callbacks = \Crontrol\get_hook_callbacks( $event->hook );
750
+
751
+ if ( 'crontrol_cron_job' === $event->hook ) {
752
+ $args = __( 'PHP Code', 'wp-crontrol' );
753
+ } elseif ( empty( $event->args ) ) {
754
+ $args = '';
755
+ } else {
756
+ $args = \Crontrol\json_output( $event->args, false );
757
+ }
758
+
759
+ if ( 'crontrol_cron_job' === $event->hook ) {
760
+ $action = __( 'WP Crontrol', 'wp-crontrol' );
761
+ } else {
762
+ $callbacks = array();
763
+
764
+ foreach ( $hook_callbacks as $callback ) {
765
+ $callbacks[] = $callback['callback']['name'];
766
+ }
767
+
768
+ $action = implode( ',', $callbacks );
769
+ }
770
+
771
+ if ( $event->schedule ) {
772
+ $recurrence = Event\get_schedule_name( $event );
773
+ if ( is_wp_error( $recurrence ) ) {
774
+ $recurrence = $recurrence->get_error_message();
775
+ }
776
+ } else {
777
+ $recurrence = __( 'Non-repeating', 'wp-crontrol' );
778
+ }
779
+
780
+ $row = array(
781
+ $event->hook,
782
+ $args,
783
+ $next_run_local,
784
+ $next_run_utc,
785
+ $action,
786
+ $recurrence,
787
+ (int) $event->interval,
788
+ );
789
+ fputcsv( $csv, $row );
790
+ }
791
+ }
792
+
793
+ fclose( $csv );
794
+
795
+ exit;
796
+ }
797
+ }
798
+
799
+ /**
800
+ * Adds options & management pages to the admin menu.
801
+ *
802
+ * Run using the 'admin_menu' action.
803
+ *
804
+ * @return void
805
+ */
806
+ function action_admin_menu() {
807
+ $schedules = add_options_page(
808
+ esc_html__( 'Cron Schedules', 'wp-crontrol' ),
809
+ esc_html__( 'Cron Schedules', 'wp-crontrol' ),
810
+ 'manage_options',
811
+ 'crontrol_admin_options_page',
812
+ __NAMESPACE__ . '\admin_options_page'
813
+ );
814
+ $events = add_management_page(
815
+ esc_html__( 'Cron Events', 'wp-crontrol' ),
816
+ esc_html__( 'Cron Events', 'wp-crontrol' ),
817
+ 'manage_options',
818
+ 'crontrol_admin_manage_page',
819
+ __NAMESPACE__ . '\admin_manage_page'
820
+ );
821
+
822
+ add_action( "load-{$schedules}", __NAMESPACE__ . '\admin_help_tab' );
823
+ add_action( "load-{$events}", __NAMESPACE__ . '\admin_help_tab' );
824
+ }
825
+
826
+ /**
827
+ * Adds a Help tab with links to help resources.
828
+ *
829
+ * @return void
830
+ */
831
+ function admin_help_tab() {
832
+ $screen = get_current_screen();
833
+
834
+ if ( ! $screen ) {
835
+ return;
836
+ }
837
+
838
+ $content = '<p>' . __( 'There are several places to get help with issues relating to WP-Cron:', 'wp-crontrol' ) . '</p>';
839
+ $content .= '<ul>';
840
+ $content .= '<li>';
841
+ $content .= sprintf(
842
+ /* translators: %s: URL to the documentation */
843
+ __( '<a href="%s">Read the WP Crontrol wiki</a> which contains information about events that have missed their schedule, problems with spawning a call to the WP-Cron system, and much more.', 'wp-crontrol' ),
844
+ 'https://github.com/johnbillion/wp-crontrol/wiki'
845
+ );
846
+ $content .= '</li>';
847
+ $content .= '<li>';
848
+ $content .= sprintf(
849
+ /* translators: %s: URL to the documentation */
850
+ __( '<a href="%s">Read the Frequently Asked Questions (FAQ)</a> which cover many common questions and answers.', 'wp-crontrol' ),
851
+ 'https://wordpress.org/plugins/wp-crontrol/faq/'
852
+ );
853
+ $content .= '</li>';
854
+ $content .= '<li>';
855
+ $content .= sprintf(
856
+ /* translators: %s: URL to the documentation */
857
+ __( '<a href="%s">Read the WordPress.org documentation on WP-Cron</a> for more technical details about the WP-Cron system for developers.', 'wp-crontrol' ),
858
+ 'https://developer.wordpress.org/plugins/cron/'
859
+ );
860
+ $content .= '</ul>';
861
+
862
+ $screen->add_help_tab(
863
+ array(
864
+ 'id' => 'crontrol-help',
865
+ 'title' => __( 'Help', 'wp-crontrol' ),
866
+ 'content' => $content,
867
+ )
868
+ );
869
+ }
870
+
871
+ /**
872
+ * Adds items to the plugin's action links on the Plugins listing screen.
873
+ *
874
+ * @param array<string,string> $actions Array of action links.
875
+ * @param string $plugin_file Path to the plugin file relative to the plugins directory.
876
+ * @param mixed[] $plugin_data An array of plugin data.
877
+ * @param string $context The plugin context.
878
+ * @return array<string,string> Array of action links.
879
+ */
880
+ function plugin_action_links( $actions, $plugin_file, $plugin_data, $context ) {
881
+ $new = array(
882
+ 'crontrol-events' => sprintf(
883
+ '<a href="%s">%s</a>',
884
+ esc_url( admin_url( 'tools.php?page=crontrol_admin_manage_page' ) ),
885
+ esc_html__( 'Events', 'wp-crontrol' )
886
+ ),
887
+ 'crontrol-schedules' => sprintf(
888
+ '<a href="%s">%s</a>',
889
+ esc_url( admin_url( 'options-general.php?page=crontrol_admin_options_page' ) ),
890
+ esc_html__( 'Schedules', 'wp-crontrol' )
891
+ ),
892
+ 'crontrol-help' => sprintf(
893
+ '<a href="%s">%s</a>',
894
+ 'https://github.com/johnbillion/wp-crontrol/wiki',
895
+ esc_html__( 'Help', 'wp-crontrol' )
896
+ ),
897
+ );
898
+
899
+ return array_merge( $new, $actions );
900
+ }
901
+
902
+ /**
903
+ * Adds items to the plugin's action links on the Network Admin -> Plugins listing screen.
904
+ *
905
+ * @param array<string,string> $actions Array of action links.
906
+ * @return array<string,string> Array of action links.
907
+ */
908
+ function network_plugin_action_links( $actions ) {
909
+ $new = array(
910
+ 'crontrol-help' => sprintf(
911
+ '<a href="%s">%s</a>',
912
+ 'https://github.com/johnbillion/wp-crontrol/wiki',
913
+ esc_html__( 'Help', 'wp-crontrol' )
914
+ ),
915
+ );
916
+
917
+ return array_merge( $new, $actions );
918
+ }
919
+
920
+ /**
921
+ * Gives WordPress the plugin's set of cron schedules.
922
+ *
923
+ * Called by the `cron_schedules` filter.
924
+ *
925
+ * @param array<string,array<string,(int|string)>> $scheds Array of cron schedule arrays. Usually empty.
926
+ * @return array<string,array<string,(int|string)>> Array of modified cron schedule arrays.
927
+ */
928
+ function filter_cron_schedules( array $scheds ) {
929
+ $new_scheds = get_option( 'crontrol_schedules', array() );
930
+
931
+ if ( ! is_array( $new_scheds ) ) {
932
+ return $scheds;
933
+ }
934
+
935
+ return array_merge( $new_scheds, $scheds );
936
+ }
937
+
938
+ /**
939
+ * Displays the options page for the plugin.
940
+ *
941
+ * @return void
942
+ */
943
+ function admin_options_page() {
944
+ $messages = array(
945
+ '2' => array(
946
+ /* translators: 1: The name of the cron schedule. */
947
+ __( 'Deleted the cron schedule %s.', 'wp-crontrol' ),
948
+ 'success',
949
+ ),
950
+ '3' => array(
951
+ /* translators: 1: The name of the cron schedule. */
952
+ __( 'Added the cron schedule %s.', 'wp-crontrol' ),
953
+ 'success',
954
+ ),
955
+ );
956
+ if ( isset( $_GET['crontrol_message'] ) && isset( $_GET['crontrol_name'] ) && isset( $messages[ $_GET['crontrol_message'] ] ) ) {
957
+ $hook = wp_unslash( $_GET['crontrol_name'] );
958
+ $message = wp_unslash( $_GET['crontrol_message'] );
959
+
960
+ printf(
961
+ '<div id="crontrol-message" class="notice notice-%1$s is-dismissible"><p>%2$s</p></div>',
962
+ esc_attr( $messages[ $message ][1] ),
963
+ sprintf(
964
+ esc_html( $messages[ $message ][0] ),
965
+ '<strong>' . esc_html( $hook ) . '</strong>'
966
+ )
967
+ );
968
+ }
969
+
970
+ $table = new Schedule_List_Table();
971
+
972
+ $table->prepare_items();
973
+
974
+ ?>
975
+ <div class="wrap">
976
+
977
+ <?php do_tabs(); ?>
978
+
979
+ <h1><?php esc_html_e( 'Cron Schedules', 'wp-crontrol' ); ?></h1>
980
+
981
+ <?php $table->views(); ?>
982
+
983
+ <div id="col-container" class="wp-clearfix">
984
+ <div id="col-left">
985
+ <div class="col-wrap">
986
+ <div class="form-wrap">
987
+ <h2><?php esc_html_e( 'Add Cron Schedule', 'wp-crontrol' ); ?></h2>
988
+ <p><?php esc_html_e( 'Adding a new cron schedule will allow you to schedule events that re-occur at the given interval.', 'wp-crontrol' ); ?></p>
989
+ <form method="post" action="options-general.php?page=crontrol_admin_options_page">
990
+ <div class="form-field form-required">
991
+ <label for="crontrol_schedule_internal_name">
992
+ <?php esc_html_e( 'Internal Name', 'wp-crontrol' ); ?>
993
+ </label>
994
+ <input type="text" value="" id="crontrol_schedule_internal_name" name="crontrol_schedule_internal_name" required/>
995
+ </div>
996
+ <div class="form-field form-required">
997
+ <label for="crontrol_schedule_interval">
998
+ <?php esc_html_e( 'Interval (seconds)', 'wp-crontrol' ); ?>
999
+ </label>
1000
+ <input type="number" value="" id="crontrol_schedule_interval" name="crontrol_schedule_interval" min="1" step="1" required/>
1001
+ </div>
1002
+ <div class="form-field form-required">
1003
+ <label for="crontrol_schedule_display_name">
1004
+ <?php esc_html_e( 'Display Name', 'wp-crontrol' ); ?>
1005
+ </label>
1006
+ <input type="text" value="" id="crontrol_schedule_display_name" name="crontrol_schedule_display_name" required/>
1007
+ </div>
1008
+ <p class="submit">
1009
+ <input type="submit" class="button button-primary" value="<?php esc_attr_e( 'Add Cron Schedule', 'wp-crontrol' ); ?>" name="crontrol_new_schedule"/>
1010
+ </p>
1011
+ <?php wp_nonce_field( 'crontrol-new-schedule' ); ?>
1012
+ </form>
1013
+ </div>
1014
+ </div>
1015
+ </div>
1016
+ <div id="col-right">
1017
+ <div class="col-wrap">
1018
+ <?php $table->display(); ?>
1019
+ </div>
1020
+ </div>
1021
+ </div>
1022
+ <?php
1023
+ }
1024
+
1025
+ /**
1026
+ * Clears the doing cron status when an event is unscheduled.
1027
+ *
1028
+ * What on earth does this function do, and why?
1029
+ *
1030
+ * Good question. The purpose of this function is to prevent other overdue cron events from firing when an event is run
1031
+ * manually with the "Run Now" action. WP Crontrol works very hard to ensure that when cron event runs manually that it
1032
+ * runs in the exact same way it would run as part of its schedule - via a properly spawned cron with a queued event in
1033
+ * place. It does this by queueing an event at time `1` (1 second into 1st January 1970) and then immediately spawning
1034
+ * cron (see the `Event\run()` function).
1035
+ *
1036
+ * The problem this causes is if other events are due then they will all run too, and this isn't desirable because if a
1037
+ * site has a large number of stuck events due to a problem with the cron runner then it's not desirable for all those
1038
+ * events to run when another is manually run. This happens because WordPress core will attempt to run all due events
1039
+ * whenever cron is spawned.
1040
+ *
1041
+ * The code in this function prevents multiple events from running by changing the value of the `doing_cron` transient
1042
+ * when an event gets unscheduled during a manual run, which prevents wp-cron.php from iterating more than one event.
1043
+ *
1044
+ * The `pre_unschedule_event` filter is used for this because it's just about the only hook available within this loop.
1045
+ *
1046
+ * Refs:
1047
+ * - https://core.trac.wordpress.org/browser/trunk/src/wp-cron.php?rev=47198&marks=127,141#L122
1048
+ *
1049
+ * @param mixed $pre The pre-flight value of the event unschedule short-circuit. Not used.
1050
+ * @return mixed The unaltered pre-flight value.
1051
+ */
1052
+ function maybe_clear_doing_cron( $pre ) {
1053
+ if ( defined( 'DOING_CRON' ) && DOING_CRON && isset( $_GET['crontrol-single-event'] ) ) {
1054
+ delete_transient( 'doing_cron' );
1055
+ }
1056
+
1057
+ return $pre;
1058
+ }
1059
+
1060
+ /**
1061
+ * Ajax handler which outputs a hash of the current list of scheduled events.
1062
+ *
1063
+ * @return void
1064
+ */
1065
+ function ajax_check_events_hash() {
1066
+ if ( ! current_user_can( 'manage_options' ) ) {
1067
+ wp_send_json_error( null, 403 );
1068
+ }
1069
+
1070
+ $data = json_encode( Event\get() );
1071
+
1072
+ if ( false === $data ) {
1073
+ wp_send_json_error( null, 500 );
1074
+ }
1075
+
1076
+ wp_send_json_success( md5( $data ) );
1077
+ }
1078
+
1079
+ /**
1080
+ * Gets the status of WP-Cron functionality on the site by performing a test spawn if necessary. Cached for one hour when all is well.
1081
+ *
1082
+ * @param bool $cache Whether to use the cached result from previous calls.
1083
+ * @return true|WP_Error Boolean true if the cron spawner is working as expected, or a `WP_Error` object if not.
1084
+ */
1085
+ function test_cron_spawn( $cache = true ) {
1086
+ global $wp_version;
1087
+
1088
+ $cron_runner_plugins = array(
1089
+ '\HM\Cavalcade\Plugin\Job' => 'Cavalcade',
1090
+ '\Automattic\WP\Cron_Control\Main' => 'Cron Control',
1091
+ '\KMM\KRoN\Core' => 'KMM KRoN',
1092
+ );
1093
+
1094
+ foreach ( $cron_runner_plugins as $class => $plugin ) {
1095
+ if ( class_exists( $class ) ) {
1096
+ return new WP_Error( 'crontrol_info', sprintf(
1097
+ /* translators: 1: The name of the plugin that controls the running of cron events. */
1098
+ __( 'WP-Cron spawning is being managed by the %s plugin.', 'wp-crontrol' ),
1099
+ $plugin
1100
+ ) );
1101
+ }
1102
+ }
1103
+
1104
+ if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
1105
+ return new WP_Error( 'crontrol_info', sprintf(
1106
+ /* translators: 1: The name of the PHP constant that is set. */
1107
+ __( 'The %s constant is set to true. WP-Cron spawning is disabled.', 'wp-crontrol' ),
1108
+ 'DISABLE_WP_CRON'
1109
+ ) );
1110
+ }
1111
+
1112
+ if ( defined( 'ALTERNATE_WP_CRON' ) && ALTERNATE_WP_CRON ) {
1113
+ return new WP_Error( 'crontrol_info', sprintf(
1114
+ /* translators: 1: The name of the PHP constant that is set. */
1115
+ __( 'The %s constant is set to true.', 'wp-crontrol' ),
1116
+ 'ALTERNATE_WP_CRON'
1117
+ ) );
1118
+ }
1119
+
1120
+ $cached_status = get_transient( 'crontrol-cron-test-ok' );
1121
+
1122
+ if ( $cache && $cached_status ) {
1123
+ return true;
1124
+ }
1125
+
1126
+ $sslverify = version_compare( $wp_version, '4.0', '<' );
1127
+ $doing_wp_cron = sprintf( '%.22F', microtime( true ) );
1128
+
1129
+ $cron_request = apply_filters( 'cron_request', array(
1130
+ 'url' => add_query_arg( 'doing_wp_cron', $doing_wp_cron, site_url( 'wp-cron.php' ) ),
1131
+ 'key' => $doing_wp_cron,
1132
+ 'args' => array(
1133
+ 'timeout' => 3,
1134
+ 'blocking' => true,
1135
+ 'sslverify' => apply_filters( 'https_local_ssl_verify', $sslverify ),
1136
+ ),
1137
+ ) );
1138
+
1139
+ $cron_request['args']['blocking'] = true;
1140
+
1141
+ $result = wp_remote_post( $cron_request['url'], $cron_request['args'] );
1142
+
1143
+ if ( is_wp_error( $result ) ) {
1144
+ return $result;
1145
+ } elseif ( wp_remote_retrieve_response_code( $result ) >= 300 ) {
1146
+ return new WP_Error( 'unexpected_http_response_code', sprintf(
1147
+ /* translators: 1: The HTTP response code. */
1148
+ __( 'Unexpected HTTP response code: %s', 'wp-crontrol' ),
1149
+ intval( wp_remote_retrieve_response_code( $result ) )
1150
+ ) );
1151
+ } else {
1152
+ set_transient( 'crontrol-cron-test-ok', 1, 3600 );
1153
+ return true;
1154
+ }
1155
+
1156
+ }
1157
+
1158
+ /**
1159
+ * Shows the status of WP-Cron functionality on the site. Only displays a message when there's a problem.
1160
+ *
1161
+ * @param string $tab The tab name.
1162
+ * @return void
1163
+ */
1164
+ function show_cron_status( $tab ) {
1165
+ if ( 'UTC' !== date_default_timezone_get() ) {
1166
+ ?>
1167
+ <div id="crontrol-timezone-warning" class="notice notice-warning">
1168
+ <?php
1169
+ printf(
1170
+ '<p>%1$s</p><p><a href="%2$s">%3$s</a></p>',
1171
+ /* translators: %s: Help page URL. */
1172
+ esc_html__( 'PHP default timezone is not set to UTC. This may cause issues with cron event timings.', 'wp-crontrol' ),
1173
+ 'https://github.com/johnbillion/wp-crontrol/wiki/PHP-default-timezone-is-not-set-to-UTC',
1174
+ esc_html__( 'More information', 'wp-crontrol' )
1175
+ );
1176
+ ?>
1177
+ </div>
1178
+ <?php
1179
+ }
1180
+
1181
+ $status = test_cron_spawn();
1182
+
1183
+ if ( is_wp_error( $status ) ) {
1184
+ if ( 'crontrol_info' === $status->get_error_code() ) {
1185
+ ?>
1186
+ <div id="crontrol-status-notice" class="notice notice-info">
1187
+ <p><?php echo esc_html( $status->get_error_message() ); ?></p>
1188
+ </div>
1189
+ <?php
1190
+ } else {
1191
+ ?>
1192
+ <div id="crontrol-status-error" class="error">
1193
+ <?php
1194
+ printf(
1195
+ '<p>%1$s</p><p><a href="%2$s">%3$s</a></p>',
1196
+ sprintf(
1197
+ /* translators: 1: Error message text. */
1198
+ esc_html__( 'There was a problem spawning a call to the WP-Cron system on your site. This means WP-Cron events on your site may not work. The problem was: %s', 'wp-crontrol' ),
1199
+ '</p><p><strong>' . esc_html( $status->get_error_message() ) . '</strong>'
1200
+ ),
1201
+ 'https://github.com/johnbillion/wp-crontrol/wiki/Problems-with-spawning-a-call-to-the-WP-Cron-system',
1202
+ esc_html__( 'More information', 'wp-crontrol' )
1203
+ );
1204
+ ?>
1205
+ </div>
1206
+ <?php
1207
+ }
1208
+ }
1209
+ }
1210
+
1211
+ /**
1212
+ * Get the display name for the site's timezone.
1213
+ *
1214
+ * @return string The name and UTC offset for the site's timezone.
1215
+ */
1216
+ function get_timezone_name() {
1217
+ /** @var string */
1218
+ $timezone_string = get_option( 'timezone_string', '' );
1219
+ $gmt_offset = get_option( 'gmt_offset', 0 );
1220
+
1221
+ if ( 'UTC' === $timezone_string || ( empty( $gmt_offset ) && empty( $timezone_string ) ) ) {
1222
+ return 'UTC';
1223
+ }
1224
+
1225
+ if ( '' === $timezone_string ) {
1226
+ return get_utc_offset();
1227
+ }
1228
+
1229
+ return sprintf(
1230
+ '%s, %s',
1231
+ str_replace( '_', ' ', $timezone_string ),
1232
+ get_utc_offset()
1233
+ );
1234
+ }
1235
+
1236
+ /**
1237
+ * Returns a display value for a UTC offset.
1238
+ *
1239
+ * Examples:
1240
+ * - UTC
1241
+ * - UTC+4
1242
+ * - UTC-6
1243
+ *
1244
+ * @return string The UTC offset display value.
1245
+ */
1246
+ function get_utc_offset() {
1247
+ $offset = get_option( 'gmt_offset', 0 );
1248
+
1249
+ if ( empty( $offset ) ) {
1250
+ return 'UTC';
1251
+ }
1252
+
1253
+ if ( 0 <= $offset ) {
1254
+ $formatted_offset = '+' . (string) $offset;
1255
+ } else {
1256
+ $formatted_offset = (string) $offset;
1257
+ }
1258
+ $formatted_offset = str_replace(
1259
+ array( '.25', '.5', '.75' ),
1260
+ array( ':15', ':30', ':45' ),
1261
+ $formatted_offset
1262
+ );
1263
+ return 'UTC' . $formatted_offset;
1264
+ }
1265
+
1266
+ /**
1267
+ * Shows the form used to add/edit cron events.
1268
+ *
1269
+ * @param bool $editing Whether the form is for the event editor.
1270
+ * @return void
1271
+ */
1272
+ function show_cron_form( $editing ) {
1273
+ $display_args = '';
1274
+ $edit_id = null;
1275
+ $existing = false;
1276
+
1277
+ if ( $editing && ! empty( $_GET['crontrol_id'] ) ) {
1278
+ $edit_id = wp_unslash( $_GET['crontrol_id'] );
1279
+
1280
+ foreach ( Event\get() as $event ) {
1281
+ if ( $edit_id === $event->hook && intval( $_GET['crontrol_next_run_utc'] ) === $event->timestamp && $event->sig === $_GET['crontrol_sig'] ) {
1282
+ $existing = array(
1283
+ 'hookname' => $event->hook,
1284
+ 'next_run' => $event->timestamp, // UTC
1285
+ 'schedule' => ( $event->schedule ? $event->schedule : '_oneoff' ),
1286
+ 'sig' => $event->sig,
1287
+ 'args' => $event->args,
1288
+ );
1289
+ break;
1290
+ }
1291
+ }
1292
+
1293
+ if ( empty( $existing ) ) {
1294
+ ?>
1295
+ <div id="crontrol-event-not-found" class="notice notice-error">
1296
+ <?php
1297
+ printf(
1298
+ '<p>%1$s</p>',
1299
+ esc_html__( 'The event you are trying to edit does not exist.', 'wp-crontrol' )
1300
+ );
1301
+ ?>
1302
+ </div>
1303
+ <?php
1304
+ return;
1305
+ }
1306
+ }
1307
+
1308
+ $is_editing_php = ( $existing && 'crontrol_cron_job' === $existing['hookname'] );
1309
+
1310
+ if ( $is_editing_php ) {
1311
+ $helper_text = esc_html__( 'Cron events trigger actions in your code. Enter the schedule of the event, as well as the PHP code to execute when the action is triggered.', 'wp-crontrol' );
1312
+ } else {
1313
+ $helper_text = sprintf(
1314
+ /* translators: %s: A file name */
1315
+ esc_html__( 'Cron events trigger actions in your code. A cron event needs a corresponding action hook somewhere in code, e.g. the %1$s file in your theme.', 'wp-crontrol' ),
1316
+ '<code>functions.php</code>'
1317
+ );
1318
+ }
1319
+
1320
+ if ( is_array( $existing ) ) {
1321
+ $other_fields = wp_nonce_field( "crontrol-edit-cron_{$existing['hookname']}_{$existing['sig']}_{$existing['next_run']}", '_wpnonce', true, false );
1322
+ $other_fields .= sprintf( '<input name="crontrol_original_hookname" type="hidden" value="%s" />',
1323
+ esc_attr( $existing['hookname'] )
1324
+ );
1325
+ $other_fields .= sprintf( '<input name="crontrol_original_sig" type="hidden" value="%s" />',
1326
+ esc_attr( $existing['sig'] )
1327
+ );
1328
+ $other_fields .= sprintf( '<input name="crontrol_original_next_run_utc" type="hidden" value="%s" />',
1329
+ esc_attr( (string) $existing['next_run'] )
1330
+ );
1331
+ if ( ! empty( $existing['args'] ) ) {
1332
+ $display_args = wp_json_encode( $existing['args'] );
1333
+
1334
+ if ( false === $display_args ) {
1335
+ $display_args = '';
1336
+ }
1337
+ }
1338
+ $button = __( 'Update Event', 'wp-crontrol' );
1339
+ $next_run_gmt = gmdate( 'Y-m-d H:i:s', $existing['next_run'] );
1340
+ $next_run_date_local = get_date_from_gmt( $next_run_gmt, 'Y-m-d' );
1341
+ $next_run_time_local = get_date_from_gmt( $next_run_gmt, 'H:i:s' );
1342
+ } else {
1343
+ $other_fields = wp_nonce_field( 'crontrol-new-cron', '_wpnonce', true, false );
1344
+ $existing = array(
1345
+ 'hookname' => '',
1346
+ 'args' => array(),
1347
+ 'next_run' => 'now', // UTC
1348
+ 'schedule' => false,
1349
+ );
1350
+
1351
+ $button = __( 'Add Event', 'wp-crontrol' );
1352
+ $next_run_date_local = '';
1353
+ $next_run_time_local = '';
1354
+ }
1355
+
1356
+ if ( $is_editing_php ) {
1357
+ if ( ! isset( $existing['args']['code'] ) ) {
1358
+ $existing['args']['code'] = '';
1359
+ }
1360
+ if ( ! isset( $existing['args']['name'] ) ) {
1361
+ $existing['args']['name'] = '';
1362
+ }
1363
+ }
1364
+
1365
+ $can_add_php = current_user_can( 'edit_files' ) && ! $editing;
1366
+ $allowed = ( ! $is_editing_php || current_user_can( 'edit_files' ) );
1367
+ ?>
1368
+ <div id="crontrol_form" class="wrap narrow">
1369
+ <?php
1370
+ if ( $allowed ) {
1371
+ if ( $editing ) {
1372
+ $heading = __( 'Edit Cron Event', 'wp-crontrol' );
1373
+ } else {
1374
+ $heading = __( 'Add Cron Event', 'wp-crontrol' );
1375
+ }
1376
+
1377
+ do_tabs();
1378
+
1379
+ printf(
1380
+ '<h1>%s</h1>',
1381
+ esc_html( $heading )
1382
+ );
1383
+ printf(
1384
+ '<p>%s</p>',
1385
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1386
+ $helper_text
1387
+ );
1388
+ ?>
1389
+ <form method="post" action="<?php echo esc_url( admin_url( 'tools.php?page=crontrol_admin_manage_page' ) ); ?>" class="crontrol-edit-event crontrol-edit-event-<?php echo ( $is_editing_php ) ? 'php' : 'standard'; ?>">
1390
+ <?php
1391
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1392
+ echo $other_fields;
1393
+ ?>
1394
+ <table class="form-table"><tbody>
1395
+ <?php
1396
+ if ( $editing ) {
1397
+ $action = $is_editing_php ? 'edit_php_cron' : 'edit_cron';
1398
+ printf(
1399
+ '<input type="hidden" name="crontrol_action" value="%s"/>',
1400
+ esc_attr( $action )
1401
+ );
1402
+ } elseif ( $can_add_php ) {
1403
+ ?>
1404
+ <tr class="hide-if-no-js">
1405
+ <th valign="top" scope="row">
1406
+ <?php esc_html_e( 'Event Type', 'wp-crontrol' ); ?>
1407
+ </th>
1408
+ <td>
1409
+ <p><label><input type="radio" name="crontrol_action" value="new_cron" checked>Standard cron event</label></p>
1410
+ <p><label><input type="radio" name="crontrol_action" value="new_php_cron">PHP cron event</label></p>
1411
+ </td>
1412
+ </tr>
1413
+ <?php
1414
+ } else {
1415
+ ?>
1416
+ <input type="hidden" name="crontrol_action" value="new_cron"/>
1417
+ <?php
1418
+ }
1419
+
1420
+ if ( $is_editing_php || $can_add_php ) {
1421
+ ?>
1422
+ <tr class="crontrol-event-php">
1423
+ <th valign="top" scope="row">
1424
+ <label for="crontrol_hookcode">
1425
+ <?php esc_html_e( 'PHP Code', 'wp-crontrol' ); ?>
1426
+ </label>
1427
+ </th>
1428
+ <td>
1429
+ <p class="description">
1430
+ <?php
1431
+ printf(
1432
+ /* translators: The PHP tag name */
1433
+ esc_html__( 'The opening %s tag must not be included.', 'wp-crontrol' ),
1434
+ '<code>&lt;?php</code>'
1435
+ );
1436
+ ?>
1437
+ </p>
1438
+ <p><textarea class="large-text code" rows="10" cols="50" id="crontrol_hookcode" name="crontrol_hookcode"><?php echo esc_textarea( $editing ? $existing['args']['code'] : '' ); ?></textarea></p>
1439
+ <?php do_action( 'crontrol/manage/hookcode', $existing ); ?>
1440
+ </td>
1441
+ </tr>
1442
+ <tr class="crontrol-event-php">
1443
+ <th valign="top" scope="row">
1444
+ <label for="crontrol_eventname">
1445
+ <?php esc_html_e( 'Event Name (optional)', 'wp-crontrol' ); ?>
1446
+ </label>
1447
+ </th>
1448
+ <td>
1449
+ <input type="text" class="regular-text" id="crontrol_eventname" name="crontrol_eventname" value="<?php echo esc_attr( $editing ? $existing['args']['name'] : '' ); ?>"/>
1450
+ <?php do_action( 'crontrol/manage/eventname', $existing ); ?>
1451
+ </td>
1452
+ </tr>
1453
+ <?php
1454
+ }
1455
+
1456
+ if ( ! $is_editing_php ) {
1457
+ ?>
1458
+ <tr class="crontrol-event-standard">
1459
+ <th valign="top" scope="row">
1460
+ <label for="crontrol_hookname">
1461
+ <?php esc_html_e( 'Hook Name', 'wp-crontrol' ); ?>
1462
+ </label>
1463
+ </th>
1464
+ <td>
1465
+ <input type="text" autocorrect="off" autocapitalize="off" spellcheck="false" class="regular-text" id="crontrol_hookname" name="crontrol_hookname" value="<?php echo esc_attr( $existing['hookname'] ); ?>" required />
1466
+ <?php do_action( 'crontrol/manage/hookname', $existing ); ?>
1467
+ </td>
1468
+ </tr>
1469
+ <tr class="crontrol-event-standard">
1470
+ <th valign="top" scope="row">
1471
+ <label for="crontrol_args">
1472
+ <?php esc_html_e( 'Arguments (optional)', 'wp-crontrol' ); ?>
1473
+ </label>
1474
+ </th>
1475
+ <td>
1476
+ <input type="text" autocorrect="off" autocapitalize="off" spellcheck="false" class="regular-text code" id="crontrol_args" name="crontrol_args" value="<?php echo esc_attr( $display_args ); ?>"/>
1477
+ <?php do_action( 'crontrol/manage/args', $existing ); ?>
1478
+ <p class="description">
1479
+ <?php
1480
+ printf(
1481
+ /* translators: 1, 2, and 3: Example values for an input field. */
1482
+ esc_html__( 'Use a JSON encoded array, e.g. %1$s, %2$s, or %3$s', 'wp-crontrol' ),
1483
+ '<code>[25]</code>',
1484
+ '<code>["asdf"]</code>',
1485
+ '<code>["i","want",25,"cakes"]</code>'
1486
+ );
1487
+ ?>
1488
+ </p>
1489
+ </td>
1490
+ </tr>
1491
+ <?php
1492
+ }
1493
+ ?>
1494
+ <tr>
1495
+ <th valign="top" scope="row">
1496
+ <label for="crontrol_next_run_date_local">
1497
+ <?php esc_html_e( 'Next Run', 'wp-crontrol' ); ?>
1498
+ </label>
1499
+ </th>
1500
+ <td>
1501
+ <ul>
1502
+ <li>
1503
+ <label>
1504
+ <input type="radio" name="crontrol_next_run_date_local" value="now" checked>
1505
+ <?php esc_html_e( 'Now', 'wp-crontrol' ); ?>
1506
+ </label>
1507
+ </li>
1508
+ <li>
1509
+ <label>
1510
+ <input type="radio" name="crontrol_next_run_date_local" value="+1 day">
1511
+ <?php esc_html_e( 'Tomorrow', 'wp-crontrol' ); ?>
1512
+ </label>
1513
+ </li>
1514
+ <li>
1515
+ <label>
1516
+ <input type="radio" name="crontrol_next_run_date_local" value="custom" id="crontrol_next_run_date_local_custom" <?php checked( $editing ); ?>>
1517
+ <?php
1518
+ printf(
1519
+ /* translators: %s: An input field for specifying a date and time */
1520
+ esc_html__( 'At: %s', 'wp-crontrol' ),
1521
+ sprintf(
1522
+ '<br>
1523
+ <input type="date" autocorrect="off" autocapitalize="off" spellcheck="false" name="crontrol_next_run_date_local_custom_date" id="crontrol_next_run_date_local_custom_date" value="%1$s" placeholder="yyyy-mm-dd" pattern="\d{4}-\d{2}-\d{2}" />
1524
+ <input type="time" autocorrect="off" autocapitalize="off" spellcheck="false" name="crontrol_next_run_date_local_custom_time" id="crontrol_next_run_date_local_custom_time" value="%2$s" step="1" placeholder="hh:mm:ss" pattern="\d{2}:\d{2}:\d{2}" />',
1525
+ esc_attr( $next_run_date_local ),
1526
+ esc_attr( $next_run_time_local )
1527
+ )
1528
+ );
1529
+ ?>
1530
+ </label>
1531
+ </li>
1532
+ </ul>
1533
+
1534
+ <?php do_action( 'crontrol/manage/next_run', $existing ); ?>
1535
+
1536
+ <p class="description">
1537
+ <?php
1538
+ printf(
1539
+ /* translators: %s Timezone name. */
1540
+ esc_html__( 'Timezone: %s', 'wp-crontrol' ),
1541
+ esc_html( get_timezone_name() )
1542
+ );
1543
+ ?>
1544
+ </p>
1545
+ </td>
1546
+ </tr>
1547
+ <tr>
1548
+ <th valign="top" scope="row">
1549
+ <label for="crontrol_schedule">
1550
+ <?php esc_html_e( 'Recurrence', 'wp-crontrol' ); ?>
1551
+ </label>
1552
+ </th>
1553
+ <td>
1554
+ <?php Schedule\dropdown( $existing['schedule'] ); ?>
1555
+ <?php do_action( 'crontrol/manage/schedule', $existing ); ?>
1556
+ </td>
1557
+ </tr>
1558
+ </tbody></table>
1559
+ <p class="submit">
1560
+ <input type="submit" class="button button-primary" value="<?php echo esc_attr( $button ); ?>"/>
1561
+ </p>
1562
+ <p class="description">
1563
+ <?php
1564
+ echo esc_html( sprintf(
1565
+ /* translators: 1: Date and time, 2: Timezone */
1566
+ __( 'Site time when page loaded: %1$s (%2$s)', 'wp-crontrol' ),
1567
+ date_i18n( 'Y-m-d H:i:s' ),
1568
+ get_timezone_name()
1569
+ ) );
1570
+ ?>
1571
+ </p>
1572
+ </form>
1573
+ <?php } else { ?>
1574
+ <div class="error inline">
1575
+ <p><?php esc_html_e( 'You cannot add, edit, or delete PHP cron events because your user account does not have the ability to edit files.', 'wp-crontrol' ); ?></p>
1576
+ </div>
1577
+ <?php } ?>
1578
+ </div>
1579
+ <?php
1580
+ }
1581
+
1582
+ /**
1583
+ * Displays the manage page for the plugin.
1584
+ *
1585
+ * @return void
1586
+ */
1587
+ function admin_manage_page() {
1588
+ $messages = array(
1589
+ '1' => array(
1590
+ /* translators: 1: The name of the cron event. */
1591
+ __( 'Scheduled the cron event %s to run now. The original event will not be affected.', 'wp-crontrol' ),
1592
+ 'success',
1593
+ ),
1594
+ '2' => array(
1595
+ /* translators: 1: The name of the cron event. */
1596
+ __( 'Deleted all %s cron events.', 'wp-crontrol' ),
1597
+ 'success',
1598
+ ),
1599
+ '3' => array(
1600
+ /* translators: 1: The name of the cron event. */
1601
+ __( 'There are no %s cron events to delete.', 'wp-crontrol' ),
1602
+ 'info',
1603
+ ),
1604
+ '4' => array(
1605
+ /* translators: 1: The name of the cron event. */
1606
+ __( 'Saved the cron event %s.', 'wp-crontrol' ),
1607
+ 'success',
1608
+ ),
1609
+ '5' => array(
1610
+ /* translators: 1: The name of the cron event. */
1611
+ __( 'Created the cron event %s.', 'wp-crontrol' ),
1612
+ 'success',
1613
+ ),
1614
+ '6' => array(
1615
+ /* translators: 1: The name of the cron event. */
1616
+ __( 'Deleted the cron event %s.', 'wp-crontrol' ),
1617
+ 'success',
1618
+ ),
1619
+ '7' => array(
1620
+ /* translators: 1: The name of the cron event. */
1621
+ __( 'Failed to the delete the cron event %s.', 'wp-crontrol' ),
1622
+ 'error',
1623
+ ),
1624
+ '8' => array(
1625
+ /* translators: 1: The name of the cron event. */
1626
+ __( 'Failed to the execute the cron event %s.', 'wp-crontrol' ),
1627
+ 'error',
1628
+ ),
1629
+ '9' => array(
1630
+ __( 'Deleted the selected cron events.', 'wp-crontrol' ),
1631
+ 'success',
1632
+ ),
1633
+ '10' => array(
1634
+ /* translators: 1: The name of the cron event. */
1635
+ __( 'Failed to save the cron event %s.', 'wp-crontrol' ),
1636
+ 'error',
1637
+ ),
1638
+ '11' => array(
1639
+ /* translators: 1: The name of the cron event. */
1640
+ __( 'Paused the %s hook.', 'wp-crontrol' ),
1641
+ 'success',
1642
+ ),
1643
+ '12' => array(
1644
+ /* translators: 1: The name of the cron event. */
1645
+ __( 'Resumed the %s hook.', 'wp-crontrol' ),
1646
+ 'success',
1647
+ ),
1648
+ 'error' => array(
1649
+ __( 'An unknown error occurred.', 'wp-crontrol' ),
1650
+ 'error',
1651
+ ),
1652
+ );
1653
+
1654
+ if ( isset( $_GET['crontrol_name'] ) && isset( $_GET['crontrol_message'] ) && isset( $messages[ $_GET['crontrol_message'] ] ) ) {
1655
+ $hook = wp_unslash( $_GET['crontrol_name'] );
1656
+ $message = wp_unslash( $_GET['crontrol_message'] );
1657
+ $link = '';
1658
+
1659
+ if ( 'error' === $message ) {
1660
+ $error = get_message();
1661
+
1662
+ if ( $error ) {
1663
+ $messages['error'][0] = $error;
1664
+ }
1665
+ }
1666
+
1667
+ printf(
1668
+ '<div id="crontrol-message" class="notice notice-%1$s is-dismissible"><p>%2$s%3$s</p></div>',
1669
+ esc_attr( $messages[ $message ][1] ),
1670
+ sprintf(
1671
+ esc_html( $messages[ $message ][0] ),
1672
+ '<strong>' . esc_html( $hook ) . '</strong>'
1673
+ ),
1674
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1675
+ $link
1676
+ );
1677
+ }
1678
+
1679
+ $tabs = get_tab_states();
1680
+ $table = Event\get_list_table();
1681
+
1682
+ switch ( true ) {
1683
+ case $tabs['events']:
1684
+ ?>
1685
+ <div class="wrap">
1686
+ <?php do_tabs(); ?>
1687
+
1688
+ <h1 class="wp-heading-inline"><?php esc_html_e( 'Cron Events', 'wp-crontrol' ); ?></h1>
1689
+
1690
+ <?php echo '<a href="' . esc_url( admin_url( 'tools.php?page=crontrol_admin_manage_page&crontrol_action=new-cron' ) ) . '" class="page-title-action">' . esc_html__( 'Add New', 'wp-crontrol' ) . '</a>'; ?>
1691
+
1692
+ <hr class="wp-header-end">
1693
+
1694
+ <?php $table->views(); ?>
1695
+
1696
+ <form id="posts-filter" method="get" action="tools.php">
1697
+ <input type="hidden" name="page" value="crontrol_admin_manage_page" />
1698
+ <?php $table->search_box( __( 'Search Hook Names', 'wp-crontrol' ), 'cron-event' ); ?>
1699
+ </form>
1700
+
1701
+ <form method="post" action="tools.php?page=crontrol_admin_manage_page">
1702
+ <div class="table-responsive">
1703
+ <?php $table->display(); ?>
1704
+ </div>
1705
+ </form>
1706
+
1707
+ <p class="description">
1708
+ <?php
1709
+ echo esc_html( sprintf(
1710
+ /* translators: 1: Date and time, 2: Timezone */
1711
+ __( 'Site time when page loaded: %1$s (%2$s)', 'wp-crontrol' ),
1712
+ date_i18n( 'Y-m-d H:i:s' ),
1713
+ get_timezone_name()
1714
+ ) );
1715
+ ?>
1716
+ </p>
1717
+ </div>
1718
+ <?php
1719
+
1720
+ break;
1721
+
1722
+ case $tabs['add-event']:
1723
+ show_cron_form( false );
1724
+ break;
1725
+
1726
+ case $tabs['edit-event']:
1727
+ show_cron_form( true );
1728
+ break;
1729
+
1730
+ }
1731
+
1732
+ }
1733
+
1734
+ /**
1735
+ * Get the states of the various cron-related tabs.
1736
+ *
1737
+ * @return array<string,bool> Array of states keyed by tab name.
1738
+ */
1739
+ function get_tab_states() {
1740
+ $tabs = array(
1741
+ 'events' => ( ! empty( $_GET['page'] ) && 'crontrol_admin_manage_page' === $_GET['page'] && empty( $_GET['crontrol_action'] ) ),
1742
+ 'schedules' => ( ! empty( $_GET['page'] ) && 'crontrol_admin_options_page' === $_GET['page'] ),
1743
+ 'add-event' => ( ! empty( $_GET['crontrol_action'] ) && 'new-cron' === $_GET['crontrol_action'] ),
1744
+ 'edit-event' => ( ! empty( $_GET['crontrol_action'] ) && 'edit-cron' === $_GET['crontrol_action'] ),
1745
+ );
1746
+
1747
+ $tabs = apply_filters( 'crontrol/tabs', $tabs );
1748
+
1749
+ return $tabs;
1750
+ }
1751
+
1752
+ /**
1753
+ * Output the cron-related tabs if we're on a cron-related admin screen.
1754
+ *
1755
+ * @return void
1756
+ */
1757
+ function do_tabs() {
1758
+ $tabs = get_tab_states();
1759
+ $tab = array_filter( $tabs );
1760
+
1761
+ if ( ! $tab ) {
1762
+ return;
1763
+ }
1764
+
1765
+ $tab = array_keys( $tab );
1766
+ $tab = reset( $tab );
1767
+ $links = array(
1768
+ 'events' => array(
1769
+ 'tools.php?page=crontrol_admin_manage_page',
1770
+ __( 'Cron Events', 'wp-crontrol' ),
1771
+ ),
1772
+ 'schedules' => array(
1773
+ 'options-general.php?page=crontrol_admin_options_page',
1774
+ __( 'Cron Schedules', 'wp-crontrol' ),
1775
+ ),
1776
+ );
1777
+
1778
+ ?>
1779
+ <div id="crontrol-header">
1780
+ <nav class="nav-tab-wrapper">
1781
+ <?php
1782
+ foreach ( $links as $id => $link ) {
1783
+ if ( ! empty( $tabs[ $id ] ) ) {
1784
+ printf(
1785
+ '<a href="%s" class="nav-tab nav-tab-active">%s</a>',
1786
+ esc_url( $link[0] ),
1787
+ esc_html( $link[1] )
1788
+ );
1789
+ } else {
1790
+ printf(
1791
+ '<a href="%s" class="nav-tab">%s</a>',
1792
+ esc_url( $link[0] ),
1793
+ esc_html( $link[1] )
1794
+ );
1795
+ }
1796
+ }
1797
+
1798
+ if ( $tabs['add-event'] ) {
1799
+ printf(
1800
+ '<span class="nav-tab nav-tab-active">%s</span>',
1801
+ esc_html__( 'Add Cron Event', 'wp-crontrol' )
1802
+ );
1803
+ } elseif ( $tabs['edit-event'] ) {
1804
+ printf(
1805
+ '<span class="nav-tab nav-tab-active">%s</span>',
1806
+ esc_html__( 'Edit Cron Event', 'wp-crontrol' )
1807
+ );
1808
+ }
1809
+ ?>
1810
+ </nav>
1811
+ <?php
1812
+ do_action( 'crontrol/tab-header', $tab, $tabs );
1813
+ ?>
1814
+ </div>
1815
+ <?php
1816
+ }
1817
+
1818
+ /**
1819
+ * Returns an array of the callback functions that are attached to the given hook name.
1820
+ *
1821
+ * @param string $name The hook name.
1822
+ * @return array<int,array<string,mixed>> Array of callbacks attached to the hook.
1823
+ * @phpstan-return array<int,array{
1824
+ * priority: int,
1825
+ * callback: array<string,mixed>,
1826
+ * }>
1827
+ */
1828
+ function get_hook_callbacks( $name ) {
1829
+ global $wp_filter;
1830
+
1831
+ $actions = array();
1832
+
1833
+ if ( isset( $wp_filter[ $name ] ) ) {
1834
+ // See http://core.trac.wordpress.org/ticket/17817.
1835
+ $action = $wp_filter[ $name ];
1836
+
1837
+ /**
1838
+ * @var int $priority
1839
+ */
1840
+ foreach ( $action as $priority => $callbacks ) {
1841
+ foreach ( $callbacks as $callback ) {
1842
+ $callback = populate_callback( $callback );
1843
+
1844
+ if ( __NAMESPACE__ . '\\pauser' === $callback['function'] ) {
1845
+ continue;
1846
+ }
1847
+
1848
+ $actions[] = array(
1849
+ 'priority' => $priority,
1850
+ 'callback' => $callback,
1851
+ );
1852
+ }
1853
+ }
1854
+ }
1855
+
1856
+ return $actions;
1857
+ }
1858
+
1859
+ /**
1860
+ * Populates the details of the given callback function.
1861
+ *
1862
+ * @param array<string,mixed> $callback A callback entry.
1863
+ * @phpstan-param array{
1864
+ * function: string|array<int,mixed>|object,
1865
+ * accepted_args: int,
1866
+ * } $callback
1867
+ * @return array<string,mixed> The updated callback entry.
1868
+ */
1869
+ function populate_callback( array $callback ) {
1870
+ // If Query Monitor is installed, use its rich callback analysis.
1871
+ if ( method_exists( '\QM_Util', 'populate_callback' ) ) {
1872
+ return \QM_Util::populate_callback( $callback );
1873
+ }
1874
+
1875
+ if ( is_string( $callback['function'] ) && ( false !== strpos( $callback['function'], '::' ) ) ) {
1876
+ $callback['function'] = explode( '::', $callback['function'] );
1877
+ }
1878
+
1879
+ if ( is_array( $callback['function'] ) ) {
1880
+ if ( is_object( $callback['function'][0] ) ) {
1881
+ $class = get_class( $callback['function'][0] );
1882
+ $access = '->';
1883
+ } else {
1884
+ $class = $callback['function'][0];
1885
+ $access = '::';
1886
+ }
1887
+
1888
+ $callback['name'] = $class . $access . $callback['function'][1] . '()';
1889
+ } elseif ( is_object( $callback['function'] ) ) {
1890
+ if ( is_a( $callback['function'], 'Closure' ) ) {
1891
+ $callback['name'] = 'Closure';
1892
+ } else {
1893
+ $class = get_class( $callback['function'] );
1894
+
1895
+ $callback['name'] = $class . '->__invoke()';
1896
+ }
1897
+ } else {
1898
+ $callback['name'] = $callback['function'] . '()';
1899
+ }
1900
+
1901
+ if ( ! method_exists( '\QM_Util', 'populate_callback' ) && ! is_callable( $callback['function'] ) ) {
1902
+ $callback['error'] = new WP_Error(
1903
+ 'not_callable',
1904
+ sprintf(
1905
+ /* translators: %s: Function name */
1906
+ __( 'Function %s does not exist', 'wp-crontrol' ),
1907
+ $callback['name']
1908
+ )
1909
+ );
1910
+ }
1911
+
1912
+ return $callback;
1913
+ }
1914
+
1915
+ /**
1916
+ * Returns a user-friendly representation of the callback function.
1917
+ *
1918
+ * @param mixed[] $callback The callback entry.
1919
+ * @return string The displayable version of the callback name.
1920
+ */
1921
+ function output_callback( array $callback ) {
1922
+ $qm = WP_PLUGIN_DIR . '/query-monitor/query-monitor.php';
1923
+ $html = plugin_dir_path( $qm ) . 'output/Html.php';
1924
+
1925
+ if ( ! empty( $callback['callback']['error'] ) ) {
1926
+ $return = '<code>' . $callback['callback']['name'] . '</code>';
1927
+ $return .= '<br><span class="status-crontrol-error"><span class="dashicons dashicons-warning" aria-hidden="true"></span> ';
1928
+ $return .= esc_html( $callback['callback']['error']->get_error_message() );
1929
+ $return .= '</span>';
1930
+ return $return;
1931
+ }
1932
+
1933
+ // If Query Monitor is installed, use its rich callback output.
1934
+ if ( class_exists( '\QueryMonitor' ) && file_exists( $html ) ) {
1935
+ require_once $html;
1936
+
1937
+ if ( class_exists( '\QM_Output_Html' ) ) {
1938
+ return \QM_Output_Html::output_filename(
1939
+ $callback['callback']['name'],
1940
+ $callback['callback']['file'],
1941
+ $callback['callback']['line']
1942
+ );
1943
+ }
1944
+ }
1945
+
1946
+ return '<code>' . $callback['callback']['name'] . '</code>';
1947
+ }
1948
+
1949
+ /**
1950
+ * Pretty-prints the difference in two times.
1951
+ *
1952
+ * @param int $older_date Unix timestamp.
1953
+ * @param int $newer_date Unix timestamp.
1954
+ * @return string The pretty time_since value
1955
+ * @link http://binarybonsai.com/code/timesince.txt
1956
+ */
1957
+ function time_since( $older_date, $newer_date ) {
1958
+ return interval( $newer_date - $older_date );
1959
+ }
1960
+
1961
+ /**
1962
+ * Converts a period of time in seconds into a human-readable format representing the interval.
1963
+ *
1964
+ * Example:
1965
+ *
1966
+ * echo \Crontrol\interval( 90 );
1967
+ * // 1 minute 30 seconds
1968
+ *
1969
+ * @param int|float $since A period of time in seconds.
1970
+ * @return string An interval represented as a string.
1971
+ */
1972
+ function interval( $since ) {
1973
+ // Array of time period chunks.
1974
+ $chunks = array(
1975
+ /* translators: 1: The number of years in an interval of time. */
1976
+ array( 60 * 60 * 24 * 365, _n_noop( '%s year', '%s years', 'wp-crontrol' ) ),
1977
+ /* translators: 1: The number of months in an interval of time. */
1978
+ array( 60 * 60 * 24 * 30, _n_noop( '%s month', '%s months', 'wp-crontrol' ) ),
1979
+ /* translators: 1: The number of weeks in an interval of time. */
1980
+ array( 60 * 60 * 24 * 7, _n_noop( '%s week', '%s weeks', 'wp-crontrol' ) ),
1981
+ /* translators: 1: The number of days in an interval of time. */
1982
+ array( 60 * 60 * 24, _n_noop( '%s day', '%s days', 'wp-crontrol' ) ),
1983
+ /* translators: 1: The number of hours in an interval of time. */
1984
+ array( 60 * 60, _n_noop( '%s hour', '%s hours', 'wp-crontrol' ) ),
1985
+ /* translators: 1: The number of minutes in an interval of time. */
1986
+ array( 60, _n_noop( '%s minute', '%s minutes', 'wp-crontrol' ) ),
1987
+ /* translators: 1: The number of seconds in an interval of time. */
1988
+ array( 1, _n_noop( '%s second', '%s seconds', 'wp-crontrol' ) ),
1989
+ );
1990
+
1991
+ if ( $since <= 0 ) {
1992
+ return __( 'now', 'wp-crontrol' );
1993
+ }
1994
+
1995
+ /**
1996
+ * We only want to output two chunks of time here, eg:
1997
+ * x years, xx months
1998
+ * x days, xx hours
1999
+ * so there's only two bits of calculation below:
2000
+ */
2001
+
2002
+ // Step one: the first chunk.
2003
+ foreach ( array_keys( $chunks ) as $i ) {
2004
+ $seconds = $chunks[ $i ][0];
2005
+ $name = $chunks[ $i ][1];
2006
+
2007
+ // Finding the biggest chunk (if the chunk fits, break).
2008
+ $count = (int) floor( $since / $seconds );
2009
+ if ( $count ) {
2010
+ break;
2011
+ }
2012
+ }
2013
+
2014
+ // Set output var.
2015
+ $output = sprintf( translate_nooped_plural( $name, $count, 'wp-crontrol' ), $count );
2016
+
2017
+ // Step two: the second chunk.
2018
+ if ( $i + 1 < count( $chunks ) ) {
2019
+ $seconds2 = $chunks[ $i + 1 ][0];
2020
+ $name2 = $chunks[ $i + 1 ][1];
2021
+ $count2 = (int) floor( ( $since - ( $seconds * $count ) ) / $seconds2 );
2022
+ if ( $count2 ) {
2023
+ // Add to output var.
2024
+ $output .= ' ' . sprintf( translate_nooped_plural( $name2, $count2, 'wp-crontrol' ), $count2 );
2025
+ }
2026
+ }
2027
+
2028
+ return $output;
2029
+ }
2030
+
2031
+ /**
2032
+ * Sets up the Events listing screen.
2033
+ *
2034
+ * @return void
2035
+ */
2036
+ function setup_manage_page() {
2037
+ // Initialise the list table
2038
+ Event\get_list_table();
2039
+
2040
+ // Add the initially hidden admin notice about the out of date events list
2041
+ add_action( 'admin_notices', function() {
2042
+ printf(
2043
+ '<div id="crontrol-hash-message" class="notice notice-info"><p>%s</p></div>',
2044
+ esc_html__( 'The scheduled cron events have changed since you first opened this page. Reload the page to see the up to date list.', 'wp-crontrol' )
2045
+ );
2046
+ } );
2047
+ }
2048
+
2049
+ /**
2050
+ * Registers the stylesheet and JavaScript for the admin areas.
2051
+ *
2052
+ * @param string $hook_suffix The admin screen ID.
2053
+ * @return void
2054
+ */
2055
+ function enqueue_assets( $hook_suffix ) {
2056
+ $tab = get_tab_states();
2057
+
2058
+ if ( ! array_filter( $tab ) ) {
2059
+ return;
2060
+ }
2061
+
2062
+ $ver = (string) filemtime( plugin_dir_path( PLUGIN_FILE ) . 'css/wp-crontrol.css' );
2063
+ wp_enqueue_style( 'wp-crontrol', plugin_dir_url( PLUGIN_FILE ) . 'css/wp-crontrol.css', array( 'dashicons' ), $ver );
2064
+
2065
+ $ver = (string) filemtime( plugin_dir_path( PLUGIN_FILE ) . 'js/wp-crontrol.js' );
2066
+ wp_enqueue_script( 'wp-crontrol', plugin_dir_url( PLUGIN_FILE ) . 'js/wp-crontrol.js', array( 'jquery', 'wp-a11y' ), $ver, true );
2067
+
2068
+ $vars = array();
2069
+
2070
+ if ( ! empty( $tab['events'] ) ) {
2071
+ $data = json_encode( Event\get() );
2072
+
2073
+ if ( false !== $data ) {
2074
+ $vars['eventsHash'] = md5( $data );
2075
+ $vars['eventsHashInterval'] = 20;
2076
+ }
2077
+ }
2078
+
2079
+ if ( ! empty( $tab['add-event'] ) || ! empty( $tab['edit-event'] ) ) {
2080
+ if ( function_exists( 'wp_enqueue_code_editor' ) && current_user_can( 'edit_files' ) ) {
2081
+ $settings = wp_enqueue_code_editor( array(
2082
+ 'type' => 'text/x-php',
2083
+ ) );
2084
+
2085
+ if ( false !== $settings ) {
2086
+ $vars['codeEditor'] = $settings;
2087
+ }
2088
+ }
2089
+ }
2090
+
2091
+ wp_localize_script( 'wp-crontrol', 'wpCrontrol', $vars );
2092
+ }
2093
+
2094
+ /**
2095
+ * Filters the list of query arguments which get removed from admin area URLs in WordPress.
2096
+ *
2097
+ * @param array<int,string> $args List of removable query arguments.
2098
+ * @return array<int,string> Updated list of removable query arguments.
2099
+ */
2100
+ function filter_removable_query_args( array $args ) {
2101
+ return array_merge( $args, array(
2102
+ 'crontrol_message',
2103
+ 'crontrol_name',
2104
+ ) );
2105
+ }
2106
+
2107
+ /**
2108
+ * Returns an array of cron event hooks that are persistently added by WordPress core.
2109
+ *
2110
+ * @return array<int,string> Array of hook names.
2111
+ */
2112
+ function get_persistent_core_hooks() {
2113
+ return array(
2114
+ 'wp_update_plugins', // 2.7.0
2115
+ 'wp_update_themes', // 2.7.0
2116
+ 'wp_version_check', // 2.7.0
2117
+ 'wp_scheduled_delete', // 2.9.0
2118
+ 'update_network_counts', // 3.1.0
2119
+ 'wp_scheduled_auto_draft_delete', // 3.4.0
2120
+ 'delete_expired_transients', // 4.9.0
2121
+ 'wp_privacy_delete_old_export_files', // 4.9.6
2122
+ 'recovery_mode_clean_expired_keys', // 5.2.0
2123
+ 'wp_site_health_scheduled_check', // 5.4.0
2124
+ 'wp_https_detection', // 5.7.0
2125
+ 'wp_update_user_counts', // 6.0.0
2126
+ );
2127
+ }
2128
+
2129
+ /**
2130
+ * Returns an array of all cron event hooks that are added by WordPress core.
2131
+ *
2132
+ * @return array<int,string> Array of hook names.
2133
+ */
2134
+ function get_all_core_hooks() {
2135
+ return array_merge(
2136
+ get_persistent_core_hooks(),
2137
+ array(
2138
+ 'do_pings', // 2.1.0
2139
+ 'publish_future_post', // 2.1.0
2140
+ 'importer_scheduled_cleanup', // 2.5.0
2141
+ 'upgrader_scheduled_cleanup', // 3.2.2
2142
+ 'wp_maybe_auto_update', // 3.7.0
2143
+ 'wp_split_shared_term_batch', // 4.3.0
2144
+ 'wp_update_comment_type_batch', // 5.5.0
2145
+ 'wp_delete_temp_updater_backups', // 5.9.0
2146
+ )
2147
+ );
2148
+ }
2149
+
2150
+ /**
2151
+ * Returns an array of cron schedules that are added by WordPress core.
2152
+ *
2153
+ * @return array<int,string> Array of schedule names.
2154
+ */
2155
+ function get_core_schedules() {
2156
+ return array(
2157
+ 'hourly',
2158
+ 'twicedaily',
2159
+ 'daily',
2160
+ 'weekly',
2161
+ );
2162
+ }
2163
+
2164
+ /**
2165
+ * Encodes some input as JSON for output.
2166
+ *
2167
+ * @param mixed $input The input.
2168
+ * @param bool $pretty Whether to pretty print the output. Default true.
2169
+ * @return string The JSON-encoded output.
2170
+ */
2171
+ function json_output( $input, $pretty = true ) {
2172
+ $json_options = 0;
2173
+
2174
+ if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
2175
+ // phpcs:ignore PHPCompatibility.Constants.NewConstants.json_unescaped_slashesFound
2176
+ $json_options |= JSON_UNESCAPED_SLASHES;
2177
+ }
2178
+
2179
+ if ( $pretty && defined( 'JSON_PRETTY_PRINT' ) ) {
2180
+ $json_options |= JSON_PRETTY_PRINT;
2181
+ }
2182
+
2183
+ $output = wp_json_encode( $input, $json_options );
2184
+
2185
+ if ( false === $output ) {
2186
+ $output = '';
2187
+ }
2188
+
2189
+ return $output;
2190
+ }
2191
+
2192
+ /**
2193
+ * Evaluates the code in a PHP cron event using eval.
2194
+ *
2195
+ * Security: Only users with the `edit_files` capability can manage PHP cron events. This means if a user cannot edit
2196
+ * files on the site (eg. through the Plugin Editor or Theme Editor) then they cannot edit or add a PHP cron event. By
2197
+ * default, only Administrators have this capability, and with Multisite enabled only Super Admins have this capability.
2198
+ *
2199
+ * If file editing has been disabled via the `DISALLOW_FILE_MODS` or `DISALLOW_FILE_EDIT` configuration constants then
2200
+ * no user will have the `edit_files` capability, which means editing or adding a PHP cron event will not be permitted.
2201
+ *
2202
+ * Therefore, the user access level required to execute arbitrary PHP code does not change with WP Crontrol activated.
2203
+ *
2204
+ * @param string $code The PHP code to evaluate.
2205
+ * @return void
2206
+ */
2207
+ function action_php_cron_event( $code ) {
2208
+ // phpcs:ignore Squiz.PHP.Eval.Discouraged
2209
+ eval( $code );
2210
+ }
src/event-list-table.php CHANGED
@@ -136,6 +136,14 @@ class Table extends \WP_List_Table {
136
  return ( ! in_array( $event->hook, $all_core_hooks, true ) );
137
  } );
138
 
 
 
 
 
 
 
 
 
139
  /**
140
  * Filters the available filtered events on the cron event listing screen.
141
  *
@@ -222,6 +230,7 @@ class Table extends \WP_List_Table {
222
  'noaction' => __( 'Events with no action', 'wp-crontrol' ),
223
  'core' => __( 'WordPress core events', 'wp-crontrol' ),
224
  'custom' => __( 'Custom events', 'wp-crontrol' ),
 
225
  );
226
 
227
  /**
@@ -305,7 +314,7 @@ class Table extends \WP_List_Table {
305
  $callbacks = \Crontrol\get_hook_callbacks( $event->hook );
306
 
307
  if ( ! $callbacks ) {
308
- $classes[] = 'crontrol-warning';
309
  } else {
310
  foreach ( $callbacks as $callback ) {
311
  if ( ! empty( $callback['callback']['error'] ) ) {
@@ -319,6 +328,10 @@ class Table extends \WP_List_Table {
319
  $classes[] = 'crontrol-warning';
320
  }
321
 
 
 
 
 
322
  printf(
323
  '<tr class="%s">',
324
  esc_attr( implode( ' ', $classes ) )
@@ -349,24 +362,50 @@ class Table extends \WP_List_Table {
349
  'crontrol_action' => 'edit-cron',
350
  'crontrol_id' => rawurlencode( $event->hook ),
351
  'crontrol_sig' => rawurlencode( $event->sig ),
352
- 'crontrol_next_run_utc' => rawurlencode( $event->time ),
353
  );
354
  $link = add_query_arg( $link, admin_url( 'tools.php' ) );
355
 
356
  $links[] = "<a href='" . esc_url( $link ) . "'>" . esc_html__( 'Edit', 'wp-crontrol' ) . '</a>';
357
  }
358
 
359
- $link = array(
360
- 'page' => 'crontrol_admin_manage_page',
361
- 'crontrol_action' => 'run-cron',
362
- 'crontrol_id' => rawurlencode( $event->hook ),
363
- 'crontrol_sig' => rawurlencode( $event->sig ),
364
- 'crontrol_next_run_utc' => rawurlencode( $event->time ),
365
- );
366
- $link = add_query_arg( $link, admin_url( 'tools.php' ) );
367
- $link = wp_nonce_url( $link, "crontrol-run-cron_{$event->hook}_{$event->sig}" );
 
368
 
369
- $links[] = "<a href='" . esc_url( $link ) . "'>" . esc_html__( 'Run Now', 'wp-crontrol' ) . '</a>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
 
371
  if ( ! in_array( $event->hook, self::$persistent_core_hooks, true ) && ( ( 'crontrol_cron_job' !== $event->hook ) || self::$can_manage_php_crons ) ) {
372
  $link = array(
@@ -374,10 +413,10 @@ class Table extends \WP_List_Table {
374
  'crontrol_action' => 'delete-cron',
375
  'crontrol_id' => rawurlencode( $event->hook ),
376
  'crontrol_sig' => rawurlencode( $event->sig ),
377
- 'crontrol_next_run_utc' => rawurlencode( $event->time ),
378
  );
379
  $link = add_query_arg( $link, admin_url( 'tools.php' ) );
380
- $link = wp_nonce_url( $link, "crontrol-delete-cron_{$event->hook}_{$event->sig}_{$event->time}" );
381
 
382
  $links[] = "<span class='delete'><a href='" . esc_url( $link ) . "'>" . esc_html__( 'Delete', 'wp-crontrol' ) . '</a></span>';
383
  }
@@ -413,7 +452,7 @@ class Table extends \WP_List_Table {
413
  protected function column_cb( $event ) {
414
  $id = sprintf(
415
  'crontrol-delete-%1$s-%2$s-%3$s',
416
- $event->time,
417
  rawurlencode( $event->hook ),
418
  $event->sig
419
  );
@@ -430,7 +469,7 @@ class Table extends \WP_List_Table {
430
  <input type="checkbox" name="crontrol_delete[%3$s][%4$s]" value="%5$s" id="%1$s">',
431
  esc_attr( $id ),
432
  esc_html__( 'Select this row', 'wp-crontrol' ),
433
- esc_attr( $event->time ),
434
  esc_attr( rawurlencode( $event->hook ) ),
435
  esc_attr( $event->sig )
436
  );
@@ -455,7 +494,17 @@ class Table extends \WP_List_Table {
455
  }
456
  }
457
 
458
- return esc_html( $event->hook );
 
 
 
 
 
 
 
 
 
 
459
  }
460
 
461
  /**
@@ -543,14 +592,14 @@ class Table extends \WP_List_Table {
543
  protected function column_crontrol_next( $event ) {
544
  $date_local_format = 'Y-m-d H:i:s';
545
  $offset_site = get_date_from_gmt( 'now', 'P' );
546
- $offset_event = get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $event->time ), 'P' );
547
 
548
  if ( $offset_site !== $offset_event ) {
549
  $date_local_format .= ' P';
550
  }
551
 
552
- $date_utc = gmdate( 'c', $event->time );
553
- $date_local = get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $event->time ), $date_local_format );
554
 
555
  $time = sprintf(
556
  '<time datetime="%1$s">%2$s</time>',
@@ -558,7 +607,7 @@ class Table extends \WP_List_Table {
558
  esc_html( $date_local )
559
  );
560
 
561
- $until = $event->time - time();
562
  $late = is_late( $event );
563
 
564
  if ( $late ) {
136
  return ( ! in_array( $event->hook, $all_core_hooks, true ) );
137
  } );
138
 
139
+ $paused = array_filter( $events, function( $event ) {
140
+ return ( is_paused( $event ) );
141
+ } );
142
+
143
+ if ( count( $paused ) > 0 ) {
144
+ $filtered['paused'] = $paused;
145
+ }
146
+
147
  /**
148
  * Filters the available filtered events on the cron event listing screen.
149
  *
230
  'noaction' => __( 'Events with no action', 'wp-crontrol' ),
231
  'core' => __( 'WordPress core events', 'wp-crontrol' ),
232
  'custom' => __( 'Custom events', 'wp-crontrol' ),
233
+ 'paused' => __( 'Paused events', 'wp-crontrol' ),
234
  );
235
 
236
  /**
314
  $callbacks = \Crontrol\get_hook_callbacks( $event->hook );
315
 
316
  if ( ! $callbacks ) {
317
+ $classes[] = 'crontrol-no-action';
318
  } else {
319
  foreach ( $callbacks as $callback ) {
320
  if ( ! empty( $callback['callback']['error'] ) ) {
328
  $classes[] = 'crontrol-warning';
329
  }
330
 
331
+ if ( is_paused( $event ) ) {
332
+ $classes[] = 'crontrol-paused';
333
+ }
334
+
335
  printf(
336
  '<tr class="%s">',
337
  esc_attr( implode( ' ', $classes ) )
362
  'crontrol_action' => 'edit-cron',
363
  'crontrol_id' => rawurlencode( $event->hook ),
364
  'crontrol_sig' => rawurlencode( $event->sig ),
365
+ 'crontrol_next_run_utc' => rawurlencode( $event->timestamp ),
366
  );
367
  $link = add_query_arg( $link, admin_url( 'tools.php' ) );
368
 
369
  $links[] = "<a href='" . esc_url( $link ) . "'>" . esc_html__( 'Edit', 'wp-crontrol' ) . '</a>';
370
  }
371
 
372
+ if ( ! is_paused( $event ) ) {
373
+ $link = array(
374
+ 'page' => 'crontrol_admin_manage_page',
375
+ 'crontrol_action' => 'run-cron',
376
+ 'crontrol_id' => rawurlencode( $event->hook ),
377
+ 'crontrol_sig' => rawurlencode( $event->sig ),
378
+ 'crontrol_next_run_utc' => rawurlencode( $event->timestamp ),
379
+ );
380
+ $link = add_query_arg( $link, admin_url( 'tools.php' ) );
381
+ $link = wp_nonce_url( $link, "crontrol-run-cron_{$event->hook}_{$event->sig}" );
382
 
383
+ $links[] = "<a href='" . esc_url( $link ) . "'>" . esc_html__( 'Run Now', 'wp-crontrol' ) . '</a>';
384
+ }
385
+
386
+ if ( is_paused( $event ) ) {
387
+ $link = array(
388
+ 'page' => 'crontrol_admin_manage_page',
389
+ 'crontrol_action' => 'resume-hook',
390
+ 'crontrol_id' => rawurlencode( $event->hook ),
391
+ );
392
+ $link = add_query_arg( $link, admin_url( 'tools.php' ) );
393
+ $link = wp_nonce_url( $link, "crontrol-resume-hook_{$event->hook}" );
394
+
395
+ /* translators: Verb */
396
+ $links[] = "<a href='" . esc_url( $link ) . "'>" . esc_html__( 'Resume', 'wp-crontrol' ) . '</a>';
397
+ } elseif ( 'crontrol_cron_job' !== $event->hook ) {
398
+ $link = array(
399
+ 'page' => 'crontrol_admin_manage_page',
400
+ 'crontrol_action' => 'pause-hook',
401
+ 'crontrol_id' => rawurlencode( $event->hook ),
402
+ );
403
+ $link = add_query_arg( $link, admin_url( 'tools.php' ) );
404
+ $link = wp_nonce_url( $link, "crontrol-pause-hook_{$event->hook}" );
405
+
406
+ /* translators: Verb */
407
+ $links[] = "<a href='" . esc_url( $link ) . "'>" . esc_html__( 'Pause', 'wp-crontrol' ) . '</a>';
408
+ }
409
 
410
  if ( ! in_array( $event->hook, self::$persistent_core_hooks, true ) && ( ( 'crontrol_cron_job' !== $event->hook ) || self::$can_manage_php_crons ) ) {
411
  $link = array(
413
  'crontrol_action' => 'delete-cron',
414
  'crontrol_id' => rawurlencode( $event->hook ),
415
  'crontrol_sig' => rawurlencode( $event->sig ),
416
+ 'crontrol_next_run_utc' => rawurlencode( $event->timestamp ),
417
  );
418
  $link = add_query_arg( $link, admin_url( 'tools.php' ) );
419
+ $link = wp_nonce_url( $link, "crontrol-delete-cron_{$event->hook}_{$event->sig}_{$event->timestamp}" );
420
 
421
  $links[] = "<span class='delete'><a href='" . esc_url( $link ) . "'>" . esc_html__( 'Delete', 'wp-crontrol' ) . '</a></span>';
422
  }
452
  protected function column_cb( $event ) {
453
  $id = sprintf(
454
  'crontrol-delete-%1$s-%2$s-%3$s',
455
+ $event->timestamp,
456
  rawurlencode( $event->hook ),
457
  $event->sig
458
  );
469
  <input type="checkbox" name="crontrol_delete[%3$s][%4$s]" value="%5$s" id="%1$s">',
470
  esc_attr( $id ),
471
  esc_html__( 'Select this row', 'wp-crontrol' ),
472
+ esc_attr( $event->timestamp ),
473
  esc_attr( rawurlencode( $event->hook ) ),
474
  esc_attr( $event->sig )
475
  );
494
  }
495
  }
496
 
497
+ $output = esc_html( $event->hook );
498
+
499
+ if ( is_paused( $event ) ) {
500
+ $output .= sprintf(
501
+ ' &mdash; <strong class="status-crontrol-paused post-state"><span class="dashicons dashicons-controls-pause" aria-hidden="true"></span> %s</strong>',
502
+ /* translators: State of a cron event, adjective */
503
+ esc_html__( 'Paused', 'wp-crontrol' )
504
+ );
505
+ }
506
+
507
+ return $output;
508
  }
509
 
510
  /**
592
  protected function column_crontrol_next( $event ) {
593
  $date_local_format = 'Y-m-d H:i:s';
594
  $offset_site = get_date_from_gmt( 'now', 'P' );
595
+ $offset_event = get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $event->timestamp ), 'P' );
596
 
597
  if ( $offset_site !== $offset_event ) {
598
  $date_local_format .= ' P';
599
  }
600
 
601
+ $date_utc = gmdate( 'c', $event->timestamp );
602
+ $date_local = get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $event->timestamp ), $date_local_format );
603
 
604
  $time = sprintf(
605
  '<time datetime="%1$s">%2$s</time>',
607
  esc_html( $date_local )
608
  );
609
 
610
+ $until = $event->timestamp - time();
611
  $late = is_late( $event );
612
 
613
  if ( $late ) {
src/event.php CHANGED
@@ -9,6 +9,8 @@ use stdClass;
9
  use Crontrol\Schedule;
10
  use WP_Error;
11
 
 
 
12
  /**
13
  * Executes a cron event immediately.
14
  *
@@ -244,6 +246,72 @@ function delete( $hook, $sig, $next_run_utc ) {
244
  return true;
245
  }
246
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  /**
248
  * Returns a flattened array of cron events.
249
  *
@@ -264,7 +332,7 @@ function get() {
264
  // This is a prime candidate for a Crontrol_Event class but I'm not bothering currently.
265
  $events[ "$hook-$sig-$time" ] = (object) array(
266
  'hook' => $hook,
267
- 'time' => $time, // UTC
268
  'sig' => $sig,
269
  'args' => $data['args'],
270
  'schedule' => $data['schedule'],
@@ -351,7 +419,7 @@ function get_schedule_name( stdClass $event ) {
351
  $schedules = Schedule\get();
352
 
353
  if ( isset( $schedules[ $event->schedule ] ) ) {
354
- return $schedules[ $event->schedule ]['display'];
355
  }
356
 
357
  return new WP_Error( 'unknown_schedule', sprintf(
@@ -386,11 +454,27 @@ function is_too_frequent( stdClass $event ) {
386
  * @return bool Whether the event is late.
387
  */
388
  function is_late( stdClass $event ) {
389
- $until = $event->time - time();
390
 
391
  return ( $until < ( 0 - ( 10 * MINUTE_IN_SECONDS ) ) );
392
  }
393
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  /**
395
  * Initialises and returns the list table for events.
396
  *
@@ -400,8 +484,6 @@ function get_list_table() {
400
  static $table = null;
401
 
402
  if ( ! $table ) {
403
- require_once __DIR__ . '/event-list-table.php';
404
-
405
  $table = new Table();
406
  $table->prepare_items();
407
 
@@ -434,13 +516,13 @@ function uasort_order_events( $a, $b ) {
434
  }
435
  break;
436
  default:
437
- if ( $a->time === $b->time ) {
438
  $compare = 0;
439
  } else {
440
  if ( 'desc' === $order ) {
441
- $compare = ( $a->time > $b->time ) ? 1 : -1;
442
  } else {
443
- $compare = ( $a->time < $b->time ) ? 1 : -1;
444
  }
445
  }
446
  break;
9
  use Crontrol\Schedule;
10
  use WP_Error;
11
 
12
+ use const Crontrol\PAUSED_OPTION;
13
+
14
  /**
15
  * Executes a cron event immediately.
16
  *
246
  return true;
247
  }
248
 
249
+ /**
250
+ * Pauses a cron event.
251
+ *
252
+ * @param string $hook The hook name of the event to pause.
253
+ * @return true|WP_Error True if the pause was successful, WP_Error otherwise.
254
+ */
255
+ function pause( $hook ) {
256
+ $paused = get_option( PAUSED_OPTION, array() );
257
+
258
+ if ( ! is_array( $paused ) ) {
259
+ $paused = array();
260
+ }
261
+
262
+ $paused[ $hook ] = true;
263
+
264
+ $result = update_option( PAUSED_OPTION, $paused, false );
265
+
266
+ if ( false === $result ) {
267
+ return new WP_Error(
268
+ 'could_not_pause',
269
+ sprintf(
270
+ /* translators: 1: The name of the cron event. */
271
+ __( 'Failed to pause the cron event %s.', 'wp-crontrol' ),
272
+ $hook
273
+ )
274
+ );
275
+ }
276
+
277
+ return true;
278
+ }
279
+
280
+ /**
281
+ * Resumes a paused cron event.
282
+ *
283
+ * @param string $hook The hook name of the event to resume.
284
+ * @return true|WP_Error True if the resumption was successful, WP_Error otherwise.
285
+ */
286
+ function resume( $hook ) {
287
+ $paused = get_option( PAUSED_OPTION );
288
+
289
+ if ( ! is_array( $paused ) || ( count( $paused ) === 0 ) ) {
290
+ return true;
291
+ }
292
+
293
+ unset( $paused[ $hook ] );
294
+
295
+ if ( count( $paused ) === 0 ) {
296
+ $result = delete_option( PAUSED_OPTION );
297
+ } else {
298
+ $result = update_option( PAUSED_OPTION, $paused, false );
299
+ }
300
+
301
+ if ( false === $result ) {
302
+ return new WP_Error(
303
+ 'could_not_resume',
304
+ sprintf(
305
+ /* translators: 1: The name of the cron event. */
306
+ __( 'Failed to resume the cron event %s.', 'wp-crontrol' ),
307
+ $hook
308
+ )
309
+ );
310
+ }
311
+
312
+ return true;
313
+ }
314
+
315
  /**
316
  * Returns a flattened array of cron events.
317
  *
332
  // This is a prime candidate for a Crontrol_Event class but I'm not bothering currently.
333
  $events[ "$hook-$sig-$time" ] = (object) array(
334
  'hook' => $hook,
335
+ 'timestamp' => $time, // UTC
336
  'sig' => $sig,
337
  'args' => $data['args'],
338
  'schedule' => $data['schedule'],
419
  $schedules = Schedule\get();
420
 
421
  if ( isset( $schedules[ $event->schedule ] ) ) {
422
+ return isset( $schedules[ $event->schedule ]['display'] ) ? $schedules[ $event->schedule ]['display'] : $schedules[ $event->schedule ]['name'];
423
  }
424
 
425
  return new WP_Error( 'unknown_schedule', sprintf(
454
  * @return bool Whether the event is late.
455
  */
456
  function is_late( stdClass $event ) {
457
+ $until = $event->timestamp - time();
458
 
459
  return ( $until < ( 0 - ( 10 * MINUTE_IN_SECONDS ) ) );
460
  }
461
 
462
+ /**
463
+ * Determines whether an event is paused.
464
+ *
465
+ * @param stdClass $event The event.
466
+ * @return bool Whether the event is paused.
467
+ */
468
+ function is_paused( stdClass $event ) {
469
+ $paused = get_option( PAUSED_OPTION );
470
+
471
+ if ( ! is_array( $paused ) ) {
472
+ return false;
473
+ }
474
+
475
+ return array_key_exists( $event->hook, $paused );
476
+ }
477
+
478
  /**
479
  * Initialises and returns the list table for events.
480
  *
484
  static $table = null;
485
 
486
  if ( ! $table ) {
 
 
487
  $table = new Table();
488
  $table->prepare_items();
489
 
516
  }
517
  break;
518
  default:
519
+ if ( $a->timestamp === $b->timestamp ) {
520
  $compare = 0;
521
  } else {
522
  if ( 'desc' === $order ) {
523
+ $compare = ( $a->timestamp > $b->timestamp ) ? 1 : -1;
524
  } else {
525
+ $compare = ( $a->timestamp < $b->timestamp ) ? 1 : -1;
526
  }
527
  }
528
  break;
src/schedule-list-table.php CHANGED
@@ -96,7 +96,7 @@ class Schedule_List_Table extends \WP_List_Table {
96
  *
97
  * @phpstan-param array{
98
  * interval: int,
99
- * display: string,
100
  * name: string,
101
  * is_too_frequent: bool,
102
  * } $schedule
@@ -139,7 +139,7 @@ class Schedule_List_Table extends \WP_List_Table {
139
  *
140
  * @phpstan-param array{
141
  * interval: int,
142
- * display: string,
143
  * name: string,
144
  * is_too_frequent: bool,
145
  * } $schedule
@@ -163,7 +163,7 @@ class Schedule_List_Table extends \WP_List_Table {
163
  *
164
  * @phpstan-param array{
165
  * interval: int,
166
- * display: string,
167
  * name: string,
168
  * is_too_frequent: bool,
169
  * } $schedule
@@ -179,7 +179,7 @@ class Schedule_List_Table extends \WP_List_Table {
179
  *
180
  * @phpstan-param array{
181
  * interval: int,
182
- * display: string,
183
  * name: string,
184
  * is_too_frequent: bool,
185
  * } $schedule
@@ -215,14 +215,14 @@ class Schedule_List_Table extends \WP_List_Table {
215
  *
216
  * @phpstan-param array{
217
  * interval: int,
218
- * display: string,
219
  * name: string,
220
  * is_too_frequent: bool,
221
  * } $schedule
222
  * @return string The cell output.
223
  */
224
  protected function column_crontrol_display( array $schedule ) {
225
- return esc_html( $schedule['display'] );
226
  }
227
 
228
  /**
96
  *
97
  * @phpstan-param array{
98
  * interval: int,
99
+ * display?: string,
100
  * name: string,
101
  * is_too_frequent: bool,
102
  * } $schedule
139
  *
140
  * @phpstan-param array{
141
  * interval: int,
142
+ * display?: string,
143
  * name: string,
144
  * is_too_frequent: bool,
145
  * } $schedule
163
  *
164
  * @phpstan-param array{
165
  * interval: int,
166
+ * display?: string,
167
  * name: string,
168
  * is_too_frequent: bool,
169
  * } $schedule
179
  *
180
  * @phpstan-param array{
181
  * interval: int,
182
+ * display?: string,
183
  * name: string,
184
  * is_too_frequent: bool,
185
  * } $schedule
215
  *
216
  * @phpstan-param array{
217
  * interval: int,
218
+ * display?: string,
219
  * name: string,
220
  * is_too_frequent: bool,
221
  * } $schedule
222
  * @return string The cell output.
223
  */
224
  protected function column_crontrol_display( array $schedule ) {
225
+ return esc_html( isset( $schedule['display'] ) ? $schedule['display'] : $schedule['name'] );
226
  }
227
 
228
  /**
src/schedule.php CHANGED
@@ -59,7 +59,7 @@ function delete( $name ) {
59
  * @return array<string,array<string,(int|string)>> Array of cron schedule arrays.
60
  * @phpstan-return array<string,array{
61
  * interval: int,
62
- * display: string,
63
  * name: string,
64
  * is_too_frequent: bool,
65
  * }>
@@ -68,7 +68,7 @@ function get() {
68
  /**
69
  * @phpstan-var array<string,array{
70
  * interval: int,
71
- * display: string,
72
  * }> $schedules
73
  */
74
  $schedules = wp_get_schedules();
@@ -84,7 +84,7 @@ function get() {
84
  /**
85
  * @phpstan-var array<string,array{
86
  * interval: int,
87
- * display: string,
88
  * name: string,
89
  * is_too_frequent: bool,
90
  * }> $schedules
@@ -108,7 +108,7 @@ function dropdown( $current = false ) {
108
  <?php
109
  printf(
110
  '%s (%s)',
111
- esc_html( $sched_data['display'] ),
112
  esc_html( $sched_name )
113
  );
114
  ?>
59
  * @return array<string,array<string,(int|string)>> Array of cron schedule arrays.
60
  * @phpstan-return array<string,array{
61
  * interval: int,
62
+ * display?: string,
63
  * name: string,
64
  * is_too_frequent: bool,
65
  * }>
68
  /**
69
  * @phpstan-var array<string,array{
70
  * interval: int,
71
+ * display?: string,
72
  * }> $schedules
73
  */
74
  $schedules = wp_get_schedules();
84
  /**
85
  * @phpstan-var array<string,array{
86
  * interval: int,
87
+ * display?: string,
88
  * name: string,
89
  * is_too_frequent: bool,
90
  * }> $schedules
108
  <?php
109
  printf(
110
  '%s (%s)',
111
+ esc_html( isset( $sched_data['display'] ) ? $sched_data['display'] : $sched_data['name'] ),
112
  esc_html( $sched_name )
113
  );
114
  ?>
vendor/autoload.php ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ // autoload.php @generated by Composer
4
+
5
+ require_once __DIR__ . '/composer/autoload_real.php';
6
+
7
+ return ComposerAutoloaderInitdbf095101a93cb72acf9efa892a78eaf::getLoader();
vendor/composer/ClassLoader.php ADDED
@@ -0,0 +1,572 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /*
4
+ * This file is part of Composer.
5
+ *
6
+ * (c) Nils Adermann <naderman@naderman.de>
7
+ * Jordi Boggiano <j.boggiano@seld.be>
8
+ *
9
+ * For the full copyright and license information, please view the LICENSE
10
+ * file that was distributed with this source code.
11
+ */
12
+
13
+ namespace Composer\Autoload;
14
+
15
+ /**
16
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
17
+ *
18
+ * $loader = new \Composer\Autoload\ClassLoader();
19
+ *
20
+ * // register classes with namespaces
21
+ * $loader->add('Symfony\Component', __DIR__.'/component');
22
+ * $loader->add('Symfony', __DIR__.'/framework');
23
+ *
24
+ * // activate the autoloader
25
+ * $loader->register();
26
+ *
27
+ * // to enable searching the include path (eg. for PEAR packages)
28
+ * $loader->setUseIncludePath(true);
29
+ *
30
+ * In this example, if you try to use a class in the Symfony\Component
31
+ * namespace or one of its children (Symfony\Component\Console for instance),
32
+ * the autoloader will first look for the class under the component/
33
+ * directory, and it will then fallback to the framework/ directory if not
34
+ * found before giving up.
35
+ *
36
+ * This class is loosely based on the Symfony UniversalClassLoader.
37
+ *
38
+ * @author Fabien Potencier <fabien@symfony.com>
39
+ * @author Jordi Boggiano <j.boggiano@seld.be>
40
+ * @see https://www.php-fig.org/psr/psr-0/
41
+ * @see https://www.php-fig.org/psr/psr-4/
42
+ */
43
+ class ClassLoader
44
+ {
45
+ /** @var ?string */
46
+ private $vendorDir;
47
+
48
+ // PSR-4
49
+ /**
50
+ * @var array[]
51
+ * @psalm-var array<string, array<string, int>>
52
+ */
53
+ private $prefixLengthsPsr4 = array();
54
+ /**
55
+ * @var array[]
56
+ * @psalm-var array<string, array<int, string>>
57
+ */
58
+ private $prefixDirsPsr4 = array();
59
+ /**
60
+ * @var array[]
61
+ * @psalm-var array<string, string>
62
+ */
63
+ private $fallbackDirsPsr4 = array();
64
+
65
+ // PSR-0
66
+ /**
67
+ * @var array[]
68
+ * @psalm-var array<string, array<string, string[]>>
69
+ */
70
+ private $prefixesPsr0 = array();
71
+ /**
72
+ * @var array[]
73
+ * @psalm-var array<string, string>
74
+ */
75
+ private $fallbackDirsPsr0 = array();
76
+
77
+ /** @var bool */
78
+ private $useIncludePath = false;
79
+
80
+ /**
81
+ * @var string[]
82
+ * @psalm-var array<string, string>
83
+ */
84
+ private $classMap = array();
85
+
86
+ /** @var bool */
87
+ private $classMapAuthoritative = false;
88
+
89
+ /**
90
+ * @var bool[]
91
+ * @psalm-var array<string, bool>
92
+ */
93
+ private $missingClasses = array();
94
+
95
+ /** @var ?string */
96
+ private $apcuPrefix;
97
+
98
+ /**
99
+ * @var self[]
100
+ */
101
+ private static $registeredLoaders = array();
102
+
103
+ /**
104
+ * @param ?string $vendorDir
105
+ */
106
+ public function __construct($vendorDir = null)
107
+ {
108
+ $this->vendorDir = $vendorDir;
109
+ }
110
+
111
+ /**
112
+ * @return string[]
113
+ */
114
+ public function getPrefixes()
115
+ {
116
+ if (!empty($this->prefixesPsr0)) {
117
+ return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
118
+ }
119
+
120
+ return array();
121
+ }
122
+
123
+ /**
124
+ * @return array[]
125
+ * @psalm-return array<string, array<int, string>>
126
+ */
127
+ public function getPrefixesPsr4()
128
+ {
129
+ return $this->prefixDirsPsr4;
130
+ }
131
+
132
+ /**
133
+ * @return array[]
134
+ * @psalm-return array<string, string>
135
+ */
136
+ public function getFallbackDirs()
137
+ {
138
+ return $this->fallbackDirsPsr0;
139
+ }
140
+
141
+ /**
142
+ * @return array[]
143
+ * @psalm-return array<string, string>
144
+ */
145
+ public function getFallbackDirsPsr4()
146
+ {
147
+ return $this->fallbackDirsPsr4;
148
+ }
149
+
150
+ /**
151
+ * @return string[] Array of classname => path
152
+ * @psalm-return array<string, string>
153
+ */
154
+ public function getClassMap()
155
+ {
156
+ return $this->classMap;
157
+ }
158
+
159
+ /**
160
+ * @param string[] $classMap Class to filename map
161
+ * @psalm-param array<string, string> $classMap
162
+ *
163
+ * @return void
164
+ */
165
+ public function addClassMap(array $classMap)
166
+ {
167
+ if ($this->classMap) {
168
+ $this->classMap = array_merge($this->classMap, $classMap);
169
+ } else {
170
+ $this->classMap = $classMap;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Registers a set of PSR-0 directories for a given prefix, either
176
+ * appending or prepending to the ones previously set for this prefix.
177
+ *
178
+ * @param string $prefix The prefix
179
+ * @param string[]|string $paths The PSR-0 root directories
180
+ * @param bool $prepend Whether to prepend the directories
181
+ *
182
+ * @return void
183
+ */
184
+ public function add($prefix, $paths, $prepend = false)
185
+ {
186
+ if (!$prefix) {
187
+ if ($prepend) {
188
+ $this->fallbackDirsPsr0 = array_merge(
189
+ (array) $paths,
190
+ $this->fallbackDirsPsr0
191
+ );
192
+ } else {
193
+ $this->fallbackDirsPsr0 = array_merge(
194
+ $this->fallbackDirsPsr0,
195
+ (array) $paths
196
+ );
197
+ }
198
+
199
+ return;
200
+ }
201
+
202
+ $first = $prefix[0];
203
+ if (!isset($this->prefixesPsr0[$first][$prefix])) {
204
+ $this->prefixesPsr0[$first][$prefix] = (array) $paths;
205
+
206
+ return;
207
+ }
208
+ if ($prepend) {
209
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
210
+ (array) $paths,
211
+ $this->prefixesPsr0[$first][$prefix]
212
+ );
213
+ } else {
214
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
215
+ $this->prefixesPsr0[$first][$prefix],
216
+ (array) $paths
217
+ );
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Registers a set of PSR-4 directories for a given namespace, either
223
+ * appending or prepending to the ones previously set for this namespace.
224
+ *
225
+ * @param string $prefix The prefix/namespace, with trailing '\\'
226
+ * @param string[]|string $paths The PSR-4 base directories
227
+ * @param bool $prepend Whether to prepend the directories
228
+ *
229
+ * @throws \InvalidArgumentException
230
+ *
231
+ * @return void
232
+ */
233
+ public function addPsr4($prefix, $paths, $prepend = false)
234
+ {
235
+ if (!$prefix) {
236
+ // Register directories for the root namespace.
237
+ if ($prepend) {
238
+ $this->fallbackDirsPsr4 = array_merge(
239
+ (array) $paths,
240
+ $this->fallbackDirsPsr4
241
+ );
242
+ } else {
243
+ $this->fallbackDirsPsr4 = array_merge(
244
+ $this->fallbackDirsPsr4,
245
+ (array) $paths
246
+ );
247
+ }
248
+ } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
249
+ // Register directories for a new namespace.
250
+ $length = strlen($prefix);
251
+ if ('\\' !== $prefix[$length - 1]) {
252
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
253
+ }
254
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
255
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
256
+ } elseif ($prepend) {
257
+ // Prepend directories for an already registered namespace.
258
+ $this->prefixDirsPsr4[$prefix] = array_merge(
259
+ (array) $paths,
260
+ $this->prefixDirsPsr4[$prefix]
261
+ );
262
+ } else {
263
+ // Append directories for an already registered namespace.
264
+ $this->prefixDirsPsr4[$prefix] = array_merge(
265
+ $this->prefixDirsPsr4[$prefix],
266
+ (array) $paths
267
+ );
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Registers a set of PSR-0 directories for a given prefix,
273
+ * replacing any others previously set for this prefix.
274
+ *
275
+ * @param string $prefix The prefix
276
+ * @param string[]|string $paths The PSR-0 base directories
277
+ *
278
+ * @return void
279
+ */
280
+ public function set($prefix, $paths)
281
+ {
282
+ if (!$prefix) {
283
+ $this->fallbackDirsPsr0 = (array) $paths;
284
+ } else {
285
+ $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Registers a set of PSR-4 directories for a given namespace,
291
+ * replacing any others previously set for this namespace.
292
+ *
293
+ * @param string $prefix The prefix/namespace, with trailing '\\'
294
+ * @param string[]|string $paths The PSR-4 base directories
295
+ *
296
+ * @throws \InvalidArgumentException
297
+ *
298
+ * @return void
299
+ */
300
+ public function setPsr4($prefix, $paths)
301
+ {
302
+ if (!$prefix) {
303
+ $this->fallbackDirsPsr4 = (array) $paths;
304
+ } else {
305
+ $length = strlen($prefix);
306
+ if ('\\' !== $prefix[$length - 1]) {
307
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
308
+ }
309
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
310
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Turns on searching the include path for class files.
316
+ *
317
+ * @param bool $useIncludePath
318
+ *
319
+ * @return void
320
+ */
321
+ public function setUseIncludePath($useIncludePath)
322
+ {
323
+ $this->useIncludePath = $useIncludePath;
324
+ }
325
+
326
+ /**
327
+ * Can be used to check if the autoloader uses the include path to check
328
+ * for classes.
329
+ *
330
+ * @return bool
331
+ */
332
+ public function getUseIncludePath()
333
+ {
334
+ return $this->useIncludePath;
335
+ }
336
+
337
+ /**
338
+ * Turns off searching the prefix and fallback directories for classes
339
+ * that have not been registered with the class map.
340
+ *
341
+ * @param bool $classMapAuthoritative
342
+ *
343
+ * @return void
344
+ */
345
+ public function setClassMapAuthoritative($classMapAuthoritative)
346
+ {
347
+ $this->classMapAuthoritative = $classMapAuthoritative;
348
+ }
349
+
350
+ /**
351
+ * Should class lookup fail if not found in the current class map?
352
+ *
353
+ * @return bool
354
+ */
355
+ public function isClassMapAuthoritative()
356
+ {
357
+ return $this->classMapAuthoritative;
358
+ }
359
+
360
+ /**
361
+ * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
362
+ *
363
+ * @param string|null $apcuPrefix
364
+ *
365
+ * @return void
366
+ */
367
+ public function setApcuPrefix($apcuPrefix)
368
+ {
369
+ $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
370
+ }
371
+
372
+ /**
373
+ * The APCu prefix in use, or null if APCu caching is not enabled.
374
+ *
375
+ * @return string|null
376
+ */
377
+ public function getApcuPrefix()
378
+ {
379
+ return $this->apcuPrefix;
380
+ }
381
+
382
+ /**
383
+ * Registers this instance as an autoloader.
384
+ *
385
+ * @param bool $prepend Whether to prepend the autoloader or not
386
+ *
387
+ * @return void
388
+ */
389
+ public function register($prepend = false)
390
+ {
391
+ spl_autoload_register(array($this, 'loadClass'), true, $prepend);
392
+
393
+ if (null === $this->vendorDir) {
394
+ return;
395
+ }
396
+
397
+ if ($prepend) {
398
+ self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
399
+ } else {
400
+ unset(self::$registeredLoaders[$this->vendorDir]);
401
+ self::$registeredLoaders[$this->vendorDir] = $this;
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Unregisters this instance as an autoloader.
407
+ *
408
+ * @return void
409
+ */
410
+ public function unregister()
411
+ {
412
+ spl_autoload_unregister(array($this, 'loadClass'));
413
+
414
+ if (null !== $this->vendorDir) {
415
+ unset(self::$registeredLoaders[$this->vendorDir]);
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Loads the given class or interface.
421
+ *
422
+ * @param string $class The name of the class
423
+ * @return true|null True if loaded, null otherwise
424
+ */
425
+ public function loadClass($class)
426
+ {
427
+ if ($file = $this->findFile($class)) {
428
+ includeFile($file);
429
+
430
+ return true;
431
+ }
432
+
433
+ return null;
434
+ }
435
+
436
+ /**
437
+ * Finds the path to the file where the class is defined.
438
+ *
439
+ * @param string $class The name of the class
440
+ *
441
+ * @return string|false The path if found, false otherwise
442
+ */
443
+ public function findFile($class)
444
+ {
445
+ // class map lookup
446
+ if (isset($this->classMap[$class])) {
447
+ return $this->classMap[$class];
448
+ }
449
+ if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
450
+ return false;
451
+ }
452
+ if (null !== $this->apcuPrefix) {
453
+ $file = apcu_fetch($this->apcuPrefix.$class, $hit);
454
+ if ($hit) {
455
+ return $file;
456
+ }
457
+ }
458
+
459
+ $file = $this->findFileWithExtension($class, '.php');
460
+
461
+ // Search for Hack files if we are running on HHVM
462
+ if (false === $file && defined('HHVM_VERSION')) {
463
+ $file = $this->findFileWithExtension($class, '.hh');
464
+ }
465
+
466
+ if (null !== $this->apcuPrefix) {
467
+ apcu_add($this->apcuPrefix.$class, $file);
468
+ }
469
+
470
+ if (false === $file) {
471
+ // Remember that this class does not exist.
472
+ $this->missingClasses[$class] = true;
473
+ }
474
+
475
+ return $file;
476
+ }
477
+
478
+ /**
479
+ * Returns the currently registered loaders indexed by their corresponding vendor directories.
480
+ *
481
+ * @return self[]
482
+ */
483
+ public static function getRegisteredLoaders()
484
+ {
485
+ return self::$registeredLoaders;
486
+ }
487
+
488
+ /**
489
+ * @param string $class
490
+ * @param string $ext
491
+ * @return string|false
492
+ */
493
+ private function findFileWithExtension($class, $ext)
494
+ {
495
+ // PSR-4 lookup
496
+ $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
497
+
498
+ $first = $class[0];
499
+ if (isset($this->prefixLengthsPsr4[$first])) {
500
+ $subPath = $class;
501
+ while (false !== $lastPos = strrpos($subPath, '\\')) {
502
+ $subPath = substr($subPath, 0, $lastPos);
503
+ $search = $subPath . '\\';
504
+ if (isset($this->prefixDirsPsr4[$search])) {
505
+ $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
506
+ foreach ($this->prefixDirsPsr4[$search] as $dir) {
507
+ if (file_exists($file = $dir . $pathEnd)) {
508
+ return $file;
509
+ }
510
+ }
511
+ }
512
+ }
513
+ }
514
+
515
+ // PSR-4 fallback dirs
516
+ foreach ($this->fallbackDirsPsr4 as $dir) {
517
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
518
+ return $file;
519
+ }
520
+ }
521
+
522
+ // PSR-0 lookup
523
+ if (false !== $pos = strrpos($class, '\\')) {
524
+ // namespaced class name
525
+ $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
526
+ . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
527
+ } else {
528
+ // PEAR-like class name
529
+ $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
530
+ }
531
+
532
+ if (isset($this->prefixesPsr0[$first])) {
533
+ foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
534
+ if (0 === strpos($class, $prefix)) {
535
+ foreach ($dirs as $dir) {
536
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
537
+ return $file;
538
+ }
539
+ }
540
+ }
541
+ }
542
+ }
543
+
544
+ // PSR-0 fallback dirs
545
+ foreach ($this->fallbackDirsPsr0 as $dir) {
546
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
547
+ return $file;
548
+ }
549
+ }
550
+
551
+ // PSR-0 include paths.
552
+ if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
553
+ return $file;
554
+ }
555
+
556
+ return false;
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Scope isolated include.
562
+ *
563
+ * Prevents access to $this/self from included files.
564
+ *
565
+ * @param string $file
566
+ * @return void
567
+ * @private
568
+ */
569
+ function includeFile($file)
570
+ {
571
+ include $file;
572
+ }
vendor/composer/InstalledVersions.php ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /*
4
+ * This file is part of Composer.
5
+ *
6
+ * (c) Nils Adermann <naderman@naderman.de>
7
+ * Jordi Boggiano <j.boggiano@seld.be>
8
+ *
9
+ * For the full copyright and license information, please view the LICENSE
10
+ * file that was distributed with this source code.
11
+ */
12
+
13
+ namespace Composer;
14
+
15
+ use Composer\Autoload\ClassLoader;
16
+ use Composer\Semver\VersionParser;
17
+
18
+ /**
19
+ * This class is copied in every Composer installed project and available to all
20
+ *
21
+ * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
22
+ *
23
+ * To require its presence, you can require `composer-runtime-api ^2.0`
24
+ */
25
+ class InstalledVersions
26
+ {
27
+ /**
28
+ * @var mixed[]|null
29
+ * @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null
30
+ */
31
+ private static $installed;
32
+
33
+ /**
34
+ * @var bool|null
35
+ */
36
+ private static $canGetVendors;
37
+
38
+ /**
39
+ * @var array[]
40
+ * @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
41
+ */
42
+ private static $installedByVendor = array();
43
+
44
+ /**
45
+ * Returns a list of all package names which are present, either by being installed, replaced or provided
46
+ *
47
+ * @return string[]
48
+ * @psalm-return list<string>
49
+ */
50
+ public static function getInstalledPackages()
51
+ {
52
+ $packages = array();
53
+ foreach (self::getInstalled() as $installed) {
54
+ $packages[] = array_keys($installed['versions']);
55
+ }
56
+
57
+ if (1 === \count($packages)) {
58
+ return $packages[0];
59
+ }
60
+
61
+ return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
62
+ }
63
+
64
+ /**
65
+ * Returns a list of all package names with a specific type e.g. 'library'
66
+ *
67
+ * @param string $type
68
+ * @return string[]
69
+ * @psalm-return list<string>
70
+ */
71
+ public static function getInstalledPackagesByType($type)
72
+ {
73
+ $packagesByType = array();
74
+
75
+ foreach (self::getInstalled() as $installed) {
76
+ foreach ($installed['versions'] as $name => $package) {
77
+ if (isset($package['type']) && $package['type'] === $type) {
78
+ $packagesByType[] = $name;
79
+ }
80
+ }
81
+ }
82
+
83
+ return $packagesByType;
84
+ }
85
+
86
+ /**
87
+ * Checks whether the given package is installed
88
+ *
89
+ * This also returns true if the package name is provided or replaced by another package
90
+ *
91
+ * @param string $packageName
92
+ * @param bool $includeDevRequirements
93
+ * @return bool
94
+ */
95
+ public static function isInstalled($packageName, $includeDevRequirements = true)
96
+ {
97
+ foreach (self::getInstalled() as $installed) {
98
+ if (isset($installed['versions'][$packageName])) {
99
+ return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
100
+ }
101
+ }
102
+
103
+ return false;
104
+ }
105
+
106
+ /**
107
+ * Checks whether the given package satisfies a version constraint
108
+ *
109
+ * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
110
+ *
111
+ * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
112
+ *
113
+ * @param VersionParser $parser Install composer/semver to have access to this class and functionality
114
+ * @param string $packageName
115
+ * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
116
+ * @return bool
117
+ */
118
+ public static function satisfies(VersionParser $parser, $packageName, $constraint)
119
+ {
120
+ $constraint = $parser->parseConstraints($constraint);
121
+ $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
122
+
123
+ return $provided->matches($constraint);
124
+ }
125
+
126
+ /**
127
+ * Returns a version constraint representing all the range(s) which are installed for a given package
128
+ *
129
+ * It is easier to use this via isInstalled() with the $constraint argument if you need to check
130
+ * whether a given version of a package is installed, and not just whether it exists
131
+ *
132
+ * @param string $packageName
133
+ * @return string Version constraint usable with composer/semver
134
+ */
135
+ public static function getVersionRanges($packageName)
136
+ {
137
+ foreach (self::getInstalled() as $installed) {
138
+ if (!isset($installed['versions'][$packageName])) {
139
+ continue;
140
+ }
141
+
142
+ $ranges = array();
143
+ if (isset($installed['versions'][$packageName]['pretty_version'])) {
144
+ $ranges[] = $installed['versions'][$packageName]['pretty_version'];
145
+ }
146
+ if (array_key_exists('aliases', $installed['versions'][$packageName])) {
147
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
148
+ }
149
+ if (array_key_exists('replaced', $installed['versions'][$packageName])) {
150
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
151
+ }
152
+ if (array_key_exists('provided', $installed['versions'][$packageName])) {
153
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
154
+ }
155
+
156
+ return implode(' || ', $ranges);
157
+ }
158
+
159
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
160
+ }
161
+
162
+ /**
163
+ * @param string $packageName
164
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
165
+ */
166
+ public static function getVersion($packageName)
167
+ {
168
+ foreach (self::getInstalled() as $installed) {
169
+ if (!isset($installed['versions'][$packageName])) {
170
+ continue;
171
+ }
172
+
173
+ if (!isset($installed['versions'][$packageName]['version'])) {
174
+ return null;
175
+ }
176
+
177
+ return $installed['versions'][$packageName]['version'];
178
+ }
179
+
180
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
181
+ }
182
+
183
+ /**
184
+ * @param string $packageName
185
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
186
+ */
187
+ public static function getPrettyVersion($packageName)
188
+ {
189
+ foreach (self::getInstalled() as $installed) {
190
+ if (!isset($installed['versions'][$packageName])) {
191
+ continue;
192
+ }
193
+
194
+ if (!isset($installed['versions'][$packageName]['pretty_version'])) {
195
+ return null;
196
+ }
197
+
198
+ return $installed['versions'][$packageName]['pretty_version'];
199
+ }
200
+
201
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
202
+ }
203
+
204
+ /**
205
+ * @param string $packageName
206
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
207
+ */
208
+ public static function getReference($packageName)
209
+ {
210
+ foreach (self::getInstalled() as $installed) {
211
+ if (!isset($installed['versions'][$packageName])) {
212
+ continue;
213
+ }
214
+
215
+ if (!isset($installed['versions'][$packageName]['reference'])) {
216
+ return null;
217
+ }
218
+
219
+ return $installed['versions'][$packageName]['reference'];
220
+ }
221
+
222
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
223
+ }
224
+
225
+ /**
226
+ * @param string $packageName
227
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
228
+ */
229
+ public static function getInstallPath($packageName)
230
+ {
231
+ foreach (self::getInstalled() as $installed) {
232
+ if (!isset($installed['versions'][$packageName])) {
233
+ continue;
234
+ }
235
+
236
+ return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
237
+ }
238
+
239
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
240
+ }
241
+
242
+ /**
243
+ * @return array
244
+ * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}
245
+ */
246
+ public static function getRootPackage()
247
+ {
248
+ $installed = self::getInstalled();
249
+
250
+ return $installed[0]['root'];
251
+ }
252
+
253
+ /**
254
+ * Returns the raw installed.php data for custom implementations
255
+ *
256
+ * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
257
+ * @return array[]
258
+ * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}
259
+ */
260
+ public static function getRawData()
261
+ {
262
+ @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
263
+
264
+ if (null === self::$installed) {
265
+ // only require the installed.php file if this file is loaded from its dumped location,
266
+ // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
267
+ if (substr(__DIR__, -8, 1) !== 'C') {
268
+ self::$installed = include __DIR__ . '/installed.php';
269
+ } else {
270
+ self::$installed = array();
271
+ }
272
+ }
273
+
274
+ return self::$installed;
275
+ }
276
+
277
+ /**
278
+ * Returns the raw data of all installed.php which are currently loaded for custom implementations
279
+ *
280
+ * @return array[]
281
+ * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
282
+ */
283
+ public static function getAllRawData()
284
+ {
285
+ return self::getInstalled();
286
+ }
287
+
288
+ /**
289
+ * Lets you reload the static array from another file
290
+ *
291
+ * This is only useful for complex integrations in which a project needs to use
292
+ * this class but then also needs to execute another project's autoloader in process,
293
+ * and wants to ensure both projects have access to their version of installed.php.
294
+ *
295
+ * A typical case would be PHPUnit, where it would need to make sure it reads all
296
+ * the data it needs from this class, then call reload() with
297
+ * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
298
+ * the project in which it runs can then also use this class safely, without
299
+ * interference between PHPUnit's dependencies and the project's dependencies.
300
+ *
301
+ * @param array[] $data A vendor/composer/installed.php data set
302
+ * @return void
303
+ *
304
+ * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} $data
305
+ */
306
+ public static function reload($data)
307
+ {
308
+ self::$installed = $data;
309
+ self::$installedByVendor = array();
310
+ }
311
+
312
+ /**
313
+ * @return array[]
314
+ * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
315
+ */
316
+ private static function getInstalled()
317
+ {
318
+ if (null === self::$canGetVendors) {
319
+ self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
320
+ }
321
+
322
+ $installed = array();
323
+
324
+ if (self::$canGetVendors) {
325
+ foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
326
+ if (isset(self::$installedByVendor[$vendorDir])) {
327
+ $installed[] = self::$installedByVendor[$vendorDir];
328
+ } elseif (is_file($vendorDir.'/composer/installed.php')) {
329
+ $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
330
+ if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
331
+ self::$installed = $installed[count($installed) - 1];
332
+ }
333
+ }
334
+ }
335
+ }
336
+
337
+ if (null === self::$installed) {
338
+ // only require the installed.php file if this file is loaded from its dumped location,
339
+ // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
340
+ if (substr(__DIR__, -8, 1) !== 'C') {
341
+ self::$installed = require __DIR__ . '/installed.php';
342
+ } else {
343
+ self::$installed = array();
344
+ }
345
+ }
346
+ $installed[] = self::$installed;
347
+
348
+ return $installed;
349
+ }
350
+ }
vendor/composer/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Copyright (c) Nils Adermann, Jordi Boggiano
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is furnished
9
+ to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ THE SOFTWARE.
21
+
vendor/composer/autoload_classmap.php ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ // autoload_classmap.php @generated by Composer
4
+
5
+ $vendorDir = dirname(dirname(__FILE__));
6
+ $baseDir = dirname($vendorDir);
7
+
8
+ return array(
9
+ 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
10
+ 'Crontrol\\Event\\Table' => $baseDir . '/src/event-list-table.php',
11
+ 'Crontrol\\Request' => $baseDir . '/src/request.php',
12
+ 'Crontrol\\Schedule_List_Table' => $baseDir . '/src/schedule-list-table.php',
13
+ );
vendor/composer/autoload_namespaces.php ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ // autoload_namespaces.php @generated by Composer
4
+
5
+ $vendorDir = dirname(dirname(__FILE__));
6
+ $baseDir = dirname($vendorDir);
7
+
8
+ return array(
9
+ );
vendor/composer/autoload_psr4.php ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ // autoload_psr4.php @generated by Composer
4
+
5
+ $vendorDir = dirname(dirname(__FILE__));
6
+ $baseDir = dirname($vendorDir);
7
+
8
+ return array(
9
+ );
vendor/composer/autoload_real.php ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ // autoload_real.php @generated by Composer
4
+
5
+ class ComposerAutoloaderInitdbf095101a93cb72acf9efa892a78eaf
6
+ {
7
+ private static $loader;
8
+
9
+ public static function loadClassLoader($class)
10
+ {
11
+ if ('Composer\Autoload\ClassLoader' === $class) {
12
+ require __DIR__ . '/ClassLoader.php';
13
+ }
14
+ }
15
+
16
+ /**
17
+ * @return \Composer\Autoload\ClassLoader
18
+ */
19
+ public static function getLoader()
20
+ {
21
+ if (null !== self::$loader) {
22
+ return self::$loader;
23
+ }
24
+
25
+ require __DIR__ . '/platform_check.php';
26
+
27
+ spl_autoload_register(array('ComposerAutoloaderInitdbf095101a93cb72acf9efa892a78eaf', 'loadClassLoader'), true, false);
28
+ self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
29
+ spl_autoload_unregister(array('ComposerAutoloaderInitdbf095101a93cb72acf9efa892a78eaf', 'loadClassLoader'));
30
+
31
+ $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
32
+ if ($useStaticLoader) {
33
+ require __DIR__ . '/autoload_static.php';
34
+
35
+ call_user_func(\Composer\Autoload\ComposerStaticInitdbf095101a93cb72acf9efa892a78eaf::getInitializer($loader));
36
+ } else {
37
+ $classMap = require __DIR__ . '/autoload_classmap.php';
38
+ if ($classMap) {
39
+ $loader->addClassMap($classMap);
40
+ }
41
+ }
42
+
43
+ $loader->setClassMapAuthoritative(true);
44
+ $loader->register(false);
45
+
46
+ return $loader;
47
+ }
48
+ }
vendor/composer/autoload_static.php ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ // autoload_static.php @generated by Composer
4
+
5
+ namespace Composer\Autoload;
6
+
7
+ class ComposerStaticInitdbf095101a93cb72acf9efa892a78eaf
8
+ {
9
+ public static $classMap = array (
10
+ 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
11
+ 'Crontrol\\Event\\Table' => __DIR__ . '/../..' . '/src/event-list-table.php',
12
+ 'Crontrol\\Request' => __DIR__ . '/../..' . '/src/request.php',
13
+ 'Crontrol\\Schedule_List_Table' => __DIR__ . '/../..' . '/src/schedule-list-table.php',
14
+ );
15
+
16
+ public static function getInitializer(ClassLoader $loader)
17
+ {
18
+ return \Closure::bind(function () use ($loader) {
19
+ $loader->classMap = ComposerStaticInitdbf095101a93cb72acf9efa892a78eaf::$classMap;
20
+
21
+ }, null, ClassLoader::class);
22
+ }
23
+ }
vendor/composer/installed.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
1
+ {
2
+ "packages": [],
3
+ "dev": false,
4
+ "dev-package-names": []
5
+ }
vendor/composer/installed.php ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php return array(
2
+ 'root' => array(
3
+ 'pretty_version' => 'dev-release',
4
+ 'version' => 'dev-release',
5
+ 'type' => 'wordpress-plugin',
6
+ 'install_path' => __DIR__ . '/../../',
7
+ 'aliases' => array(),
8
+ 'reference' => '1d6304ed14bdca543e49832dcbf9542826b54cef',
9
+ 'name' => 'johnbillion/wp-crontrol',
10
+ 'dev' => false,
11
+ ),
12
+ 'versions' => array(
13
+ 'johnbillion/wp-crontrol' => array(
14
+ 'pretty_version' => 'dev-release',
15
+ 'version' => 'dev-release',
16
+ 'type' => 'wordpress-plugin',
17
+ 'install_path' => __DIR__ . '/../../',
18
+ 'aliases' => array(),
19
+ 'reference' => '1d6304ed14bdca543e49832dcbf9542826b54cef',
20
+ 'dev_requirement' => false,
21
+ ),
22
+ ),
23
+ );
vendor/composer/platform_check.php ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ // platform_check.php @generated by Composer
4
+
5
+ $issues = array();
6
+
7
+ if (!(PHP_VERSION_ID >= 50600)) {
8
+ $issues[] = 'Your Composer dependencies require a PHP version ">= 5.6.0". You are running ' . PHP_VERSION . '.';
9
+ }
10
+
11
+ if ($issues) {
12
+ if (!headers_sent()) {
13
+ header('HTTP/1.1 500 Internal Server Error');
14
+ }
15
+ if (!ini_get('display_errors')) {
16
+ if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
17
+ fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
18
+ } elseif (!headers_sent()) {
19
+ echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
20
+ }
21
+ }
22
+ trigger_error(
23
+ 'Composer detected issues in your platform: ' . implode(' ', $issues),
24
+ E_USER_ERROR
25
+ );
26
+ }
wp-crontrol.php CHANGED
@@ -5,10 +5,10 @@
5
  * Description: WP Crontrol enables you to view and control what's happening in the WP-Cron system.
6
  * Author: John Blackbourn & crontributors
7
  * Author URI: https://github.com/johnbillion/wp-crontrol/graphs/contributors
8
- * Version: 1.14.0
9
  * Text Domain: wp-crontrol
10
  * Domain Path: /languages/
11
- * Requires PHP: 5.3.6
12
  * License: GPL v2 or later
13
  *
14
  * LICENSE
@@ -29,2082 +29,30 @@
29
  * @copyright Copyright 2008 Edward Dale, 2012-2022 John Blackbourn
30
  * @license http://www.gnu.org/licenses/gpl.txt GPL 2.0
31
  * @link https://wordpress.org/plugins/wp-crontrol/
32
- * @since 0.2
33
  */
34
 
35
  namespace Crontrol;
36
 
37
- use Crontrol\Event\Table;
38
- use stdClass;
39
- use WP_Error;
40
 
41
  if ( ! defined( 'ABSPATH' ) ) {
42
  exit;
43
  }
44
 
45
- require_once __DIR__ . '/src/event.php';
46
- require_once __DIR__ . '/src/request.php';
47
- require_once __DIR__ . '/src/schedule.php';
48
-
49
- const TRANSIENT = 'crontrol-message-%d';
50
-
51
- /**
52
- * Hook onto all of the actions and filters needed by the plugin.
53
- *
54
- * @return void
55
- */
56
- function init_hooks() {
57
- $plugin_file = plugin_basename( __FILE__ );
58
-
59
- add_action( 'init', __NAMESPACE__ . '\action_init' );
60
- add_action( 'init', __NAMESPACE__ . '\action_handle_posts' );
61
- add_action( 'admin_menu', __NAMESPACE__ . '\action_admin_menu' );
62
- add_action( 'wp_ajax_crontrol_checkhash', __NAMESPACE__ . '\ajax_check_events_hash' );
63
- add_filter( "plugin_action_links_{$plugin_file}", __NAMESPACE__ . '\plugin_action_links', 10, 4 );
64
- add_filter( 'removable_query_args', __NAMESPACE__ . '\filter_removable_query_args' );
65
- add_filter( 'pre_unschedule_event', __NAMESPACE__ . '\maybe_clear_doing_cron' );
66
- add_filter( 'plugin_row_meta', __NAMESPACE__ . '\filter_plugin_row_meta', 10, 4 );
67
-
68
- add_action( 'load-tools_page_crontrol_admin_manage_page', __NAMESPACE__ . '\setup_manage_page' );
69
-
70
- add_filter( 'cron_schedules', __NAMESPACE__ . '\filter_cron_schedules' );
71
- add_action( 'crontrol_cron_job', __NAMESPACE__ . '\action_php_cron_event' );
72
- add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\enqueue_assets' );
73
- add_action( 'crontrol/tab-header', __NAMESPACE__ . '\show_cron_status', 20 );
74
- }
75
-
76
- /**
77
- * Sets an error message to show to the current user after a redirect.
78
- *
79
- * @param string $message The error message text.
80
- * @return bool Whether the message was saved.
81
- */
82
- function set_message( $message ) {
83
- $key = sprintf(
84
- TRANSIENT,
85
- get_current_user_id()
86
- );
87
- return set_transient( $key, $message, 60 );
88
- }
89
-
90
- /**
91
- * Gets the error message to show to the current user after a redirect.
92
- *
93
- * @return string The error message text.
94
- */
95
- function get_message() {
96
- $key = sprintf(
97
- TRANSIENT,
98
- get_current_user_id()
99
- );
100
- return get_transient( $key );
101
- }
102
-
103
- /**
104
- * Filters the array of row meta for each plugin in the Plugins list table.
105
- *
106
- * @param array<int,string> $plugin_meta An array of the plugin row's meta data.
107
- * @param string $plugin_file Path to the plugin file relative to the plugins directory.
108
- * @return array<int,string> An array of the plugin row's meta data.
109
- */
110
- function filter_plugin_row_meta( array $plugin_meta, $plugin_file ) {
111
- if ( 'wp-crontrol/wp-crontrol.php' !== $plugin_file ) {
112
- return $plugin_meta;
113
- }
114
-
115
- $plugin_meta[] = sprintf(
116
- '<a href="%1$s"><span class="dashicons dashicons-star-filled" aria-hidden="true" style="font-size:14px;line-height:1.3"></span>%2$s</a>',
117
- 'https://github.com/sponsors/johnbillion',
118
- esc_html_x( 'Sponsor', 'verb', 'wp-crontrol' )
119
- );
120
-
121
- return $plugin_meta;
122
- }
123
-
124
- /**
125
- * Run using the 'init' action.
126
- *
127
- * @return void
128
- */
129
- function action_init() {
130
- load_plugin_textdomain( 'wp-crontrol', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
131
- }
132
-
133
- /**
134
- * Handles any POSTs made by the plugin. Run using the 'init' action.
135
- *
136
- * @return void
137
- */
138
- function action_handle_posts() {
139
- $request = new Request();
140
-
141
- if ( isset( $_POST['crontrol_action'] ) && ( 'new_cron' === $_POST['crontrol_action'] ) ) {
142
- if ( ! current_user_can( 'manage_options' ) ) {
143
- wp_die( esc_html__( 'You are not allowed to add new cron events.', 'wp-crontrol' ), 401 );
144
- }
145
- check_admin_referer( 'crontrol-new-cron' );
146
-
147
- $cr = $request->init( wp_unslash( $_POST ) );
148
-
149
- if ( 'crontrol_cron_job' === $cr->hookname && ! current_user_can( 'edit_files' ) ) {
150
- wp_die( esc_html__( 'You are not allowed to add new PHP cron events.', 'wp-crontrol' ), 401 );
151
- }
152
- $args = json_decode( $cr->args, true );
153
-
154
- if ( empty( $args ) || ! is_array( $args ) ) {
155
- $args = array();
156
- }
157
-
158
- $next_run_local = ( 'custom' === $cr->next_run_date_local ) ? $cr->next_run_date_local_custom_date . ' ' . $cr->next_run_date_local_custom_time : $cr->next_run_date_local;
159
-
160
- add_filter( 'schedule_event', function( $event ) {
161
- if ( ! $event ) {
162
- return $event;
163
- }
164
-
165
- /**
166
- * Fires after a new cron event is added.
167
- *
168
- * @param stdClass $event {
169
- * An object containing the event's data.
170
- *
171
- * @type string $hook Action hook to execute when the event is run.
172
- * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
173
- * @type string|false $schedule How often the event should subsequently recur.
174
- * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
175
- * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
176
- * }
177
- */
178
- do_action( 'crontrol/added_new_event', $event );
179
-
180
- return $event;
181
- }, 99 );
182
-
183
- $added = Event\add( $next_run_local, $cr->schedule, $cr->hookname, $args );
184
-
185
- $redirect = array(
186
- 'page' => 'crontrol_admin_manage_page',
187
- 'crontrol_message' => '5',
188
- 'crontrol_name' => rawurlencode( $cr->hookname ),
189
- );
190
-
191
- if ( is_wp_error( $added ) ) {
192
- set_message( $added->get_error_message() );
193
- $redirect['crontrol_message'] = 'error';
194
- }
195
-
196
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
197
- exit;
198
-
199
- } elseif ( isset( $_POST['crontrol_action'] ) && ( 'new_php_cron' === $_POST['crontrol_action'] ) ) {
200
- if ( ! current_user_can( 'edit_files' ) ) {
201
- wp_die( esc_html__( 'You are not allowed to add new PHP cron events.', 'wp-crontrol' ), 401 );
202
- }
203
- check_admin_referer( 'crontrol-new-cron' );
204
-
205
- $cr = $request->init( wp_unslash( $_POST ) );
206
-
207
- $next_run_local = ( 'custom' === $cr->next_run_date_local ) ? $cr->next_run_date_local_custom_date . ' ' . $cr->next_run_date_local_custom_time : $cr->next_run_date_local;
208
- $args = array(
209
- 'code' => $cr->hookcode,
210
- 'name' => $cr->eventname,
211
- );
212
-
213
- add_filter( 'schedule_event', function( $event ) {
214
- if ( ! $event ) {
215
- return $event;
216
- }
217
-
218
- /**
219
- * Fires after a new PHP cron event is added.
220
- *
221
- * @param stdClass $event {
222
- * An object containing the event's data.
223
- *
224
- * @type string $hook Action hook to execute when the event is run.
225
- * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
226
- * @type string|false $schedule How often the event should subsequently recur.
227
- * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
228
- * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
229
- * }
230
- */
231
- do_action( 'crontrol/added_new_php_event', $event );
232
-
233
- return $event;
234
- }, 99 );
235
-
236
- $added = Event\add( $next_run_local, $cr->schedule, 'crontrol_cron_job', $args );
237
-
238
- $hookname = ( ! empty( $cr->eventname ) ) ? $cr->eventname : __( 'PHP Cron', 'wp-crontrol' );
239
- $redirect = array(
240
- 'page' => 'crontrol_admin_manage_page',
241
- 'crontrol_message' => '5',
242
- 'crontrol_name' => rawurlencode( $hookname ),
243
- );
244
-
245
- if ( is_wp_error( $added ) ) {
246
- set_message( $added->get_error_message() );
247
- $redirect['crontrol_message'] = 'error';
248
- }
249
-
250
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
251
- exit;
252
-
253
- } elseif ( isset( $_POST['crontrol_action'] ) && ( 'edit_cron' === $_POST['crontrol_action'] ) ) {
254
- if ( ! current_user_can( 'manage_options' ) ) {
255
- wp_die( esc_html__( 'You are not allowed to edit cron events.', 'wp-crontrol' ), 401 );
256
- }
257
-
258
- $cr = $request->init( wp_unslash( $_POST ) );
259
-
260
- check_admin_referer( "crontrol-edit-cron_{$cr->original_hookname}_{$cr->original_sig}_{$cr->original_next_run_utc}" );
261
-
262
- if ( 'crontrol_cron_job' === $cr->hookname && ! current_user_can( 'edit_files' ) ) {
263
- wp_die( esc_html__( 'You are not allowed to edit PHP cron events.', 'wp-crontrol' ), 401 );
264
- }
265
-
266
- $args = json_decode( $cr->args, true );
267
-
268
- if ( empty( $args ) || ! is_array( $args ) ) {
269
- $args = array();
270
- }
271
-
272
- $redirect = array(
273
- 'page' => 'crontrol_admin_manage_page',
274
- 'crontrol_message' => '4',
275
- 'crontrol_name' => rawurlencode( $cr->hookname ),
276
- );
277
-
278
- $original = Event\get_single( $cr->original_hookname, $cr->original_sig, $cr->original_next_run_utc );
279
-
280
- if ( is_wp_error( $original ) ) {
281
- set_message( $original->get_error_message() );
282
- $redirect['crontrol_message'] = 'error';
283
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
284
- exit;
285
- }
286
-
287
- $deleted = Event\delete( $cr->original_hookname, $cr->original_sig, $cr->original_next_run_utc );
288
-
289
- if ( is_wp_error( $deleted ) ) {
290
- set_message( $deleted->get_error_message() );
291
- $redirect['crontrol_message'] = 'error';
292
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
293
- exit;
294
- }
295
-
296
- $next_run_local = ( 'custom' === $cr->next_run_date_local ) ? $cr->next_run_date_local_custom_date . ' ' . $cr->next_run_date_local_custom_time : $cr->next_run_date_local;
297
-
298
- /**
299
- * Modifies an event before it is scheduled.
300
- *
301
- * @param stdClass|false $event An object containing the new event's data, or boolean false.
302
- */
303
- add_filter( 'schedule_event', function( $event ) use ( $original ) {
304
- if ( ! $event ) {
305
- return $event;
306
- }
307
-
308
- /**
309
- * Fires after a cron event is edited.
310
- *
311
- * @param stdClass $event {
312
- * An object containing the new event's data.
313
- *
314
- * @type string $hook Action hook to execute when the event is run.
315
- * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
316
- * @type string|false $schedule How often the event should subsequently recur.
317
- * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
318
- * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
319
- * }
320
- * @param stdClass $original {
321
- * An object containing the original event's data.
322
- *
323
- * @type string $hook Action hook to execute when the event is run.
324
- * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
325
- * @type string|false $schedule How often the event should subsequently recur.
326
- * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
327
- * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
328
- * }
329
- */
330
- do_action( 'crontrol/edited_event', $event, $original );
331
-
332
- return $event;
333
- }, 99 );
334
-
335
- $added = Event\add( $next_run_local, $cr->schedule, $cr->hookname, $args );
336
-
337
- if ( is_wp_error( $added ) ) {
338
- set_message( $added->get_error_message() );
339
- $redirect['crontrol_message'] = 'error';
340
- }
341
-
342
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
343
- exit;
344
-
345
- } elseif ( isset( $_POST['crontrol_action'] ) && ( 'edit_php_cron' === $_POST['crontrol_action'] ) ) {
346
- if ( ! current_user_can( 'edit_files' ) ) {
347
- wp_die( esc_html__( 'You are not allowed to edit PHP cron events.', 'wp-crontrol' ), 401 );
348
- }
349
-
350
- $cr = $request->init( wp_unslash( $_POST ) );
351
-
352
- check_admin_referer( "crontrol-edit-cron_{$cr->original_hookname}_{$cr->original_sig}_{$cr->original_next_run_utc}" );
353
- $args = array(
354
- 'code' => $cr->hookcode,
355
- 'name' => $cr->eventname,
356
- );
357
- $hookname = ( ! empty( $cr->eventname ) ) ? $cr->eventname : __( 'PHP Cron', 'wp-crontrol' );
358
- $redirect = array(
359
- 'page' => 'crontrol_admin_manage_page',
360
- 'crontrol_message' => '4',
361
- 'crontrol_name' => rawurlencode( $hookname ),
362
- );
363
-
364
- $original = Event\get_single( $cr->original_hookname, $cr->original_sig, $cr->original_next_run_utc );
365
-
366
- if ( is_wp_error( $original ) ) {
367
- set_message( $original->get_error_message() );
368
- $redirect['crontrol_message'] = 'error';
369
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
370
- exit;
371
- }
372
-
373
- $deleted = Event\delete( $cr->original_hookname, $cr->original_sig, $cr->original_next_run_utc );
374
-
375
- if ( is_wp_error( $deleted ) ) {
376
- set_message( $deleted->get_error_message() );
377
- $redirect['crontrol_message'] = 'error';
378
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
379
- exit;
380
- }
381
-
382
- $next_run_local = ( 'custom' === $cr->next_run_date_local ) ? $cr->next_run_date_local_custom_date . ' ' . $cr->next_run_date_local_custom_time : $cr->next_run_date_local;
383
-
384
- /**
385
- * Modifies an event before it is scheduled.
386
- *
387
- * @param stdClass|false $event An object containing the new event's data, or boolean false.
388
- */
389
- add_filter( 'schedule_event', function( $event ) use ( $original ) {
390
- if ( ! $event ) {
391
- return $event;
392
- }
393
-
394
- /**
395
- * Fires after a PHP cron event is edited.
396
- *
397
- * @param stdClass $event {
398
- * An object containing the new event's data.
399
- *
400
- * @type string $hook Action hook to execute when the event is run.
401
- * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
402
- * @type string|false $schedule How often the event should subsequently recur.
403
- * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
404
- * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
405
- * }
406
- * @param stdClass $original {
407
- * An object containing the original event's data.
408
- *
409
- * @type string $hook Action hook to execute when the event is run.
410
- * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
411
- * @type string|false $schedule How often the event should subsequently recur.
412
- * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
413
- * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
414
- * }
415
- */
416
- do_action( 'crontrol/edited_php_event', $event, $original );
417
-
418
- return $event;
419
- }, 99 );
420
-
421
- $added = Event\add( $next_run_local, $cr->schedule, 'crontrol_cron_job', $args );
422
-
423
- if ( is_wp_error( $added ) ) {
424
- set_message( $added->get_error_message() );
425
- $redirect['crontrol_message'] = 'error';
426
- }
427
-
428
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
429
- exit;
430
-
431
- } elseif ( isset( $_POST['crontrol_new_schedule'] ) ) {
432
- if ( ! current_user_can( 'manage_options' ) ) {
433
- wp_die( esc_html__( 'You are not allowed to add new cron schedules.', 'wp-crontrol' ), 401 );
434
- }
435
- check_admin_referer( 'crontrol-new-schedule' );
436
- $name = wp_unslash( $_POST['crontrol_schedule_internal_name'] );
437
- $interval = absint( $_POST['crontrol_schedule_interval'] );
438
- $display = wp_unslash( $_POST['crontrol_schedule_display_name'] );
439
-
440
- Schedule\add( $name, $interval, $display );
441
- $redirect = array(
442
- 'page' => 'crontrol_admin_options_page',
443
- 'crontrol_message' => '3',
444
- 'crontrol_name' => rawurlencode( $name ),
445
- );
446
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'options-general.php' ) ) );
447
- exit;
448
-
449
- } elseif ( isset( $_GET['crontrol_action'] ) && 'delete-schedule' === $_GET['crontrol_action'] ) {
450
- if ( ! current_user_can( 'manage_options' ) ) {
451
- wp_die( esc_html__( 'You are not allowed to delete cron schedules.', 'wp-crontrol' ), 401 );
452
- }
453
- $schedule = wp_unslash( $_GET['crontrol_id'] );
454
- check_admin_referer( "crontrol-delete-schedule_{$schedule}" );
455
- Schedule\delete( $schedule );
456
- $redirect = array(
457
- 'page' => 'crontrol_admin_options_page',
458
- 'crontrol_message' => '2',
459
- 'crontrol_name' => rawurlencode( $schedule ),
460
- );
461
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'options-general.php' ) ) );
462
- exit;
463
-
464
- } elseif ( ( isset( $_POST['action'] ) && 'crontrol_delete_crons' === $_POST['action'] ) || ( isset( $_POST['action2'] ) && 'crontrol_delete_crons' === $_POST['action2'] ) ) {
465
- if ( ! current_user_can( 'manage_options' ) ) {
466
- wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
467
- }
468
- check_admin_referer( 'bulk-crontrol-events' );
469
-
470
- if ( empty( $_POST['crontrol_delete'] ) ) {
471
- return;
472
- }
473
-
474
- /**
475
- * @var array<string,array<string,string>>
476
- */
477
- $delete = (array) wp_unslash( $_POST['crontrol_delete'] );
478
- $deleted = 0;
479
-
480
- foreach ( $delete as $next_run_utc => $events ) {
481
- foreach ( (array) $events as $hook => $sig ) {
482
- if ( 'crontrol_cron_job' === $hook && ! current_user_can( 'edit_files' ) ) {
483
- continue;
484
- }
485
-
486
- $event = Event\get_single( urldecode( $hook ), $sig, $next_run_utc );
487
- $deleted = Event\delete( urldecode( $hook ), $sig, $next_run_utc );
488
-
489
- if ( ! is_wp_error( $deleted ) ) {
490
- $deleted++;
491
-
492
- /** This action is documented in wp-crontrol.php */
493
- do_action( 'crontrol/deleted_event', $event );
494
- }
495
- }
496
- }
497
-
498
- $redirect = array(
499
- 'page' => 'crontrol_admin_manage_page',
500
- 'crontrol_name' => $deleted,
501
- 'crontrol_message' => '9',
502
- );
503
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
504
- exit;
505
-
506
- } elseif ( isset( $_GET['crontrol_action'] ) && 'delete-cron' === $_GET['crontrol_action'] ) {
507
- if ( ! current_user_can( 'manage_options' ) ) {
508
- wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
509
- }
510
- $hook = wp_unslash( $_GET['crontrol_id'] );
511
- $sig = wp_unslash( $_GET['crontrol_sig'] );
512
- $next_run_utc = wp_unslash( $_GET['crontrol_next_run_utc'] );
513
- check_admin_referer( "crontrol-delete-cron_{$hook}_{$sig}_{$next_run_utc}" );
514
-
515
- if ( 'crontrol_cron_job' === $hook && ! current_user_can( 'edit_files' ) ) {
516
- wp_die( esc_html__( 'You are not allowed to delete PHP cron events.', 'wp-crontrol' ), 401 );
517
- }
518
-
519
- $redirect = array(
520
- 'page' => 'crontrol_admin_manage_page',
521
- 'crontrol_message' => '6',
522
- 'crontrol_name' => rawurlencode( $hook ),
523
- );
524
-
525
- $event = Event\get_single( $hook, $sig, $next_run_utc );
526
-
527
- if ( is_wp_error( $event ) ) {
528
- set_message( $event->get_error_message() );
529
- $redirect['crontrol_message'] = 'error';
530
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
531
- exit;
532
- }
533
-
534
- $deleted = Event\delete( $hook, $sig, $next_run_utc );
535
-
536
- if ( is_wp_error( $deleted ) ) {
537
- set_message( $deleted->get_error_message() );
538
- $redirect['crontrol_message'] = 'error';
539
- } else {
540
- /**
541
- * Fires after a cron event is deleted.
542
- *
543
- * @param stdClass $event {
544
- * An object containing the event's data.
545
- *
546
- * @type string $hook Action hook to execute when the event is run.
547
- * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
548
- * @type string|false $schedule How often the event should subsequently recur.
549
- * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
550
- * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
551
- * }
552
- */
553
- do_action( 'crontrol/deleted_event', $event );
554
- }
555
-
556
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
557
- exit;
558
-
559
- } elseif ( isset( $_GET['crontrol_action'] ) && 'delete-hook' === $_GET['crontrol_action'] ) {
560
- if ( ! current_user_can( 'manage_options' ) ) {
561
- wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
562
- }
563
- $hook = wp_unslash( $_GET['crontrol_id'] );
564
- $deleted = false;
565
- check_admin_referer( "crontrol-delete-hook_{$hook}" );
566
-
567
- if ( 'crontrol_cron_job' === $hook ) {
568
- wp_die( esc_html__( 'You are not allowed to delete PHP cron events.', 'wp-crontrol' ), 401 );
569
- }
570
-
571
- if ( function_exists( 'wp_unschedule_hook' ) ) {
572
- /** @var int|false */
573
- $deleted = wp_unschedule_hook( $hook );
574
- }
575
-
576
- if ( 0 === $deleted ) {
577
- $redirect = array(
578
- 'page' => 'crontrol_admin_manage_page',
579
- 'crontrol_message' => '3',
580
- 'crontrol_name' => rawurlencode( $hook ),
581
- );
582
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
583
- exit;
584
- } elseif ( $deleted ) {
585
- /**
586
- * Fires after all cron events with the given hook are deleted.
587
- *
588
- * @param string $hook The hook name.
589
- * @param int $deleted The number of events that were deleted.
590
- */
591
- do_action( 'crontrol/deleted_all_with_hook', $hook, $deleted );
592
-
593
- $redirect = array(
594
- 'page' => 'crontrol_admin_manage_page',
595
- 'crontrol_message' => '2',
596
- 'crontrol_name' => rawurlencode( $hook ),
597
- );
598
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
599
- exit;
600
- } else {
601
- $redirect = array(
602
- 'page' => 'crontrol_admin_manage_page',
603
- 'crontrol_message' => '7',
604
- 'crontrol_name' => rawurlencode( $hook ),
605
- );
606
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
607
- exit;
608
- }
609
- } elseif ( isset( $_GET['crontrol_action'] ) && 'run-cron' === $_GET['crontrol_action'] ) {
610
- if ( ! current_user_can( 'manage_options' ) ) {
611
- wp_die( esc_html__( 'You are not allowed to run cron events.', 'wp-crontrol' ), 401 );
612
- }
613
- $hook = wp_unslash( $_GET['crontrol_id'] );
614
- $sig = wp_unslash( $_GET['crontrol_sig'] );
615
- check_admin_referer( "crontrol-run-cron_{$hook}_{$sig}" );
616
-
617
- $ran = Event\run( $hook, $sig );
618
-
619
- $redirect = array(
620
- 'page' => 'crontrol_admin_manage_page',
621
- 'crontrol_message' => '1',
622
- 'crontrol_name' => rawurlencode( $hook ),
623
- );
624
-
625
- if ( is_wp_error( $ran ) ) {
626
- $set = set_message( $ran->get_error_message() );
627
-
628
- // If we can't store the error message in a transient, just display it.
629
- if ( ! $set ) {
630
- wp_die(
631
- esc_html( $ran->get_error_message() ),
632
- '',
633
- array(
634
- 'response' => 500,
635
- 'back_link' => true,
636
- )
637
- );
638
- }
639
- $redirect['crontrol_message'] = 'error';
640
- }
641
-
642
- wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
643
- exit;
644
- } elseif ( isset( $_POST['crontrol_action'] ) && 'export-event-csv' === $_POST['crontrol_action'] ) {
645
- check_admin_referer( 'crontrol-export-event-csv', 'crontrol_nonce' );
646
-
647
- require_once __DIR__ . '/src/event-list-table.php';
648
-
649
- $type = isset( $_POST['crontrol_hooks_type'] ) ? $_POST['crontrol_hooks_type'] : 'all';
650
- $headers = array(
651
- 'hook',
652
- 'arguments',
653
- 'next_run',
654
- 'next_run_gmt',
655
- 'action',
656
- 'recurrence',
657
- 'interval',
658
- );
659
- $filename = sprintf(
660
- 'cron-events-%s-%s.csv',
661
- $type,
662
- gmdate( 'Y-m-d-H.i.s' )
663
- );
664
- $csv = fopen( 'php://output', 'w' );
665
-
666
- if ( false === $csv ) {
667
- wp_die( esc_html__( 'Could not save CSV file.', 'wp-crontrol' ) );
668
- }
669
-
670
- $events = Table::get_filtered_events( Event\get() );
671
-
672
- header( 'Content-Type: text/csv; charset=utf-8' );
673
- header(
674
- sprintf(
675
- 'Content-Disposition: attachment; filename="%s"',
676
- esc_attr( $filename )
677
- )
678
- );
679
-
680
- fputcsv( $csv, $headers );
681
-
682
- if ( isset( $events[ $type ] ) ) {
683
- foreach ( $events[ $type ] as $event ) {
684
- $next_run_local = get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $event->time ), 'c' );
685
- $next_run_utc = gmdate( 'c', $event->time );
686
- $hook_callbacks = \Crontrol\get_hook_callbacks( $event->hook );
687
-
688
- if ( 'crontrol_cron_job' === $event->hook ) {
689
- $args = __( 'PHP Code', 'wp-crontrol' );
690
- } elseif ( empty( $event->args ) ) {
691
- $args = '';
692
- } else {
693
- $args = \Crontrol\json_output( $event->args, false );
694
- }
695
-
696
- if ( 'crontrol_cron_job' === $event->hook ) {
697
- $action = __( 'WP Crontrol', 'wp-crontrol' );
698
- } else {
699
- $callbacks = array();
700
-
701
- foreach ( $hook_callbacks as $callback ) {
702
- $callbacks[] = $callback['callback']['name'];
703
- }
704
-
705
- $action = implode( ',', $callbacks );
706
- }
707
-
708
- if ( $event->schedule ) {
709
- $recurrence = Event\get_schedule_name( $event );
710
- if ( is_wp_error( $recurrence ) ) {
711
- $recurrence = $recurrence->get_error_message();
712
- }
713
- } else {
714
- $recurrence = __( 'Non-repeating', 'wp-crontrol' );
715
- }
716
-
717
- $row = array(
718
- $event->hook,
719
- $args,
720
- $next_run_local,
721
- $next_run_utc,
722
- $action,
723
- $recurrence,
724
- (int) $event->interval,
725
- );
726
- fputcsv( $csv, $row );
727
- }
728
- }
729
-
730
- fclose( $csv );
731
-
732
- exit;
733
- }
734
  }
735
 
736
- /**
737
- * Adds options & management pages to the admin menu.
738
- *
739
- * Run using the 'admin_menu' action.
740
- *
741
- * @return void
742
- */
743
- function action_admin_menu() {
744
- $schedules = add_options_page(
745
- esc_html__( 'Cron Schedules', 'wp-crontrol' ),
746
- esc_html__( 'Cron Schedules', 'wp-crontrol' ),
747
- 'manage_options',
748
- 'crontrol_admin_options_page',
749
- __NAMESPACE__ . '\admin_options_page'
750
- );
751
- $events = add_management_page(
752
- esc_html__( 'Cron Events', 'wp-crontrol' ),
753
- esc_html__( 'Cron Events', 'wp-crontrol' ),
754
- 'manage_options',
755
- 'crontrol_admin_manage_page',
756
- __NAMESPACE__ . '\admin_manage_page'
757
- );
758
 
759
- add_action( "load-{$schedules}", __NAMESPACE__ . '\admin_help_tab' );
760
- add_action( "load-{$events}", __NAMESPACE__ . '\admin_help_tab' );
761
  }
762
 
763
- /**
764
- * Adds a Help tab with links to help resources.
765
- *
766
- * @return void
767
- */
768
- function admin_help_tab() {
769
- $screen = get_current_screen();
770
-
771
- if ( ! $screen ) {
772
- return;
773
- }
774
-
775
- $content = '<p>' . __( 'There are several places to get help with issues relating to WP-Cron:', 'wp-crontrol' ) . '</p>';
776
- $content .= '<ul>';
777
- $content .= '<li>';
778
- $content .= sprintf(
779
- /* translators: %s: URL to the documentation */
780
- __( '<a href="%s">Read the WP Crontrol wiki</a> which contains information about events that have missed their schedule, problems with spawning a call to the WP-Cron system, and much more.', 'wp-crontrol' ),
781
- 'https://github.com/johnbillion/wp-crontrol/wiki'
782
- );
783
- $content .= '</li>';
784
- $content .= '<li>';
785
- $content .= sprintf(
786
- /* translators: %s: URL to the documentation */
787
- __( '<a href="%s">Read the Frequently Asked Questions (FAQ)</a> which cover many common questions and answers.', 'wp-crontrol' ),
788
- 'https://wordpress.org/plugins/wp-crontrol/faq/'
789
- );
790
- $content .= '</li>';
791
- $content .= '<li>';
792
- $content .= sprintf(
793
- /* translators: %s: URL to the documentation */
794
- __( '<a href="%s">Read the WordPress.org documentation on WP-Cron</a> for more technical details about the WP-Cron system for developers.', 'wp-crontrol' ),
795
- 'https://developer.wordpress.org/plugins/cron/'
796
- );
797
- $content .= '</ul>';
798
-
799
- $screen->add_help_tab(
800
- array(
801
- 'id' => 'crontrol-help',
802
- 'title' => __( 'Help', 'wp-crontrol' ),
803
- 'content' => $content,
804
- )
805
- );
806
- }
807
-
808
- /**
809
- * Adds items to the plugin's action links on the Plugins listing screen.
810
- *
811
- * @param array<string,string> $actions Array of action links.
812
- * @param string $plugin_file Path to the plugin file relative to the plugins directory.
813
- * @param mixed[] $plugin_data An array of plugin data.
814
- * @param string $context The plugin context.
815
- * @return array<string,string> Array of action links.
816
- */
817
- function plugin_action_links( $actions, $plugin_file, $plugin_data, $context ) {
818
- $new = array(
819
- 'crontrol-events' => sprintf(
820
- '<a href="%s">%s</a>',
821
- esc_url( admin_url( 'tools.php?page=crontrol_admin_manage_page' ) ),
822
- esc_html__( 'Events', 'wp-crontrol' )
823
- ),
824
- 'crontrol-schedules' => sprintf(
825
- '<a href="%s">%s</a>',
826
- esc_url( admin_url( 'options-general.php?page=crontrol_admin_options_page' ) ),
827
- esc_html__( 'Schedules', 'wp-crontrol' )
828
- ),
829
- 'crontrol-help' => sprintf(
830
- '<a href="%s">%s</a>',
831
- 'https://github.com/johnbillion/wp-crontrol/wiki',
832
- esc_html__( 'Help', 'wp-crontrol' )
833
- ),
834
- );
835
-
836
- return array_merge( $new, $actions );
837
- }
838
-
839
- /**
840
- * Gives WordPress the plugin's set of cron schedules.
841
- *
842
- * Called by the `cron_schedules` filter.
843
- *
844
- * @param array<string,array<string,(int|string)>> $scheds Array of cron schedule arrays. Usually empty.
845
- * @return array<string,array<string,(int|string)>> Array of modified cron schedule arrays.
846
- */
847
- function filter_cron_schedules( array $scheds ) {
848
- $new_scheds = get_option( 'crontrol_schedules', array() );
849
-
850
- if ( ! is_array( $new_scheds ) ) {
851
- return $scheds;
852
- }
853
-
854
- return array_merge( $new_scheds, $scheds );
855
- }
856
-
857
- /**
858
- * Displays the options page for the plugin.
859
- *
860
- * @return void
861
- */
862
- function admin_options_page() {
863
- $messages = array(
864
- '2' => array(
865
- /* translators: 1: The name of the cron schedule. */
866
- __( 'Deleted the cron schedule %s.', 'wp-crontrol' ),
867
- 'success',
868
- ),
869
- '3' => array(
870
- /* translators: 1: The name of the cron schedule. */
871
- __( 'Added the cron schedule %s.', 'wp-crontrol' ),
872
- 'success',
873
- ),
874
- );
875
- if ( isset( $_GET['crontrol_message'] ) && isset( $_GET['crontrol_name'] ) && isset( $messages[ $_GET['crontrol_message'] ] ) ) {
876
- $hook = wp_unslash( $_GET['crontrol_name'] );
877
- $message = wp_unslash( $_GET['crontrol_message'] );
878
-
879
- printf(
880
- '<div id="crontrol-message" class="notice notice-%1$s is-dismissible"><p>%2$s</p></div>',
881
- esc_attr( $messages[ $message ][1] ),
882
- sprintf(
883
- esc_html( $messages[ $message ][0] ),
884
- '<strong>' . esc_html( $hook ) . '</strong>'
885
- )
886
- );
887
- }
888
-
889
- require_once __DIR__ . '/src/schedule-list-table.php';
890
-
891
- $table = new Schedule_List_Table();
892
-
893
- $table->prepare_items();
894
-
895
- ?>
896
- <div class="wrap">
897
-
898
- <?php do_tabs(); ?>
899
-
900
- <h1><?php esc_html_e( 'Cron Schedules', 'wp-crontrol' ); ?></h1>
901
-
902
- <?php $table->views(); ?>
903
-
904
- <div id="col-container" class="wp-clearfix">
905
- <div id="col-left">
906
- <div class="col-wrap">
907
- <div class="form-wrap">
908
- <h2><?php esc_html_e( 'Add Cron Schedule', 'wp-crontrol' ); ?></h2>
909
- <p><?php esc_html_e( 'Adding a new cron schedule will allow you to schedule events that re-occur at the given interval.', 'wp-crontrol' ); ?></p>
910
- <form method="post" action="options-general.php?page=crontrol_admin_options_page">
911
- <div class="form-field form-required">
912
- <label for="crontrol_schedule_internal_name">
913
- <?php esc_html_e( 'Internal Name', 'wp-crontrol' ); ?>
914
- </label>
915
- <input type="text" value="" id="crontrol_schedule_internal_name" name="crontrol_schedule_internal_name" required/>
916
- </div>
917
- <div class="form-field form-required">
918
- <label for="crontrol_schedule_interval">
919
- <?php esc_html_e( 'Interval (seconds)', 'wp-crontrol' ); ?>
920
- </label>
921
- <input type="number" value="" id="crontrol_schedule_interval" name="crontrol_schedule_interval" min="1" step="1" required/>
922
- </div>
923
- <div class="form-field form-required">
924
- <label for="crontrol_schedule_display_name">
925
- <?php esc_html_e( 'Display Name', 'wp-crontrol' ); ?>
926
- </label>
927
- <input type="text" value="" id="crontrol_schedule_display_name" name="crontrol_schedule_display_name" required/>
928
- </div>
929
- <p class="submit">
930
- <input type="submit" class="button button-primary" value="<?php esc_attr_e( 'Add Cron Schedule', 'wp-crontrol' ); ?>" name="crontrol_new_schedule"/>
931
- </p>
932
- <?php wp_nonce_field( 'crontrol-new-schedule' ); ?>
933
- </form>
934
- </div>
935
- </div>
936
- </div>
937
- <div id="col-right">
938
- <div class="col-wrap">
939
- <?php $table->display(); ?>
940
- </div>
941
- </div>
942
- </div>
943
- <?php
944
- }
945
-
946
- /**
947
- * Clears the doing cron status when an event is unscheduled.
948
- *
949
- * What on earth does this function do, and why?
950
- *
951
- * Good question. The purpose of this function is to prevent other overdue cron events from firing when an event is run
952
- * manually with the "Run Now" action. WP Crontrol works very hard to ensure that when cron event runs manually that it
953
- * runs in the exact same way it would run as part of its schedule - via a properly spawned cron with a queued event in
954
- * place. It does this by queueing an event at time `1` (1 second into 1st January 1970) and then immediately spawning
955
- * cron (see the `Event\run()` function).
956
- *
957
- * The problem this causes is if other events are due then they will all run too, and this isn't desirable because if a
958
- * site has a large number of stuck events due to a problem with the cron runner then it's not desirable for all those
959
- * events to run when another is manually run. This happens because WordPress core will attempt to run all due events
960
- * whenever cron is spawned.
961
- *
962
- * The code in this function prevents multiple events from running by changing the value of the `doing_cron` transient
963
- * when an event gets unscheduled during a manual run, which prevents wp-cron.php from iterating more than one event.
964
- *
965
- * The `pre_unschedule_event` filter is used for this because it's just about the only hook available within this loop.
966
- *
967
- * Refs:
968
- * - https://core.trac.wordpress.org/browser/trunk/src/wp-cron.php?rev=47198&marks=127,141#L122
969
- *
970
- * @param mixed $pre The pre-flight value of the event unschedule short-circuit. Not used.
971
- * @return mixed The unaltered pre-flight value.
972
- */
973
- function maybe_clear_doing_cron( $pre ) {
974
- if ( defined( 'DOING_CRON' ) && DOING_CRON && isset( $_GET['crontrol-single-event'] ) ) {
975
- delete_transient( 'doing_cron' );
976
- }
977
-
978
- return $pre;
979
- }
980
-
981
- /**
982
- * Ajax handler which outputs a hash of the current list of scheduled events.
983
- *
984
- * @return void
985
- */
986
- function ajax_check_events_hash() {
987
- if ( ! current_user_can( 'manage_options' ) ) {
988
- wp_send_json_error( null, 403 );
989
- }
990
-
991
- $data = json_encode( Event\get() );
992
-
993
- if ( false === $data ) {
994
- wp_send_json_error( null, 500 );
995
- }
996
-
997
- wp_send_json_success( md5( $data ) );
998
- }
999
-
1000
- /**
1001
- * Gets the status of WP-Cron functionality on the site by performing a test spawn if necessary. Cached for one hour when all is well.
1002
- *
1003
- * @param bool $cache Whether to use the cached result from previous calls.
1004
- * @return true|WP_Error Boolean true if the cron spawner is working as expected, or a `WP_Error` object if not.
1005
- */
1006
- function test_cron_spawn( $cache = true ) {
1007
- global $wp_version;
1008
-
1009
- $cron_runner_plugins = array(
1010
- '\HM\Cavalcade\Plugin\Job' => 'Cavalcade',
1011
- '\Automattic\WP\Cron_Control\Main' => 'Cron Control',
1012
- '\KMM\KRoN\Core' => 'KMM KRoN',
1013
- );
1014
-
1015
- foreach ( $cron_runner_plugins as $class => $plugin ) {
1016
- if ( class_exists( $class ) ) {
1017
- return new WP_Error( 'crontrol_info', sprintf(
1018
- /* translators: 1: The name of the plugin that controls the running of cron events. */
1019
- __( 'WP-Cron spawning is being managed by the %s plugin.', 'wp-crontrol' ),
1020
- $plugin
1021
- ) );
1022
- }
1023
- }
1024
-
1025
- if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
1026
- return new WP_Error( 'crontrol_info', sprintf(
1027
- /* translators: 1: The name of the PHP constant that is set. */
1028
- __( 'The %s constant is set to true. WP-Cron spawning is disabled.', 'wp-crontrol' ),
1029
- 'DISABLE_WP_CRON'
1030
- ) );
1031
- }
1032
-
1033
- if ( defined( 'ALTERNATE_WP_CRON' ) && ALTERNATE_WP_CRON ) {
1034
- return new WP_Error( 'crontrol_info', sprintf(
1035
- /* translators: 1: The name of the PHP constant that is set. */
1036
- __( 'The %s constant is set to true.', 'wp-crontrol' ),
1037
- 'ALTERNATE_WP_CRON'
1038
- ) );
1039
- }
1040
-
1041
- $cached_status = get_transient( 'crontrol-cron-test-ok' );
1042
-
1043
- if ( $cache && $cached_status ) {
1044
- return true;
1045
- }
1046
-
1047
- $sslverify = version_compare( $wp_version, '4.0', '<' );
1048
- $doing_wp_cron = sprintf( '%.22F', microtime( true ) );
1049
-
1050
- $cron_request = apply_filters( 'cron_request', array(
1051
- 'url' => add_query_arg( 'doing_wp_cron', $doing_wp_cron, site_url( 'wp-cron.php' ) ),
1052
- 'key' => $doing_wp_cron,
1053
- 'args' => array(
1054
- 'timeout' => 3,
1055
- 'blocking' => true,
1056
- 'sslverify' => apply_filters( 'https_local_ssl_verify', $sslverify ),
1057
- ),
1058
- ) );
1059
-
1060
- $cron_request['args']['blocking'] = true;
1061
-
1062
- $result = wp_remote_post( $cron_request['url'], $cron_request['args'] );
1063
-
1064
- if ( is_wp_error( $result ) ) {
1065
- return $result;
1066
- } elseif ( wp_remote_retrieve_response_code( $result ) >= 300 ) {
1067
- return new WP_Error( 'unexpected_http_response_code', sprintf(
1068
- /* translators: 1: The HTTP response code. */
1069
- __( 'Unexpected HTTP response code: %s', 'wp-crontrol' ),
1070
- intval( wp_remote_retrieve_response_code( $result ) )
1071
- ) );
1072
- } else {
1073
- set_transient( 'crontrol-cron-test-ok', 1, 3600 );
1074
- return true;
1075
- }
1076
-
1077
- }
1078
-
1079
- /**
1080
- * Shows the status of WP-Cron functionality on the site. Only displays a message when there's a problem.
1081
- *
1082
- * @param string $tab The tab name.
1083
- * @return void
1084
- */
1085
- function show_cron_status( $tab ) {
1086
- if ( 'UTC' !== date_default_timezone_get() ) {
1087
- ?>
1088
- <div id="crontrol-timezone-warning" class="notice notice-warning">
1089
- <?php
1090
- printf(
1091
- '<p>%1$s</p><p><a href="%2$s">%3$s</a></p>',
1092
- /* translators: %s: Help page URL. */
1093
- esc_html__( 'PHP default timezone is not set to UTC. This may cause issues with cron event timings.', 'wp-crontrol' ),
1094
- 'https://github.com/johnbillion/wp-crontrol/wiki/PHP-default-timezone-is-not-set-to-UTC',
1095
- esc_html__( 'More information', 'wp-crontrol' )
1096
- );
1097
- ?>
1098
- </div>
1099
- <?php
1100
- }
1101
-
1102
- $status = test_cron_spawn();
1103
-
1104
- if ( is_wp_error( $status ) ) {
1105
- if ( 'crontrol_info' === $status->get_error_code() ) {
1106
- ?>
1107
- <div id="crontrol-status-notice" class="notice notice-info">
1108
- <p><?php echo esc_html( $status->get_error_message() ); ?></p>
1109
- </div>
1110
- <?php
1111
- } else {
1112
- ?>
1113
- <div id="crontrol-status-error" class="error">
1114
- <?php
1115
- printf(
1116
- '<p>%1$s</p><p><a href="%2$s">%3$s</a></p>',
1117
- sprintf(
1118
- /* translators: 1: Error message text. */
1119
- esc_html__( 'There was a problem spawning a call to the WP-Cron system on your site. This means WP-Cron events on your site may not work. The problem was: %s', 'wp-crontrol' ),
1120
- '</p><p><strong>' . esc_html( $status->get_error_message() ) . '</strong>'
1121
- ),
1122
- 'https://github.com/johnbillion/wp-crontrol/wiki/Problems-with-spawning-a-call-to-the-WP-Cron-system',
1123
- esc_html__( 'More information', 'wp-crontrol' )
1124
- );
1125
- ?>
1126
- </div>
1127
- <?php
1128
- }
1129
- }
1130
- }
1131
-
1132
- /**
1133
- * Get the display name for the site's timezone.
1134
- *
1135
- * @return string The name and UTC offset for the site's timezone.
1136
- */
1137
- function get_timezone_name() {
1138
- /** @var string */
1139
- $timezone_string = get_option( 'timezone_string', '' );
1140
- $gmt_offset = get_option( 'gmt_offset', 0 );
1141
-
1142
- if ( 'UTC' === $timezone_string || ( empty( $gmt_offset ) && empty( $timezone_string ) ) ) {
1143
- return 'UTC';
1144
- }
1145
-
1146
- if ( '' === $timezone_string ) {
1147
- return get_utc_offset();
1148
- }
1149
-
1150
- return sprintf(
1151
- '%s, %s',
1152
- str_replace( '_', ' ', $timezone_string ),
1153
- get_utc_offset()
1154
- );
1155
- }
1156
-
1157
- /**
1158
- * Returns a display value for a UTC offset.
1159
- *
1160
- * Examples:
1161
- * - UTC
1162
- * - UTC+4
1163
- * - UTC-6
1164
- *
1165
- * @return string The UTC offset display value.
1166
- */
1167
- function get_utc_offset() {
1168
- $offset = get_option( 'gmt_offset', 0 );
1169
-
1170
- if ( empty( $offset ) ) {
1171
- return 'UTC';
1172
- }
1173
-
1174
- if ( 0 <= $offset ) {
1175
- $formatted_offset = '+' . (string) $offset;
1176
- } else {
1177
- $formatted_offset = (string) $offset;
1178
- }
1179
- $formatted_offset = str_replace(
1180
- array( '.25', '.5', '.75' ),
1181
- array( ':15', ':30', ':45' ),
1182
- $formatted_offset
1183
- );
1184
- return 'UTC' . $formatted_offset;
1185
- }
1186
-
1187
- /**
1188
- * Shows the form used to add/edit cron events.
1189
- *
1190
- * @param bool $editing Whether the form is for the event editor.
1191
- * @return void
1192
- */
1193
- function show_cron_form( $editing ) {
1194
- $display_args = '';
1195
- $edit_id = null;
1196
- $existing = false;
1197
-
1198
- if ( $editing && ! empty( $_GET['crontrol_id'] ) ) {
1199
- $edit_id = wp_unslash( $_GET['crontrol_id'] );
1200
-
1201
- foreach ( Event\get() as $event ) {
1202
- if ( $edit_id === $event->hook && intval( $_GET['crontrol_next_run_utc'] ) === $event->time && $event->sig === $_GET['crontrol_sig'] ) {
1203
- $existing = array(
1204
- 'hookname' => $event->hook,
1205
- 'next_run' => $event->time, // UTC
1206
- 'schedule' => ( $event->schedule ? $event->schedule : '_oneoff' ),
1207
- 'sig' => $event->sig,
1208
- 'args' => $event->args,
1209
- );
1210
- break;
1211
- }
1212
- }
1213
-
1214
- if ( empty( $existing ) ) {
1215
- ?>
1216
- <div id="crontrol-event-not-found" class="notice notice-error">
1217
- <?php
1218
- printf(
1219
- '<p>%1$s</p>',
1220
- esc_html__( 'The event you are trying to edit does not exist.', 'wp-crontrol' )
1221
- );
1222
- ?>
1223
- </div>
1224
- <?php
1225
- return;
1226
- }
1227
- }
1228
-
1229
- $is_editing_php = ( $existing && 'crontrol_cron_job' === $existing['hookname'] );
1230
-
1231
- if ( $is_editing_php ) {
1232
- $helper_text = esc_html__( 'Cron events trigger actions in your code. Enter the schedule of the event, as well as the PHP code to execute when the action is triggered.', 'wp-crontrol' );
1233
- } else {
1234
- $helper_text = sprintf(
1235
- /* translators: %s: A file name */
1236
- esc_html__( 'Cron events trigger actions in your code. A cron event needs a corresponding action hook somewhere in code, e.g. the %1$s file in your theme.', 'wp-crontrol' ),
1237
- '<code>functions.php</code>'
1238
- );
1239
- }
1240
-
1241
- if ( is_array( $existing ) ) {
1242
- $other_fields = wp_nonce_field( "crontrol-edit-cron_{$existing['hookname']}_{$existing['sig']}_{$existing['next_run']}", '_wpnonce', true, false );
1243
- $other_fields .= sprintf( '<input name="crontrol_original_hookname" type="hidden" value="%s" />',
1244
- esc_attr( $existing['hookname'] )
1245
- );
1246
- $other_fields .= sprintf( '<input name="crontrol_original_sig" type="hidden" value="%s" />',
1247
- esc_attr( $existing['sig'] )
1248
- );
1249
- $other_fields .= sprintf( '<input name="crontrol_original_next_run_utc" type="hidden" value="%s" />',
1250
- esc_attr( (string) $existing['next_run'] )
1251
- );
1252
- if ( ! empty( $existing['args'] ) ) {
1253
- $display_args = wp_json_encode( $existing['args'] );
1254
-
1255
- if ( false === $display_args ) {
1256
- $display_args = '';
1257
- }
1258
- }
1259
- $button = __( 'Update Event', 'wp-crontrol' );
1260
- $next_run_gmt = gmdate( 'Y-m-d H:i:s', $existing['next_run'] );
1261
- $next_run_date_local = get_date_from_gmt( $next_run_gmt, 'Y-m-d' );
1262
- $next_run_time_local = get_date_from_gmt( $next_run_gmt, 'H:i:s' );
1263
- } else {
1264
- $other_fields = wp_nonce_field( 'crontrol-new-cron', '_wpnonce', true, false );
1265
- $existing = array(
1266
- 'hookname' => '',
1267
- 'args' => array(),
1268
- 'next_run' => 'now', // UTC
1269
- 'schedule' => false,
1270
- );
1271
-
1272
- $button = __( 'Add Event', 'wp-crontrol' );
1273
- $next_run_date_local = '';
1274
- $next_run_time_local = '';
1275
- }
1276
-
1277
- if ( $is_editing_php ) {
1278
- if ( ! isset( $existing['args']['code'] ) ) {
1279
- $existing['args']['code'] = '';
1280
- }
1281
- if ( ! isset( $existing['args']['name'] ) ) {
1282
- $existing['args']['name'] = '';
1283
- }
1284
- }
1285
-
1286
- $can_add_php = current_user_can( 'edit_files' ) && ! $editing;
1287
- $allowed = ( ! $is_editing_php || current_user_can( 'edit_files' ) );
1288
- ?>
1289
- <div id="crontrol_form" class="wrap narrow">
1290
- <?php
1291
- if ( $allowed ) {
1292
- if ( $editing ) {
1293
- $heading = __( 'Edit Cron Event', 'wp-crontrol' );
1294
- } else {
1295
- $heading = __( 'Add Cron Event', 'wp-crontrol' );
1296
- }
1297
-
1298
- do_tabs();
1299
-
1300
- printf(
1301
- '<h1>%s</h1>',
1302
- esc_html( $heading )
1303
- );
1304
- printf(
1305
- '<p>%s</p>',
1306
- // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1307
- $helper_text
1308
- );
1309
- ?>
1310
- <form method="post" action="<?php echo esc_url( admin_url( 'tools.php?page=crontrol_admin_manage_page' ) ); ?>" class="crontrol-edit-event crontrol-edit-event-<?php echo ( $is_editing_php ) ? 'php' : 'standard'; ?>">
1311
- <?php
1312
- // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1313
- echo $other_fields;
1314
- ?>
1315
- <table class="form-table"><tbody>
1316
- <?php
1317
- if ( $editing ) {
1318
- $action = $is_editing_php ? 'edit_php_cron' : 'edit_cron';
1319
- printf(
1320
- '<input type="hidden" name="crontrol_action" value="%s"/>',
1321
- esc_attr( $action )
1322
- );
1323
- } elseif ( $can_add_php ) {
1324
- ?>
1325
- <tr class="hide-if-no-js">
1326
- <th valign="top" scope="row">
1327
- <?php esc_html_e( 'Event Type', 'wp-crontrol' ); ?>
1328
- </th>
1329
- <td>
1330
- <p><label><input type="radio" name="crontrol_action" value="new_cron" checked>Standard cron event</label></p>
1331
- <p><label><input type="radio" name="crontrol_action" value="new_php_cron">PHP cron event</label></p>
1332
- </td>
1333
- </tr>
1334
- <?php
1335
- } else {
1336
- ?>
1337
- <input type="hidden" name="crontrol_action" value="new_cron"/>
1338
- <?php
1339
- }
1340
-
1341
- if ( $is_editing_php || $can_add_php ) {
1342
- ?>
1343
- <tr class="crontrol-event-php">
1344
- <th valign="top" scope="row">
1345
- <label for="crontrol_hookcode">
1346
- <?php esc_html_e( 'PHP Code', 'wp-crontrol' ); ?>
1347
- </label>
1348
- </th>
1349
- <td>
1350
- <p class="description">
1351
- <?php
1352
- printf(
1353
- /* translators: The PHP tag name */
1354
- esc_html__( 'The opening %s tag must not be included.', 'wp-crontrol' ),
1355
- '<code>&lt;?php</code>'
1356
- );
1357
- ?>
1358
- </p>
1359
- <p><textarea class="large-text code" rows="10" cols="50" id="crontrol_hookcode" name="crontrol_hookcode"><?php echo esc_textarea( $editing ? $existing['args']['code'] : '' ); ?></textarea></p>
1360
- <?php do_action( 'crontrol/manage/hookcode', $existing ); ?>
1361
- </td>
1362
- </tr>
1363
- <tr class="crontrol-event-php">
1364
- <th valign="top" scope="row">
1365
- <label for="crontrol_eventname">
1366
- <?php esc_html_e( 'Event Name (optional)', 'wp-crontrol' ); ?>
1367
- </label>
1368
- </th>
1369
- <td>
1370
- <input type="text" class="regular-text" id="crontrol_eventname" name="crontrol_eventname" value="<?php echo esc_attr( $editing ? $existing['args']['name'] : '' ); ?>"/>
1371
- <?php do_action( 'crontrol/manage/eventname', $existing ); ?>
1372
- </td>
1373
- </tr>
1374
- <?php
1375
- }
1376
-
1377
- if ( ! $is_editing_php ) {
1378
- ?>
1379
- <tr class="crontrol-event-standard">
1380
- <th valign="top" scope="row">
1381
- <label for="crontrol_hookname">
1382
- <?php esc_html_e( 'Hook Name', 'wp-crontrol' ); ?>
1383
- </label>
1384
- </th>
1385
- <td>
1386
- <input type="text" autocorrect="off" autocapitalize="off" spellcheck="false" class="regular-text" id="crontrol_hookname" name="crontrol_hookname" value="<?php echo esc_attr( $existing['hookname'] ); ?>" required />
1387
- <?php do_action( 'crontrol/manage/hookname', $existing ); ?>
1388
- </td>
1389
- </tr>
1390
- <tr class="crontrol-event-standard">
1391
- <th valign="top" scope="row">
1392
- <label for="crontrol_args">
1393
- <?php esc_html_e( 'Arguments (optional)', 'wp-crontrol' ); ?>
1394
- </label>
1395
- </th>
1396
- <td>
1397
- <input type="text" autocorrect="off" autocapitalize="off" spellcheck="false" class="regular-text code" id="crontrol_args" name="crontrol_args" value="<?php echo esc_attr( $display_args ); ?>"/>
1398
- <?php do_action( 'crontrol/manage/args', $existing ); ?>
1399
- <p class="description">
1400
- <?php
1401
- printf(
1402
- /* translators: 1, 2, and 3: Example values for an input field. */
1403
- esc_html__( 'Use a JSON encoded array, e.g. %1$s, %2$s, or %3$s', 'wp-crontrol' ),
1404
- '<code>[25]</code>',
1405
- '<code>["asdf"]</code>',
1406
- '<code>["i","want",25,"cakes"]</code>'
1407
- );
1408
- ?>
1409
- </p>
1410
- </td>
1411
- </tr>
1412
- <?php
1413
- }
1414
- ?>
1415
- <tr>
1416
- <th valign="top" scope="row">
1417
- <label for="crontrol_next_run_date_local">
1418
- <?php esc_html_e( 'Next Run', 'wp-crontrol' ); ?>
1419
- </label>
1420
- </th>
1421
- <td>
1422
- <ul>
1423
- <li>
1424
- <label>
1425
- <input type="radio" name="crontrol_next_run_date_local" value="now" checked>
1426
- <?php esc_html_e( 'Now', 'wp-crontrol' ); ?>
1427
- </label>
1428
- </li>
1429
- <li>
1430
- <label>
1431
- <input type="radio" name="crontrol_next_run_date_local" value="+1 day">
1432
- <?php esc_html_e( 'Tomorrow', 'wp-crontrol' ); ?>
1433
- </label>
1434
- </li>
1435
- <li>
1436
- <label>
1437
- <input type="radio" name="crontrol_next_run_date_local" value="custom" id="crontrol_next_run_date_local_custom" <?php checked( $editing ); ?>>
1438
- <?php
1439
- printf(
1440
- /* translators: %s: An input field for specifying a date and time */
1441
- esc_html__( 'At: %s', 'wp-crontrol' ),
1442
- sprintf(
1443
- '<br>
1444
- <input type="date" autocorrect="off" autocapitalize="off" spellcheck="false" name="crontrol_next_run_date_local_custom_date" id="crontrol_next_run_date_local_custom_date" value="%1$s" placeholder="yyyy-mm-dd" pattern="\d{4}-\d{2}-\d{2}" />
1445
- <input type="time" autocorrect="off" autocapitalize="off" spellcheck="false" name="crontrol_next_run_date_local_custom_time" id="crontrol_next_run_date_local_custom_time" value="%2$s" step="1" placeholder="hh:mm:ss" pattern="\d{2}:\d{2}:\d{2}" />',
1446
- esc_attr( $next_run_date_local ),
1447
- esc_attr( $next_run_time_local )
1448
- )
1449
- );
1450
- ?>
1451
- </label>
1452
- </li>
1453
- </ul>
1454
-
1455
- <?php do_action( 'crontrol/manage/next_run', $existing ); ?>
1456
-
1457
- <p class="description">
1458
- <?php
1459
- printf(
1460
- /* translators: %s Timezone name. */
1461
- esc_html__( 'Timezone: %s', 'wp-crontrol' ),
1462
- esc_html( get_timezone_name() )
1463
- );
1464
- ?>
1465
- </p>
1466
- </td>
1467
- </tr>
1468
- <tr>
1469
- <th valign="top" scope="row">
1470
- <label for="crontrol_schedule">
1471
- <?php esc_html_e( 'Recurrence', 'wp-crontrol' ); ?>
1472
- </label>
1473
- </th>
1474
- <td>
1475
- <?php Schedule\dropdown( $existing['schedule'] ); ?>
1476
- <?php do_action( 'crontrol/manage/schedule', $existing ); ?>
1477
- </td>
1478
- </tr>
1479
- </tbody></table>
1480
- <p class="submit">
1481
- <input type="submit" class="button button-primary" value="<?php echo esc_attr( $button ); ?>"/>
1482
- </p>
1483
- </form>
1484
- <?php } else { ?>
1485
- <div class="error inline">
1486
- <p><?php esc_html_e( 'You cannot add, edit, or delete PHP cron events because your user account does not have the ability to edit files.', 'wp-crontrol' ); ?></p>
1487
- </div>
1488
- <?php } ?>
1489
- </div>
1490
- <?php
1491
- }
1492
-
1493
- /**
1494
- * Displays the manage page for the plugin.
1495
- *
1496
- * @return void
1497
- */
1498
- function admin_manage_page() {
1499
- $messages = array(
1500
- '1' => array(
1501
- /* translators: 1: The name of the cron event. */
1502
- __( 'Scheduled the cron event %s to run now. The original event will not be affected.', 'wp-crontrol' ),
1503
- 'success',
1504
- ),
1505
- '2' => array(
1506
- /* translators: 1: The name of the cron event. */
1507
- __( 'Deleted all %s cron events.', 'wp-crontrol' ),
1508
- 'success',
1509
- ),
1510
- '3' => array(
1511
- /* translators: 1: The name of the cron event. */
1512
- __( 'There are no %s cron events to delete.', 'wp-crontrol' ),
1513
- 'info',
1514
- ),
1515
- '4' => array(
1516
- /* translators: 1: The name of the cron event. */
1517
- __( 'Saved the cron event %s.', 'wp-crontrol' ),
1518
- 'success',
1519
- ),
1520
- '5' => array(
1521
- /* translators: 1: The name of the cron event. */
1522
- __( 'Created the cron event %s.', 'wp-crontrol' ),
1523
- 'success',
1524
- ),
1525
- '6' => array(
1526
- /* translators: 1: The name of the cron event. */
1527
- __( 'Deleted the cron event %s.', 'wp-crontrol' ),
1528
- 'success',
1529
- ),
1530
- '7' => array(
1531
- /* translators: 1: The name of the cron event. */
1532
- __( 'Failed to the delete the cron event %s.', 'wp-crontrol' ),
1533
- 'error',
1534
- ),
1535
- '8' => array(
1536
- /* translators: 1: The name of the cron event. */
1537
- __( 'Failed to the execute the cron event %s.', 'wp-crontrol' ),
1538
- 'error',
1539
- ),
1540
- '9' => array(
1541
- __( 'Deleted the selected cron events.', 'wp-crontrol' ),
1542
- 'success',
1543
- ),
1544
- '10' => array(
1545
- /* translators: 1: The name of the cron event. */
1546
- __( 'Failed to save the cron event %s.', 'wp-crontrol' ),
1547
- 'error',
1548
- ),
1549
- 'error' => array(
1550
- __( 'An unknown error occurred.', 'wp-crontrol' ),
1551
- 'error',
1552
- ),
1553
- );
1554
-
1555
- if ( isset( $_GET['crontrol_name'] ) && isset( $_GET['crontrol_message'] ) && isset( $messages[ $_GET['crontrol_message'] ] ) ) {
1556
- $hook = wp_unslash( $_GET['crontrol_name'] );
1557
- $message = wp_unslash( $_GET['crontrol_message'] );
1558
- $link = '';
1559
-
1560
- if ( 'error' === $message ) {
1561
- $error = get_message();
1562
-
1563
- if ( $error ) {
1564
- $messages['error'][0] = $error;
1565
- }
1566
- }
1567
-
1568
- printf(
1569
- '<div id="crontrol-message" class="notice notice-%1$s is-dismissible"><p>%2$s%3$s</p></div>',
1570
- esc_attr( $messages[ $message ][1] ),
1571
- sprintf(
1572
- esc_html( $messages[ $message ][0] ),
1573
- '<strong>' . esc_html( $hook ) . '</strong>'
1574
- ),
1575
- // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1576
- $link
1577
- );
1578
- }
1579
-
1580
- $tabs = get_tab_states();
1581
- $table = Event\get_list_table();
1582
-
1583
- switch ( true ) {
1584
- case $tabs['events']:
1585
- ?>
1586
- <div class="wrap">
1587
- <?php do_tabs(); ?>
1588
-
1589
- <h1 class="wp-heading-inline"><?php esc_html_e( 'Cron Events', 'wp-crontrol' ); ?></h1>
1590
-
1591
- <?php echo '<a href="' . esc_url( admin_url( 'tools.php?page=crontrol_admin_manage_page&crontrol_action=new-cron' ) ) . '" class="page-title-action">' . esc_html__( 'Add New', 'wp-crontrol' ) . '</a>'; ?>
1592
-
1593
- <hr class="wp-header-end">
1594
-
1595
- <?php $table->views(); ?>
1596
-
1597
- <form id="posts-filter" method="get" action="tools.php">
1598
- <input type="hidden" name="page" value="crontrol_admin_manage_page" />
1599
- <?php $table->search_box( __( 'Search Hook Names', 'wp-crontrol' ), 'cron-event' ); ?>
1600
- </form>
1601
-
1602
- <form method="post" action="tools.php?page=crontrol_admin_manage_page">
1603
- <div class="table-responsive">
1604
- <?php $table->display(); ?>
1605
- </div>
1606
- </form>
1607
-
1608
- <p>
1609
- <?php
1610
- echo esc_html( sprintf(
1611
- /* translators: 1: Date and time, 2: Timezone */
1612
- __( 'Site time: %1$s (%2$s)', 'wp-crontrol' ),
1613
- date_i18n( 'Y-m-d H:i:s' ),
1614
- get_timezone_name()
1615
- ) );
1616
- ?>
1617
- </p>
1618
- </div>
1619
- <?php
1620
-
1621
- break;
1622
-
1623
- case $tabs['add-event']:
1624
- show_cron_form( false );
1625
- break;
1626
-
1627
- case $tabs['edit-event']:
1628
- show_cron_form( true );
1629
- break;
1630
-
1631
- }
1632
-
1633
- }
1634
-
1635
- /**
1636
- * Get the states of the various cron-related tabs.
1637
- *
1638
- * @return array<string,bool> Array of states keyed by tab name.
1639
- */
1640
- function get_tab_states() {
1641
- $tabs = array(
1642
- 'events' => ( ! empty( $_GET['page'] ) && 'crontrol_admin_manage_page' === $_GET['page'] && empty( $_GET['crontrol_action'] ) ),
1643
- 'schedules' => ( ! empty( $_GET['page'] ) && 'crontrol_admin_options_page' === $_GET['page'] ),
1644
- 'add-event' => ( ! empty( $_GET['crontrol_action'] ) && 'new-cron' === $_GET['crontrol_action'] ),
1645
- 'edit-event' => ( ! empty( $_GET['crontrol_action'] ) && 'edit-cron' === $_GET['crontrol_action'] ),
1646
- );
1647
-
1648
- $tabs = apply_filters( 'crontrol/tabs', $tabs );
1649
-
1650
- return $tabs;
1651
- }
1652
-
1653
- /**
1654
- * Output the cron-related tabs if we're on a cron-related admin screen.
1655
- *
1656
- * @return void
1657
- */
1658
- function do_tabs() {
1659
- $tabs = get_tab_states();
1660
- $tab = array_filter( $tabs );
1661
-
1662
- if ( ! $tab ) {
1663
- return;
1664
- }
1665
-
1666
- $tab = array_keys( $tab );
1667
- $tab = reset( $tab );
1668
- $links = array(
1669
- 'events' => array(
1670
- 'tools.php?page=crontrol_admin_manage_page',
1671
- __( 'Cron Events', 'wp-crontrol' ),
1672
- ),
1673
- 'schedules' => array(
1674
- 'options-general.php?page=crontrol_admin_options_page',
1675
- __( 'Cron Schedules', 'wp-crontrol' ),
1676
- ),
1677
- );
1678
-
1679
- ?>
1680
- <div id="crontrol-header">
1681
- <nav class="nav-tab-wrapper">
1682
- <?php
1683
- foreach ( $links as $id => $link ) {
1684
- if ( ! empty( $tabs[ $id ] ) ) {
1685
- printf(
1686
- '<a href="%s" class="nav-tab nav-tab-active">%s</a>',
1687
- esc_url( $link[0] ),
1688
- esc_html( $link[1] )
1689
- );
1690
- } else {
1691
- printf(
1692
- '<a href="%s" class="nav-tab">%s</a>',
1693
- esc_url( $link[0] ),
1694
- esc_html( $link[1] )
1695
- );
1696
- }
1697
- }
1698
-
1699
- if ( $tabs['add-event'] ) {
1700
- printf(
1701
- '<span class="nav-tab nav-tab-active">%s</span>',
1702
- esc_html__( 'Add Cron Event', 'wp-crontrol' )
1703
- );
1704
- } elseif ( $tabs['edit-event'] ) {
1705
- printf(
1706
- '<span class="nav-tab nav-tab-active">%s</span>',
1707
- esc_html__( 'Edit Cron Event', 'wp-crontrol' )
1708
- );
1709
- }
1710
- ?>
1711
- </nav>
1712
- <?php
1713
- do_action( 'crontrol/tab-header', $tab, $tabs );
1714
- ?>
1715
- </div>
1716
- <?php
1717
- }
1718
-
1719
- /**
1720
- * Returns an array of the callback functions that are attached to the given hook name.
1721
- *
1722
- * @param string $name The hook name.
1723
- * @return array<int,array<string,mixed>> Array of callbacks attached to the hook.
1724
- * @phpstan-return array<int,array{
1725
- * priority: int,
1726
- * callback: array<string,mixed>,
1727
- * }>
1728
- */
1729
- function get_hook_callbacks( $name ) {
1730
- global $wp_filter;
1731
-
1732
- $actions = array();
1733
-
1734
- if ( isset( $wp_filter[ $name ] ) ) {
1735
- // See http://core.trac.wordpress.org/ticket/17817.
1736
- $action = $wp_filter[ $name ];
1737
-
1738
- /**
1739
- * @var int $priority
1740
- */
1741
- foreach ( $action as $priority => $callbacks ) {
1742
- foreach ( $callbacks as $callback ) {
1743
- $callback = populate_callback( $callback );
1744
-
1745
- $actions[] = array(
1746
- 'priority' => $priority,
1747
- 'callback' => $callback,
1748
- );
1749
- }
1750
- }
1751
- }
1752
-
1753
- return $actions;
1754
- }
1755
-
1756
- /**
1757
- * Populates the details of the given callback function.
1758
- *
1759
- * @param array<string,mixed> $callback A callback entry.
1760
- * @phpstan-param array{
1761
- * function: string|array<int,mixed>|object,
1762
- * accepted_args: int,
1763
- * } $callback
1764
- * @return array<string,mixed> The updated callback entry.
1765
- */
1766
- function populate_callback( array $callback ) {
1767
- // If Query Monitor is installed, use its rich callback analysis.
1768
- if ( method_exists( '\QM_Util', 'populate_callback' ) ) {
1769
- return \QM_Util::populate_callback( $callback );
1770
- }
1771
-
1772
- if ( is_string( $callback['function'] ) && ( false !== strpos( $callback['function'], '::' ) ) ) {
1773
- $callback['function'] = explode( '::', $callback['function'] );
1774
- }
1775
-
1776
- if ( is_array( $callback['function'] ) ) {
1777
- if ( is_object( $callback['function'][0] ) ) {
1778
- $class = get_class( $callback['function'][0] );
1779
- $access = '->';
1780
- } else {
1781
- $class = $callback['function'][0];
1782
- $access = '::';
1783
- }
1784
-
1785
- $callback['name'] = $class . $access . $callback['function'][1] . '()';
1786
- } elseif ( is_object( $callback['function'] ) ) {
1787
- if ( is_a( $callback['function'], 'Closure' ) ) {
1788
- $callback['name'] = 'Closure';
1789
- } else {
1790
- $class = get_class( $callback['function'] );
1791
-
1792
- $callback['name'] = $class . '->__invoke()';
1793
- }
1794
- } else {
1795
- $callback['name'] = $callback['function'] . '()';
1796
- }
1797
-
1798
- if ( ! method_exists( '\QM_Util', 'populate_callback' ) && ! is_callable( $callback['function'] ) ) {
1799
- $callback['error'] = new WP_Error(
1800
- 'not_callable',
1801
- sprintf(
1802
- /* translators: %s: Function name */
1803
- __( 'Function %s does not exist', 'wp-crontrol' ),
1804
- $callback['name']
1805
- )
1806
- );
1807
- }
1808
-
1809
- return $callback;
1810
- }
1811
-
1812
- /**
1813
- * Returns a user-friendly representation of the callback function.
1814
- *
1815
- * @param mixed[] $callback The callback entry.
1816
- * @return string The displayable version of the callback name.
1817
- */
1818
- function output_callback( array $callback ) {
1819
- $qm = WP_PLUGIN_DIR . '/query-monitor/query-monitor.php';
1820
- $html = plugin_dir_path( $qm ) . 'output/Html.php';
1821
-
1822
- if ( ! empty( $callback['callback']['error'] ) ) {
1823
- $return = '<code>' . $callback['callback']['name'] . '</code>';
1824
- $return .= '<br><span class="status-crontrol-error"><span class="dashicons dashicons-warning" aria-hidden="true"></span> ';
1825
- $return .= esc_html( $callback['callback']['error']->get_error_message() );
1826
- $return .= '</span>';
1827
- return $return;
1828
- }
1829
-
1830
- // If Query Monitor is installed, use its rich callback output.
1831
- if ( class_exists( '\QueryMonitor' ) && file_exists( $html ) ) {
1832
- require_once $html;
1833
-
1834
- if ( class_exists( '\QM_Output_Html' ) ) {
1835
- return \QM_Output_Html::output_filename(
1836
- $callback['callback']['name'],
1837
- $callback['callback']['file'],
1838
- $callback['callback']['line']
1839
- );
1840
- }
1841
- }
1842
-
1843
- return '<code>' . $callback['callback']['name'] . '</code>';
1844
- }
1845
-
1846
- /**
1847
- * Pretty-prints the difference in two times.
1848
- *
1849
- * @param int $older_date Unix timestamp.
1850
- * @param int $newer_date Unix timestamp.
1851
- * @return string The pretty time_since value
1852
- * @link http://binarybonsai.com/code/timesince.txt
1853
- */
1854
- function time_since( $older_date, $newer_date ) {
1855
- return interval( $newer_date - $older_date );
1856
- }
1857
-
1858
- /**
1859
- * Converts a period of time in seconds into a human-readable format representing the interval.
1860
- *
1861
- * Example:
1862
- *
1863
- * echo \Crontrol\interval( 90 );
1864
- * // 1 minute 30 seconds
1865
- *
1866
- * @param int|float $since A period of time in seconds.
1867
- * @return string An interval represented as a string.
1868
- */
1869
- function interval( $since ) {
1870
- // Array of time period chunks.
1871
- $chunks = array(
1872
- /* translators: 1: The number of years in an interval of time. */
1873
- array( 60 * 60 * 24 * 365, _n_noop( '%s year', '%s years', 'wp-crontrol' ) ),
1874
- /* translators: 1: The number of months in an interval of time. */
1875
- array( 60 * 60 * 24 * 30, _n_noop( '%s month', '%s months', 'wp-crontrol' ) ),
1876
- /* translators: 1: The number of weeks in an interval of time. */
1877
- array( 60 * 60 * 24 * 7, _n_noop( '%s week', '%s weeks', 'wp-crontrol' ) ),
1878
- /* translators: 1: The number of days in an interval of time. */
1879
- array( 60 * 60 * 24, _n_noop( '%s day', '%s days', 'wp-crontrol' ) ),
1880
- /* translators: 1: The number of hours in an interval of time. */
1881
- array( 60 * 60, _n_noop( '%s hour', '%s hours', 'wp-crontrol' ) ),
1882
- /* translators: 1: The number of minutes in an interval of time. */
1883
- array( 60, _n_noop( '%s minute', '%s minutes', 'wp-crontrol' ) ),
1884
- /* translators: 1: The number of seconds in an interval of time. */
1885
- array( 1, _n_noop( '%s second', '%s seconds', 'wp-crontrol' ) ),
1886
- );
1887
-
1888
- if ( $since <= 0 ) {
1889
- return __( 'now', 'wp-crontrol' );
1890
- }
1891
-
1892
- /**
1893
- * We only want to output two chunks of time here, eg:
1894
- * x years, xx months
1895
- * x days, xx hours
1896
- * so there's only two bits of calculation below:
1897
- */
1898
-
1899
- // Step one: the first chunk.
1900
- foreach ( array_keys( $chunks ) as $i ) {
1901
- $seconds = $chunks[ $i ][0];
1902
- $name = $chunks[ $i ][1];
1903
-
1904
- // Finding the biggest chunk (if the chunk fits, break).
1905
- $count = (int) floor( $since / $seconds );
1906
- if ( $count ) {
1907
- break;
1908
- }
1909
- }
1910
-
1911
- // Set output var.
1912
- $output = sprintf( translate_nooped_plural( $name, $count, 'wp-crontrol' ), $count );
1913
-
1914
- // Step two: the second chunk.
1915
- if ( $i + 1 < count( $chunks ) ) {
1916
- $seconds2 = $chunks[ $i + 1 ][0];
1917
- $name2 = $chunks[ $i + 1 ][1];
1918
- $count2 = (int) floor( ( $since - ( $seconds * $count ) ) / $seconds2 );
1919
- if ( $count2 ) {
1920
- // Add to output var.
1921
- $output .= ' ' . sprintf( translate_nooped_plural( $name2, $count2, 'wp-crontrol' ), $count2 );
1922
- }
1923
- }
1924
-
1925
- return $output;
1926
- }
1927
-
1928
- /**
1929
- * Sets up the Events listing screen.
1930
- *
1931
- * @return void
1932
- */
1933
- function setup_manage_page() {
1934
- // Initialise the list table
1935
- Event\get_list_table();
1936
-
1937
- // Add the initially hidden admin notice about the out of date events list
1938
- add_action( 'admin_notices', function() {
1939
- printf(
1940
- '<div id="crontrol-hash-message" class="notice notice-info"><p>%s</p></div>',
1941
- esc_html__( 'The scheduled cron events have changed since you first opened this page. Reload the page to see the up to date list.', 'wp-crontrol' )
1942
- );
1943
- } );
1944
- }
1945
-
1946
- /**
1947
- * Registers the stylesheet and JavaScript for the admin areas.
1948
- *
1949
- * @param string $hook_suffix The admin screen ID.
1950
- * @return void
1951
- */
1952
- function enqueue_assets( $hook_suffix ) {
1953
- $tab = get_tab_states();
1954
-
1955
- if ( ! array_filter( $tab ) ) {
1956
- return;
1957
- }
1958
-
1959
- $ver = (string) filemtime( plugin_dir_path( __FILE__ ) . 'css/wp-crontrol.css' );
1960
- wp_enqueue_style( 'wp-crontrol', plugin_dir_url( __FILE__ ) . 'css/wp-crontrol.css', array( 'dashicons' ), $ver );
1961
-
1962
- $ver = (string) filemtime( plugin_dir_path( __FILE__ ) . 'js/wp-crontrol.js' );
1963
- wp_enqueue_script( 'wp-crontrol', plugin_dir_url( __FILE__ ) . 'js/wp-crontrol.js', array( 'jquery', 'wp-a11y' ), $ver, true );
1964
-
1965
- $vars = array();
1966
-
1967
- if ( ! empty( $tab['events'] ) ) {
1968
- $data = json_encode( Event\get() );
1969
-
1970
- if ( false !== $data ) {
1971
- $vars['eventsHash'] = md5( $data );
1972
- $vars['eventsHashInterval'] = 20;
1973
- }
1974
- }
1975
-
1976
- if ( ! empty( $tab['add-event'] ) || ! empty( $tab['edit-event'] ) ) {
1977
- if ( function_exists( 'wp_enqueue_code_editor' ) && current_user_can( 'edit_files' ) ) {
1978
- $settings = wp_enqueue_code_editor( array(
1979
- 'type' => 'text/x-php',
1980
- ) );
1981
-
1982
- if ( false !== $settings ) {
1983
- $vars['codeEditor'] = $settings;
1984
- }
1985
- }
1986
- }
1987
-
1988
- wp_localize_script( 'wp-crontrol', 'wpCrontrol', $vars );
1989
- }
1990
-
1991
- /**
1992
- * Filters the list of query arguments which get removed from admin area URLs in WordPress.
1993
- *
1994
- * @param array<int,string> $args List of removable query arguments.
1995
- * @return array<int,string> Updated list of removable query arguments.
1996
- */
1997
- function filter_removable_query_args( array $args ) {
1998
- return array_merge( $args, array(
1999
- 'crontrol_message',
2000
- 'crontrol_name',
2001
- ) );
2002
- }
2003
-
2004
- /**
2005
- * Returns an array of cron event hooks that are persistently added by WordPress core.
2006
- *
2007
- * @return array<int,string> Array of hook names.
2008
- */
2009
- function get_persistent_core_hooks() {
2010
- return array(
2011
- 'wp_update_plugins', // 2.7.0
2012
- 'wp_update_themes', // 2.7.0
2013
- 'wp_version_check', // 2.7.0
2014
- 'wp_scheduled_delete', // 2.9.0
2015
- 'update_network_counts', // 3.1.0
2016
- 'wp_scheduled_auto_draft_delete', // 3.4.0
2017
- 'delete_expired_transients', // 4.9.0
2018
- 'wp_privacy_delete_old_export_files', // 4.9.6
2019
- 'recovery_mode_clean_expired_keys', // 5.2.0
2020
- 'wp_site_health_scheduled_check', // 5.4.0
2021
- 'wp_https_detection', // 5.7.0
2022
- 'wp_update_user_counts', // 6.0.0
2023
- );
2024
- }
2025
-
2026
- /**
2027
- * Returns an array of all cron event hooks that are added by WordPress core.
2028
- *
2029
- * @return array<int,string> Array of hook names.
2030
- */
2031
- function get_all_core_hooks() {
2032
- return array_merge(
2033
- get_persistent_core_hooks(),
2034
- array(
2035
- 'do_pings', // 2.1.0
2036
- 'publish_future_post', // 2.1.0
2037
- 'importer_scheduled_cleanup', // 2.5.0
2038
- 'upgrader_scheduled_cleanup', // 3.2.2
2039
- 'wp_maybe_auto_update', // 3.7.0
2040
- 'wp_split_shared_term_batch', // 4.3.0
2041
- 'wp_update_comment_type_batch', // 5.5.0
2042
- 'wp_delete_temp_updater_backups', // 5.9.0
2043
- )
2044
- );
2045
- }
2046
-
2047
- /**
2048
- * Returns an array of cron schedules that are added by WordPress core.
2049
- *
2050
- * @return array<int,string> Array of schedule names.
2051
- */
2052
- function get_core_schedules() {
2053
- return array(
2054
- 'hourly',
2055
- 'twicedaily',
2056
- 'daily',
2057
- 'weekly',
2058
- );
2059
- }
2060
-
2061
- /**
2062
- * Encodes some input as JSON for output.
2063
- *
2064
- * @param mixed $input The input.
2065
- * @param bool $pretty Whether to pretty print the output. Default true.
2066
- * @return string The JSON-encoded output.
2067
- */
2068
- function json_output( $input, $pretty = true ) {
2069
- $json_options = 0;
2070
-
2071
- if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
2072
- // phpcs:ignore PHPCompatibility.Constants.NewConstants.json_unescaped_slashesFound
2073
- $json_options |= JSON_UNESCAPED_SLASHES;
2074
- }
2075
-
2076
- if ( $pretty && defined( 'JSON_PRETTY_PRINT' ) ) {
2077
- $json_options |= JSON_PRETTY_PRINT;
2078
- }
2079
-
2080
- $output = wp_json_encode( $input, $json_options );
2081
-
2082
- if ( false === $output ) {
2083
- $output = '';
2084
- }
2085
-
2086
- return $output;
2087
- }
2088
-
2089
- /**
2090
- * Evaluates the code in a PHP cron event using eval.
2091
- *
2092
- * Security: Only users with the `edit_files` capability can manage PHP cron events. This means if a user cannot edit
2093
- * files on the site (eg. through the Plugin Editor or Theme Editor) then they cannot edit or add a PHP cron event. By
2094
- * default, only Administrators have this capability, and with Multisite enabled only Super Admins have this capability.
2095
- *
2096
- * If file editing has been disabled via the `DISALLOW_FILE_MODS` or `DISALLOW_FILE_EDIT` configuration constants then
2097
- * no user will have the `edit_files` capability, which means editing or adding a PHP cron event will not be permitted.
2098
- *
2099
- * Therefore, the user access level required to execute arbitrary PHP code does not change with WP Crontrol activated.
2100
- *
2101
- * @param string $code The PHP code to evaluate.
2102
- * @return void
2103
- */
2104
- function action_php_cron_event( $code ) {
2105
- // phpcs:ignore Squiz.PHP.Eval.Discouraged
2106
- eval( $code );
2107
- }
2108
 
2109
  // Get this show on the road.
2110
  init_hooks();
5
  * Description: WP Crontrol enables you to view and control what's happening in the WP-Cron system.
6
  * Author: John Blackbourn & crontributors
7
  * Author URI: https://github.com/johnbillion/wp-crontrol/graphs/contributors
8
+ * Version: 1.15.0
9
  * Text Domain: wp-crontrol
10
  * Domain Path: /languages/
11
+ * Requires PHP: 5.6
12
  * License: GPL v2 or later
13
  *
14
  * LICENSE
29
  * @copyright Copyright 2008 Edward Dale, 2012-2022 John Blackbourn
30
  * @license http://www.gnu.org/licenses/gpl.txt GPL 2.0
31
  * @link https://wordpress.org/plugins/wp-crontrol/
 
32
  */
33
 
34
  namespace Crontrol;
35
 
36
+ const PLUGIN_FILE = __FILE__;
 
 
37
 
38
  if ( ! defined( 'ABSPATH' ) ) {
39
  exit;
40
  }
41
 
42
+ if ( ! version_compare( PHP_VERSION, '5.6', '>=' ) ) {
43
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
 
46
+ $autoload = __DIR__ . '/vendor/autoload.php';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
+ if ( ! file_exists( $autoload ) ) {
49
+ return;
50
  }
51
 
52
+ require_once $autoload;
53
+ require_once __DIR__ . '/src/bootstrap.php';
54
+ require_once __DIR__ . '/src/event.php';
55
+ require_once __DIR__ . '/src/schedule.php';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
  // Get this show on the road.
58
  init_hooks();