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

marscoin / martianrepublic / 29796968107

21 Jul 2026 02:52AM UTC coverage: 10.965% (-0.03%) from 10.997%
29796968107

push

github

Martian Congress
fix: Drop dynamic DateInterval->w in time_elapsed_string

PHPStan (bumped to larastan 3.10 in the Laravel 12 upgrade) flagged the
$diff->w dynamic property on DateInterval, and its new error-message
format no longer matched the phpstan-baseline entry, failing the Test
Coverage job on both counts. Derive weeks from days in local vars
instead (also removes a PHP 8.2 dynamic-property deprecation) and drop
the now-stale baseline entry. Output unchanged; phpstan clean.

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

0 of 15 new or added lines in 1 file covered. (0.0%)

238 existing lines in 4 files now uncovered.

665 of 6065 relevant lines covered (10.96%)

1.55 hits per line

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

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

3
namespace App\Includes;
4

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

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

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

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

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

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

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

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

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

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

98
        // Get and check extension against blocklist
99
        $extension = strtolower($file->getClientOriginalExtension());
×
100
        if (self::isExtensionBlocked($extension)) {
×
101
            Log::warning('Blocked file upload attempt with dangerous extension: '.$extension);
×
102

103
            return ['valid' => false, 'error' => 'File type is not allowed (blocked extension: '.$extension.').'];
×
104
        }
105

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

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

116
        if (! in_array($detectedMime, $allowedMimes, true)) {
×
117
            Log::warning('File MIME mismatch: extension='.$extension.', detected='.$detectedMime);
×
118

119
            return ['valid' => false, 'error' => 'File MIME type ('.$detectedMime.') does not match expected type for .'.$extension.'.'];
×
120
        }
121

122
        // Check for embedded PHP code in the file contents
123
        $contents = file_get_contents($file->getRealPath());
×
124
        if ($contents !== false && self::containsPhpCode($contents)) {
×
125
            Log::warning('Blocked file upload containing PHP code');
×
126

127
            return ['valid' => false, 'error' => 'File contains potentially dangerous content.'];
×
128
        }
129

130
        return ['valid' => true, 'error' => null];
×
131
    }
132

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

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

159
        // Extract the extension from the MIME type
160
        $extension = '';
×
161
        if (strpos($mimeFromUri, '/') !== false) {
×
162
            [, $extension] = explode('/', $mimeFromUri, 2);
×
163
        }
164
        $extension = strtolower(trim($extension));
×
165

166
        // Normalize jpeg -> jpg for consistency
167
        $lookupExt = ($extension === 'jpeg') ? 'jpg' : $extension;
×
168

169
        // Check blocked extensions
170
        if (self::isExtensionBlocked($extension) || self::isExtensionBlocked($lookupExt)) {
×
171
            Log::warning('Blocked base64 upload attempt with dangerous type: '.$extension);
×
172

173
            return ['valid' => false, 'error' => 'File type is not allowed (blocked type: '.$extension.').', 'extension' => null, 'data' => null];
×
174
        }
175

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

181
        // Decode the base64 data
182
        $dataPart = $parts[1];
×
183
        if (strpos($dataPart, ',') === false) {
×
184
            return ['valid' => false, 'error' => 'Invalid base64 data format.', 'extension' => null, 'data' => null];
×
185
        }
186
        [, $base64Data] = explode(',', $dataPart, 2);
×
187
        $decodedData = base64_decode($base64Data, true);
×
188

189
        if ($decodedData === false) {
×
190
            return ['valid' => false, 'error' => 'Failed to decode base64 data.', 'extension' => null, 'data' => null];
×
191
        }
192

193
        // Check file size (max 5MB)
194
        if (strlen($decodedData) > self::$maxFileSize) {
×
195
            return ['valid' => false, 'error' => 'Image exceeds maximum size of 5MB.', 'extension' => null, 'data' => null];
×
196
        }
197

198
        // Validate actual MIME type of the decoded binary data using finfo
199
        $finfo = new \finfo(FILEINFO_MIME_TYPE);
×
200
        $detectedMime = $finfo->buffer($decodedData);
×
201

202
        $acceptableMimes = self::$allowedImageTypes[$lookupExt] ?? [];
×
203
        if (! in_array($detectedMime, $acceptableMimes, true)) {
×
204
            Log::warning('Base64 image MIME mismatch: claimed='.$extension.', detected='.$detectedMime);
×
205

206
            return ['valid' => false, 'error' => 'Detected MIME type ('.$detectedMime.') does not match claimed image type ('.$lookupExt.').', 'extension' => null, 'data' => null];
×
207
        }
208

209
        // Check for embedded PHP code
210
        if (self::containsPhpCode($decodedData)) {
×
211
            Log::warning('Blocked base64 image upload containing PHP code');
×
212

213
            return ['valid' => false, 'error' => 'Image contains potentially dangerous content.', 'extension' => null, 'data' => null];
×
214
        }
215

216
        return ['valid' => true, 'error' => null, 'extension' => $lookupExt, 'data' => $decodedData];
×
217
    }
218

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

235
        return $sanitized;
×
236
    }
237

238
    /**
239
     * Validate a Marscoin address format (Base58Check starting with 'M').
240
     * Valid Marscoin addresses are 25-35 characters, start with 'M',
241
     * and only use Base58 characters (no 0, O, I, l).
242
     */
243
    public static function isValidMarscoinAddress(string $address): bool
2✔
244
    {
245
        return (bool) preg_match('/^M[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{24,34}$/', $address);
2✔
246
    }
247

248
    /**
249
     * Validate an IPFS CID format (CIDv0 starting with 'Qm' or CIDv1 starting with 'b').
250
     */
251
    public static function isValidCID(string $cid): bool
×
252
    {
253
        // CIDv0: starts with Qm, 46 chars total, Base58
254
        $cidv0 = '/^Qm[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{44}$/';
×
255
        // CIDv1: starts with b, base32 encoded
256
        $cidv1 = '/^b[a-z2-7]{58,}$/';
×
257

258
        return (bool) (preg_match($cidv0, $cid) || preg_match($cidv1, $cid));
×
259
    }
260

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

280
        return false;
×
281
    }
282

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

301
    public static function ago($time)
×
302
    {
303
        $periods = ['second', 'minute', 'hour', 'day', 'week', 'month', 'year', 'decade'];
×
304
        $lengths = ['60', '60', '24', '7', '4.35', '12', '10'];
×
305

306
        $now = time();
×
307

308
        $difference = $now - $time;
×
309
        $tense = 'ago';
×
310

311
        for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths) - 1; $j++) {
×
312
            $difference /= $lengths[$j];
×
313
        }
314

315
        $difference = round($difference);
×
316

317
        if ($difference != 1) {
×
318
            $periods[$j] .= 's';
×
319
        }
320

321
        return "$difference $periods[$j] ago ";
×
322
    }
323

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

335
        return $array;
×
336
    }
337

338
    public static function file_get_contents_curl($url, $timeout = 10)
1✔
339
    {
340
        $ch = curl_init();
1✔
341

342
        curl_setopt($ch, CURLOPT_AUTOREFERER, true);
1✔
343
        curl_setopt($ch, CURLOPT_HEADER, 0);
1✔
344
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-CMC_PRO_API_KEY: cf191ba7-4840-4a9a-bee4-617608afd8a4']);
1✔
345
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
1✔
346
        curl_setopt($ch, CURLOPT_URL, $url);
1✔
347
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
1✔
348
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
1✔
349
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
1✔
350

351
        $data = curl_exec($ch);
1✔
352
        curl_close($ch);
1✔
353

354
        return $data;
1✔
355
    }
356

357
    public static function uploadFile($name, $url)
×
358
    {
359

360
        $postField = [];
×
361
        $tmpfile = $_FILES[$name]['tmp_name'];
×
362
        $filename = basename($_FILES[$name]['name']);
×
363
        $postField['files'] = curl_file_create($tmpfile, $_FILES[$name]['type'], $filename);
×
364
        $headers = ['Content-Type' => 'multipart/form-data'];
×
365
        $curl_handle = curl_init();
×
366
        curl_setopt($curl_handle, CURLOPT_URL, $url);
×
367

368
        curl_setopt($curl_handle, CURLOPT_HTTPHEADER, $headers);
×
369
        curl_setopt($curl_handle, CURLOPT_POST, true);
×
370
        curl_setopt($curl_handle, CURLOPT_POSTFIELDS, $postField);
×
371
        curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, true);
×
372
        $returned_fileName = curl_exec($curl_handle);
×
373
        curl_close($curl_handle);
×
374

375
        return json_decode($returned_fileName);
×
376
    }
377

378
    public static function upload($filepath, $url)
×
379
    {
380
        $filename = realpath($filepath);
×
381
        if (! $filename || ! str_starts_with($filename, public_path()) && ! str_starts_with($filename, storage_path())) {
×
382
            throw new \RuntimeException('Upload path outside allowed directories');
×
383
        }
384
        $finfo = new \finfo(FILEINFO_MIME_TYPE);
×
385
        $mimetype = $finfo->file($filename);
×
386
        $ch = curl_init($url);
×
387
        $cfile = curl_file_create($filename, $mimetype, basename($filename));
×
388
        $data = ['file' => $cfile];
×
389
        $headers = ['Content-Type' => $mimetype];
×
390
        curl_setopt($ch, CURLOPT_URL, $url);
×
391
        curl_setopt($ch, CURLOPT_POST, 1);
×
392
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
×
393
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
×
394
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
×
395
        $result = curl_exec($ch);
×
396
        $r = curl_getinfo($ch);
×
397
        if ($r['http_code'] != 200) {
×
398
            $detais = json_decode($result, true);
×
399
            if (isset($detais['msg'])) {
×
400
                throw new \Exception($detais['msg'], 1);
×
401
            } else {
402
                return 'Error';
×
403
            }
404
        }
405
        $details = json_decode($result, true);
×
406
        $res = [];
×
407

408
        return $details['Hash'];
×
409
    }
410

411
    public static function uploadFolder($filepath, $url)
×
412
    {
413
        if (! is_dir($filepath) || ! is_readable($filepath)) {
×
414
            throw new \Exception('Directory is not accessible');
×
415
        }
416

417
        $files = scandir($filepath);
×
418
        $data = [];
×
419
        $headers = ['Content-Type: multipart/form-data'];
×
420
        $ch = curl_init($url);
×
421

422
        foreach ($files as $i => $filep) {
×
423
            $filename = realpath($filepath.'/'.$filep);
×
424
            if (! $filename || (! str_starts_with($filename, public_path()) && ! str_starts_with($filename, storage_path()))) {
×
425
                continue;
×
426
            }
427
            if (! is_file($filename)) {
×
428
                continue;
×
429
            }
430

431
            $finfo = new \finfo(FILEINFO_MIME_TYPE);
×
432
            $mimetype = $finfo->file($filename);
×
433
            $cfile = curl_file_create($filename, $mimetype, basename($filename));
×
434
            $data['file['.$i.']'] = $cfile;
×
435
        }
436

437
        curl_setopt_array($ch, [
×
438
            CURLOPT_URL => $url,
×
439
            CURLOPT_POST => true,
×
440
            CURLOPT_POSTFIELDS => $data,
×
441
            CURLOPT_HTTPHEADER => $headers,
×
442
            CURLOPT_RETURNTRANSFER => true,
×
443
        ]);
×
444

445
        $result = curl_exec($ch);
×
446
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
×
447
        curl_close($ch);
×
448

449
        if ($httpCode < 200 || $httpCode >= 300) {
×
450
            $details = json_decode($result, true);
×
451
            $errorMsg = $details['msg'] ?? 'Unknown error occurred';
×
452
            throw new \Exception($errorMsg);
×
453
        }
454

455
        // print_r($result);
456
        // die;
457
        // Prepare the result string by wrapping with brackets and replacing the '}{'
458
        // between JSON objects with '},{', which creates a JSON array
459
        $resultArrayString = '['.preg_replace('/}\s*{/', '},{', $result).']';
×
460

461
        // Decode the JSON array string
462
        $jsonResult = json_decode($resultArrayString, true);
×
463

464
        if (json_last_error() !== JSON_ERROR_NONE) {
×
465
            throw new \Exception('Invalid JSON returned from API');
×
466
        }
467

468
        // Now $jsonResult is a proper array
469
        // Search for the folder hash by finding the object with an empty "Name"
470
        $folderHash = '';
×
471
        foreach ($jsonResult as $item) {
×
472
            if ($item['Name'] === '') {
×
473
                $folderHash = $item['Hash'];
×
474
                break; // No need to continue if the folder hash is found
×
475
            }
476
        }
477

478
        // If the folder hash was found, return it as a JSON object
479
        if ($folderHash !== '') {
×
480
            return $folderHash;
×
481
        } else {
482
            // Handle the case where no folder hash was found
483
            throw new \Exception('Folder hash not found in API response');
×
484
        }
485

486
    }
487

488
    public static function file_post_content($url, $data)
×
489
    {
490

491
        $postdata = http_build_query($data);
×
492

493
        $opts = ['http' => [
×
494
            'method' => 'POST',
×
495
            'header' => 'Content-Type: application/x-www-form-urlencoded',
×
496
            'content' => $postdata,
×
497
        ],
×
498
        ];
×
499

500
        $context = stream_context_create($opts);
×
501

502
        $result = file_get_contents($url, false, $context);
×
503

504
        return $result;
×
505
    }
506

507
    public static function getUserFromCache($address)
×
508
    {
509
        $user = [];
×
510
        $file_path = './assets/citizen/'.$address.'/';
×
511

512
        if (! file_exists($file_path)) {
×
513
            mkdir($file_path);
×
514
            AppHelper::addUserToLocalCache($address);
×
515
        }
516

517
        $json_string = file_get_contents($file_path.'data.json');
×
518
        $user['data'] = json_decode($json_string);
×
519
        $user['pic'] = '/assets/citizen/'.$address.'/profile_pic.png';
×
520
        $user['vid'] = '/assets/citizen/'.$address.'/profile_video.webm';
×
521

522
        return $user;
×
523
    }
524

525
    /**
526
     * @todo Pulls in user data from IPFS links as per blockchain
527
     */
528
    public static function addUserToLocalCache($address)
×
529
    {
530
        // find user's GP transaction in cache
531
        $transaction_gp = Feed::where('address', '=', $address)->where('tag', '=', 'GP')->first();
×
532
        // pull up transaction using blockchain explorer
533
        $json = AppHelper::file_get_contents_curl(config('blockchain.explorer.fallback_url').'/api/tx/'.$transaction_gp['txid']);
×
534
        if ($json) {
×
535
            $tx = json_decode($json);
×
536
            $op_return = $tx->vout[0]->scriptPubKey->asm;
×
537
            $parts = explode(' ', $op_return);
×
538
            if (count($parts) > 0) {
×
539
                $ipfs_gp_hash = AppHelper::hex2str($parts[1]);
×
540
                $p = explode('_', $ipfs_gp_hash);
×
541
                if (count($p) > 0) {
×
542
                    $ipfs_hash = $p[1];
×
543
                    $data = AppHelper::file_get_contents_curl(config('blockchain.ipfs.gateway_url').$ipfs_hash);
×
544
                    $sanitizedAddr = preg_replace('/[^A-Za-z0-9]/', '', $address);
×
545
                    $file_path = public_path("assets/citizen/{$sanitizedAddr}/data.json");
×
546
                    if (! is_dir(dirname($file_path))) {
×
547
                        mkdir(dirname($file_path), 0755, true);
×
548
                    }
549
                    file_put_contents($file_path, $data);
×
550
                    $d = json_decode($data);
×
551

552
                    $img = AppHelper::file_get_contents_curl($d->data->picture);
×
553
                    $file_path = public_path("assets/citizen/{$sanitizedAddr}/profile_pic.png");
×
554
                    file_put_contents($file_path, $img);
×
555
                }
556
            }
557

558
        }
559

560
    }
561

562
    /**
563
     * Helper function keeping a local cache of the Marscoin blockchain embedded data feed
564
     * as it pertains to MartianRepublic protocol anchors.
565
     */
566
    public static function insertBlockchainCache($address, $uid, $action_tag, $message, $embedded_link, $txid)
2✔
567
    {
568
        // First, check if a duplicate entry exists
569
        $existingFeed = Feed::where('address', $address)
2✔
570
            ->where('tag', $action_tag)
2✔
571
            ->where('txid', $txid)
2✔
572
            ->first();
2✔
573

574
        if ($existingFeed) {
2✔
575
            // If a duplicate is found, return the txid
576
            return $existingFeed->txid;
×
577
        }
578

579
        // If no duplicate, proceed to create a new entry
580
        $feed = new Feed;
2✔
581
        if ($uid) {
2✔
582
            $feed->userid = $uid;
2✔
583
        }
584
        if (! $action_tag) {
2✔
585
            return false;
×
586
        }
587
        $feed->tag = $action_tag;
2✔
588
        $feed->address = $address;
2✔
589
        $feed->message = $message;
2✔
590
        $feed->embedded_link = $embedded_link;
2✔
591
        $feed->txid = $txid;
2✔
592
        $feed->save();
2✔
593

594
        return true;
2✔
595
    }
596

597
    public static function insertPublicationCache($uid, $local_path, $ipfs_hash, $title)
×
598
    {
599
        $pub = new Publication;
×
600
        if ($uid) {
×
601
            $pub->userid = $uid;
×
602
            $pub->ipfs_hash = $ipfs_hash;
×
603
            $pub->local_path = $local_path;
×
604
            $pub->title = $title;
×
605
            $pub->save();
×
606

607
            return true;
×
608
        }
609
    }
610

611
    public static function time_elapsed_string($datetime, $full = false)
×
612
    {
613
        date_default_timezone_set('America/New_York');
×
614
        $now = new DateTime;
×
615
        $ago = new DateTime($datetime);
×
616
        $diff = $now->diff($ago);
×
617

618
        // Weeks are not a native DateInterval property; derive them from days
619
        // in local vars rather than assigning a dynamic $diff->w (deprecated on
620
        // internal classes in PHP 8.2+, and flagged by static analysis).
NEW
621
        $weeks = (int) floor($diff->d / 7);
×
622

NEW
623
        $counts = [
×
NEW
624
            'y' => $diff->y,
×
NEW
625
            'm' => $diff->m,
×
NEW
626
            'w' => $weeks,
×
NEW
627
            'd' => $diff->d - $weeks * 7,
×
NEW
628
            'h' => $diff->h,
×
NEW
629
            'i' => $diff->i,
×
NEW
630
            's' => $diff->s,
×
NEW
631
        ];
×
NEW
632
        $labels = [
×
633
            'y' => 'year',
×
634
            'm' => 'month',
×
635
            'w' => 'week',
×
636
            'd' => 'day',
×
637
            'h' => 'hour',
×
638
            'i' => 'minute',
×
639
            's' => 'second',
×
640
        ];
×
641

NEW
642
        $string = [];
×
NEW
643
        foreach ($counts as $k => $count) {
×
NEW
644
            if ($count) {
×
NEW
645
                $string[$k] = $count.' '.$labels[$k].($count > 1 ? 's' : '');
×
646
            }
647
        }
648

649
        if (! $full) {
×
650
            $string = array_slice($string, 0, 1);
×
651
        }
652

653
        return $string ? implode(', ', $string).' ago' : 'just now';
×
654
    }
655

656
    public static function days_elapsed_string($datetime, $full = false)
×
657
    {
658
        date_default_timezone_set('America/New_York');
×
659
        $now = new DateTime;
×
660
        $ago = new DateTime($datetime);
×
661
        $diff = $now->diff($ago);
×
662

663
        return $diff->days;
×
664
    }
665

666
    public static function hex2str($hex)
×
667
    {
668
        $str = '';
×
669
        for ($i = 0; $i < strlen($hex); $i += 2) {
×
670
            $str .= chr(hexdec(substr($hex, $i, 2)));
×
671
        }
672

673
        return $str;
×
674
    }
675

676
    // Function to get the price from CoinGecko with caching
677
    public static function getMarscoinPrice()
×
678
    {
679
        $url = config('blockchain.price.coingecko_url');
×
680

681
        // Use the Cache facade with the remember method
682
        $marsPriceData = Cache::remember('marscoin_price', 5, function () use ($url) {
×
683
            // Inside the closure, fetch the data from CoinGecko
684
            try {
685
                $response = file_get_contents($url);
×
686

687
                return json_decode($response);
×
688
            } catch (\Exception $e) {
×
689
                // Handle the exception if the API call fails
690
                return null;
×
691
            }
692
        });
×
693

694
        if ($marsPriceData) {
×
695
            return $marsPriceData->marscoin->usd;
×
696
        }
697

698
        // Handle the case where the API call was not successful or caching failed
699
        return 0;
×
700
    }
701

702
    public static function getMarscoinBalance($publicAddr)
1✔
703
    {
704
        $cacheKey = 'marscoin_balance_'.$publicAddr;
1✔
705

706
        // Cache for 2 minutes to reduce API load while staying reasonably fresh
707
        return Cache::remember($cacheKey, 120, function () use ($publicAddr) {
1✔
708
            // Primary: use local pebas (Electrum-based, more reliable)
709
            try {
710
                $ctx = stream_context_create(['http' => ['timeout' => 5]]);
1✔
711
                $response = Http::timeout(5)->get(config('blockchain.pebas.url')."/api/mars/balance?address={$publicAddr}")->body();
1✔
712
                if ($response) {
×
713
                    $data = json_decode($response, true);
×
714
                    if (isset($data['balance'])) {
×
715
                        return (float) $data['balance'];
×
716
                    }
717
                }
718
            } catch (\Exception $e) {
1✔
719
                // Fall through to explorer
720
            }
721

722
            // Fallback: explorer API (returns balance in satoshis)
723
            try {
724
                $response = Http::timeout(5)->get(config('blockchain.explorer.primary_url')."/api/addr/{$publicAddr}/balance")->body();
1✔
725
                if ($response !== false && is_numeric($response)) {
1✔
726
                    return $response * 0.00000001;
1✔
727
                }
728
            } catch (\Exception $e) {
×
729
                // Both failed
730
            }
731

732
            return 0;
1✔
733
        });
1✔
734
    }
735

736
    public static function getMarscoinTotalReceived($publicAddr)
×
737
    {
738
        $url = config('blockchain.explorer.primary_url')."/api/addr/{$publicAddr}/totalReceived";
×
739
        $cacheKey = 'marscoin_total_received_'.$publicAddr;
×
740

741
        $totalReceived = Cache::remember($cacheKey, 300, function () use ($url) {
×
742
            try {
743
                $response = file_get_contents($url);
×
744

745
                return $response; // Assuming the response is the total amount received
×
746
            } catch (\Exception $e) {
×
747
                return null;
×
748
            }
749
        });
×
750

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

755
        return null;
×
756
    }
757

758
    public static function getMarscoinTotalSent($publicAddr)
×
759
    {
760
        $url = config('blockchain.explorer.primary_url')."/api/addr/{$publicAddr}/totalSent";
×
761
        $cacheKey = 'marscoin_total_sent_'.$publicAddr;
×
762

763
        $totalSent = Cache::remember($cacheKey, 300, function () use ($url) {
×
764
            try {
765
                $response = file_get_contents($url);
×
766

767
                return $response; // Assuming the response is the total amount sent
×
768
            } catch (\Exception $e) {
×
769
                return null;
×
770
            }
771
        });
×
772

773
        if ($totalSent !== null) {
×
774
            return $totalSent * 0.00000001; // Convert from satoshis to Marscoin if necessary
×
775
        }
776

777
        return null;
×
778
    }
779

780
    public static function getMarscoinTotalAmount()
×
781
    {
782
        $cacheKey = 'marscoin_total_amount';
×
783

784
        $totalAmount = Cache::remember($cacheKey, 180, function () {
×
785
            // Primary: marscoin-cli (fastest, most reliable)
786
            try {
787
                $output = shell_exec(config('blockchain.rpc.cli_path').' -datadir='.config('blockchain.rpc.data_dir').' gettxoutsetinfo 2>/dev/null');
×
788
                if ($output) {
×
789
                    $data = json_decode($output, true);
×
790
                    if ($data && isset($data['total_amount'])) {
×
791
                        return round($data['total_amount'], 2);
×
792
                    }
793
                }
794
            } catch (\Exception $e) {
×
795
                // Fall through to explorer
796
            }
797

798
            // Fallback: explorer API
799
            try {
800
                $url = config('blockchain.explorer.primary_url').'/api/status?q=getTxOutSetInfo';
×
801
                $response = file_get_contents($url);
×
802
                $data = json_decode($response, true);
×
803
                if ($data && isset($data['txoutsetinfo']['total_amount'])) {
×
804
                    return round($data['txoutsetinfo']['total_amount'], 2);
×
805
                }
806
            } catch (\Exception $e) {
×
807
                return 39000000;
×
808
            }
809

810
            return 39000000;
×
811
        });
×
812

813
        return $totalAmount;
×
814
    }
815

816
    public static function getMarscoinNetworkInfo()
×
817
    {
818
        $url = config('blockchain.explorer.secondary_url').'/api/status?q=getInfo';
×
819
        $cacheKey = 'marscoin_network_info';
×
820

821
        $networkInfo = Cache::remember($cacheKey, 60, function () use ($url) {
×
822
            try {
823
                $response = file_get_contents($url);
×
824
                $data = json_decode($response, true);
×
825
                if (is_array($data)) {
×
826
                    return $data; // Return the network info if the response is valid
×
827
                }
828
                Log::debug('Set Network status cache');
×
829
            } catch (\Exception $e) {
×
830
                return []; // Return an empty array in case of an error
×
831
            }
832

833
            return []; // Also return an empty array if the API does not return a valid response
×
834
        });
×
835

836
        return $networkInfo;
×
837
    }
838

839
    /**
840
     * Check for recent posts in the forum.
841
     *
842
     * @return int Number of recent posts.
843
     */
844
    public static function checkForRecentPosts()
×
845
    {
846
        // Define the time frame for "recent" posts. For example, within the last 24 hours.
847
        $recentThreshold = Carbon::now()->subDay();
×
848

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

852
        return $recentPostsCount;
×
853
    }
854

855
    /**
856
     * Determines the Citizen Status of a user.
857
     *
858
     * @param  int  $userId  The ID of the user.
859
     * @return object An associative array containing the 'status' and 'type'.
860
     */
861
    public static function getCitizenStatus(int $userId)
×
862
    {
863
        // Default status for users without a profile entry.
864
        $statusDetails = [
×
865
            'status' => 'Newcomer',
×
866
            'type' => 'NC',
×
867
        ];
×
868

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

872
        if ($profile) {
×
873
            if ($profile->has_application > 0) {
×
874
                $statusDetails = [
×
875
                    'status' => 'Applicant',
×
876
                    'type' => 'AP',
×
877
                ];
×
878
            } elseif ($profile->general_public > 0) {
×
879
                $statusDetails = [
×
880
                    'status' => 'General Public',
×
881
                    'type' => 'GP',
×
882
                ];
×
883
            }
884

885
            // Check if the user is a citizen.
886
            $isCitizen = Citizen::where('userid', $userId)->exists();
×
887
            if ($isCitizen) {
×
888
                $statusDetails = [
×
889
                    'status' => 'Citizen',
×
890
                    'type' => 'CT',
×
891
                ];
×
892
            }
893
        }
894

895
        return (object) $statusDetails;
×
896
    }
897

898
    public static function createSlug($id, $title)
×
899
    {
900
        // Step 1: Concatenate
901
        $combined = $id.'-'.$title;
×
902

903
        // Step 2: Lowercase
904
        $combined = strtolower($combined);
×
905

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

909
        // Step 4: Trim whitespace
910
        $combined = trim($combined);
×
911

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

915
        // Step 6: Ensure uniqueness (not implemented here, depends on context)
916

917
        return $combined;
×
918
    }
919
}
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