WordPressCollector::setInventory()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 1
b 0
f 0
1
<?php
2
3
namespace Startwind\Inventorio\Collector\Website\WordPress;
4
5
use Startwind\Inventorio\Collector\BasicCollector;
6
use Startwind\Inventorio\Collector\InventoryAwareCollector;
7
use Startwind\Inventorio\Exec\File;
8
use Startwind\Inventorio\Exec\Runner;
9
use Startwind\Inventorio\Util\WebserverUtil;
10
11
class WordPressCollector extends BasicCollector implements InventoryAwareCollector
12
{
13
    public const COLLECTOR_IDENTIFIER = 'WordPress';
14
15
    protected string $identifier = self::COLLECTOR_IDENTIFIER;
16
17
    private array $inventory;
18
19
    public function setInventory(array $inventory): void
20
    {
21
        $this->inventory = $inventory;
22
    }
23
24
    public function collect(): array
25
    {
26
        $runner = Runner::getInstance();
27
        $wordPressInstallations = [];
28
29
        $documentRoots = WebserverUtil::extractDocumentRoots($this->inventory);
30
31
        foreach ($documentRoots as $domain => $documentRoot) {
32
            if ($runner->fileExists($documentRoot . '/wp-config.php')) {
33
                if (!str_ends_with($documentRoot, '/')) {
34
                    $documentRoot .= '/';
35
                }
36
37
                $wordPressInstallations[$domain] = [
38
                    'domain' => $domain,
39
                    'version' => $this->extractVersion($documentRoot),
40
                    'plugins' => $this->extractPlugins($documentRoot),
41
                    'path' => $documentRoot
42
                ];
43
            }
44
        }
45
46
        return $wordPressInstallations;
47
    }
48
49
    private function extractPlugins(string $documentRoot): array
50
    {
51
        $file = File::getInstance();
52
53
        $pluginDir = $documentRoot . 'wp-content/plugins';
54
        if (!$file->isDir($pluginDir)) return [];
55
56
        $plugins = array_diff($file->scanDir($pluginDir), ['.', '..']);
57
        $pluginArray = [];
58
59
        foreach ($plugins as $pluginFolder) {
60
            $path = $pluginDir . '/' . $pluginFolder;
61
62
            if (!$file->isDir($path)) continue;
63
64
            $entries = array_diff($file->scanDir($path), ['.', '..']);
65
66
            foreach ($entries as $entry) {
67
                $fullPath = $path . '/' . $entry;
68
69
                if ($file->isFile($fullPath) && pathinfo($entry, PATHINFO_EXTENSION) === 'php') {
70
                    $info = $this->parsePluginHeader($fullPath);
71
                    if (!empty($info['Name']) && !empty($info['Version'])) {
72
                        $slug = $this->deriveSlugFromHeader($info, $pluginFolder);
73
                        $update = $this->checkWordPressPluginUpdate($slug, $info['Version']);
74
                        $pluginArray[$info['Name']] = [
75
                            'name' => $info['Name'],
76
                            'slug' => $slug,
77
                            'version' => $info['Version'],
78
                            'update_available' => $update['update_available'] ?? false,
79
                            'latest_version' => $update['latest_version'] ?? null,
80
                        ];
81
                        break;
82
                    }
83
                }
84
            }
85
        }
86
87
        return $pluginArray;
88
    }
89
90
    private function parsePluginHeader(string $file): array
91
    {
92
        $headers = [
93
            'Name' => 'Plugin Name',
94
            'Version' => 'Version',
95
            'PluginURI' => 'Plugin URI',
96
        ];
97
98
        $data = File::getInstance()->getContents($file);
99
100
        $info = [];
101
        foreach ($headers as $key => $header) {
102
            if (preg_match('/' . preg_quote($header, '/') . ':\s*(.+)/i', $data, $matches)) {
103
                $info[$key] = trim($matches[1]);
104
            }
105
        }
106
107
        return $info;
108
    }
109
110
    private function deriveSlugFromHeader(array $info, string $fallback): string
111
    {
112
        // Try to extract slug from Plugin URI (e.g., https://wordpress.org/plugins/contact-form-7/)
113
        if (!empty($info['PluginURI'])) {
114
            $urlParts = parse_url($info['PluginURI']);
115
            if (isset($urlParts['path'])) {
116
                $segments = explode('/', trim($urlParts['path'], '/'));
117
                $lastSegment = end($segments);
118
                if ($lastSegment) return $lastSegment;
119
            }
120
        }
121
122
        // Fallback to directory name
123
        return strtolower($fallback);
124
    }
125
126
    private function checkWordPressPluginUpdate(string $slug, string $currentVersion): ?array
127
    {
128
        $url = "https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=" . urlencode($slug);
129
        $response = @file_get_contents($url);
130
        if (!$response) return null;
131
132
        $pluginData = json_decode($response, true);
133
        if (!isset($pluginData['version'])) return null;
134
135
        $latestVersion = $pluginData['version'];
136
137
        return [
138
            'update_available' => version_compare($latestVersion, $currentVersion, '>'),
139
            'latest_version' => $latestVersion,
140
        ];
141
    }
142
143
    private function extractVersion(string $documentRoot): string
144
    {
145
        $wpVersionFile = $documentRoot . 'wp-includes/version.php';
146
147
        $version = 'unknown';
148
149
        $runner = Runner::getInstance();
150
151
        if ($runner->fileExists($wpVersionFile)) {
152
            $content = $runner->getFileContents($wpVersionFile);
153
            if (preg_match("/\\\$wp_version\s*=\s*'([^']+)'/", $content, $matches)) {
154
                $version = $matches[1];
155
            }
156
        }
157
158
        return $version;
159
    }
160
}
161