Passed
Push — master ( 6b1f1c...2533b4 )
by Nils
02:48
created

WordPressCollector::deriveSlugFromHeader()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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