Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
66.67% covered (warning)
66.67%
18 / 27
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ShellboxClientFactory
66.67% covered (warning)
66.67%
18 / 27
66.67% covered (warning)
66.67%
4 / 6
17.33
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 isEnabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getClient
33.33% covered (danger)
33.33%
4 / 12
0.00% covered (danger)
0.00%
0 / 1
3.19
 getRemoteRpcClient
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRpcClient
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getUrl
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3namespace MediaWiki\Shell;
4
5use GuzzleHttp\Psr7\Uri;
6use GuzzleHttp\RequestOptions;
7use MediaWiki\Http\HttpRequestFactory;
8use RuntimeException;
9use Shellbox\Client;
10use Shellbox\RPC\LocalRpcClient;
11use Shellbox\RPC\RpcClient;
12
13/**
14 * This is a service which provides a configured client to access a remote
15 * Shellbox installation.
16 *
17 * @since 1.36
18 */
19class ShellboxClientFactory {
20    /** @var HttpRequestFactory */
21    private $requestFactory;
22    /** @var (string|false|null)[]|null */
23    private $urls;
24    /** @var string|null */
25    private $key;
26
27    /** The default request timeout, in seconds */
28    public const DEFAULT_TIMEOUT = 10;
29
30    /**
31     * @internal Use MediaWikiServices::getShellboxClientFactory()
32     * @param HttpRequestFactory $requestFactory The factory which will be used
33     *   to make HTTP clients.
34     * @param (string|false|null)[]|null $urls The Shellbox base URL mapping
35     * @param string|null $key The shared secret key used for HMAC authentication
36     */
37    public function __construct( HttpRequestFactory $requestFactory, $urls, $key ) {
38        $this->requestFactory = $requestFactory;
39        $this->urls = $urls;
40        $this->key = $key;
41    }
42
43    /**
44     * Test whether remote Shellbox is enabled by configuration.
45     *
46     * @param string|null $service Same as the service option for getClient.
47     * @return bool
48     */
49    public function isEnabled( ?string $service = null ): bool {
50        return $this->getUrl( $service ) !== null;
51    }
52
53    /**
54     * Get a Shellbox client with the specified options. If remote Shellbox is
55     * not configured (isEnabled() returns false), an exception will be thrown.
56     *
57     * @param array $options Associative array of options:
58     *   - timeout: The request timeout in seconds
59     *   - service: the shellbox backend name to get the URL from the mapping
60     * @return Client
61     * @throws RuntimeException
62     */
63    public function getClient( array $options = [] ) {
64        $url = $this->getUrl( $options['service'] ?? null );
65        if ( $url === null ) {
66            throw new RuntimeException( 'To use a remote shellbox to run shell commands, ' .
67                '$wgShellboxUrls and $wgShellboxSecretKey must be configured.' );
68        }
69
70        return new Client(
71            $this->requestFactory->createGuzzleClient( [
72                RequestOptions::TIMEOUT => $options['timeout'] ?? self::DEFAULT_TIMEOUT,
73                RequestOptions::HTTP_ERRORS => false,
74            ] ),
75            new Uri( $url ),
76            $this->key
77        );
78    }
79
80    /**
81     * Get a Shellbox RPC client with the specified options. If remote Shellbox is
82     * not configured (isEnabled() returns false), an exception will be thrown.
83     *
84     * @param array $options Associative array of options:
85     *   - timeout: The request timeout in seconds
86     *   - service: the shellbox backend name to get the URL from the mapping
87     * @return RpcClient
88     * @throws RuntimeException
89     */
90    public function getRemoteRpcClient( array $options = [] ): RpcClient {
91        return $this->getClient( $options );
92    }
93
94    /**
95     * Get a Shellbox RPC client with specified options. If remote Shellbox is
96     * not configured (isEnabled() returns false), a local fallback is returned.
97     *
98     * @param array $options
99     * @return RpcClient
100     */
101    public function getRpcClient( array $options = [] ): RpcClient {
102        $url = $this->getUrl( $options['service'] ?? null );
103        if ( $url === null ) {
104            return new LocalRpcClient();
105        }
106        return $this->getRemoteRpcClient( $options );
107    }
108
109    private function getUrl( ?string $service ): ?string {
110        if ( $this->urls === null || $this->key === null || $this->key === '' ) {
111            return null;
112        }
113        // @phan-suppress-next-line PhanTypeMismatchDimFetchNullable False positive
114        $url = $this->urls[$service] ?? $this->urls['default'] ?? null;
115        if ( !is_string( $url ) ) {
116            return null;
117        }
118        return $url;
119    }
120
121}