Passed
Pull Request — master (#6)
by James
15:34
created

Client::showClientData()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 18
nc 2
nop 0
dl 0
loc 32
rs 9.3554
c 1
b 0
f 0
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
introduced by
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
introduced by
The private property $fetch_delay_warning is not used, and could be removed.
Loading history...
46
    private static $db = [
0 ignored issues
show
introduced by
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
introduced by
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
introduced by
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->getClientDataArray()) {
185
            $clientDataArray = $this->getClientDataArray();
186
187
            //Find all the classes which implement our client interface and see if the data array contains something for them
188
            $clientClasses = ClassInfo::implementorsOf(MonitoringClientInterface::class);
189
            foreach ($clientClasses as $fqcn) {
190
191
                $ref = new \ReflectionClass($fqcn);
192
                $monitorClass = $ref->newInstance();
193
                $monitorName = $monitorClass->getClientName();
194
195
                if (isset($clientDataArray[$monitorName])) {
196
                    $reports .= $monitorClass->getReport($clientDataArray[$monitorName]);
197
                    $monitorWarnings = $monitorClass->getWarnings($clientDataArray[$monitorName]);
198
                    if ($monitorWarnings) {
199
                        $warnings = array_merge($warnings, $monitorWarnings);
200
                    }
201
                }
202
            }
203
        }
204
205
        $res .= $this->getWarningsMarkup($warnings);
206
        $res .= $reports;
207
208
        return $res;
209
    }
210
211
    /**
212
     * Get the client data in an array
213
     * @return false|array
214
     */
215
    function getClientDataArray()
216
    {
217
        if ($this->ClientData) {
218
            $helper = new ClientHelper($this);
219
            $encHelper = new EncryptionHelper($helper->getEncryptionSecret(), $helper->getEncryptionSalt());
220
            $clientData = $encHelper->decrypt($this->ClientData);
221
            if ($clientData) {
222
                $clientDataArray = unserialize($clientData);
223
                return $clientDataArray;
224
            }
225
        }
226
        return false;
227
    }
228
229
    /**
230
     * Generate some markup for the warnings
231
     * @param array $warnings
232
     * @return \SilverStripe\ORM\FieldType\DBHTMLText
233
     */
234
    private function getWarningsMarkup(array $warnings) {
235
        $warningList = ArrayList::create();
236
        foreach ($warnings as $warning) {
237
            $warningList->push(ArrayData::create([
238
                'Message' => $warning
239
            ]));
240
        }
241
        $viewer = new SSViewer('BiffBangPow/SSMonitor/Server/Client/Warnings');
242
        return $viewer->process(ArrayData::create([
243
            'Warnings' => $warningList
244
        ]));
245
    }
246
247
    /**
248
     * Get the connection info for the client and return an HTML snippet for the client screen
249
     * @return string
250
     */
251
    private function getConnectionReport()
252
    {
253
        if ($this->FetchError) {
254
            //Can't connect to the client
255
            $status = 'error';
256
        } else {
257
            $lastFetch = ($this->LastFetch) ? $this->LastFetch : 0;
258
            $threshold = strtotime($lastFetch) + $this->config()->get('fetch_delay_warning');
259
            if ($threshold <= time() && !$this->FetchError) {
260
                //Last connection was a while ago
261
                $status = 'warning';
262
            } else {
263
                //Connection OK - just report
264
                $status = 'ok';
265
            }
266
        }
267
268
        $viewer = new SSViewer('BiffBangPow/SSMonitor/Server/Client/ConnectionStatus');
269
        return $viewer->process(ArrayData::create([
270
            'Status' => $status,
271
            'LastFetch' => $this->LastFetch,
272
            'LastFetchFormatted' => DBDatetime::create()->setValue($this->LastFetch)->FormatFromSettings()
273
        ]));
274
    }
275
276
    /**
277
     * Update the warning status for the client based on the latest data
278
     * Generally called from onAfterWrite() to remove the need to analyse the data for every gridfield view
279
     * @return void
280
     */
281
    private function updateWarningStatus()
282
    {
283
        if (!$this->ClientData) {
284
            return;
285
        }
286
        $helper = new ClientHelper($this);
287
        $encHelper = new EncryptionHelper($helper->getEncryptionSecret(), $helper->getEncryptionSalt());
288
        $clientData = $encHelper->decrypt($this->ClientData);
289
        if ($clientData) {
290
            $res = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $res is dead and can be removed.
Loading history...
291
            $clientDataArray = unserialize($clientData);
292
            $clientClasses = ClassInfo::implementorsOf(MonitoringClientInterface::class);
293
294
            foreach ($clientClasses as $fqcn) {
295
296
                $ref = new \ReflectionClass($fqcn);
297
                $monitorClass = $ref->newInstance();
298
                $monitorName = $monitorClass->getClientName();
299
300
                //Injector::inst()->get(LoggerInterface::class)->info("Checking " . $monitorName);
301
302
                if (isset($clientDataArray[$monitorName])) {
303
                    if ($monitorClass->getWarnings($clientDataArray[$monitorName]) !== false) {
304
                        $tableName = self::getSchema()->tableName(self::class);
305
                        SQLUpdate::create()
306
                            ->setTable($tableName)
307
                            ->setAssignments([
308
                                'HasWarnings' => 1
309
                            ])
310
                            ->setWhere([
311
                                'ID' => $this->ID
312
                            ])
313
                            ->execute();
314
315
                        return;
316
                    }
317
                }
318
            }
319
        }
320
    }
321
322
    /**
323
     * @throws \Exception
324
     */
325
    public function onBeforeWrite()
326
    {
327
        parent::onBeforeWrite();
328
        if (!$this->isInDB()) {
329
            $security = $this->generateSecurity();
330
331
            //Encrypt the data for storage
332
            $storageSecret = Environment::getEnv('MONITORING_STORAGE_SECRET');
333
            $storageSalt = Environment::getEnv('MONITORING_STORAGE_SALT');
334
335
            $encHelper = new EncryptionHelper($storageSecret, $storageSalt);
336
            $this->EncSecret = $encHelper->encrypt($security['secret']);
337
            $this->EncSalt = $encHelper->encrypt($security['salt']);
338
            $this->APIKey = $encHelper->encrypt($security['apikey']);
339
            $uuid = Uuid::uuid4();
340
            $this->UUID = $uuid->toString();
341
342
            $session = Controller::curr()->getRequest()->getSession();
343
            $session->set('initial', 'yes');
344
        }
345
346
        //Default the warnings to false, we will update the status in onAfterWrite()
347
        $this->HasWarnings = false;
348
    }
349
350
    public function onAfterWrite()
351
    {
352
        parent::onAfterWrite();
353
        $this->updateWarningStatus();
354
    }
355
356
357
    /**
358
     * @return false|string[]
359
     * @todo - Handle the exceptions nicely
360
     */
361
    private function generateSecurity()
362
    {
363
        try {
364
            $encSecret = EncryptionHelper::generateRandomString(64);
365
            $encSalt = EncryptionHelper::generateRandomString(32);
366
            $apiKey = EncryptionHelper::generateRandomString(50);
367
368
            return [
369
                'secret' => $encSecret,
370
                'salt' => $encSalt,
371
                'apikey' => $apiKey
372
            ];
373
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
374
375
        }
376
        return false;
377
    }
378
379
}
380