Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
BrowscapCommand
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 4
272
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 configure
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 checkCachePermissions
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 execute
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2
3/**
4 * Console command: browscap
5 *
6 * PHP version 8
7 *
8 * Copyright (C) The National Library of Finland 2024.
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  Console
25 * @author   Ere Maijala <ere.maijala@helsinki.fi>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org/wiki/development Wiki
28 */
29
30namespace VuFindConsole\Command\Util;
31
32use Exception;
33use Laminas\Cache\Psr\SimpleCache\SimpleCacheDecorator;
34use Laminas\Cache\Storage\StorageInterface;
35use Psr\Log\InvalidArgumentException;
36use Psr\Log\LogLevel;
37use Symfony\Component\Console\Attribute\AsCommand;
38use Symfony\Component\Console\Command\Command;
39use Symfony\Component\Console\Input\InputArgument;
40use Symfony\Component\Console\Input\InputInterface;
41use Symfony\Component\Console\Input\InputOption;
42use Symfony\Component\Console\Logger\ConsoleLogger;
43use Symfony\Component\Console\Output\OutputInterface;
44use VuFind\Cache\Manager as CacheManager;
45use VuFind\Http\GuzzleService;
46
47/**
48 * Console command: browscap
49 *
50 * @category VuFind
51 * @package  Console
52 * @author   Ere Maijala <ere.maijala@helsinki.fi>
53 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
54 * @link     https://vufind.org/wiki/development Wiki
55 */
56#[AsCommand(
57    name: 'util/browscap',
58    description: 'Browscap Cache Manager'
59)]
60class BrowscapCommand extends Command
61{
62    /**
63     * Cache manager
64     *
65     * @var CacheManager
66     */
67    protected $cacheManager;
68
69    /**
70     * Guzzle service
71     *
72     * @var GuzzleService
73     */
74    protected $guzzleService;
75
76    /**
77     * Constructor
78     *
79     * @param CacheManager  $cacheManager  Cache manager
80     * @param GuzzleService $guzzleService Guzzle service
81     */
82    public function __construct(
83        CacheManager $cacheManager,
84        GuzzleService $guzzleService
85    ) {
86        parent::__construct();
87        $this->cacheManager = $cacheManager;
88        $this->guzzleService = $guzzleService;
89    }
90
91    /**
92     * Configure the command.
93     *
94     * @return void
95     */
96    protected function configure()
97    {
98        $this
99            ->setHelp('Manages the browscap cache.')
100            ->addArgument(
101                'function',
102                InputArgument::REQUIRED,
103                'Function to execute. Currently the only supported function is: update'
104            )
105            ->addOption(
106                'file-type',
107                null,
108                InputOption::VALUE_REQUIRED,
109                'Browscap file type (standard, lite or full). See https://browscap.org/ for more information.',
110                'standard'
111            );
112    }
113
114    /**
115     * Display a warning message if generated files are not owned by the Apache user.
116     *
117     * @param ConsoleLogger    $logger        Logger (for warning output)
118     * @param StorageInterface $browscapCache Browscap cache object
119     *
120     * @return void
121     *
122     * @throws Exception
123     * @throws InvalidArgumentException
124     */
125    protected function checkCachePermissions(ConsoleLogger $logger, StorageInterface $browscapCache)
126    {
127        // If we have a file cache, let's make sure the created files are consistent
128        // with expectations.
129        if ($browscapCacheDir = ($browscapCache->getOptions()->cacheDir ?? null)) {
130            // Figure out the username of the web server -- there is no perfect way to do
131            // this, but if an object cache exists, it was most likely generated by the
132            // web server. We'll try to use that, and fall back on the base directory
133            // otherwise.
134            $baseCacheDir = $this->cacheManager->getCacheDir(false);
135            $objectCacheDir = $baseCacheDir . '/objects';
136            $dirToCheck = file_exists($objectCacheDir) ? $objectCacheDir : $baseCacheDir;
137            $baseCacheOwner = fileowner($dirToCheck);
138            // Look through the browscap cache for files owned by somebody other than the
139            // expected web user.
140            $dir = opendir($browscapCacheDir);
141            while ($file = readdir($dir)) {
142                if (fileowner($file) !== $baseCacheOwner) {
143                    $baseCacheUserDetails = posix_getpwuid($baseCacheOwner);
144                    $baseCacheUsername = $baseCacheUserDetails['name'] ?? $baseCacheOwner;
145                    $logger->warning(
146                        "Generated files are not owned by $baseCacheUsername"
147                        . "if persistent login attempts fail, try recursively changing ownership of $browscapCacheDir."
148                    );
149                    return;
150                }
151            }
152        }
153    }
154
155    /**
156     * Run the command.
157     *
158     * Note that there's also similar functionality in MaintenanceController.
159     *
160     * @param InputInterface  $input  Input object
161     * @param OutputInterface $output Output object
162     *
163     * @return int 0 for success
164     */
165    protected function execute(InputInterface $input, OutputInterface $output)
166    {
167        ini_set('memory_limit', '1024M');
168        if ($input->getArgument('function') !== 'update') {
169            $output->writeln('<error>Invalid function specified</error>');
170            return Command::FAILURE;
171        }
172        switch ($input->getOption('file-type')) {
173            case 'full':
174                $type = \BrowscapPHP\Helper\IniLoaderInterface::PHP_INI_FULL;
175                break;
176            case 'lite':
177                $type = \BrowscapPHP\Helper\IniLoaderInterface::PHP_INI_LITE;
178                break;
179            case 'standard':
180                $type = \BrowscapPHP\Helper\IniLoaderInterface::PHP_INI;
181                break;
182            default:
183                $output->writeln('<error>Invalid file-type specified</error>');
184                return Command::FAILURE;
185        }
186
187        $browscapCache = $this->cacheManager->getCache('browscap');
188        $cache = new SimpleCacheDecorator($browscapCache);
189        $logger = new ConsoleLogger($output, [LogLevel::INFO => OutputInterface::VERBOSITY_NORMAL]);
190        $client = $this->guzzleService->createClient();
191
192        $bc = new \BrowscapPHP\BrowscapUpdater($cache, $logger, $client);
193        $logger->info('Checking for update...');
194        try {
195            $bc->checkUpdate();
196        } catch (\BrowscapPHP\Exception\NoNewVersionException $e) {
197            $logger->info('No newer version available.');
198            return Command::SUCCESS;
199        } catch (\BrowscapPHP\Exception\NoCachedVersionException $e) {
200            $logger->info('No cached version available.');
201        } catch (\Exception $e) {
202            // Output the exception and continue (assume we don't have a current version):
203            $logger->warning((string)$e);
204        }
205        $logger->info('Updating browscap cache...');
206        $bc->update($type);
207        $logger->info('Update complete.');
208
209        $this->checkCachePermissions($logger, $browscapCache);
210
211        return Command::SUCCESS;
212    }
213}