Passed
Push — main ( 23e6dc...bef3c8 )
by Paul
19:12 queued 11:57
created

SystemInfo::isWebhost()   B

Complexity

Conditions 8
Paths 19

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 6
c 1
b 0
f 1
dl 0
loc 8
ccs 0
cts 7
cp 0
rs 8.4444
cc 8
nc 19
nop 1
crap 72
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\Helper;
11
use GeminiLabs\SiteReviews\Helpers\Arr;
12
use GeminiLabs\SiteReviews\Helpers\Cast;
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($key, 'get');
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
            $hook = 'system/'.Str::dashCase($key);
58
            $title = strtoupper(Arr::get($details, 'title'));
59
            $values = glsr()->filterArray($hook, Arr::get($details, 'values'));
60
            return $carry.$this->implode($title, $values);
61
        }));
62
    }
63
64
    public function getActionScheduler(): array
65
    {
66
        $counts = glsr(Queue::class)->actionCounts();
67
        $values = [];
68
        foreach ($counts as $key => $value) {
69
            $label = sprintf('Action (%s)', $key);
70
            if ($value['count'] > 1) {
71
                $values[$label] = sprintf('%s (latest: %s, oldest: %s)',
72
                    $value['count'],
73
                    $value['latest'],
74
                    $value['oldest']
75
                );
76
            } else {
77
                $values[$label] = sprintf('%s (latest: %s)',
78
                    $value['count'],
79
                    $value['latest']
80
                );
81
            }
82
        }
83
        $values['Data Store'] = get_class(\ActionScheduler_Store::instance());
84
        $values['Version'] = \ActionScheduler_Versions::instance()->latest_version();
85
        return [
86
            'title' => 'Action Scheduler',
87
            'values' => $values,
88
        ];
89
    }
90
91
    public function getActivePlugins(): array
92
    {
93
        return [
94
            'title' => 'Active Plugins',
95
            'values' => $this->plugins($this->group('wp-plugins-active')),
96
        ];
97
    }
98
99
    public function getAddon(): array
100
    {
101
        $details = [];
102
        foreach (glsr()->retrieveAs('array', 'addons') as $id => $version) {
103
            if ($addon = glsr($id)) {
104
                $details[$addon->name] = $addon->version;
105
            }
106
        }
107
        ksort($details);
108
        return [
109
            'title' => 'Addon Details',
110
            'values' => $details,
111
        ];
112
    }
113
114
    public function getBrowser(): array
115
    {
116
        $browser = new Browser();
117
        $name = esc_attr($browser->getName());
118
        $userAgent = esc_attr($browser->getUserAgent()->getUserAgentString());
119
        $version = esc_attr($browser->getVersion());
120
        return [
121
            'title' => 'Browser Details',
122
            'values' => [
123
                'Browser Name' => sprintf('%s %s', $name, $version),
124
                'Browser UA' => $userAgent,
125
            ],
126
        ];
127
    }
128
129
    public function getDatabase(): array
130
    {
131
        $engines = glsr(Tables::class)->tableEngines($removeDBPrefix = true);
132
        foreach ($engines as $engine => $tables) {
133
            $engines[$engine] = sprintf('%s (%s)', $engine, implode('|', $tables));
134
        }
135
        return [
136
            'title' => 'Database Details',
137
            'values' => [
138
                'Charset' => $this->value('wp-database.database_charset'),
139
                'Collation' => $this->value('wp-database.database_collate'),
140
                'Extension' => $this->value('wp-database.extension'),
141
                'Table Engines' => implode(', ', $engines),
142
                'Version (client)' => $this->value('wp-database.client_version'),
143
                'Version (server)' => $this->value('wp-database.server_version'),
144
            ],
145
        ];
146
    }
147
148
    public function getDropIns(): array
149
    {
150
        return [
151
            'title' => 'Drop-ins',
152
            'values' => $this->group('wp-dropins'),
153
        ];
154
    }
155
156
    public function getInactivePlugins(): array
157
    {
158
        return [
159
            'title' => 'Inactive Plugins',
160
            'values' => $this->plugins($this->group('wp-plugins-inactive')),
161
        ];
162
    }
163
164
    public function getMuPlugins()
165
    {
166
        return [
167
            'title' => 'Must-Use Plugins',
168
            'values' => $this->plugins($this->group('wp-mu-plugins')),
169
        ];
170
    }
171
172
    public function getPlugin(): array
173
    {
174
        $merged = array_keys(array_filter([
175
            'css' => glsr()->filterBool('optimize/css', false),
176
            'js' => glsr()->filterBool('optimize/js', false),
177
        ]));
178
        return [
179
            'title' => 'Plugin Details',
180
            'values' => [
181
                'Console Level' => glsr(Console::class)->humanLevel(),
182
                'Console Size' => glsr(Console::class)->humanSize(),
183
                'Database Version' => (string) get_option(glsr()->prefix.'db_version'),
184
                'Last Migration Run' => glsr(Date::class)->localized(glsr(OptionManager::class)->get('last_migration_run'), 'unknown'),
185
                'Merged Assets' => implode('/', Helper::ifEmpty($merged, ['No'])),
186
                'Network Activated' => Helper::ifTrue(is_plugin_active_for_network(plugin_basename(glsr()->file)), 'Yes', 'No'),
187
                'Version' => sprintf('%s (%s)', glsr()->version, glsr(OptionManager::class)->get('version_upgraded_from')),
188
            ],
189
        ];
190
    }
191
192
    public function getReviews(): array
193
    {
194
        $values = array_merge($this->ratingCounts(), $this->reviewCounts());
195
        ksort($values);
196
        return [
197
            'title' => 'Review Details',
198
            'values' => $values,
199
        ];
200
    }
201
202
    public function getServer(): array
203
    {
204
        return [
205
            'title' => 'Server Details',
206
            'values' => [
207
                'cURL Version' => $this->value('wp-server.curl_version'),
208
                'Display Errors' => $this->ini('display_errors', 'No'),
209
                'File Uploads' => $this->value('wp-media.file_uploads'),
210
                'GD version' => $this->value('wp-media.gd_version'),
211
                'Ghostscript version' => $this->value('wp-media.ghostscript_version'),
212
                'Host Name' => $this->hostname(),
213
                'ImageMagick version' => $this->value('wp-media.imagemagick_version'),
214
                'Intl' => Helper::ifEmpty(phpversion('intl'), 'No'),
215
                'IPv6' => var_export(defined('AF_INET6'), true),
216
                'Max Effective File Size' => $this->value('wp-media.max_effective_size'),
217
                'Max Execution Time' => $this->value('wp-server.time_limit'),
218
                'Max File Uploads' => $this->value('wp-media.max_file_uploads'),
219
                'Max Input Time' => $this->value('wp-server.max_input_time'),
220
                'Max Input Variables' => $this->value('wp-server.max_input_variables'),
221
                'Memory Limit' => $this->value('wp-server.memory_limit'),
222
                'Multibyte' => Helper::ifEmpty(phpversion('mbstring'), 'No'),
223
                'Permalinks Supported' => $this->value('wp-server.pretty_permalinks'),
224
                'PHP Version' => $this->value('wp-server.php_version'),
225
                'Post Max Size' => $this->value('wp-server.php_post_max_size'),
226
                'SAPI' => $this->value('wp-server.php_sapi'),
227
                'Sendmail' => $this->ini('sendmail_path'),
228
                'Server Architecture' => $this->value('wp-server.server_architecture'),
229
                'Server Software' => $this->value('wp-server.httpd_software'),
230
                'SUHOSIN Installed' => $this->value('wp-server.suhosin'),
231
                'Upload Max Filesize' => $this->value('wp-server.upload_max_filesize'),
232
            ],
233
        ];
234
    }
235
236
    public function getSettings(): array
237
    {
238
        $settings = glsr(OptionManager::class)->getArray('settings');
239
        $settings = Arr::flatten($settings, true);
240
        $settings = $this->purgeSensitiveData($settings);
241
        ksort($settings);
242
        $details = [];
243
        foreach ($settings as $key => $value) {
244
            if (Str::startsWith($key, 'strings') && Str::endsWith($key, 'id')) {
245
                continue;
246
            }
247
            $value = htmlspecialchars(trim(preg_replace('/\s\s+/u', '\\n', $value)), ENT_QUOTES, 'UTF-8');
248
            $details[$key] = $value;
249
        }
250
        return [
251
            'title' => 'Plugin Settings',
252
            'values' => $details,
253
        ];
254
    }
255
256
    public function getWordpress(): array
257
    {
258
        return [
259
            'title' => 'WordPress Configuration',
260
            'values' => [
261
                'Email Domain' => substr(strrchr((string) get_option('admin_email'), '@'), 1),
262
                'Environment' => $this->value('wp-core.environment_type'),
263
                'Hidden From Search Engines' => $this->value('wp-core.blog_public'),
264
                'Home URL' => $this->value('wp-core.home_url'),
265
                'HTTPS' => $this->value('wp-core.https_status'),
266
                'Language (site)' => $this->value('wp-core.site_language'),
267
                'Language (user)' => $this->value('wp-core.user_language'),
268
                'Multisite' => $this->value('wp-core.multisite'),
269
                'Page For Posts ID' => (string) get_option('page_for_posts'),
270
                'Page On Front ID' => (string) get_option('page_on_front'),
271
                'Permalink Structure' => $this->value('wp-core.permalink'),
272
                'Post Stati' => implode(', ', get_post_stati()), // @phpstan-ignore-line
273
                'Remote Post' => glsr(Cache::class)->getRemotePostTest(),
274
                'SCRIPT_DEBUG' => $this->value('wp-constants.SCRIPT_DEBUG'),
275
                'Show On Front' => (string) get_option('show_on_front'),
276
                'Site URL' => $this->value('wp-core.site_url'),
277
                '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')),
278
                'Theme (parent)' => $this->value('wp-parent-theme.name', 'No'),
279
                'Timezone' => $this->value('wp-core.timezone'),
280
                'User Count' => $this->value('wp-core.user_count'),
281
                'Version' => $this->value('wp-core.version'),
282
                'WP_CACHE' => $this->value('wp-constants.WP_CACHE'),
283
                'WP_DEBUG' => $this->value('wp-constants.WP_DEBUG'),
284
                'WP_DEBUG_DISPLAY' => $this->value('wp-constants.WP_DEBUG_DISPLAY'),
285
                'WP_DEBUG_LOG' => $this->value('wp-constants.WP_DEBUG_LOG'),
286
                'WP_MAX_MEMORY_LIMIT' => $this->value('wp-constants.WP_MAX_MEMORY_LIMIT'),
287
            ],
288
        ];
289
    }
290
291
    protected function data(): array
292
    {
293
        if (empty($this->data)) {
294
            $this->data = glsr(Cache::class)->getSystemInfo();
295
            array_walk($this->data, function (&$section) {
296
                $fields = Arr::consolidate(Arr::get($section, 'fields'));
297
                array_walk($fields, function (&$values) {
298
                    $values = Arr::get($values, 'value');
299
                });
300
                $section = $fields;
301
            });
302
        }
303
        return $this->data;
304
    }
305
306
    protected function group(string $key): array
307
    {
308
        return Arr::getAs('array', $this->data(), $key);
309
    }
310
311
    protected function hostname(): string
312
    {
313
        $checks = [
314
            '.accountservergroup.com' => 'Site5',
315
            '.gridserver.com' => 'MediaTemple Grid',
316
            '.inmotionhosting.com' => 'InMotion Hosting',
317
            '.ovh.net' => 'OVH',
318
            '.pair.com' => 'pair Networks',
319
            '.stabletransit.com' => 'Rackspace Cloud',
320
            '.stratoserver.net' => 'STRATO',
321
            '.sysfix.eu' => 'SysFix.eu Power Hosting',
322
            'bluehost.com' => 'Bluehost',
323
            'DH_USER' => 'DreamHost',
324
            'Flywheel' => 'Flywheel',
325
            'ipagemysql.com' => 'iPage',
326
            'ipowermysql.com' => 'IPower',
327
            'localhost:/tmp/mysql5.sock' => 'ICDSoft',
328
            'mysqlv5' => 'NetworkSolutions',
329
            'PAGELYBIN' => 'Pagely',
330
            'secureserver.net' => 'GoDaddy',
331
            'WPE_APIKEY' => 'WP Engine',
332
        ];
333
        $webhost = implode(',', array_filter([DB_HOST, filter_input(INPUT_SERVER, 'SERVER_NAME')]));
334
        foreach ($checks as $key => $value) {
335
            if ($this->isWebhost($key)) {
336
                $webhost = $value;
337
                break;
338
            }
339
        }
340
        return sprintf('%s (%s)', $webhost, Helper::getIpAddress());
341
    }
342
343
    protected function implode(string $title, array $details): string
344
    {
345
        $strings = ['['.$title.']'];
346
        $padding = max(static::PAD, ...array_map(function ($key) {
347
            return mb_strlen(html_entity_decode($key, ENT_HTML5), 'UTF-8');
348
        }, array_keys($details)));
349
        foreach ($details as $key => $value) {
350
            $key = html_entity_decode((string) $key, ENT_HTML5);
351
            $pad = $padding - (mb_strlen($key, 'UTF-8') - strlen($key)); // handle unicode character lengths
352
            $strings[] = sprintf('%s : %s', str_pad($key, $pad, '.'), $value);
353
        }
354
        return implode(PHP_EOL, $strings).PHP_EOL.PHP_EOL;
355
    }
356
357
    protected function ini(string $name, string $fallback = ''): string
358
    {
359
        if (function_exists('ini_get')) {
360
            return Helper::ifEmpty(ini_get($name), $fallback);
361
        }
362
        return 'ini_get() is disabled.';
363
    }
364
365
    protected function isWebhost(string $key): bool
366
    {
367
        return defined($key)
368
            || filter_input(INPUT_SERVER, $key)
369
            || Str::contains(filter_input(INPUT_SERVER, 'SERVER_NAME'), $key)
370
            || Str::contains(DB_HOST, $key)
371
            || (function_exists('php_uname') && Str::contains(php_uname(), $key))
372
            || ('WPE_APIKEY' === $key && function_exists('is_wpe')); // WP Engine
373
    }
374
375
    protected function plugins(array $plugins): array
376
    {
377
        return array_map(function ($value) {
378
            $patterns = ['/^(Version )/', '/( \| Auto-updates (en|dis)abled)$/'];
379
            return preg_replace($patterns, ['v', ''], $value);
380
        }, $plugins);
381
    }
382
383
    protected function purgeSensitiveData(array $settings): array
384
    {
385
        $keys = glsr()->filterArray('addon/system-info/purge', [
386
            'licenses.' => 8,
387
            'forms.friendlycaptcha.key' => 0,
388
            'forms.friendlycaptcha.secret' => 0,
389
            'forms.hcaptcha.key' => 0,
390
            'forms.hcaptcha.secret' => 0,
391
            'forms.recaptcha.key' => 0,
392
            'forms.recaptcha.secret' => 0,
393
            'forms.recaptcha_v3.key' => 0,
394
            'forms.recaptcha_v3.secret' => 0,
395
            'forms.turnstile.key' => 0,
396
            'forms.turnstile.secret' => 0,
397
        ]);
398
        array_walk($settings, function (&$value, $setting) use ($keys) {
399
            foreach ($keys as $key => $preserve) {
400
                if (!is_string($key)) { // @compat for older addons
401
                    $key = $preserve;
402
                    $preserve = 0;
403
                }
404
                if (Str::startsWith($setting, $key) && !empty($value)) {
405
                    $preserve = Cast::toInt($preserve);
406
                    $value = Str::mask($value, 0, $preserve, 13);
407
                    break;
408
                }
409
            }
410
        });
411
        return $settings;
412
    }
413
414
    protected function ratingCounts(): array
415
    {
416
        $ratings = glsr(Query::class)->ratings();
417
        $results = [];
418
        foreach ($ratings as $type => $counts) {
419
            if (is_array($counts)) {
420
                $label = sprintf('Type: %s', $type);
421
                $results[$label] = array_sum($counts).' ('.implode(', ', $counts).')';
422
                continue;
423
            }
424
            glsr_log()->error('$ratings is not an array, possibly due to incorrectly imported reviews.')
425
                ->debug(compact('counts', 'ratings'));
426
        }
427
        if (empty($results)) {
428
            return ['Type: local' => 'No reviews'];
429
        }
430
        return $results;
431
    }
432
433
    protected function reviewCounts(): array
434
    {
435
        $reviews = array_filter((array) wp_count_posts(glsr()->post_type));
436
        $counts = array_sum($reviews);
437
        foreach ($reviews as $status => &$num) {
438
            $num = sprintf('%s: %d', $status, $num);
439
        }
440
        $results = $counts.' ('.implode(', ', $reviews).')';
441
        return ['Reviews' => $results];
442
    }
443
444
    protected function value(string $path = '', string $fallback = ''): string
445
    {
446
        return Arr::getAs('string', $this->data(), $path, $fallback);
447
    }
448
}
449