Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 111
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
MaintenanceController
0.00% covered (danger)
0.00%
0 / 111
0.00% covered (danger)
0.00%
0 / 10
812
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 homeAction
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getScripts
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 scriptAction
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 clearcacheAction
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 deleteexpiredsearchesAction
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 deleteexpiredsessionsAction
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 updatebrowscapcacheAction
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 expire
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 updateBrowscapCache
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
110
1<?php
2
3/**
4 * Admin Maintenance Controller
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  Controller
25 * @author   Demian Katz <demian.katz@villanova.edu>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org Main Site
28 */
29
30namespace VuFindAdmin\Controller;
31
32use DateTime;
33use Laminas\Cache\Psr\SimpleCache\SimpleCacheDecorator;
34use Laminas\Log\LoggerInterface;
35use Laminas\ServiceManager\ServiceLocatorInterface;
36use VuFind\Cache\Manager as CacheManager;
37use VuFind\Db\Service\Feature\DeleteExpiredInterface;
38use VuFind\Db\Service\SearchServiceInterface;
39use VuFind\Db\Service\SessionServiceInterface;
40use VuFind\Http\GuzzleService;
41
42use function ini_get;
43use function intval;
44
45/**
46 * Class helps maintain database
47 *
48 * @category VuFind
49 * @package  Controller
50 * @author   Demian Katz <demian.katz@villanova.edu>
51 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
52 * @link     https://vufind.org Main Site
53 */
54class MaintenanceController extends AbstractAdmin
55{
56    /**
57     * Cache manager
58     *
59     * @var CacheManager
60     */
61    protected $cacheManager;
62
63    /**
64     * Guzzle service
65     *
66     * @var GuzzleService
67     */
68    protected $guzzleService;
69
70    /**
71     * Logger
72     *
73     * @var LoggerInterface
74     */
75    protected $logger;
76
77    /**
78     * Constructor
79     *
80     * @param ServiceLocatorInterface $sm            Service locator
81     * @param CacheManager            $cacheManager  Cache manager
82     * @param GuzzleService           $guzzleService Guzzle service
83     * @param LoggerInterface         $logger        Logger
84     */
85    public function __construct(
86        ServiceLocatorInterface $sm,
87        CacheManager $cacheManager,
88        GuzzleService $guzzleService,
89        LoggerInterface $logger
90    ) {
91        parent::__construct($sm);
92        $this->cacheManager = $cacheManager;
93        $this->guzzleService = $guzzleService;
94        $this->logger = $logger;
95    }
96
97    /**
98     * System Maintenance
99     *
100     * @return \Laminas\View\Model\ViewModel
101     */
102    public function homeAction()
103    {
104        $view = $this->createViewModel();
105        $cacheManager = $this->serviceLocator->get(\VuFind\Cache\Manager::class);
106        $view->caches = $cacheManager->getCacheList();
107        $view->nonPersistentCaches = $cacheManager->getNonPersistentCacheList();
108        $view->scripts = $this->getScripts();
109        $view->setTemplate('admin/maintenance/home');
110        return $view;
111    }
112
113    /**
114     * Get a list of the names of scripts available to run through the admin panel.
115     *
116     * @return array
117     */
118    protected function getScripts(): array
119    {
120        // Load the AdminScripts.ini settings
121        $config = $this->serviceLocator->get(\VuFind\Config\PluginManager::class)
122            ->get('AdminScripts')->toArray();
123        $globalConfig = $config['Global'] ?? [];
124        unset($config['Global']);
125
126        // Filter out any commands that the current user does not have permission to run:
127        $permission = $this->permission();
128        $filter = function ($script) use ($permission, $globalConfig) {
129            $requiredPermission = $script['permission'] ?? $globalConfig['defaultPermission'] ?? null;
130            return empty($requiredPermission) || $permission->isAuthorized($requiredPermission);
131        };
132        return array_filter($config, $filter);
133    }
134
135    /**
136     * Run script action.
137     *
138     * @return mixed
139     */
140    public function scriptAction()
141    {
142        $script = $this->params()->fromRoute('name');
143        $scripts = $this->getScripts();
144        $details = $scripts[$script] ?? null;
145        if (empty($details['command'])) {
146            $this->flashMessenger()->addErrorMessage('Unknown command: ' . $script);
147        } else {
148            $code = $output = null;
149            exec($details['command'], $output, $code);
150            $successCode = intval($details['successCode'] ?? 0);
151            if ($code !== $successCode) {
152                $this->flashMessenger()->addErrorMessage(
153                    "Command failed; expected $successCode but received $code"
154                );
155            } else {
156                $this->flashMessenger()->addSuccessMessage(
157                    "Success ($script)! Output = " . implode("\n", $output)
158                );
159            }
160        }
161        return $this->redirect()->toRoute('admin/maintenance');
162    }
163
164    /**
165     * Clear cache(s).
166     *
167     * @return mixed
168     */
169    public function clearcacheAction()
170    {
171        $cache = null;
172        $cacheManager = $this->serviceLocator->get(\VuFind\Cache\Manager::class);
173        foreach ($this->params()->fromQuery('cache', []) as $cache) {
174            $cacheManager->getCache($cache)->flush();
175        }
176        // If cache is unset, we didn't go through the loop above, so no message
177        // needs to be displayed.
178        if (isset($cache)) {
179            $this->flashMessenger()->addSuccessMessage('Cache(s) cleared.');
180        }
181        return $this->forwardTo('AdminMaintenance', 'Home');
182    }
183
184    /**
185     * Delete expired searches.
186     *
187     * @return mixed
188     */
189    public function deleteexpiredsearchesAction()
190    {
191        // Delete the expired searches--this cleans up any junk left in the
192        // database from old search histories that were not caught by the
193        // session garbage collector.
194        return $this->expire(
195            SearchServiceInterface::class,
196            '%%count%% expired searches deleted.',
197            'No expired searches to delete.'
198        );
199    }
200
201    /**
202     * Delete expired sessions.
203     *
204     * @return mixed
205     */
206    public function deleteexpiredsessionsAction()
207    {
208        // Delete the expired sessions--this cleans up any junk left in the
209        // database by the session garbage collector.
210        return $this->expire(
211            SessionServiceInterface::class,
212            '%%count%% expired sessions deleted.',
213            'No expired sessions to delete.'
214        );
215    }
216
217    /**
218     * Update browscap cache action.
219     *
220     * @return mixed
221     */
222    public function updatebrowscapcacheAction()
223    {
224        if (ini_get('max_execution_time') < 3600) {
225            ini_set('max_execution_time', '3600');
226        }
227        $this->updateBrowscapCache();
228        return $this->forwardTo('AdminMaintenance', 'Home');
229    }
230
231    /**
232     * Abstract delete method.
233     *
234     * @param string $serviceName   Service to operate on.
235     * @param string $successString String for reporting success.
236     * @param string $failString    String for reporting failure.
237     * @param int    $minAge        Minimum age allowed for expiration (also used
238     * as default value).
239     *
240     * @return mixed
241     */
242    protected function expire($serviceName, $successString, $failString, $minAge = 2)
243    {
244        $daysOld = intval($this->params()->fromQuery('daysOld', $minAge));
245        if ($daysOld < $minAge) {
246            $this->flashMessenger()->addErrorMessage(
247                str_replace(
248                    '%%age%%',
249                    $minAge,
250                    'Expiration age must be at least %%age%% days.'
251                )
252            );
253        } else {
254            $service = $this->getDbService($serviceName);
255            if (!$service instanceof DeleteExpiredInterface) {
256                throw new \Exception("Unsupported service: $serviceName");
257            }
258            $count = $service->deleteExpired(new DateTime("now - $daysOld days"));
259            if ($count == 0) {
260                $msg = $failString;
261            } else {
262                $msg = str_replace('%%count%%', $count, $successString);
263            }
264            $this->flashMessenger()->addSuccessMessage($msg);
265        }
266        return $this->forwardTo('AdminMaintenance', 'Home');
267    }
268
269    /**
270     * Update browscap cache.
271     *
272     * Note that there's also similar functionality in BrowscapCommand CLI utility.
273     *
274     * @return void
275     */
276    protected function updateBrowscapCache(): void
277    {
278        ini_set('memory_limit', '1024M');
279        $type = $this->params()->fromQuery('cacheType', 'standard');
280        switch ($type) {
281            case 'full':
282                $type = \BrowscapPHP\Helper\IniLoaderInterface::PHP_INI_FULL;
283                break;
284            case 'lite':
285                $type = \BrowscapPHP\Helper\IniLoaderInterface::PHP_INI_LITE;
286                break;
287            case 'standard':
288                $type = \BrowscapPHP\Helper\IniLoaderInterface::PHP_INI;
289                break;
290            default:
291                $this->flashMessenger()->addErrorMessage('Invalid browscap file-type specified');
292                return;
293        }
294
295        $cache = new SimpleCacheDecorator($this->cacheManager->getCache('browscap'));
296        $client = $this->guzzleService->createClient();
297
298        $bc = new \BrowscapPHP\BrowscapUpdater($cache, new \Laminas\Log\PsrLoggerAdapter($this->logger), $client);
299        try {
300            $bc->checkUpdate();
301        } catch (\BrowscapPHP\Exception\NoNewVersionException $e) {
302            $this->flashMessenger()
303                ->addSuccessMessage('No newer browscap version available. Clear the cache to force update.');
304            return;
305        } catch (\BrowscapPHP\Exception\FetcherException $e) {
306            $this->flashMessenger()->addErrorMessage($e->getMessage());
307            $this->logger->err((string)$e);
308            return;
309        } catch (\BrowscapPHP\Exception\NoCachedVersionException $e) {
310            // Fall through...
311        } catch (\Exception $e) {
312            // Output the exception and continue (assume we don't have a current version):
313            $this->flashMessenger()->addWarningMessage($e->getMessage());
314            $this->logger->warn((string)$e);
315        }
316        try {
317            $bc->update($type);
318            $this->logger->info('Browscap cache updated');
319            $this->flashMessenger()->addSuccessMessage('Browscap cache successfully updated.');
320        } catch (\Exception $e) {
321            $this->flashMessenger()->addErrorMessage($e->getMessage());
322            $this->logger->warn((string)$e);
323        }
324    }
325}