Issues (30)

src/Model/Client.php (7 issues)

1
<?php
2
3
namespace BiffBangPow\SSMonitor\Server\Model;
4
5
use BiffBangPow\SSMonitor\Server\Client\MonitoringClientInterface;
6
use BiffBangPow\SSMonitor\Server\Helper\ClientHelper;
7
use BiffBangPow\SSMonitor\Server\Helper\EncryptionHelper;
8
use Psr\Log\LoggerInterface;
9
use Ramsey\Uuid\Uuid;
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Core\ClassInfo;
12
use SilverStripe\Core\Environment;
13
use SilverStripe\View\ArrayData;
14
use SilverStripe\Forms\HeaderField;
15
use SilverStripe\Forms\LiteralField;
16
use SilverStripe\ORM\ArrayList;
17
use SilverStripe\ORM\DataObject;
18
use SilverStripe\ORM\FieldType\DBDatetime;
19
use SilverStripe\ORM\FieldType\DBField;
20
use SilverStripe\ORM\Queries\SQLUpdate;
21
use SilverStripe\View\HTML;
22
use SilverStripe\Core\Injector\Injector;
23
use SilverStripe\View\SSViewer;
24
25
/**
26
 * Class \BiffBangPow\SSMonitor\Server\Model\Client
27
 *
28
 * @property string $Title
29
 * @property string $BaseURL
30
 * @property string $UUID
31
 * @property string $EncSecret
32
 * @property string $EncSalt
33
 * @property string $APIKey
34
 * @property string $LastFetch
35
 * @property string $ClientData
36
 * @property bool $FetchError
37
 * @property bool $HasWarnings
38
 * @property bool $Active
39
 * @property string $ErrorMessage
40
 * @property bool $Notified
41
 */
42
class Client extends DataObject
43
{
44
    private static $table_name = 'BBP_Monitoring_Client';
0 ignored issues
show
The private property $table_name is not used, and could be removed.
Loading history...
45
    private static $fetch_delay_warning = 600;
0 ignored issues
show
The private property $fetch_delay_warning is not used, and could be removed.
Loading history...
46
    private static $db = [
0 ignored issues
show
The private property $db is not used, and could be removed.
Loading history...
47
        'Title' => 'Varchar',
48
        'BaseURL' => 'Varchar',
49
        'UUID' => 'Varchar',
50
        'EncSecret' => 'Text',
51
        'EncSalt' => 'Text',
52
        'APIKey' => 'Text',
53
        'LastFetch' => 'Datetime',
54
        'ClientData' => 'Text',
55
        'FetchError' => 'Boolean',
56
        'HasWarnings' => 'Boolean',
57
        'Active' => 'Boolean',
58
        'ErrorMessage' => 'Text',
59
        'Notified' => 'Boolean'
60
    ];
61
62
    private static $summary_fields = [
0 ignored issues
show
The private property $summary_fields is not used, and could be removed.
Loading history...
63
        'Title' => 'Site',
64
        'BaseURL' => 'Base URL',
65
        'UUID' => 'Client ID',
66
        'Active.Nice' => 'Active',
67
        'LastFetch.Nice' => 'Last Comms',
68
        'StatusHTML' => 'Status'
69
    ];
70
71
    private static $indexes = [
0 ignored issues
show
The private property $indexes is not used, and could be removed.
Loading history...
72
        'UUID' => true
73
    ];
74
75
    /**
76
     * Get an HTML snippet to show the connection status of the client
77
     * @return DBField
78
     */
79
    public function getStatusHTML()
80
    {
81
        $statusClass = ($this->FetchError) ? 'status-error' : 'status-ok';
82
        $lastFetch = ($this->LastFetch) ? $this->LastFetch : 0;
83
        $threshold = strtotime($lastFetch) + $this->config()->get('fetch_delay_warning');
84
        if ($threshold <= time() && !$this->FetchError) {
85
            $statusClass = 'status-warn';
86
        }
87
88
        $warning = ($this->HasWarnings) ? "âš " : "";
89
90
        return DBField::create_field(
91
            'HTMLFragment',
92
            HTML::createTag('div', ['class' => 'bbp-monitoring_status-dot ' . $statusClass], ' ') .
93
            HTML::createTag('div', ['class' => 'bbp-monitoring_status-warning ' . $warning], $warning)
94
        );
95
    }
96
97
98
    /**
99
     * @param $uuid
100
     * @return Client|null
101
     */
102
    public static function getByUUID($uuid)
103
    {
104
        return self::get_one(self::class, ['UUID' => $uuid]);
105
    }
106
107
    public function getCMSFields()
108
    {
109
        $fields = parent::getCMSFields();
110
        $fields->removeByName([
111
            'EncSecret',
112
            'EncSalt',
113
            'APIKey',
114
            'LastFetch',
115
            'ClientData',
116
            'FetchError',
117
            'UUID',
118
            'ErrorMessage',
119
            'HasWarnings',
120
            'Notified'
121
        ]);
122
123
        $session = Controller::curr()->getRequest()->getSession();
124
        $showSecurity = ($session->get('initial') === 'yes');
125
126
        if ($showSecurity) {
127
            $warning = _t(__CLASS__ . '.credentialswarning',
128
                'Warning!  These will not be displayed again!  Make sure you make a note of them, you will need them for the client machine.');
129
            $info = _t(__CLASS__ . '.credentialsinfo',
130
                'Copy and paste these environment variables into your client configuration:');
131
            $envTemplate = <<<EOT
132
MONITORING_ENC_SECRET=%s
133
MONITORING_ENC_SALT=%s
134
MONITORING_API_KEY=%s
135
MONITORING_UUID=%s
136
EOT;
137
138
            $storageSecret = Environment::getEnv('MONITORING_STORAGE_SECRET');
139
            $storageSalt = Environment::getEnv('MONITORING_STORAGE_SALT');
140
141
            $encHelper = new EncryptionHelper($storageSecret, $storageSalt);
142
            $encSecret = $encHelper->decrypt($this->EncSecret);
143
            $encSalt = $encHelper->decrypt($this->EncSalt);
144
            $apikey = $encHelper->decrypt($this->APIKey);
145
146
            $creds = sprintf($envTemplate, $encSecret, $encSalt, $apikey, $this->UUID);
147
148
            $credsContent = HTML::createTag('h2', [], _t(__CLASS__ . '.client-credentials', 'Client Credentials')) .
149
                HTML::createTag('p', [
150
                    'class' => 'bbp-monitoring_text-bold'
151
                ], $warning) .
152
                HTML::createTag('p', [], $info) .
153
                HTML::createTag('pre', [], $creds);
154
155
            $fields->addFieldsToTab('Root.Main', [
156
                LiteralField::create('monitoringclientdata', HTML::createTag('div', [
157
                    'class' => 'bbp-monitoring_alert-box'
158
                ], $credsContent))
159
            ]);
160
161
            $session->clear('initial');
162
        } else {
163
            $clientData = $this->showClientData();
164
            if ($clientData) {
165
                $fields->addFieldsToTab('Root.Main', LiteralField::create('clientdata', $clientData));
166
            }
167
        }
168
169
        return $fields;
170
    }
171
172
    /**
173
     * Get the client data in HTML format
174
     * @return false|mixed
175
     * @throws \ReflectionException
176
     */
177
    private function showClientData()
178
    {
179
        $warnings = [];
180
        $reports = '';
181
182
        $res = $this->getConnectionReport();
183
184
        if ($this->ClientData) {
185
            $helper = new ClientHelper($this);
186
            $encHelper = new EncryptionHelper($helper->getEncryptionSecret(), $helper->getEncryptionSalt());
187
            $clientData = $encHelper->decrypt($this->ClientData);
188
            if ($clientData) {
189
                $clientDataArray = unserialize($clientData);
190
191
                //Find all the classes which implement our client interface and see if the data array contains something for them
192
                $clientClasses = ClassInfo::implementorsOf(MonitoringClientInterface::class);
193
                foreach ($clientClasses as $fqcn) {
194
195
                    $ref = new \ReflectionClass($fqcn);
196
                    $monitorClass = $ref->newInstance();
197
                    $monitorName = $monitorClass->getClientName();
198
199
                    if (isset($clientDataArray[$monitorName])) {
200
                        $reports .= $monitorClass->getReport($clientDataArray[$monitorName]);
201
                        $monitorWarnings = $monitorClass->getWarnings($clientDataArray[$monitorName]);
202
                        if ($monitorWarnings) {
203
                            $warnings = array_merge($warnings, $monitorWarnings);
204
                        }
205
                    }
206
                }
207
            }
208
209
            $res .= $this->getWarningsMarkup($warnings);
210
            $res .= $reports;
211
        }
212
213
214
        return $res;
215
    }
216
217
218
    /**
219
     * Generate some markup for the warnings
220
     * @param array $warnings
221
     * @return \SilverStripe\ORM\FieldType\DBHTMLText
222
     */
223
    private function getWarningsMarkup(array $warnings) {
224
        $warningList = ArrayList::create();
225
        foreach ($warnings as $warning) {
226
            $warningList->push(ArrayData::create([
227
                'Message' => $warning
228
            ]));
229
        }
230
        $viewer = new SSViewer('BiffBangPow/SSMonitor/Server/Client/Warnings');
231
        return $viewer->process(ArrayData::create([
232
            'Warnings' => $warningList
233
        ]));
234
    }
235
236
    /**
237
     * Get the connection info for the client and return an HTML snippet for the client screen
238
     * @return string
239
     */
240
    private function getConnectionReport()
241
    {
242
        if ($this->FetchError) {
243
            //Can't connect to the client
244
            $status = 'error';
245
        } else {
246
            $lastFetch = ($this->LastFetch) ? $this->LastFetch : 0;
247
            $threshold = strtotime($lastFetch) + $this->config()->get('fetch_delay_warning');
248
            if ($threshold <= time() && !$this->FetchError) {
249
                //Last connection was a while ago
250
                $status = 'warning';
251
            } else {
252
                //Connection OK - just report
253
                $status = 'ok';
254
            }
255
        }
256
257
        $viewer = new SSViewer('BiffBangPow/SSMonitor/Server/Client/ConnectionStatus');
258
        return $viewer->process(ArrayData::create([
259
            'Status' => $status,
260
            'LastFetch' => $this->LastFetch,
261
            'LastFetchFormatted' => DBDatetime::create()->setValue($this->LastFetch)->FormatFromSettings()
262
        ]));
263
    }
264
265
    /**
266
     * Update the warning status for the client based on the latest data
267
     * Generally called from onAfterWrite() to remove the need to analyse the data for every gridfield view
268
     * @return void
269
     */
270
    private function updateWarningStatus()
271
    {
272
        if (!$this->ClientData) {
273
            return;
274
        }
275
        $helper = new ClientHelper($this);
276
        $encHelper = new EncryptionHelper($helper->getEncryptionSecret(), $helper->getEncryptionSalt());
277
        $clientData = $encHelper->decrypt($this->ClientData);
278
        if ($clientData) {
279
            $res = null;
0 ignored issues
show
The assignment to $res is dead and can be removed.
Loading history...
280
            $clientDataArray = unserialize($clientData);
281
            $clientClasses = ClassInfo::implementorsOf(MonitoringClientInterface::class);
282
283
            foreach ($clientClasses as $fqcn) {
284
285
                $ref = new \ReflectionClass($fqcn);
286
                $monitorClass = $ref->newInstance();
287
                $monitorName = $monitorClass->getClientName();
288
289
                //Injector::inst()->get(LoggerInterface::class)->info("Checking " . $monitorName);
290
291
                if (isset($clientDataArray[$monitorName])) {
292
                    if ($monitorClass->getWarnings($clientDataArray[$monitorName]) !== false) {
293
                        $tableName = self::getSchema()->tableName(self::class);
294
                        SQLUpdate::create()
295
                            ->setTable($tableName)
296
                            ->setAssignments([
297
                                'HasWarnings' => 1
298
                            ])
299
                            ->setWhere([
300
                                'ID' => $this->ID
301
                            ])
302
                            ->execute();
303
304
                        return;
305
                    }
306
                }
307
            }
308
        }
309
    }
310
311
    /**
312
     * @throws \Exception
313
     */
314
    public function onBeforeWrite()
315
    {
316
        parent::onBeforeWrite();
317
        if (!$this->isInDB()) {
318
            $security = $this->generateSecurity();
319
320
            //Encrypt the data for storage
321
            $storageSecret = Environment::getEnv('MONITORING_STORAGE_SECRET');
322
            $storageSalt = Environment::getEnv('MONITORING_STORAGE_SALT');
323
324
            $encHelper = new EncryptionHelper($storageSecret, $storageSalt);
325
            $this->EncSecret = $encHelper->encrypt($security['secret']);
326
            $this->EncSalt = $encHelper->encrypt($security['salt']);
327
            $this->APIKey = $encHelper->encrypt($security['apikey']);
328
            $uuid = Uuid::uuid4();
329
            $this->UUID = $uuid->toString();
330
331
            $session = Controller::curr()->getRequest()->getSession();
332
            $session->set('initial', 'yes');
333
        }
334
335
        //Default the warnings to false, we will update the status in onAfterWrite()
336
        $this->HasWarnings = false;
337
    }
338
339
    public function onAfterWrite()
340
    {
341
        parent::onAfterWrite();
342
        $this->updateWarningStatus();
343
    }
344
345
346
    /**
347
     * @return false|string[]
348
     * @todo - Handle the exceptions nicely
349
     */
350
    private function generateSecurity()
351
    {
352
        try {
353
            $encSecret = EncryptionHelper::generateRandomString(64);
354
            $encSalt = EncryptionHelper::generateRandomString(32);
355
            $apiKey = EncryptionHelper::generateRandomString(50);
356
357
            return [
358
                'secret' => $encSecret,
359
                'salt' => $encSalt,
360
                'apikey' => $apiKey
361
            ];
362
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
363
364
        }
365
        return false;
366
    }
367
368
}
369