Passed
Push — develop ( f9d2ce...6f0b3c )
by Paul
13:35
created

SystemInfo   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 346
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 3
Bugs 1 Features 1
Metric Value
wmc 52
eloc 211
c 3
b 1
f 1
dl 0
loc 346
ccs 0
cts 244
cp 0
rs 7.44

25 Methods

Rating   Name   Duplication   Size   Complexity  
A sectionMuPlugins() 0 3 1
A sectionActionScheduler() 0 25 4
A group() 0 3 1
A __construct() 0 3 1
A __toString() 0 3 1
A sectionReviews() 0 3 1
A implode() 0 15 2
A plugins() 0 6 1
A ini() 0 6 2
A sectionSettings() 0 13 4
A sectionDropIns() 0 3 1
A purgeSensitiveData() 0 14 5
A get() 0 30 4
A reviewCounts() 0 12 3
A sectionInactivePlugins() 0 3 1
A hostingProvider() 0 14 4
A ratingCounts() 0 17 4
A sectionPlugin() 0 14 1
A sectionWordpress() 0 29 1
A sectionServer() 0 29 1
A sectionDatabase() 0 19 3
A sectionAddons() 0 9 3
A sectionActivePlugins() 0 3 1
A value() 0 3 1
A data() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like SystemInfo often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SystemInfo, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace GeminiLabs\SiteReviews\Modules;
4
5
use GeminiLabs\SiteReviews\Database\Cache;
6
use GeminiLabs\SiteReviews\Database\OptionManager;
7
use GeminiLabs\SiteReviews\Database\Query;
8
use GeminiLabs\SiteReviews\Database\Tables;
9
use GeminiLabs\SiteReviews\Geolocation;
10
use GeminiLabs\SiteReviews\Helper;
11
use GeminiLabs\SiteReviews\Helpers\Arr;
12
use GeminiLabs\SiteReviews\Helpers\Str;
13
14
class SystemInfo implements \Stringable
15
{
16
    public const PAD = 40;
17
18
    protected $data;
19
20
    public function __construct()
21
    {
22
        require_once ABSPATH.'wp-admin/includes/plugin.php';
23
    }
24
25
    public function __toString()
26
    {
27
        return $this->get();
28
    }
29
30
    public function get(): string
31
    {
32
        $sections = [ // order is intentional
33
            'plugin' => 'Plugin',
34
            'addon' => 'Addon',
35
            'reviews' => 'Reviews',
36
            'action-scheduler' => 'Action Scheduler',
37
            'database' => 'Database',
38
            'server' => 'Server',
39
            'wordpress' => 'WordPress',
40
            'drop-ins' => 'Drop-ins',
41
            'mu-plugins' => 'Must-Use Plugins',
42
            'active-plugins' => 'Active Plugins',
43
            'inactive-plugins' => 'Inactive Plugins',
44
            'settings' => 'Plugin Settings',
45
        ];
46
        $results = [];
47
        foreach ($sections as $sectionKey => $sectionTitle) {
48
            $method = Helper::buildMethodName('section', $sectionKey);
49
            if (!method_exists($this, $method)) {
50
                continue;
51
            }
52
            $values = call_user_func([$this, $method]);
53
            $values = glsr()->filterArray("system-info/section/{$sectionKey}", $values, $this->data);
54
            if (empty($values)) {
55
                continue;
56
            }
57
            $results[] = $this->implode($sectionTitle, $values);
58
        }
59
        return implode('', $results);
60
    }
61
62
    public function sectionActionScheduler(): array
63
    {
64
        $counts = glsr(Queue::class)->actionCounts();
65
        $counts = shortcode_atts(['complete' => [], 'pending' => [], 'failed' => []], $counts);
66
        $result = [];
67
        foreach ($counts as $status => $data) {
68
            $data = wp_parse_args($data, ['count' => 0, 'latest' => '', 'oldest' => '']);
69
            $label = "Actions ({$status})";
70
            if (0 === $data['count']) {
71
                $result[$label] = $data['count'];
72
                continue;
73
            }
74
            if (1 === $data['count']) {
75
                $result[$label] = sprintf('%s (latest: %s)', $data['count'], $data['latest']);
76
                continue;
77
            }
78
            $result[$label] = sprintf('%s (latest: %s, oldest: %s)',
79
                $data['count'],
80
                $data['latest'],
81
                $data['oldest']
82
            );
83
        }
84
        $result['Data Store'] = get_class(\ActionScheduler_Store::instance());
85
        $result['Version'] = \ActionScheduler_Versions::instance()->latest_version();
86
        return $result;
87
    }
88
89
    public function sectionActivePlugins(): array
90
    {
91
        return $this->plugins($this->group('wp-plugins-active'));
92
    }
93
94
    public function sectionAddons(): array
95
    {
96
        $details = [];
97
        foreach (array_keys(glsr()->retrieveAs('array', 'addons')) as $addonId) {
98
            if ($addon = glsr($addonId)) {
99
                $details[$addon->name] = $addon->version;
100
            }
101
        }
102
        return $details;
103
    }
104
105
    public function sectionDatabase(): array
106
    {
107
        if (glsr(Tables::class)->isSqlite()) {
108
            return [
109
                'Database Engine' => $this->value('wp-database.db_engine'),
110
                'Database Version' => $this->value('wp-database.database_version'),
111
            ];
112
        }
113
        $engines = glsr(Tables::class)->tableEngines($removePrefix = true);
114
        foreach ($engines as $engine => $tables) {
115
            $engines[$engine] = sprintf('%s (%s)', $engine, implode('|', $tables));
116
        }
117
        return [
118
            'Charset' => $this->value('wp-database.database_charset'),
119
            'Collation' => $this->value('wp-database.database_collate'),
120
            'Extension' => $this->value('wp-database.extension'),
121
            'Table Engines' => implode(', ', $engines),
122
            'Version (client)' => $this->value('wp-database.client_version'),
123
            'Version (server)' => $this->value('wp-database.server_version'),
124
        ];
125
    }
126
127
    public function sectionDropIns(): array
128
    {
129
        return $this->group('wp-dropins');
130
    }
131
132
    public function sectionInactivePlugins(): array
133
    {
134
        return $this->plugins($this->group('wp-plugins-inactive'));
135
    }
136
137
    public function sectionMuPlugins()
138
    {
139
        return $this->plugins($this->group('wp-mu-plugins'));
140
    }
141
142
    public function sectionPlugin(): array
143
    {
144
        $merged = array_keys(array_filter([
145
            'css' => glsr()->filterBool('optimize/css', false),
146
            'js' => glsr()->filterBool('optimize/js', false),
147
        ]));
148
        return [
149
            'Console Level' => glsr(Console::class)->humanLevel(),
150
            'Console Size' => glsr(Console::class)->humanSize(),
151
            'Database Version' => (string) get_option(glsr()->prefix.'db_version'),
152
            'Last Migration Run' => glsr(Date::class)->localized(glsr(Migrate::class)->lastRun(), 'unknown'),
153
            'Merged Assets' => implode('/', Helper::ifEmpty($merged, ['No'])),
154
            'Network Activated' => Helper::ifTrue(is_plugin_active_for_network(glsr()->basename), 'Yes', 'No'),
155
            'Version' => sprintf('%s (%s)', glsr()->version, glsr(OptionManager::class)->get('version_upgraded_from')),
156
        ];
157
    }
158
159
    public function sectionReviews(): array
160
    {
161
        return array_merge($this->ratingCounts(), $this->reviewCounts());
162
    }
163
164
    public function sectionServer(): array
165
    {
166
        return [
167
            'cURL Version' => $this->value('wp-server.curl_version'),
168
            'Display Errors' => $this->ini('display_errors', 'No'),
169
            'File Uploads' => $this->value('wp-media.file_uploads'),
170
            'GD version' => $this->value('wp-media.gd_version'),
171
            'Ghostscript Version' => $this->value('wp-media.ghostscript_version'),
172
            'Hosting Provider' => $this->hostingProvider(),
173
            'ImageMagick Version' => $this->value('wp-media.imagemagick_version'),
174
            'Intl' => Helper::ifEmpty(phpversion('intl'), 'No'),
175
            'IPv6' => var_export(defined('AF_INET6'), true),
176
            'Max Effective File Size' => $this->value('wp-media.max_effective_size'),
177
            'Max Execution Time' => $this->value('wp-server.time_limit'),
178
            'Max File Uploads' => $this->value('wp-media.max_file_uploads'),
179
            'Max Input Time' => $this->value('wp-server.max_input_time'),
180
            'Max Input Variables' => $this->value('wp-server.max_input_variables'),
181
            'Memory Limit' => $this->value('wp-server.memory_limit'),
182
            'Multibyte' => Helper::ifEmpty(phpversion('mbstring'), 'No'),
183
            'Permalinks Supported' => $this->value('wp-server.pretty_permalinks'),
184
            'PHP Version' => $this->value('wp-server.php_version'),
185
            'Post Max Size' => $this->value('wp-server.php_post_max_size'),
186
            'SAPI' => $this->value('wp-server.php_sapi'),
187
            'Sendmail' => $this->ini('sendmail_path'),
188
            'Server Architecture' => $this->value('wp-server.server_architecture'),
189
            'Server IP Address' => Helper::serverIp(),
190
            'Server Software' => $this->value('wp-server.httpd_software'),
191
            'SUHOSIN Installed' => $this->value('wp-server.suhosin'),
192
            'Upload Max Filesize' => $this->value('wp-server.upload_max_filesize'),
193
        ];
194
    }
195
196
    public function sectionSettings(): array
197
    {
198
        $settings = glsr(OptionManager::class)->getArray('settings');
199
        $settings = Arr::flatten($settings, true);
200
        $settings = $this->purgeSensitiveData($settings);
201
        $details = [];
202
        foreach ($settings as $key => $value) {
203
            if (str_starts_with($key, 'strings') && str_ends_with($key, 'id')) {
204
                continue;
205
            }
206
            $details[$key] = trim(preg_replace('/\s\s+/u', '\\n', $value));
207
        }
208
        return $details;
209
    }
210
211
    public function sectionWordpress(): array
212
    {
213
        return [
214
            'Email Domain' => substr(strrchr((string) get_option('admin_email'), '@'), 1),
215
            'Environment' => $this->value('wp-core.environment_type'),
216
            'Hidden From Search Engines' => $this->value('wp-core.blog_public'),
217
            'Home URL' => $this->value('wp-core.home_url'),
218
            'HTTPS' => $this->value('wp-core.https_status'),
219
            'Language (site)' => $this->value('wp-core.site_language'),
220
            'Language (user)' => $this->value('wp-core.user_language'),
221
            'Multisite' => $this->value('wp-core.multisite'),
222
            'Page For Posts ID' => (string) get_option('page_for_posts'),
223
            'Page On Front ID' => (string) get_option('page_on_front'),
224
            'Permalink Structure' => $this->value('wp-core.permalink'),
225
            'Post Stati' => implode(', ', get_post_stati()), // @phpstan-ignore-line
226
            'Remote Post' => glsr(Cache::class)->getRemotePostTest(),
227
            'SCRIPT_DEBUG' => $this->value('wp-constants.SCRIPT_DEBUG'),
228
            'Show On Front' => (string) get_option('show_on_front'),
229
            'Site URL' => $this->value('wp-core.site_url'),
230
            'Theme (active)' => sprintf('%s v%s by %s', $this->value('wp-active-theme.name'), $this->value('wp-active-theme.version'), $this->value('wp-active-theme.author')),
231
            'Theme (parent)' => $this->value('wp-parent-theme.name', 'No'),
232
            'Timezone' => $this->value('wp-core.timezone'),
233
            'User Count' => $this->value('wp-core.user_count'),
234
            'Version' => $this->value('wp-core.version'),
235
            'WP_CACHE' => $this->value('wp-constants.WP_CACHE'),
236
            'WP_DEBUG' => $this->value('wp-constants.WP_DEBUG'),
237
            'WP_DEBUG_DISPLAY' => $this->value('wp-constants.WP_DEBUG_DISPLAY'),
238
            'WP_DEBUG_LOG' => $this->value('wp-constants.WP_DEBUG_LOG'),
239
            'WP_MAX_MEMORY_LIMIT' => $this->value('wp-constants.WP_MAX_MEMORY_LIMIT'),
240
        ];
241
    }
242
243
    protected function data(): array
244
    {
245
        return $this->data ??= array_map(
246
            fn ($section) => array_combine(
247
                array_keys($fields = Arr::consolidate($section['fields'] ?? [])),
248
                wp_list_pluck($fields, 'value')
249
            ),
250
            glsr(Cache::class)->getSystemInfo()
251
        );
252
    }
253
254
    protected function group(string $key): array
255
    {
256
        return Arr::getAs('array', $this->data(), $key);
257
    }
258
259
    protected function hostingProvider(): string
260
    {
261
        if (Helper::isLocalServer()) {
262
            return 'localhost';
263
        }
264
        $domain = parse_url($this->value('wp-core.home_url'), \PHP_URL_HOST);
265
        $response = glsr(Geolocation::class)->lookup($domain, true);
266
        $location = $response->body();
267
        if ($response->successful() && 'success' === ($location['status'] ?? '')) {
268
            $isp = $location['isp'] ?? '';
269
            $ip = $location['query'] ?? '';
270
            return "$isp ($ip)";
271
        }
272
        return 'unknown';
273
    }
274
275
    protected function implode(string $title, array $details): string
276
    {
277
        $strings = ['['.strtoupper($title).']'];
278
        $padding = max(static::PAD, ...array_map(
279
            fn ($key) => mb_strlen(html_entity_decode($key, ENT_HTML5), 'UTF-8'),
280
            array_keys($details)
281
        ));
282
        ksort($details);
283
        foreach ($details as $key => $value) {
284
            $key = html_entity_decode((string) $key, ENT_HTML5);
285
            $pad = $padding - (mb_strlen($key, 'UTF-8') - strlen($key)); // handle unicode character lengths
286
            $label = str_pad($key, $pad, '.');
287
            $strings[] = "{$label} : {$value}";
288
        }
289
        return implode(PHP_EOL, $strings).PHP_EOL.PHP_EOL;
290
    }
291
292
    protected function ini(string $name, string $fallback = ''): string
293
    {
294
        if (function_exists('ini_get')) {
295
            return Helper::ifEmpty(ini_get($name), $fallback);
296
        }
297
        return 'ini_get() is disabled.';
298
    }
299
300
    protected function plugins(array $plugins): array
301
    {
302
        return array_map(function ($value) {
303
            $patterns = ['/^(Version )/', '/( \| Auto-updates (en|dis)abled)$/'];
304
            return preg_replace($patterns, ['v', ''], $value);
305
        }, $plugins);
306
    }
307
308
    protected function purgeSensitiveData(array $settings): array
309
    {
310
        $config = glsr()->settings();
311
        $config = array_filter($config, function ($field, $key) {
312
            return str_starts_with($key, 'settings.licenses.') || str_ends_with($key, 'api_key') || 'secret' === ($field['type'] ?? '');
313
        }, ARRAY_FILTER_USE_BOTH);
314
        $keys = array_keys($config);
315
        $keys = array_map(fn ($key) => Str::removePrefix($key, 'settings.'), $keys);
316
        foreach ($settings as $key => &$value) {
317
            if (in_array($key, $keys)) {
318
                $value = Str::mask($value, 0, 8, 24);
319
            }
320
        }
321
        return $settings;
322
    }
323
324
    protected function ratingCounts(): array
325
    {
326
        $ratings = glsr(Query::class)->ratings();
327
        $results = [];
328
        foreach ($ratings as $type => $counts) {
329
            if (is_array($counts)) {
330
                $label = sprintf('Type: %s', $type);
331
                $results[$label] = array_sum($counts).' ('.implode(', ', $counts).')';
332
                continue;
333
            }
334
            glsr_log()->error('$ratings is not an array, possibly due to incorrectly imported reviews.')
335
                ->debug(compact('counts', 'ratings'));
336
        }
337
        if (empty($results)) {
338
            return ['Type: local' => 'No reviews'];
339
        }
340
        return $results;
341
    }
342
343
    protected function reviewCounts(): array
344
    {
345
        $reviews = array_filter((array) wp_count_posts(glsr()->post_type));
346
        $results = array_sum($reviews);
347
        if (0 < $results) {
348
            foreach ($reviews as $status => &$num) {
349
                $num = sprintf('%s: %d', $status, $num);
350
            }
351
            $details = implode(', ', $reviews);
352
            $results = "{$results} ({$details})";
353
        }
354
        return ['Reviews' => $results];
355
    }
356
357
    protected function value(string $path = '', string $fallback = ''): string
358
    {
359
        return Arr::getAs('string', $this->data(), $path, $fallback);
360
    }
361
}
362