Completed
Push — master ( a718de...b66a0b )
by Russell
02:12
created

VerifiableAdminController::setVisualiser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
/**
4
 * @author  Russell Michell 2018 <[email protected]>
5
 * @package silverstripe-verifiable
6
 */
7
8
namespace PhpTek\Verifiable\Control;
9
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\HTTPRequest;
12
use SilverStripe\Versioned\Versioned;
13
use SilverStripe\ORM\ValidationException;
14
use SilverStripe\Security\Security;
15
use SilverStripe\Security\Permission;
16
use Dcentrica\Viz\ChainpointViz;
17
use PhpTek\Verifiable\ORM\FieldType\ChainpointProof;
18
19
/**
20
 * Accepts incoming requests for data verification e.g. from within the CMS
21
 * or framework's admin area, proxies them through {@link VerifiableService} and
22
 * sends them on their way.
23
 *
24
 * Will proxy validation requests to the currently configured backend for both
25
 * {@link SiteTree} and {@link DataObject} subclasses.
26
 */
27
class VerifiableAdminController extends Controller
28
{
29
    /**
30
     * No local proof was found for this version. If this is the first version,
31
     * you can safely ignore this message. Otherwise, this is evidence that this
32
     * version has been tampered-with.
33
     *
34
     * @var string
35
     */
36
    const STATUS_LOCAL_PROOF_NONE = 'Local Proof Not Found';
37
38
    /**
39
     * One or more key components of the local proof, were found to be invalid.
40
     * Evidence that the record has been tampered-with.
41
     *
42
     * @var string
43
     */
44
    const STATUS_LOCAL_COMPONENT_INVALID = 'Local Components Invalid';
45
46
    /**
47
     * A mismatch exists between the stored hash for this version, and the data the
48
     * hash was generated from. Evidence that the record has been tampered-with.
49
     *
50
     * @var string
51
     */
52
    const STATUS_LOCAL_HASH_INVALID = 'Local Hash Invalid';
53
54
    /**
55
     * All verification checks passed. This version's hash and proof are intact and verified.
56
     *
57
     * @var string
58
     */
59
    const STATUS_VERIFIED_OK = 'Verified';
60
61
    /**
62
     * Some or all verification checks failed. This version's hash and proof are not intact.
63
     * Evidence that the record has been tampered-with.
64
     *
65
     * @var string
66
     */
67
    const STATUS_VERIFIED_FAIL = 'Verification failure';
68
69
    /**
70
     * This version is unverified. If this state persists, something is not working
71
     * correctly. Please consult your developer.
72
     *
73
     * @var string
74
     */
75
    const STATUS_UNVERIFIED = 'Unverified';
76
77
    /**
78
     * This version's hash confirmation is currently pending. If it's been more than
79
     * two hours since submission, try again.
80
     *
81
     * @var string
82
     */
83
    const STATUS_PENDING = 'Pending';
84
85
    /**
86
     * This version's hash confirmation is currently awaiting processing. If it's
87
     * been more than two hours since submission, please check the automated update job.
88
     * Consult your developer..
89
     *
90
     * @var string
91
     */
92
    const STATUS_INITIAL = 'Initial';
93
94
    /**
95
     * The verification process encountered a network error communicating with the
96
     * backend. Try again in a moment.
97
     *
98
     * @var string
99
     */
100
    const STATUS_UPSTREAM_ERROR = 'Upstream Error';
101
102
    /**
103
     * @var array
104
     */
105
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
106
        'verifyhash',
107
    ];
108
109
    /**
110
     * Verify the integrity of arbitrary data by means of a single hash.
111
     *
112
     * Responds to URIs of the following prototype: /verifiable/verify/<model>/<ID>/<VID>
113
     * by echoing a JSON response for consumption by client-side logic.
114
     *
115
     * @param  HTTPRequest $request
116
     * @return string
117
     */
118
    public function verifyhash(HTTPRequest $request) : string
119
    {
120
        if (!Permission::checkMember(Security::getCurrentUser(), 'ADMIN')) {
121
            return $this->httpError(401, 'Unauthorised');
122
        }
123
124
        $class = $request->param('ClassName');
125
        $id = $request->param('ModelID');
126
        $version = $request->param('VersionID');
127
        $verificationData = [];
128
129
        if (
130
                empty($id) || !is_numeric($id) ||
131
                empty($version) || !is_numeric($version) ||
132
                empty($class)
133
            ) {
134
            return $this->httpError(400, 'Bad request');
135
        }
136
137
        // Class is passed as dash-separated FQCN
138
        $class = str_replace('-', '\\', $class);
139
140
        if (!class_exists($class)) {
141
            return $this->httpError(400, 'Bad request');
142
        }
143
144
        if (!$record = Versioned::get_version($class, $id, $version)) {
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of SilverStripe\Versioned\Versioned::get_version(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

144
        if (!$record = Versioned::get_version($class, /** @scrutinizer ignore-type */ $id, $version)) {
Loading history...
Bug introduced by
$version of type string is incompatible with the type integer expected by parameter $version of SilverStripe\Versioned\Versioned::get_version(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

144
        if (!$record = Versioned::get_version($class, $id, /** @scrutinizer ignore-type */ $version)) {
Loading history...
145
            return $this->httpError(400, 'Bad request');
146
        }
147
148
        try {
149
            $status = $this->getStatus($record, $record->getExtraByIndex(), $verificationData);
150
        } catch (ValidationException $ex) {
151
            $status = self::STATUS_UPSTREAM_ERROR;
152
        }
153
154
        $response = json_encode([
155
            'Record' => [
156
                'RecordID' => "$record->RecordID",
157
                'CreatedDate' => self::display_date($record->Created),
158
                'Version' => "$record->Version",
159
                'Class' => get_class($record),
160
                'VerifiableFields' => $record->verifiableFields(),
161
            ],
162
            'Status' => [
163
                'Nice' => $status,
164
                'Code' => $this->getCodeMeta($status, 'code'),
165
                'Def' => $this->getCodeMeta($status, 'defn'),
166
            ],
167
            'Proof' => [
168
                'SubmittedDate' => self::display_date($verificationData['SubmittedAt'] ?? ''),
169
                'SubmittedTo' => $record->dbObject('Extra')->getStoreAsArray(),
170
                'ChainpointProof' => $verificationData['ChainpointProof'] ?? '',
171
                'ChainpointViz' => $verificationData['ChainpointViz'] ?? '',
172
                'MerkleRoot' => $verificationData['MerkleRoot'] ?? '',
173
                'BlockHeight' => $verificationData['BlockHeight'] ?? '',
174
                'Hashes' => $verificationData['Hashes'] ?? '',
175
                'UUID' => $verificationData['UUID'] ?? '',
176
                'GV' => $verificationData['Dotfile'] ?? '',
177
                'TXID' => $verificationData['TXID'] ?? '',
178
                'OPRET' => $verificationData['OPRET'] ?? '',
179
            ]
180
        ], JSON_UNESCAPED_SLASHES);
181
182
        $this->renderJSON($response);
183
184
        return $response;
185
    }
186
187
    /**
188
     * Return data used for verifiable statuses.
189
     *
190
     * @param  string $status
191
     * @param  string $key
192
     * @return mixed
193
     */
194
    private function getCodeMeta($status, $key)
195
    {
196
        $refl = new \ReflectionClass(__CLASS__);
197
        $const = array_search($status, $refl->getConstants());
198
        $keyJson = file_get_contents(realpath(__DIR__) . '/../../statuses.json');
199
        $keyMap = json_decode($keyJson, true);
200
        $defn = '';
201
202
        foreach ($keyMap as $map) {
203
            if (isset($map[$const])) {
204
                $defn = $map[$const];
205
            }
206
        }
207
208
        $data = [
209
            'code' => $const,
210
            'defn' => $defn,
211
        ];
212
213
        return $data[$key] ?? $data;
214
    }
215
216
    /**
217
     * Centerpiece of verification controller requests. Gives us the current
218
     * verification status of the given record. Takes into account the state of
219
     * the saved proof as well as by making a backend verification call.
220
     *
221
     * @param  DataObject $record           The versioned record we're checking.
0 ignored issues
show
Bug introduced by
The type PhpTek\Verifiable\Control\DataObject was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
222
     * @param  array      $nodes            Array of cached chainpoint node IPs.
223
     * @param  array      $verificationData Array of data for manual verification.
224
     * @return string
225
     */
226
    public function getStatus($record, $nodes, &$verificationData) : string
227
    {
228
        // Set some extra data on the service. In this case, the actual chainpoint
229
        // node addresses, used to submit hashes for the given $record
230
        $this->service->setExtra($nodes);
0 ignored issues
show
Bug introduced by
The method setExtra() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

230
        $this->service->/** @scrutinizer ignore-call */ 
231
                        setExtra($nodes);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug Best Practice introduced by
The property service does not exist on PhpTek\Verifiable\Contro...rifiableAdminController. Since you implemented __get, consider adding a @property annotation.
Loading history...
231
        $proof = $record->dbObject('Proof');
232
233
        // Basic existence of proof
234
        if (!$proof->getValue()) {
235
            return self::STATUS_LOCAL_PROOF_NONE;
236
        }
237
238
        if ($proof->isInitial()) {
239
            return self::STATUS_INITIAL;
240
        }
241
242
        if ($proof->isPending()) {
243
            return self::STATUS_PENDING;
244
        }
245
246
        // So the saved proof claims to be full. Perform some rudimentary checks
247
        // before we send a full-blown verification request to the backend
248
        if ($proof->isFull()) {
249
            // Tests 3 of the key components of the local proof. Sending a verification
250
            // request will do this and much more for us, but rudimentary local checks
251
            // may prevent unnecessary network requests
252
            // TODO Integrate or port github.com/chainpoint/chainpoint-node-validator
253
            // and replace these local checks
254
            if (!$proof->getHashIdNode() || !$proof->getProof() || !count($proof->getAnchorsComplete())) {
255
                return self::STATUS_LOCAL_COMPONENT_INVALID;
256
            }
257
258
            // We've got this far. The local proof seems to be good. Let's verify
259
            // it against the backend
260
            $response = $this->service->call('verify', $proof->getProof());
261
            $responseModel = ChainpointProof::create()->setValue($response);
262
            $isVerified = $responseModel->isVerified();
263
264
            if (!$isVerified) {
265
                return self::STATUS_VERIFIED_FAIL;
266
            }
267
268
            // OK, so we have an intact local full proof, let's ensure it still
269
            // matches a hash of the data it purports to represent
270
            $remoteHash = $responseModel->getHash();
271
            $reCalculated = $this->service->hash($record->getSource());
272
273
            if ($reCalculated !== $remoteHash) {
274
                return self::STATUS_LOCAL_HASH_INVALID;
275
            }
276
277
            // Tight coupling with ChainpointViz and our
278
            // reliance on getBtc*() for the proper function of this controller
279
            $chainpointViz = $this->visualiser
280
                ->setReceipt($proof->getProofJson())
281
                ->setChain('btc');
282
283
            // Setup data for display & manual re-verification
284
            $v3proof = ChainpointProof::create()->setValue($proof->getProofJson());
285
            $verificationData['ChainpointProof'] = $proof->getProofJson();
286
            $verificationData['MerkleRoot'] = $v3proof->getMerkleRoot('btc');
287
            $verificationData['BlockHeight'] = $v3proof->getBitcoinBlockHeight();
288
            $verificationData['UUID'] = $v3proof->getHashIdNode();
289
            $verificationData['TXID'] = $chainpointViz->getBtcTXID();
290
            $verificationData['OPRET'] = $chainpointViz->getBtcOpReturn();
291
            $verificationData['ChainpointViz'] = $this->getProofViz($chainpointViz);
292
            $verificationData['SubmittedAt'] = $v3proof->getSubmittedAt();
293
            $verificationData['Hashes'] = [
294
                'local' => $reCalculated,
295
                'remote' => $v3proof->getHash(),
296
            ];
297
298
            // All is well. As you were...
299
            return self::STATUS_VERIFIED_OK;
300
        }
301
302
        // Default status
303
        return self::STATUS_UNVERIFIED;
304
    }
305
306
    /**
307
     * Properly return JSON, allowing consumers to render returned JSON correctly.
308
     *
309
     * @param  string $json
310
     * @return void
311
     */
312
    private function renderJSON(string $json)
313
    {
314
        header('Content-Type: application/json');
315
        echo $json;
316
        exit(0);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
317
    }
318
319
    /**
320
     * Common date format in ISO 8601.
321
     *
322
     * @return string
323
     */
324
    private static function display_date(string $date) : string
325
    {
326
        return date('Y-m-d H:i:s', strtotime($date));
327
    }
328
329
    /**
330
     * Generate a visualisation of a chainpoint proof.
331
     *
332
     * @param  ChainpointViz $viz         A passed ChainpointViz object to work with.
333
     * @param  string $format             Any format accepted by Graphviz.
334
     * @return string                     A URI path to the location of the generated graphic.
335
     */
336
    private function getProofViz(ChainpointViz $viz, $format = 'svg')
337
    {
338
        $fileName = sprintf('chainpoint.%s', $format);
339
        $filePath = sprintf('%s/%s', ASSETS_PATH, $fileName);
340
        $fileHref = sprintf('/%s/%s', ASSETS_DIR, $fileName);
341
342
        $viz->setFilename($filePath);
343
        $viz->visualise();
344
345
        if (!file_exists($filePath)) {
346
            return '';
347
        }
348
349
        return $fileHref;
350
    }
351
352
    /**
353
     * Simple setter.
354
     *
355
     * @param  ChainpointViz $visualiser
356
     * @return VerifiableAdminController
357
     */
358
    public function setVisualiser($visualiser) : VerifiableAdminController
359
    {
360
        $this->visualiser = $visualiser;
0 ignored issues
show
Bug Best Practice introduced by
The property visualiser does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
361
362
        return $this;
363
    }
364
365
}
366