Completed
Pull Request — master (#31)
by Russell
04:37
created

VerifiableAdminController::display_date()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
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\Controller;
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\Model\VerifiableExtension;
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 void
117
     */
118
    public function verifyhash(HTTPRequest $request)
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 (empty($id) || !is_numeric($id) ||
130
                empty($version) || !is_numeric($version) ||
131
                empty($class)) {
132
            return $this->httpError(400, 'Bad request');
133
        }
134
135
        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

135
        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

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

217
        $this->service->/** @scrutinizer ignore-call */ 
218
                        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...
218
        $proof = $record->dbObject('Proof');
219
220
        // Basic existence of proof
221
        if (!$proof->getValue()) {
222
            return self::STATUS_LOCAL_PROOF_NONE;
223
        }
224
225
        if ($proof->isInitial()) {
226
            return self::STATUS_INITIAL;
227
        }
228
229
        if ($proof->isPending()) {
230
            return self::STATUS_PENDING;
231
        }
232
233
        // So the saved proof claims to be full. Perform some rudimentary checks
234
        // before we send a full-blown verification request to the backend
235
        if ($proof->isFull()) {
236
            // Tests 3 of the key components of the local proof. Sending a verification
237
            // request will do this and much more for us, but rudimentary local checks
238
            // can prevent a network request
239
            if (!$proof->getHashIdNode() || !$proof->getProof() || !count($proof->getAnchorsComplete())) {
240
                return self::STATUS_LOCAL_COMPONENT_INVALID;
241
            }
242
243
            // We've got this far. The local proof seems to be good. Let's verify
244
            // it against the backend
245
            $response = $this->service->call('verify', $proof->getProof());
246
            $responseModel = ChainpointProof::create()->setValue($response);
247
            $isVerified = $responseModel->isVerified();
248
249
            if (!$isVerified) {
250
                return self::STATUS_VERIFIED_FAIL;
251
            }
252
253
            // OK, so we have an intact local full proof, let's ensure it still
254
            // matches a hash of the data it purports to represent
255
            $remoteHash = $responseModel->getHash();
256
            $reCalculated = $this->service->hash($record->source());
257
258
            if ($reCalculated !== $remoteHash) {
259
                return self::STATUS_LOCAL_HASH_INVALID;
260
            }
261
262
            // Setup data for display & manual re-verification
263
            $v3proof = ChainpointProof::create()->setValue($proof->getProofJson());
264
            $verificationData['ChainpointProof'] = $proof->getProofJson(true);
265
            $verificationData['MerkleRoot'] = $v3proof->getMerkleRoot('btc');
266
            $verificationData['BlockHeight'] = $v3proof->getBitcoinBlockHeight();
267
            $verificationData['UUID'] = $v3proof->getHashIdNode();
268
            $verificationData['SubmittedAt'] = $v3proof->getSubmittedAt();
269
            $verificationData['Hashes'] = [
270
                'local' => $reCalculated,
271
                'remote' => $v3proof->getHash(),
272
            ];
273
274
            // All is well. As you were...
275
            return self::STATUS_VERIFIED_OK;
276
        }
277
278
        // Default status
279
        return self::STATUS_UNVERIFIED;
280
    }
281
282
    /**
283
     * Properly return JSON, allowing consumers to render returned JSON correctly.
284
     *
285
     * @param  string $json
286
     * @return void
287
     */
288
    private function renderJSON(string $json)
289
    {
290
        header('Content-Type: application/json');
291
        echo $json;
292
        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...
293
    }
294
295
    /**
296
     * Common date format in ISO 8601.
297
     *
298
     * @return string
299
     */
300
    private static function display_date(string $date) : string
301
    {
302
        return date('Y-m-d H:i:s', strtotime($date));
303
    }
304
305
}
306