Passed
Push — develop ( afff43...70b7ee )
by Paul
14:29
created

SystemInfo::hostingProvider()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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