-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathAcronPlugin.php
380 lines (330 loc) · 12.8 KB
/
AcronPlugin.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
<?php
/**
* @file AcronPlugin.php
*
* Copyright (c) 2013-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class AcronPlugin
*
* @brief Removes dependency on 'cron' for scheduled tasks, including
* possible tasks defined by plugins. See the AcronPlugin::parseCrontab
* hook implementation.
*/
namespace APP\plugins\generic\acron;
use APP\core\Application;
use APP\notification\NotificationManager;
use Closure;
use Illuminate\Support\Facades\Event;
use PKP\config\Config;
use PKP\core\JSONMessage;
use PKP\core\PKPPageRouter;
use PKP\db\DAORegistry;
use PKP\linkAction\LinkAction;
use PKP\linkAction\request\AjaxAction;
use PKP\notification\Notification;
use PKP\observers\events\PluginSettingChanged;
use PKP\plugins\GenericPlugin;
use PKP\plugins\Hook;
use PKP\plugins\PluginRegistry;
use PKP\scheduledTask\ScheduledTask;
use PKP\scheduledTask\ScheduledTaskDAO;
use PKP\scheduledTask\ScheduledTaskHelper;
use PKP\xml\PKPXMLParser;
use PKP\xml\XMLNode;
use ReflectionFunction;
// TODO: Error handling. If a scheduled task encounters an error...?
class AcronPlugin extends GenericPlugin
{
private array $_tasksToRun;
/**
* @copydoc Plugin::register()
*
* @param null|mixed $mainContextId
*/
public function register($category, $path, $mainContextId = null): bool
{
$success = parent::register($category, $path, $mainContextId);
// Hook::add('Installer::postInstall', fn (string $hookName, array $args) => $this->_callbackPostInstall($hookName, $args));
if (Application::isUnderMaintenance()) {
return $success;
}
if ($success) {
// $this->addLocaleData();
// Hook::add('LoadHandler', fn (string $hookName, array $args) => $this->_callbackLoadHandler($hookName, $args));
// // Reload cron tab when a plugin is enabled/disabled
// Event::listen(PluginSettingChanged::class, fn (PluginSettingChanged $event) => $this->_callbackManage($event));
}
return $success;
}
/**
* @copydoc Plugin::isSitePlugin()
*/
public function isSitePlugin(): bool
{
// This is a site-wide plugin.
return true;
}
/**
* @copydoc LazyLoadPlugin::getName()
*/
public function getName(): string
{
return 'acronPlugin';
}
/**
* @copydoc Plugin::getDisplayName()
*/
public function getDisplayName(): string
{
return __('plugins.generic.acron.name');
}
/**
* @copydoc Plugin::getDescription()
*/
public function getDescription(): string
{
return __('plugins.generic.acron.description');
}
/**
* @copydoc Plugin::getInstallSitePluginSettingsFile()
*/
public function getInstallSitePluginSettingsFile(): string
{
return "{$this->getPluginPath()}/settings.xml";
}
/**
* @copydoc Plugin::getActions()
*/
public function getActions($request, $actionArgs): array
{
$router = $request->getRouter();
$actions = parent::getActions($request, $actionArgs);
if ($this->getEnabled()) {
$url = $router->url($request, null, null, 'manage', null, ['verb' => 'reload', 'plugin' => $this->getName(), 'category' => 'generic']);
array_unshift($actions, new LinkAction('reload', new AjaxAction($url), __('plugins.generic.acron.reload')));
}
return $actions;
}
/**
* @see Plugin::manage()
*/
public function manage($args, $request): JSONMessage
{
if ($request->getUserVar('verb') !== 'reload') {
return parent::manage($args, $request);
}
$this->_parseCrontab();
$notificationManager = new NotificationManager();
$user = $request->getUser();
$notificationManager->createTrivialNotification(
$user->getId(),
Notification::NOTIFICATION_TYPE_SUCCESS,
['contents' => __('plugins.generic.acron.tasksReloaded')]
);
return \PKP\db\DAO::getDataChangedEvent();
}
/**
* Post install hook to flag cron tab reload on every install/upgrade.
*
* @see Installer::postInstall() for the hook call.
*/
private function _callbackPostInstall(string $hookName, array $args): bool
{
$this->_parseCrontab();
return false;
}
/**
* Load handler hook to check for tasks to run.
*
* @see PKPPageRouter::loadHandler() for the hook call.
*/
private function _callbackLoadHandler(string $hookName, array $args): bool
{
$request = Application::get()->getRequest();
$router = $request->getRouter();
// Avoid controllers requests because of the shutdown function usage.
if (!($router instanceof PKPPageRouter)) {
return false;
}
// Application is set to sandbox mode and will not run any schedule tasks
if (Config::getVar('general', 'sandbox', false)) {
error_log('Application is set to sandbox mode and will not run any schedule tasks');
return false;
}
$tasksToRun = $this->_getTasksToRun();
if (empty($tasksToRun)) {
return false;
}
// Save the current working directory, so we can fix
// it inside the shutdown function.
$workingDir = getcwd();
// Save the tasks to be executed.
$this->_tasksToRun = $tasksToRun;
// Need output buffering to send a finish message
// to browser inside the shutdown function. Couldn't
// do without the buffer.
ob_start();
// This callback will be used as soon as the main script
// is finished. It will not stop running, even if the user cancels
// the request or the time limit is reach.
register_shutdown_function(fn () => $this->_shutdownFunction($workingDir));
return false;
}
/**
* Synchronize crontab with lazy load plugins management.
*
* @see PluginHandler::plugin() for the hook call.
*/
private function _callbackManage(PluginSettingChanged $event): bool
{
if ($event->settingName !== 'enabled') {
return false;
}
// Check if the plugin wants to add its own scheduled task into the cron tab.
foreach (Hook::getHooks('AcronPlugin::parseCronTab') ?? [] as $hookPriorityList) {
foreach ($hookPriorityList as $callback) {
$reflection = new ReflectionFunction(Closure::fromCallable($callback));
if ($reflection->getClosureThis() === $event->plugin) {
$this->_parseCrontab();
break 2;
}
}
}
return false;
}
/**
* Shutdown callback.
*/
private function _shutdownFunction(string $workingDir): void
{
// Release requests from waiting the processing.
header('Connection: close');
// This header is needed so avoid using any kind of compression. If zlib is
// enabled, for example, the buffer will not output until the end of the
// script execution.
header('Content-Encoding: none');
header('Content-Length: ' . ob_get_length());
ob_end_flush();
flush();
set_time_limit(0);
// Fix the current working directory. See
// http://www.php.net/manual/en/function.register-shutdown-function.php#92657
chdir($workingDir);
/** @var ScheduledTaskDAO */
$taskDao = DAORegistry::getDAO('ScheduledTaskDAO');
foreach ($this->_tasksToRun as $task) {
$className = $task['className'];
$taskArgs = $task['args'] ?? [];
// There's a race here. Several requests may come in closely spaced.
// Each may decide it's time to run scheduled tasks, and more than one
// can happily go ahead and do it before the "last run" time is updated.
// By updating the last run time as soon as feasible, we can minimize
// the race window. See bug #8737.
$tasksToRun = $this->_getTasksToRun();
$updateResult = 0;
if (in_array($task, $tasksToRun, true)) {
$updateResult = $taskDao->updateLastRunTime($className, time());
}
if ($updateResult === false || $updateResult === 1) {
// DB doesn't support the get affected rows used inside update method, or one row was updated when we introduced a new last run time.
// Load and execute the task.
//
if (preg_match('/^[a-zA-Z0-9_.]+$/', $className)) {
// DEPRECATED as of 3.4.0: Use old class.name.style and import() function (pre-PSR classloading) pkp/pkp-lib#8186
// Strip off the package name(s) to get the base class name
$pos = strrpos($className, '.');
$baseClassName = $pos === false ? $className : substr($className, $pos + 1);
import($className);
$task = new $baseClassName($taskArgs);
} else {
$task = new $className($taskArgs);
if (!$task instanceof ScheduledTask) {
throw new \Exception("Scheduled task {$className} was an unexpected class!");
}
}
$task->execute();
}
}
}
/**
* Parse all scheduled tasks files and
* save the result object in database.
*/
private function _parseCrontab(): void
{
$xmlParser = new PKPXMLParser();
$taskFilesPath = [];
// Load all plugins so any plugin can register a crontab.
PluginRegistry::loadAllPlugins();
// Let plugins register their scheduled tasks too.
Hook::call('AcronPlugin::parseCronTab', [&$taskFilesPath]); // Reference needed.
// Add the default tasks file.
$taskFilesPath[] = 'registry/scheduledTasks.xml'; // TODO: make this a plugin setting, rather than assuming.
$tasks = [];
foreach ($taskFilesPath as $filePath) {
$tree = $xmlParser->parse($filePath);
if (!$tree) {
throw new \Exception('Error parsing scheduled tasks XML file: ' . $filePath);
}
foreach ($tree->getChildren() as $task) {
$frequency = $task->getChildByName('frequency');
$args = ScheduledTaskHelper::getTaskArgs($task);
// Tasks without a frequency defined, or defined to zero, will run on every request.
// To avoid that happening (may cause performance problems) we
// setup a default period of time.
$setDefaultFrequency = true;
$minHoursRunPeriod = 24;
if ($frequency) {
$frequencyAttributes = $frequency->getAttributes();
if (is_array($frequencyAttributes)) {
foreach ($frequencyAttributes as $value) {
if ($value != 0) {
$setDefaultFrequency = false;
break;
}
}
}
}
$tasks[] = [
'className' => $task->getAttribute('class'),
'frequency' => $setDefaultFrequency ? ['hour' => $minHoursRunPeriod] : $frequencyAttributes,
'args' => $args
];
}
}
// Store the object.
$this->updateSetting(Application::SITE_CONTEXT_ID, 'crontab', $tasks, 'object');
}
/**
* Get all scheduled tasks that needs to be executed.
*/
private function _getTasksToRun(): array
{
$isEnabled = $this->getSetting(Application::SITE_CONTEXT_ID, 'enabled');
if (!$isEnabled) {
return [];
}
$tasksToRun = [];
// Grab the scheduled scheduled tree
$scheduledTasks = $this->getSetting(Application::SITE_CONTEXT_ID, 'crontab');
if (is_null($scheduledTasks)) {
$this->_parseCrontab();
$scheduledTasks = $this->getSetting(Application::SITE_CONTEXT_ID, 'crontab');
}
foreach ($scheduledTasks as $task) {
// We don't allow tasks without frequency, see _parseCronTab().
$frequency = new XMLNode();
$frequency->setAttribute(key($task['frequency']), current($task['frequency']));
$canExecute = ScheduledTaskHelper::checkFrequency($task['className'], $frequency);
if ($canExecute) {
$tasksToRun[] = $task;
}
}
return $tasksToRun;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\plugins\generic\acron\AcronPlugin', '\AcronPlugin');
}