• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

marscoin / martianrepublic / 23751722506

30 Mar 2026 03:03PM UTC coverage: 8.391% (-0.01%) from 8.401%
23751722506

push

github

Martian Congress
refactor: Sprint 2 — architecture cleanup (A3, A4, A7, A9)

A3: Added $fillable to 7 models (Proposals, Vote, Threads, HDWallet,
    IPFSRoot, Publication, Voucher) — closes mass assignment risk

A4: Created config/blockchain.php centralizing all hardcoded URLs
    (RPC, Pebas, IPFS, Explorer, Ballot, Price). Updated 7 files
    to use config() instead of hardcoded strings.

A7: Removed dead code:
    - Deleted ApiController.php.bak, AppHelper.php.bak
    - Deleted unused models: Pkimport.php, Recovery.php
    - Deleted resources/views/wallet/testing.html
    - Removed 8 commented-out routes from web.php and api.php

A9: Added performance indexes migration:
    - proposals.status (phase sync, tab filtering)
    - ballots.userid, ballots.proposalid, ballots(userid,status)
    - votes.proposal_id (vote tallying)
    - forum_posts.author_id, forum_threads.author_id (joins)

A1 (controller split) deferred to dedicated session — too much
in-page JS coupling to batch safely.

79 tests passing, 0 PHPStan errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

2 of 48 new or added lines in 7 files covered. (4.17%)

2 existing lines in 2 files now uncovered.

445 of 5303 relevant lines covered (8.39%)

1.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

2.82
/app/Includes/AppHelper.php
1
<?php
2

3

4
namespace App\Includes;
5
use App\Models\Feed;
6
use App\Models\Publication;
7
use DateTime;
8
use Illuminate\Support\Facades\Cache;
9
use Illuminate\Support\Facades\Log;
10
use TeamTeaTime\Forum\Models\Post;
11
use Carbon\Carbon;
12
use App\Models\Profile;
13
use App\Models\Citizen;
14

15
class AppHelper{
16

17
                /**
18
                 * =========================================================================
19
                 * FILE UPLOAD SECURITY HELPERS
20
                 * =========================================================================
21
                 * Added to prevent malicious file uploads (PHP shells, etc.).
22
                 * Every upload endpoint MUST call one of these validation methods.
23
                 */
24

25
                /**
26
                 * Dangerous file extensions that must never be uploaded.
27
                 * This blocklist prevents execution of server-side scripts.
28
                 */
29
                private static $blockedExtensions = [
30
                        'php', 'phtml', 'php5', 'php7', 'php8', 'phar', 'phps',
31
                        'cgi', 'pl', 'py', 'sh', 'bash', 'exe', 'bat', 'cmd',
32
                        'asp', 'aspx', 'jsp', 'shtml', 'htaccess', 'svg',
33
                ];
34

35
                /**
36
                 * Allowed image extensions and their corresponding MIME types.
37
                 */
38
                private static $allowedImageTypes = [
39
                        'jpg'  => ['image/jpeg'],
40
                        'jpeg' => ['image/jpeg'],
41
                        'png'  => ['image/png'],
42
                        'gif'  => ['image/gif'],
43
                        'webp' => ['image/webp'],
44
                ];
45

46
                /**
47
                 * Allowed extensions for general file uploads (images + json + webm video).
48
                 */
49
                private static $allowedUploadTypes = [
50
                        'jpg'  => ['image/jpeg'],
51
                        'jpeg' => ['image/jpeg'],
52
                        'png'  => ['image/png'],
53
                        'gif'  => ['image/gif'],
54
                        'webp' => ['image/webp'],
55
                        'json' => ['application/json', 'text/plain'],
56
                        'webm' => ['video/webm', 'audio/webm'],
57
                        'markdown' => ['text/plain', 'text/markdown'],
58
                ];
59

60
                /**
61
                 * Maximum upload file size in bytes (5 MB).
62
                 */
63
                private static $maxFileSize = 5242880;
64

65
                /**
66
                 * Check if a file extension is on the blocklist.
67
                 *
68
                 * @param string $extension
69
                 * @return bool True if the extension is blocked (dangerous).
70
                 */
71
                public static function isExtensionBlocked(string $extension): bool
×
72
                {
73
                        return in_array(strtolower(trim($extension)), self::$blockedExtensions, true);
×
74
                }
75

76
                /**
77
                 * Validate an uploaded file (from $request->file()) for security.
78
                 * Checks extension blocklist, allowed extensions, MIME type, and file size.
79
                 *
80
                 * @param \Illuminate\Http\UploadedFile $file
81
                 * @param array|null $allowedTypes Associative array of ext => [mimes]. Defaults to $allowedUploadTypes.
82
                 * @return array ['valid' => bool, 'error' => string|null]
83
                 */
84
                public static function validateUploadedFile($file, ?array $allowedTypes = null): array
×
85
                {
86
                        if (!$file || !$file->isValid()) {
×
87
                                return ['valid' => false, 'error' => 'No valid file provided.'];
×
88
                        }
89

90
                        $allowedTypes = $allowedTypes ?? self::$allowedUploadTypes;
×
91

92
                        // Check file size (max 5MB)
93
                        if ($file->getSize() > self::$maxFileSize) {
×
94
                                return ['valid' => false, 'error' => 'File exceeds maximum size of 5MB.'];
×
95
                        }
96

97
                        // Get and check extension against blocklist
98
                        $extension = strtolower($file->getClientOriginalExtension());
×
99
                        if (self::isExtensionBlocked($extension)) {
×
100
                                Log::warning('Blocked file upload attempt with dangerous extension: ' . $extension);
×
101
                                return ['valid' => false, 'error' => 'File type is not allowed (blocked extension: ' . $extension . ').'];
×
102
                        }
103

104
                        // Check extension is in the allowed list
105
                        if (!array_key_exists($extension, $allowedTypes)) {
×
106
                                return ['valid' => false, 'error' => 'File extension .' . $extension . ' is not permitted.'];
×
107
                        }
108

109
                        // Validate MIME type server-side using finfo (do NOT trust client-reported type)
110
                        $finfo = new \finfo(FILEINFO_MIME_TYPE);
×
111
                        $detectedMime = $finfo->file($file->getRealPath());
×
112
                        $allowedMimes = $allowedTypes[$extension];
×
113

114
                        if (!in_array($detectedMime, $allowedMimes, true)) {
×
115
                                Log::warning('File MIME mismatch: extension=' . $extension . ', detected=' . $detectedMime);
×
116
                                return ['valid' => false, 'error' => 'File MIME type (' . $detectedMime . ') does not match expected type for .' . $extension . '.'];
×
117
                        }
118

119
                        // Check for embedded PHP code in the file contents
120
                        $contents = file_get_contents($file->getRealPath());
×
121
                        if ($contents !== false && self::containsPhpCode($contents)) {
×
122
                                Log::warning('Blocked file upload containing PHP code');
×
123
                                return ['valid' => false, 'error' => 'File contains potentially dangerous content.'];
×
124
                        }
125

126
                        return ['valid' => true, 'error' => null];
×
127
                }
128

129
                /**
130
                 * Validate a base64-decoded image for security.
131
                 * Checks the extracted extension against blocklist and allowed list,
132
                 * validates the decoded binary data MIME type via finfo, and checks size.
133
                 *
134
                 * @param string $dataUri The full data URI (e.g. "data:image/png;base64,iVBOR...")
135
                 * @return array ['valid' => bool, 'error' => string|null, 'extension' => string|null, 'data' => string|null]
136
                 */
137
                public static function validateBase64Image(string $dataUri): array
×
138
                {
139
                        // Parse the data URI -- expected format: "data:image/png;base64,XXXX"
140
                        $parts = explode(';', $dataUri, 2);
×
141
                        if (count($parts) < 2) {
×
142
                                return ['valid' => false, 'error' => 'Invalid data URI format.', 'extension' => null, 'data' => null];
×
143
                        }
144

145
                        // Extract the MIME type from the data URI header (e.g. "data:image/png")
146
                        $typePart = $parts[0];
×
147
                        $mimeFromUri = '';
×
148
                        if (strpos($typePart, '/') !== false) {
×
149
                                $colonPos = strpos($typePart, ':');
×
150
                                if ($colonPos !== false) {
×
151
                                        $mimeFromUri = substr($typePart, $colonPos + 1);
×
152
                                }
153
                        }
154

155
                        // Extract the extension from the MIME type
156
                        $extension = '';
×
157
                        if (strpos($mimeFromUri, '/') !== false) {
×
158
                                list(, $extension) = explode('/', $mimeFromUri, 2);
×
159
                        }
160
                        $extension = strtolower(trim($extension));
×
161

162
                        // Normalize jpeg -> jpg for consistency
163
                        $lookupExt = ($extension === 'jpeg') ? 'jpg' : $extension;
×
164

165
                        // Check blocked extensions
166
                        if (self::isExtensionBlocked($extension) || self::isExtensionBlocked($lookupExt)) {
×
167
                                Log::warning('Blocked base64 upload attempt with dangerous type: ' . $extension);
×
168
                                return ['valid' => false, 'error' => 'File type is not allowed (blocked type: ' . $extension . ').', 'extension' => null, 'data' => null];
×
169
                        }
170

171
                        // Check allowed image types
172
                        if (!array_key_exists($lookupExt, self::$allowedImageTypes)) {
×
173
                                return ['valid' => false, 'error' => 'Image type .' . $lookupExt . ' is not permitted. Allowed: jpg, png, gif, webp.', 'extension' => null, 'data' => null];
×
174
                        }
175

176
                        // Decode the base64 data
177
                        $dataPart = $parts[1];
×
178
                        if (strpos($dataPart, ',') === false) {
×
179
                                return ['valid' => false, 'error' => 'Invalid base64 data format.', 'extension' => null, 'data' => null];
×
180
                        }
181
                        list(, $base64Data) = explode(',', $dataPart, 2);
×
182
                        $decodedData = base64_decode($base64Data, true);
×
183

184
                        if ($decodedData === false) {
×
185
                                return ['valid' => false, 'error' => 'Failed to decode base64 data.', 'extension' => null, 'data' => null];
×
186
                        }
187

188
                        // Check file size (max 5MB)
189
                        if (strlen($decodedData) > self::$maxFileSize) {
×
190
                                return ['valid' => false, 'error' => 'Image exceeds maximum size of 5MB.', 'extension' => null, 'data' => null];
×
191
                        }
192

193
                        // Validate actual MIME type of the decoded binary data using finfo
194
                        $finfo = new \finfo(FILEINFO_MIME_TYPE);
×
195
                        $detectedMime = $finfo->buffer($decodedData);
×
196

197
                        $acceptableMimes = self::$allowedImageTypes[$lookupExt] ?? [];
×
198
                        if (!in_array($detectedMime, $acceptableMimes, true)) {
×
199
                                Log::warning('Base64 image MIME mismatch: claimed=' . $extension . ', detected=' . $detectedMime);
×
200
                                return ['valid' => false, 'error' => 'Detected MIME type (' . $detectedMime . ') does not match claimed image type (' . $lookupExt . ').', 'extension' => null, 'data' => null];
×
201
                        }
202

203
                        // Check for embedded PHP code
204
                        if (self::containsPhpCode($decodedData)) {
×
205
                                Log::warning('Blocked base64 image upload containing PHP code');
×
206
                                return ['valid' => false, 'error' => 'Image contains potentially dangerous content.', 'extension' => null, 'data' => null];
×
207
                        }
208

209
                        return ['valid' => true, 'error' => null, 'extension' => $lookupExt, 'data' => $decodedData];
×
210
                }
211

212
                /**
213
                 * Sanitize a path segment (e.g. a public address) for safe use in file paths.
214
                 * Prevents directory traversal attacks by stripping path separators and
215
                 * enforcing an alphanumeric-only pattern.
216
                 *
217
                 * @param string $input
218
                 * @return string|null Returns sanitized string or null if invalid.
219
                 */
220
                public static function sanitizePathSegment(string $input): ?string
×
221
                {
222
                        // Use basename to strip any directory components
223
                        $sanitized = basename($input);
×
224
                        // Only allow alphanumeric characters, underscores, and hyphens
225
                        if (!preg_match('/^[a-zA-Z0-9_\-]+$/', $sanitized) || empty($sanitized)) {
×
226
                                return null;
×
227
                        }
228
                        return $sanitized;
×
229
                }
230

231
                /**
232
                 * Validate a Marscoin address format (Base58Check starting with 'M').
233
                 * Valid Marscoin addresses are 25-35 characters, start with 'M',
234
                 * and only use Base58 characters (no 0, O, I, l).
235
                 */
236
                public static function isValidMarscoinAddress(string $address): bool
×
237
                {
238
                        return (bool) preg_match('/^M[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{24,34}$/', $address);
×
239
                }
240

241
                /**
242
                 * Validate an IPFS CID format (CIDv0 starting with 'Qm' or CIDv1 starting with 'b').
243
                 */
244
                public static function isValidCID(string $cid): bool
×
245
                {
246
                        // CIDv0: starts with Qm, 46 chars total, Base58
247
                        $cidv0 = '/^Qm[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{44}$/';
×
248
                        // CIDv1: starts with b, base32 encoded
249
                        $cidv1 = '/^b[a-z2-7]{58,}$/';
×
250
                        return (bool) (preg_match($cidv0, $cid) || preg_match($cidv1, $cid));
×
251
                }
252

253
                /**
254
                 * Check if content contains PHP code markers that could be executed
255
                 * if the file is somehow served through the web server.
256
                 *
257
                 * @param string $content
258
                 * @return bool True if content contains PHP code indicators.
259
                 */
260
                public static function containsPhpCode(string $content): bool
×
261
                {
262
                        // Only check the first 256 bytes (header area) for PHP signatures
263
                        // Binary image data often contains random byte sequences that match
264
                        // PHP patterns, causing false positives on valid camera photos
265
                        $header = substr($content, 0, 256);
×
266
                        $patterns = ['<?php', '<?=', '<script language="php"', '<script language=\'php\''];
×
267
                        foreach ($patterns as $pattern) {
×
268
                                if (stripos($header, $pattern) !== false) {
×
269
                                        return true;
×
270
                                }
271
                        }
272
                        return false;
×
273
                }
274

275
                /**
276
                 * Write an .htaccess file in the upload directory to prevent PHP execution.
277
                 * This is a defense-in-depth measure.
278
                 *
279
                 * @param string $directory
280
                 * @return void
281
                 */
282
                public static function writeUploadHtaccess(string $directory): void
×
283
                {
284
                        $htaccessPath = rtrim($directory, '/') . '/.htaccess';
×
285
                        if (!file_exists($htaccessPath)) {
×
286
                                $htaccessContent = "# Prevent PHP execution in upload directories\n";
×
287
                                $htaccessContent .= "php_flag engine off\n";
×
288
                                $htaccessContent .= "<FilesMatch \"\\.(php|phtml|php5|php7|php8|phar|phps|cgi|pl|py|sh|asp|aspx|jsp)$\">\n";
×
289
                                $htaccessContent .= "    Require all denied\n";
×
290
                                $htaccessContent .= "</FilesMatch>\n";
×
291
                                $htaccessContent .= "AddHandler default-handler .php .phtml .php5 .phar\n";
×
292
                                @file_put_contents($htaccessPath, $htaccessContent);
×
293
                        }
294
                }
295

296

297
                public static function ago($time)
×
298
                {
299
                   $periods = array("second", "minute", "hour", "day", "week", "month", "year", "decade");
×
300
                   $lengths = array("60","60","24","7","4.35","12","10");
×
301

302
                   $now = time();
×
303

304
                       $difference     = $now - $time;
×
305
                       $tense         = "ago";
×
306

307
                   for($j = 0; $difference >= $lengths[$j] && $j < count($lengths)-1; $j++) {
×
308
                       $difference /= $lengths[$j];
×
309
                   }
310

311
                   $difference = round($difference);
×
312

313
                   if($difference != 1) {
×
314
                       $periods[$j].= "s";
×
315
                   }
316

317
                   return "$difference $periods[$j] ago ";
×
318
                }
319

320

321
                public static function stats()
×
322
                {
323
                        $array["coincount"] = 35000000;
×
NEW
324
                        $json = AppHelper::file_get_contents_curl(config('blockchain.explorer.primary_url') . '/api/status?q=getInfo');
×
325
                        $array["network"] = json_decode($json, true);
×
NEW
326
                        $json2 = AppHelper::file_get_contents_curl(config('blockchain.explorer.primary_url') . '/api/status?q=getTxOutSetInfo');
×
327
                        $total = json_decode($json2, true);
×
328
                        if ($total && count($total) > 0)
×
329
                                $array["coincount"] = round($total['txoutsetinfo']['total_amount'], 2);
×
330

331
                        return $array;
×
332
                }
333

334
                public static function file_get_contents_curl($url, $timeout = 10)
×
335
                {
336
                        $ch = curl_init();
×
337

338
                        curl_setopt($ch, CURLOPT_AUTOREFERER, TRUE);
×
339
                        curl_setopt($ch, CURLOPT_HEADER, 0);
×
340
                        curl_setopt($ch, CURLOPT_HTTPHEADER, array('X-CMC_PRO_API_KEY: cf191ba7-4840-4a9a-bee4-617608afd8a4'));
×
341
                        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
×
342
                        curl_setopt($ch, CURLOPT_URL, $url);
×
343
                        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
×
344
                        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
×
345
                        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
×
346

347
                        $data = curl_exec($ch);
×
348
                        curl_close($ch);
×
349

350
                        return $data;
×
351
                }
352

353

354
                public static function uploadFile($name, $url){
×
355

356
                        $postField = array();
×
357
                        $tmpfile = $_FILES[$name]['tmp_name'];
×
358
                        $filename = basename($_FILES[$name]['name']);
×
359
                        $postField['files'] =  curl_file_create($tmpfile, $_FILES[$name]['type'], $filename);
×
360
                        $headers = array("Content-Type" => "multipart/form-data");
×
361
                        $curl_handle = curl_init();
×
362
                        curl_setopt($curl_handle, CURLOPT_URL, $url);
×
363

364
                        curl_setopt($curl_handle, CURLOPT_HTTPHEADER, $headers);
×
365
                        curl_setopt($curl_handle, CURLOPT_POST, TRUE);
×
366
                        curl_setopt($curl_handle, CURLOPT_POSTFIELDS, $postField);
×
367
                        curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, TRUE);
×
368
                        $returned_fileName = curl_exec($curl_handle);
×
369
                        curl_close($curl_handle);
×
370
                        return json_decode($returned_fileName);
×
371
                }
372

373

374
                public static function upload($filepath, $url)
×
375
                {
376
                        $filename = realpath($filepath);
×
377
                        $finfo = new \finfo(FILEINFO_MIME_TYPE);
×
378
                        $mimetype = $finfo->file($filename);
×
379
                        $ch = curl_init($url);
×
380
                        $cfile = curl_file_create($filename, $mimetype, basename($filename));
×
381
                        $data = ['file' => $cfile];
×
382
                        $headers = array("Content-Type" => $mimetype);
×
383
                        curl_setopt($ch, CURLOPT_URL, $url);
×
384
                        curl_setopt($ch, CURLOPT_POST, 1);
×
385
                        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
×
386
                        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
×
387
                        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
×
388
                        $result = curl_exec($ch);
×
389
                        $r = curl_getinfo($ch);
×
390
                        if ($r["http_code"] != 200) {
×
391
                                $detais = json_decode($result, true);
×
392
                                if (isset($detais["msg"])) {
×
393
                                        throw new \Exception($detais["msg"], 1);
×
394
                                } else {
395
                                        return "Error";
×
396
                                }
397
                        }
398
                        $details = json_decode($result, true);
×
399
                        $res = array();
×
400
                        return $details['Hash'];
×
401
                }
402

403

404
                public static function uploadFolder($filepath, $url)
×
405
                {
406
                        if (!is_dir($filepath) || !is_readable($filepath)) {
×
407
                                throw new \Exception("Directory is not accessible");
×
408
                        }
409

410
                        $files = scandir($filepath);
×
411
                        $data = [];
×
412
                        $headers = ["Content-Type: multipart/form-data"];
×
413
                        $ch = curl_init($url);
×
414

415
                        foreach ($files as $i => $filep) {
×
416
                                $filename = realpath($filepath . "/" . $filep);
×
417
                                if (!is_file($filename)) {
×
418
                                        continue;
×
419
                                }
420

421
                                $finfo = new \finfo(FILEINFO_MIME_TYPE);
×
422
                                $mimetype = $finfo->file($filename);
×
423
                                $cfile = curl_file_create($filename, $mimetype, basename($filename));
×
424
                                $data['file['.$i.']'] = $cfile;
×
425
                        }
426

427
                        curl_setopt_array($ch, [
×
428
                                CURLOPT_URL => $url,
×
429
                                CURLOPT_POST => true,
×
430
                                CURLOPT_POSTFIELDS => $data,
×
431
                                CURLOPT_HTTPHEADER => $headers,
×
432
                                CURLOPT_RETURNTRANSFER => true,
×
433
                        ]);
×
434

435
                        $result = curl_exec($ch);
×
436
                        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
×
437
                        curl_close($ch);
×
438

439
                        if ($httpCode < 200 || $httpCode >= 300) {
×
440
                                $details = json_decode($result, true);
×
441
                                $errorMsg = $details['msg'] ?? 'Unknown error occurred';
×
442
                                throw new \Exception($errorMsg);
×
443
                        }
444

445
                        //print_r($result);
446
                        //die;
447
                        // Prepare the result string by wrapping with brackets and replacing the '}{'
448
                        // between JSON objects with '},{', which creates a JSON array
449
                        $resultArrayString = '[' . preg_replace('/}\s*{/', '},{', $result) . ']';
×
450

451
                        // Decode the JSON array string
452
                        $jsonResult = json_decode($resultArrayString, true);
×
453

454
                        if (json_last_error() !== JSON_ERROR_NONE) {
×
455
                                throw new \Exception('Invalid JSON returned from API');
×
456
                        }
457

458
                        // Now $jsonResult is a proper array
459
                        // Search for the folder hash by finding the object with an empty "Name"
460
                        $folderHash = "";
×
461
                        foreach ($jsonResult as $item) {
×
462
                                if ($item['Name'] === "") {
×
463
                                        $folderHash = $item['Hash'];
×
464
                                        break; // No need to continue if the folder hash is found
×
465
                                }
466
                        }
467

468
                        // If the folder hash was found, return it as a JSON object
469
                        if ($folderHash !== "") {
×
470
                                return $folderHash;
×
471
                        } else {
472
                                // Handle the case where no folder hash was found
473
                                throw new \Exception('Folder hash not found in API response');
×
474
                        }
475

476
                }
477

478

479
                public static function file_post_content($url, $data)
×
480
                {
481

482
                        $postdata = http_build_query($data);
×
483

484
                        $opts = array('http' =>
×
485
                            array(
×
486
                                'method'  => 'POST',
×
487
                                'header'  => 'Content-Type: application/x-www-form-urlencoded',
×
488
                                'content' => $postdata
×
489
                            )
×
490
                        );
×
491

492
                        $context  = stream_context_create($opts);
×
493

494
                        $result = file_get_contents($url, false, $context);
×
495
                        return $result;
×
496
                }
497

498

499
                public static function getUserFromCache($address)
×
500
                {
501
                        $user = array();
×
502
                        $file_path = "./assets/citizen/" . $address . "/";
×
503

504
                        if (!file_exists($file_path)) {
×
505
                                mkdir($file_path);
×
506
                                AppHelper::addUserToLocalCache($address);
×
507
                        }
508

509
                        $json_string = file_get_contents($file_path . "data.json");
×
510
                        $user['data'] = json_decode($json_string);
×
511
                        $user['pic'] = "/assets/citizen/" . $address . "/profile_pic.png";
×
512
                        $user['vid'] = "/assets/citizen/" . $address . "/profile_video.webm";
×
513
                        return $user;
×
514
                }
515

516

517
                /**
518
                 * @todo Pulls in user data from IPFS links as per blockchain
519
                 */
520
                public static function addUserToLocalCache($address)
×
521
                {
522
                        //find user's GP transaction in cache
523
                        $transaction_gp = Feed::where('address', '=', $address)->where('tag', '=', "GP")->first();
×
524
                        //pull up transaction using blockchain explorer
NEW
525
                        $json = AppHelper::file_get_contents_curl(config('blockchain.explorer.fallback_url') . "/api/tx/" . $transaction_gp['txid']);
×
526
                        if($json)
×
527
                        {
528
                                $tx = json_decode($json);
×
529
                                $op_return = $tx->vout[0]->scriptPubKey->asm;
×
530
                                $parts = explode(" ", $op_return);
×
531
                                if(count($parts)>0)
×
532
                                {
533
                                        $ipfs_gp_hash = AppHelper::hex2str($parts[1]);
×
534
                                        $p = explode("_", $ipfs_gp_hash);
×
535
                                        if(count($p) > 0)
×
536
                                        {
537
                                                $ipfs_hash = $p[1];
×
NEW
538
                                                $data = AppHelper::file_get_contents_curl(config('blockchain.ipfs.gateway_url') . $ipfs_hash);
×
539
                                                $file_path = "./assets/citizen/" . $address . "/data.json";
×
540
                                                file_put_contents($file_path, $data);
×
541
                                                $d = json_decode($data);
×
542

543
                                                $img = AppHelper::file_get_contents_curl($d->data->picture);
×
544
                                                $file_path = "./assets/citizen/" . $address . "/profile_pic.png";
×
545
                                                file_put_contents($file_path, $img);
×
546
                                        }
547
                                }
548

549
                        }
550

551
                }
552

553

554
                /**
555
                 * Helper function keeping a local cache of the Marscoin blockchain embedded data feed
556
                 * as it pertains to MartianRepublic protocol anchors.
557
                 */
558
                public static function insertBlockchainCache($address, $uid, $action_tag, $message, $embedded_link, $txid)
×
559
                {
560
                        // First, check if a duplicate entry exists
561
                        $existingFeed = Feed::where('address', $address)
×
562
                                                                ->where('tag', $action_tag)
×
563
                                                                ->where('txid', $txid)
×
564
                                                                ->first();
×
565

566
                        if ($existingFeed) {
×
567
                                // If a duplicate is found, return the txid
568
                                return $existingFeed->txid;
×
569
                        }
570

571
                        // If no duplicate, proceed to create a new entry
572
                        $feed = new Feed;
×
573
                        if ($uid) {
×
574
                                $feed->userid = $uid;
×
575
                        }
576
                        if (!$action_tag) {
×
577
                                return false;
×
578
                        }
579
                        $feed->tag = $action_tag;
×
580
                        $feed->address = $address;
×
581
                        $feed->message = $message;
×
582
                        $feed->embedded_link = $embedded_link;
×
583
                        $feed->txid = $txid;
×
584
                        $feed->save();
×
585

586
                        return true;
×
587
                }
588

589

590

591
                public static function insertPublicationCache($uid, $local_path, $ipfs_hash, $title)
×
592
                {
593
                        $pub = new Publication;
×
594
                        if($uid)
×
595
                        {
596
                                $pub->userid = $uid;
×
597
                                $pub->ipfs_hash = $ipfs_hash;
×
598
                                $pub->local_path = $local_path;
×
599
                                $pub->title = $title;
×
600
                                $pub->save();
×
601
                                return TRUE;
×
602
                        }
603
                }
604

605

606
                public static function time_elapsed_string($datetime, $full = false)
×
607
                {
608
                        date_default_timezone_set('America/New_York');
×
609
                    $now = new DateTime;
×
610
                    $ago = new DateTime($datetime);
×
611
                                $diff = $now->diff($ago);
×
612

613
                    $diff->w = floor($diff->d / 7);
×
614
                    $diff->d -= $diff->w * 7;
×
615

616
                    $string = array(
×
617
                        'y' => 'year',
×
618
                        'm' => 'month',
×
619
                        'w' => 'week',
×
620
                        'd' => 'day',
×
621
                        'h' => 'hour',
×
622
                        'i' => 'minute',
×
623
                        's' => 'second',
×
624
                    );
×
625
                    foreach ($string as $k => &$v) {
×
626
                        if ($diff->$k) {
×
627
                            $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : '');
×
628
                        } else {
629
                            unset($string[$k]);
×
630
                        }
631
                    }
632

633
                    if (!$full) $string = array_slice($string, 0, 1);
×
634
                    return $string ? implode(', ', $string) . ' ago' : 'just now';
×
635
                }
636

637
                public static function days_elapsed_string($datetime, $full = false)
×
638
                {
639
                        date_default_timezone_set('America/New_York');
×
640
                    $now = new DateTime;
×
641
                    $ago = new DateTime($datetime);
×
642
                                $diff = $now->diff($ago);
×
643

644
                                return $diff->days;
×
645
                }
646

647

648
                public static function hex2str($hex)
×
649
                {
650
                    $str = '';
×
651
                    for($i=0;$i<strlen($hex);$i+=2) $str .= chr(hexdec(substr($hex,$i,2)));
×
652
                    return $str;
×
653
                }
654

655

656
                        // Function to get the price from CoinGecko with caching
657
                public static function getMarscoinPrice()
×
658
                {
NEW
659
                        $url = config('blockchain.price.coingecko_url');
×
660

661
                        // Use the Cache facade with the remember method
662
                        $marsPriceData = Cache::remember('marscoin_price', 5, function () use ($url) {
×
663
                                // Inside the closure, fetch the data from CoinGecko
664
                                try {
665
                                        $response = file_get_contents($url);
×
666
                                        return json_decode($response);
×
667
                                } catch (\Exception $e) {
×
668
                                        // Handle the exception if the API call fails
669
                                        return null;
×
670
                                }
671
                        });
×
672

673
                        if ($marsPriceData) {
×
674
                                return $marsPriceData->marscoin->usd;
×
675
                        }
676

677
                        // Handle the case where the API call was not successful or caching failed
678
                        return 0;
×
679
                }
680

681

682
                public static function getMarscoinBalance($publicAddr)
1✔
683
                {
684
                        $cacheKey = 'marscoin_balance_' . $publicAddr;
1✔
685

686
                        // Cache for 2 minutes to reduce API load while staying reasonably fresh
687
                        return Cache::remember($cacheKey, 120, function () use ($publicAddr) {
1✔
688
                                // Primary: use local pebas (Electrum-based, more reliable)
689
                                try {
690
                                        $ctx = stream_context_create(['http' => ['timeout' => 5]]);
1✔
691
                                $response = @file_get_contents(config('blockchain.pebas.url') . "/api/mars/balance?address={$publicAddr}", false, $ctx);
1✔
692
                                        if ($response) {
1✔
693
                                                $data = json_decode($response, true);
×
694
                                                if (isset($data['balance'])) {
×
695
                                                        return (float) $data['balance'];
1✔
696
                                                }
697
                                        }
698
                                } catch (\Exception $e) {
×
699
                                        // Fall through to explorer
700
                                }
701

702
                                // Fallback: explorer API (returns balance in satoshis)
703
                                try {
704
                                        $response = @file_get_contents(config('blockchain.explorer.primary_url') . "/api/addr/{$publicAddr}/balance");
1✔
705
                                        if ($response !== false && is_numeric($response)) {
1✔
706
                                                return $response * 0.00000001;
1✔
707
                                        }
708
                                } catch (\Exception $e) {
×
709
                                        // Both failed
710
                                }
711

712
                                return 0;
1✔
713
                        });
1✔
714
                }
715

716
                public static function getMarscoinTotalReceived($publicAddr)
×
717
                {
NEW
718
                        $url = config('blockchain.explorer.primary_url') . "/api/addr/{$publicAddr}/totalReceived";
×
719
                        $cacheKey = 'marscoin_total_received_' . $publicAddr;
×
720

721
                        $totalReceived = Cache::remember($cacheKey, 300, function () use ($url) {
×
722
                                try {
723
                                        $response = file_get_contents($url);
×
724
                                        return $response; // Assuming the response is the total amount received
×
725
                                } catch (\Exception $e) {
×
726
                                        return null;
×
727
                                }
728
                        });
×
729

730
                        if ($totalReceived !== null) {
×
731
                                return $totalReceived * 0.00000001; // Convert from satoshis to Marscoin if necessary
×
732
                        }
733

734
                        return null;
×
735
                }
736

737

738
                public static function getMarscoinTotalSent($publicAddr)
×
739
                {
NEW
740
                        $url = config('blockchain.explorer.primary_url') . "/api/addr/{$publicAddr}/totalSent";
×
741
                        $cacheKey = 'marscoin_total_sent_' . $publicAddr;
×
742

743
                        $totalSent = Cache::remember($cacheKey, 300, function () use ($url) {
×
744
                                try {
745
                                        $response = file_get_contents($url);
×
746
                                        return $response; // Assuming the response is the total amount sent
×
747
                                } catch (\Exception $e) {
×
748
                                        return null;
×
749
                                }
750
                        });
×
751

752
                        if ($totalSent !== null) {
×
753
                                return $totalSent * 0.00000001; // Convert from satoshis to Marscoin if necessary
×
754
                        }
755

756
                        return null;
×
757
                }
758

759

760
                public static function getMarscoinTotalAmount()
×
761
                {
762
                        $cacheKey = 'marscoin_total_amount';
×
763

764
                        $totalAmount = Cache::remember($cacheKey, 180, function () {
×
765
                                // Primary: marscoin-cli (fastest, most reliable)
766
                                try {
NEW
767
                                        $output = shell_exec(config('blockchain.rpc.cli_path') . ' -datadir=' . config('blockchain.rpc.data_dir') . ' gettxoutsetinfo 2>/dev/null');
×
768
                                        if ($output) {
×
769
                                                $data = json_decode($output, true);
×
770
                                                if ($data && isset($data['total_amount'])) {
×
771
                                                        return round($data['total_amount'], 2);
×
772
                                                }
773
                                        }
774
                                } catch (\Exception $e) {
×
775
                                        // Fall through to explorer
776
                                }
777

778
                                // Fallback: explorer API
779
                                try {
NEW
780
                                        $url = config('blockchain.explorer.primary_url') . "/api/status?q=getTxOutSetInfo";
×
781
                                        $response = file_get_contents($url);
×
782
                                        $data = json_decode($response, true);
×
783
                                        if ($data && isset($data['txoutsetinfo']['total_amount'])) {
×
784
                                                return round($data['txoutsetinfo']['total_amount'], 2);
×
785
                                        }
786
                                } catch (\Exception $e) {
×
787
                                        return 39000000;
×
788
                                }
789
                                return 39000000;
×
790
                        });
×
791

792
                        return $totalAmount;
×
793
                }
794

795
                public static function getMarscoinNetworkInfo()
×
796
                {
NEW
797
                        $url = config('blockchain.explorer.secondary_url') . "/api/status?q=getInfo";
×
798
                        $cacheKey = 'marscoin_network_info';
×
799

800
                        $networkInfo = Cache::remember($cacheKey, 60, function () use ($url) {
×
801
                                try {
802
                                        $response = file_get_contents($url);
×
803
                                        $data = json_decode($response, true);
×
804
                                        if (is_array($data)) {
×
805
                                                return $data; // Return the network info if the response is valid
×
806
                                        }
807
                                        Log::debug("Set Network status cache");
×
808
                                } catch (\Exception $e) {
×
809
                                        return []; // Return an empty array in case of an error
×
810
                                }
811
                                return []; // Also return an empty array if the API does not return a valid response
×
812
                        });
×
813

814
                        return $networkInfo;
×
815
                }
816

817
                 /**
818
                 * Check for recent posts in the forum.
819
                 *
820
                 * @return int Number of recent posts.
821
                 */
822
                public static function checkForRecentPosts()
×
823
                {
824
                        // Define the time frame for "recent" posts. For example, within the last 24 hours.
825
                        $recentThreshold = Carbon::now()->subDay();
×
826

827
                        // Count the number of posts created after the recent threshold.
828
                        $recentPostsCount = Post::where('created_at', '>', $recentThreshold)->count();
×
829

830
                        return $recentPostsCount;
×
831
                }
832

833

834
                /**
835
                 * Determines the Citizen Status of a user.
836
                 *
837
                 * @param int $userId The ID of the user.
838
                 * @return object An associative array containing the 'status' and 'type'.
839
                 */
840
                public static function getCitizenStatus(int $userId)
×
841
                {
842
                        // Default status for users without a profile entry.
843
                        $statusDetails = [
×
844
                                'status' => 'Newcomer',
×
845
                                'type' => 'NC',
×
846
                        ];
×
847

848
                        // Attempt to retrieve the user's profile.
849
                        $profile = Profile::where('userid', $userId)->first();
×
850

851
                        if ($profile) {
×
852
                                if ($profile->has_application > 0) {
×
853
                                        $statusDetails = [
×
854
                                                'status' => 'Applicant',
×
855
                                                'type' => 'AP',
×
856
                                        ];
×
857
                                } elseif ($profile->general_public > 0) {
×
858
                                        $statusDetails = [
×
859
                                                'status' => 'General Public',
×
860
                                                'type' => 'GP',
×
861
                                        ];
×
862
                                }
863

864
                                // Check if the user is a citizen.
865
                                $isCitizen = Citizen::where('userid', $userId)->exists();
×
866
                                if ($isCitizen) {
×
867
                                        $statusDetails = [
×
868
                                                'status' => 'Citizen',
×
869
                                                'type' => 'CT',
×
870
                                        ];
×
871
                                }
872
                        }
873

874
                        return (object)$statusDetails;
×
875
                }
876

877

878
                public static function createSlug($id, $title) {
×
879
                        // Step 1: Concatenate
880
                        $combined = $id . '-' . $title;
×
881

882
                        // Step 2: Lowercase
883
                        $combined = strtolower($combined);
×
884

885
                        // Step 3: Remove special characters
886
                        $combined = preg_replace('/[^a-z0-9\s-]/', '', $combined);
×
887

888
                        // Step 4: Trim whitespace
889
                        $combined = trim($combined);
×
890

891
                        // Step 5: Replace spaces and underscores with hyphens
892
                        $combined = preg_replace('/[\s_]+/', '-', $combined);
×
893

894
                        // Step 6: Ensure uniqueness (not implemented here, depends on context)
895

896
                        return $combined;
×
897
                }
898

899

900

901
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc