Completed
Push — master ( 7c5f78...354d51 )
by Russell
02:35
created

VerifiableAdminController::verifyhash()   B

Complexity

Conditions 11
Paths 6

Size

Total Lines 67
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 46
dl 0
loc 67
rs 7.3166
c 0
b 0
f 0
cc 11
nc 6
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

145
        if (!$record = Versioned::get_version($class, $id, /** @scrutinizer ignore-type */ $version)) {
Loading history...
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

145
        if (!$record = Versioned::get_version($class, /** @scrutinizer ignore-type */ $id, $version)) {
Loading history...
146
            return $this->httpError(400, 'Bad request');
147
        }
148
149
        try {
150
            $status = $this->getStatus($record, $record->getExtraByIndex(), $verificationData);
151
        } catch (ValidationException $ex) {
152
            $status = self::STATUS_UPSTREAM_ERROR;
153
        }
154
155
        $response = json_encode([
156
            'Record' => [
157
                'RecordID' => "$record->RecordID",
158
                'CreatedDate' => self::display_date($record->Created),
159
                'Version' => "$record->Version",
160
                'Class' => get_class($record),
161
                'VerifiableFields' => $record->sourceMode() === VerifiableExtension::SOURCE_MODE_FIELD ?
162
                    json_decode($record->VerifiableFields) :
163
                    [],
164
            ],
165
            'Status' => [
166
                'Nice' => $status,
167
                'Code' => $this->getCodeMeta($status, 'code'),
168
                'Def' => $this->getCodeMeta($status, 'defn'),
169
            ],
170
            'Proof' => [
171
                'SubmittedDate' => self::display_date($verificationData['SubmittedAt'] ?? ''),
172
                'SubmittedTo' => $record->dbObject('Extra')->getStoreAsArray(),
173
                'ChainpointProof' => $verificationData['ChainpointProof'] ?? '',
174
                'ChainpointViz' => $verificationData['ChainpointViz'] ?? '',
175
                'MerkleRoot' => $verificationData['MerkleRoot'] ?? '',
176
                'BlockHeight' => $verificationData['BlockHeight'] ?? '',
177
                'Hashes' => $verificationData['Hashes'] ?? '',
178
                'UUID' => $verificationData['UUID'] ?? '',
179
                'GV' => $verificationData['Dotfile'] ?? '',
180
                'TXID' => $verificationData['TXID'] ?? '',
181
                'OPRET' => $verificationData['OPRET'] ?? '',
182
            ]
183
        ], JSON_UNESCAPED_SLASHES);
184
185
        $this->renderJSON($response);
186
    }
187
188
    /**
189
     * Return data used for verifiable statuses.
190
     *
191
     * @param  string $status
192
     * @param  string $key
193
     * @return mixed
194
     */
195
    private function getCodeMeta($status, $key)
196
    {
197
        $refl = new \ReflectionClass(__CLASS__);
198
        $const = array_search($status, $refl->getConstants());
199
        $keyJson = file_get_contents(realpath(__DIR__) . '/../../statuses.json');
200
        $keyMap = json_decode($keyJson, true);
201
        $defn = '';
202
203
        foreach ($keyMap as $map) {
204
            if (isset($map[$const])) {
205
                $defn = $map[$const];
206
            }
207
        }
208
209
        $data = [
210
            'code' => $const,
211
            'defn' => $defn,
212
        ];
213
214
        return $data[$key] ?? $data;
215
    }
216
217
    /**
218
     * Gives us the current verification status of the given record. Takes into
219
     * account the state of the saved proof as well as by making a backend
220
     * verification call.
221
     *
222
     * @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...
223
     * @param  array      $nodes            Array of cached chainpoint node IPs.
224
     * @param  array      $verificationData Array of data for manual verification.
225
     * @return string
226
     */
227
    public function getStatus($record, $nodes, &$verificationData) : string
228
    {
229
        // Set some extra data on the service. In this case, the actual chainpoint
230
        // node addresses, used to submit hashes for the given $record
231
        $this->service->setExtra($nodes);
0 ignored issues
show
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...
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

231
        $this->service->/** @scrutinizer ignore-call */ 
232
                        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...
232
        $proof = $record->dbObject('Proof');
233
234
        // Basic existence of proof
235
        if (!$proof->getValue()) {
236
            return self::STATUS_LOCAL_PROOF_NONE;
237
        }
238
239
        if ($proof->isInitial()) {
240
            return self::STATUS_INITIAL;
241
        }
242
243
        if ($proof->isPending()) {
244
            return self::STATUS_PENDING;
245
        }
246
247
        // So the saved proof claims to be full. Perform some rudimentary checks
248
        // before we send a full-blown verification request to the backend
249
        if ($proof->isFull()) {
250
            // Tests 3 of the key components of the local proof. Sending a verification
251
            // request will do this and much more for us, but rudimentary local checks
252
            // can prevent a network request
253
            if (!$proof->getHashIdNode() || !$proof->getProof() || !count($proof->getAnchorsComplete())) {
254
                return self::STATUS_LOCAL_COMPONENT_INVALID;
255
            }
256
257
            // We've got this far. The local proof seems to be good. Let's verify
258
            // it against the backend
259
            $response = $this->service->call('verify', $proof->getProof());
260
            $responseModel = ChainpointProof::create()->setValue($response);
261
            $isVerified = $responseModel->isVerified();
262
263
            if (!$isVerified) {
264
                return self::STATUS_VERIFIED_FAIL;
265
            }
266
267
            // OK, so we have an intact local full proof, let's ensure it still
268
            // matches a hash of the data it purports to represent
269
            $remoteHash = $responseModel->getHash();
270
            $reCalculated = $this->service->hash($record->source());
271
272
            if ($reCalculated !== $remoteHash) {
273
                return self::STATUS_LOCAL_HASH_INVALID;
274
            }
275
276
            $chainpointViz = new ChainpointViz($proof->getProofJson(), 'btc');
277
278
            // Setup data for display & manual re-verification
279
            $v3proof = ChainpointProof::create()->setValue($proof->getProofJson());
280
            $verificationData['ChainpointProof'] = $proof->getProofJson();
281
            $verificationData['MerkleRoot'] = $v3proof->getMerkleRoot('btc');
282
            $verificationData['BlockHeight'] = $v3proof->getBitcoinBlockHeight();
283
            $verificationData['UUID'] = $v3proof->getHashIdNode();
284
            $verificationData['TXID'] = $chainpointViz->getBtcTXID();
285
            $verificationData['OPRET'] = $chainpointViz->getBtcOpReturn();
286
            $verificationData['SubmittedAt'] = $v3proof->getSubmittedAt();
287
            $verificationData['Hashes'] = [
288
                'local' => $reCalculated,
289
                'remote' => $v3proof->getHash(),
290
            ];
291
            $verificationData['ChainpointViz'] = $this->getProofViz($chainpointViz);
292
293
            // All is well. As you were...
294
            return self::STATUS_VERIFIED_OK;
295
        }
296
297
        // Default status
298
        return self::STATUS_UNVERIFIED;
299
    }
300
301
    /**
302
     * Properly return JSON, allowing consumers to render returned JSON correctly.
303
     *
304
     * @param  string $json
305
     * @return void
306
     */
307
    private function renderJSON(string $json)
308
    {
309
        header('Content-Type: application/json');
310
        echo $json;
311
        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...
312
    }
313
314
    /**
315
     * Common date format in ISO 8601.
316
     *
317
     * @return string
318
     */
319
    private static function display_date(string $date) : string
320
    {
321
        return date('Y-m-d H:i:s', strtotime($date));
322
    }
323
324
    /**
325
     * Generate a visualisation of a chainpoint proof.
326
     *
327
     * @param  ChainpointViz $viz         A passed ChainpointViz object to work with.
328
     * @param  string $format             Any format accepted by Graphviz.
329
     * @return string                     A URI path to the location of the generated graphic.
330
     */
331
    private function getProofViz(ChainpointViz $viz, $format = 'svg')
332
    {
333
        $fileName = sprintf('chainpoint.%s', $format);
334
        $filePath = sprintf('%s/%s', ASSETS_PATH, $fileName);
335
        $fileHref = sprintf('/%s/%s', ASSETS_DIR, $fileName);
336
337
        $viz->setFilename($filePath);
338
        $viz->visualize();
339
340
        if (!file_exists($filePath)) {
341
            return '';
342
        }
343
344
        return $fileHref;
345
    }
346
347
}
348