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

marscoin / martianrepublic / 22642335023

03 Mar 2026 08:55PM UTC coverage: 4.588% (-0.3%) from 4.846%
22642335023

push

github

Lennart Lopin
security: Add gitleaks pre-commit hook, harden .gitignore for public repo

- Installed gitleaks 8.21.2 as pre-commit hook to prevent secret leaks
- Added .gitleaks.toml configuration
- Expanded .gitignore: user upload dirs (citizen/wallet/congress), backup files,
  IDE files, .ralphrc, Ralph logs
- Added PUBLIC REPO warning to Ralph PROMPT.md
- Removed hardcoded credentials from PROMPT.md
- Updated fix_plan.md with expanded 6-phase roadmap
- Added testing & security audit instructions to PROMPT.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

168 of 3662 relevant lines covered (4.59%)

0.23 hits per line

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

0.0
/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
                        $patterns = ['<?php', '<?=', '<? ', "<?\n", "<?\r", '<%', '<script language="php"', '<script language=\'php\''];
×
263
                        foreach ($patterns as $pattern) {
×
264
                                if (stripos($content, $pattern) !== false) {
×
265
                                        return true;
×
266
                                }
267
                        }
268
                        return false;
×
269
                }
270

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

292

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

298
                   $now = time();
×
299

300
                       $difference     = $now - $time;
×
301
                       $tense         = "ago";
×
302

303
                   for($j = 0; $difference >= $lengths[$j] && $j < count($lengths)-1; $j++) {
×
304
                       $difference /= $lengths[$j];
×
305
                   }
306

307
                   $difference = round($difference);
×
308

309
                   if($difference != 1) {
×
310
                       $periods[$j].= "s";
×
311
                   }
312

313
                   return "$difference $periods[$j] ago ";
×
314
                }
315

316

317
                public static function stats()
×
318
                {
319
                        $array["coincount"] = 35000000;
×
320
                        $json = AppHelper::file_get_contents_curl('http://explore.marscoin.org/api/status?q=getInfo');
×
321
                        $array["network"] = json_decode($json, true);
×
322
                        $json2 = AppHelper::file_get_contents_curl('http://explore.marscoin.org/api/status?q=getTxOutSetInfo');
×
323
                        $total = json_decode($json2, true);
×
324
                        if ($total && count($total) > 0)
×
325
                                $array["coincount"] = round($total['txoutsetinfo']['total_amount'], 2);
×
326

327
                        return $array;
×
328
                }
329

330
                public static function file_get_contents_curl($url)
×
331
                {
332
                        $ch = curl_init();
×
333

334
                        curl_setopt($ch, CURLOPT_AUTOREFERER, TRUE);
×
335
                        curl_setopt($ch, CURLOPT_HEADER, 0);
×
336
                        curl_setopt($ch, CURLOPT_HTTPHEADER, array('X-CMC_PRO_API_KEY: cf191ba7-4840-4a9a-bee4-617608afd8a4'));
×
337
                        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
×
338
                        curl_setopt($ch, CURLOPT_URL, $url);
×
339
                        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
×
340

341
                        $data = curl_exec($ch);
×
342
                        curl_close($ch);
×
343

344
                        return $data;
×
345
                }
346

347

348
                public static function uploadFile($name, $url){
×
349

350
                        $postField = array();
×
351
                        $tmpfile = $_FILES[$name]['tmp_name'];
×
352
                        $filename = basename($_FILES[$name]['name']);
×
353
                        $postField['files'] =  curl_file_create($tmpfile, $_FILES[$name]['type'], $filename);
×
354
                        $headers = array("Content-Type" => "multipart/form-data");
×
355
                        $curl_handle = curl_init();
×
356
                        curl_setopt($curl_handle, CURLOPT_URL, $url);
×
357

358
                        curl_setopt($curl_handle, CURLOPT_HTTPHEADER, $headers);
×
359
                        curl_setopt($curl_handle, CURLOPT_POST, TRUE);
×
360
                        curl_setopt($curl_handle, CURLOPT_POSTFIELDS, $postField);
×
361
                        curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, TRUE);
×
362
                        $returned_fileName = curl_exec($curl_handle);
×
363
                        curl_close($curl_handle);
×
364
                        return json_decode($returned_fileName);
×
365
                }
366

367

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

397

398
                public static function uploadFolder($filepath, $url)
×
399
                {
400
                        if (!is_dir($filepath) || !is_readable($filepath)) {
×
401
                                throw new \Exception("Directory is not accessible");
×
402
                        }
403

404
                        $files = scandir($filepath);
×
405
                        $data = [];
×
406
                        $headers = ["Content-Type: multipart/form-data"];
×
407
                        $ch = curl_init($url);
×
408

409
                        foreach ($files as $i => $filep) {
×
410
                                $filename = realpath($filepath . "/" . $filep);
×
411
                                if (!is_file($filename)) {
×
412
                                        continue;
×
413
                                }
414

415
                                $finfo = new \finfo(FILEINFO_MIME_TYPE);
×
416
                                $mimetype = $finfo->file($filename);
×
417
                                $cfile = curl_file_create($filename, $mimetype, basename($filename));
×
418
                                $data['file['.$i.']'] = $cfile;
×
419
                        }
420

421
                        curl_setopt_array($ch, [
×
422
                                CURLOPT_URL => $url,
×
423
                                CURLOPT_POST => true,
×
424
                                CURLOPT_POSTFIELDS => $data,
×
425
                                CURLOPT_HTTPHEADER => $headers,
×
426
                                CURLOPT_RETURNTRANSFER => true,
×
427
                        ]);
×
428

429
                        $result = curl_exec($ch);
×
430
                        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
×
431
                        curl_close($ch);
×
432

433
                        if ($httpCode < 200 || $httpCode >= 300) {
×
434
                                $details = json_decode($result, true);
×
435
                                $errorMsg = $details['msg'] ?? 'Unknown error occurred';
×
436
                                throw new \Exception($errorMsg);
×
437
                        }
438

439
                        //print_r($result);
440
                        //die;
441
                        // Prepare the result string by wrapping with brackets and replacing the '}{'
442
                        // between JSON objects with '},{', which creates a JSON array
443
                        $resultArrayString = '[' . preg_replace('/}\s*{/', '},{', $result) . ']';
×
444

445
                        // Decode the JSON array string
446
                        $jsonResult = json_decode($resultArrayString, true);
×
447

448
                        if (json_last_error() !== JSON_ERROR_NONE) {
×
449
                                throw new \Exception('Invalid JSON returned from API');
×
450
                        }
451

452
                        // Now $jsonResult is a proper array
453
                        // Search for the folder hash by finding the object with an empty "Name"
454
                        $folderHash = "";
×
455
                        foreach ($jsonResult as $item) {
×
456
                                if ($item['Name'] === "") {
×
457
                                        $folderHash = $item['Hash'];
×
458
                                        break; // No need to continue if the folder hash is found
×
459
                                }
460
                        }
461

462
                        // If the folder hash was found, return it as a JSON object
463
                        if ($folderHash !== "") {
×
464
                                return $folderHash;
×
465
                        } else {
466
                                // Handle the case where no folder hash was found
467
                                throw new \Exception('Folder hash not found in API response');
×
468
                        }
469

470
                }
471

472

473
                public static function file_post_content($url, $data)
×
474
                {
475

476
                        $postdata = http_build_query($data);
×
477

478
                        $opts = array('http' =>
×
479
                            array(
×
480
                                'method'  => 'POST',
×
481
                                'header'  => 'Content-Type: application/x-www-form-urlencoded',
×
482
                                'content' => $postdata
×
483
                            )
×
484
                        );
×
485

486
                        $context  = stream_context_create($opts);
×
487

488
                        $result = file_get_contents($url, false, $context);
×
489
                        return $result;
×
490
                }
491

492

493
                public static function getUserFromCache($address)
×
494
                {
495
                        $user = array();
×
496
                        $file_path = "./assets/citizen/" . $address . "/";
×
497

498
                        if (!file_exists($file_path)) {
×
499
                                mkdir($file_path);
×
500
                                AppHelper::addUserToLocalCache($address);
×
501
                        }
502

503
                        $json_string = file_get_contents($file_path . "data.json");
×
504
                        $user['data'] = json_decode($json_string);
×
505
                        $user['pic'] = "/assets/citizen/" . $address . "/profile_pic.png";
×
506
                        $user['vid'] = "/assets/citizen/" . $address . "/profile_video.webm";
×
507
                        return $user;
×
508
                }
509

510

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

537
                                                $img = AppHelper::file_get_contents_curl($d->data->picture);
×
538
                                                $file_path = "./assets/citizen/" . $address . "/profile_pic.png";
×
539
                                                file_put_contents($file_path, $img);
×
540
                                        }
541
                                }
542

543
                        }
544

545
                }
546

547

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

560
                        if ($existingFeed) {
×
561
                                // If a duplicate is found, return the txid
562
                                return $existingFeed->txid;
×
563
                        }
564

565
                        // If no duplicate, proceed to create a new entry
566
                        $feed = new Feed;
×
567
                        if ($uid) {
×
568
                                $feed->userid = $uid;
×
569
                        }
570
                        if (!$action_tag) {
×
571
                                return false;
×
572
                        }
573
                        $feed->tag = $action_tag;
×
574
                        $feed->address = $address;
×
575
                        $feed->message = $message;
×
576
                        $feed->embedded_link = $embedded_link;
×
577
                        $feed->txid = $txid;
×
578
                        $feed->save();
×
579

580
                        return true;
×
581
                }
582

583

584

585
                public static function insertPublicationCache($uid, $local_path, $ipfs_hash, $title)
×
586
                {
587
                        $pub = new Publication;
×
588
                        if($uid)
×
589
                        {
590
                                $pub->userid = $uid;
×
591
                                $pub->ipfs_hash = $ipfs_hash;
×
592
                                $pub->local_path = $local_path;
×
593
                                $pub->title = $title;
×
594
                                $pub->save();
×
595
                                return TRUE;
×
596
                        }
597
                }
598

599

600
                public static function time_elapsed_string($datetime, $full = false)
×
601
                {
602
                        date_default_timezone_set('America/New_York');
×
603
                    $now = new DateTime;
×
604
                    $ago = new DateTime($datetime);
×
605
                                $diff = $now->diff($ago);
×
606

607
                    $diff->w = floor($diff->d / 7);
×
608
                    $diff->d -= $diff->w * 7;
×
609

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

627
                    if (!$full) $string = array_slice($string, 0, 1);
×
628
                    return $string ? implode(', ', $string) . ' ago' : 'just now';
×
629
                }
630

631
                public static function days_elapsed_string($datetime, $full = false)
×
632
                {
633
                        date_default_timezone_set('America/New_York');
×
634
                    $now = new DateTime;
×
635
                    $ago = new DateTime($datetime);
×
636
                                $diff = $now->diff($ago);
×
637

638
                                return $diff->days;
×
639
                }
640

641

642
                public static function hex2str($hex)
×
643
                {
644
                    $str = '';
×
645
                    for($i=0;$i<strlen($hex);$i+=2) $str .= chr(hexdec(substr($hex,$i,2)));
×
646
                    return $str;
×
647
                }
648

649

650
                        // Function to get the price from CoinGecko with caching
651
                public static function getMarscoinPrice()
×
652
                {
653
                        $url = "https://api.coingecko.com/api/v3/simple/price?ids=marscoin&vs_currencies=usd";
×
654

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

667
                        if ($marsPriceData) {
×
668
                                return $marsPriceData->marscoin->usd;
×
669
                        }
670

671
                        // Handle the case where the API call was not successful or caching failed
672
                        return 0;
×
673
                }
674

675

676
                public static function getMarscoinBalance($publicAddr)
×
677
                {
678
                        $url = "https://explore.marscoin.org/api/addr/{$publicAddr}/balance";
×
679

680
                        // Unique cache key to store the balance for each address
681
                        $cacheKey = 'marscoin_balance_' . $publicAddr;
×
682

683
                        // Use the Cache facade with the remember method
684
                        $curBalance = Cache::remember($cacheKey, 5, function () use ($url) {
×
685
                                // Inside the closure, fetch the balance from the explorer
686
                                try {
687
                                        $response = file_get_contents($url);
×
688
                                        return $response; // Assuming the response is the balance
×
689
                                } catch (\Exception $e) {
×
690
                                        // Handle the exception if the API call fails
691
                                        return null;
×
692
                                }
693
                        });
×
694

695
                        if ($curBalance !== null) {
×
696
                                // Assuming the balance is returned in satoshis, convert to Marscoin if necessary
697
                                // The conversion logic depends on the API's response format
698
                                return $curBalance * 0.00000001; // Example conversion, adjust based on actual response
×
699
                        }
700

701
                        // Handle the case where the API call was not successful or caching failed
702
                        return 0;
×
703
                }
704

705
                public static function getMarscoinTotalReceived($publicAddr)
×
706
                {
707
                        $url = "https://explore.marscoin.org/api/addr/{$publicAddr}/totalReceived";
×
708
                        $cacheKey = 'marscoin_total_received_' . $publicAddr;
×
709

710
                        $totalReceived = Cache::remember($cacheKey, 5, function () use ($url) {
×
711
                                try {
712
                                        $response = file_get_contents($url);
×
713
                                        return $response; // Assuming the response is the total amount received
×
714
                                } catch (\Exception $e) {
×
715
                                        return null;
×
716
                                }
717
                        });
×
718

719
                        if ($totalReceived !== null) {
×
720
                                return $totalReceived * 0.00000001; // Convert from satoshis to Marscoin if necessary
×
721
                        }
722

723
                        return null;
×
724
                }
725

726

727
                public static function getMarscoinTotalSent($publicAddr)
×
728
                {
729
                        $url = "https://explore.marscoin.org/api/addr/{$publicAddr}/totalSent";
×
730
                        $cacheKey = 'marscoin_total_sent_' . $publicAddr;
×
731

732
                        $totalSent = Cache::remember($cacheKey, 5, function () use ($url) {
×
733
                                try {
734
                                        $response = file_get_contents($url);
×
735
                                        return $response; // Assuming the response is the total amount sent
×
736
                                } catch (\Exception $e) {
×
737
                                        return null;
×
738
                                }
739
                        });
×
740

741
                        if ($totalSent !== null) {
×
742
                                return $totalSent * 0.00000001; // Convert from satoshis to Marscoin if necessary
×
743
                        }
744

745
                        return null;
×
746
                }
747

748

749
                public static function getMarscoinTotalAmount()
×
750
                {
751
                        $url = "https://explore.marscoin.org/api/status?q=getTxOutSetInfo";
×
752
                        $cacheKey = 'marscoin_total_amount';
×
753

754
                        $totalAmount = Cache::remember($cacheKey, 180, function () use ($url) {
×
755
                                try {
756
                                        $response = file_get_contents($url);
×
757
                                        $data = json_decode($response, true);
×
758
                                        if ($data && count($data) > 0) {
×
759
                                                return round($data['txoutsetinfo']['total_amount'], 2);
×
760
                                        }
761
                                } catch (\Exception $e) {
×
762
                                        return 39000000; // Default value in case of an error
×
763
                                }
764
                                return 39000000; // Default value if the API does not return a valid response
×
765
                        });
×
766

767
                        return $totalAmount;
×
768
                }
769

770
                public static function getMarscoinNetworkInfo()
×
771
                {
772
                        $url = "http://explore2.marscoin.org/api/status?q=getInfo";
×
773
                        $cacheKey = 'marscoin_network_info';
×
774

775
                        $networkInfo = Cache::remember($cacheKey, 60, function () use ($url) {
×
776
                                try {
777
                                        $response = file_get_contents($url);
×
778
                                        $data = json_decode($response, true);
×
779
                                        if (is_array($data)) {
×
780
                                                return $data; // Return the network info if the response is valid
×
781
                                        }
782
                                        Log::debug("Set Network status cache");
×
783
                                } catch (\Exception $e) {
×
784
                                        return []; // Return an empty array in case of an error
×
785
                                }
786
                                return []; // Also return an empty array if the API does not return a valid response
×
787
                        });
×
788

789
                        return $networkInfo;
×
790
                }
791

792
                 /**
793
                 * Check for recent posts in the forum.
794
                 *
795
                 * @return int Number of recent posts.
796
                 */
797
                public static function checkForRecentPosts()
×
798
                {
799
                        // Define the time frame for "recent" posts. For example, within the last 24 hours.
800
                        $recentThreshold = Carbon::now()->subDay();
×
801

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

805
                        return $recentPostsCount;
×
806
                }
807

808

809
                /**
810
                 * Determines the Citizen Status of a user.
811
                 *
812
                 * @param int $userId The ID of the user.
813
                 * @return object An associative array containing the 'status' and 'type'.
814
                 */
815
                public static function getCitizenStatus(int $userId)
×
816
                {
817
                        // Default status for users without a profile entry.
818
                        $statusDetails = [
×
819
                                'status' => 'Newcomer',
×
820
                                'type' => 'NC',
×
821
                        ];
×
822

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

826
                        if ($profile) {
×
827
                                if ($profile->has_application > 0) {
×
828
                                        $statusDetails = [
×
829
                                                'status' => 'Applicant',
×
830
                                                'type' => 'AP',
×
831
                                        ];
×
832
                                } elseif ($profile->general_public > 0) {
×
833
                                        $statusDetails = [
×
834
                                                'status' => 'General Public',
×
835
                                                'type' => 'GP',
×
836
                                        ];
×
837
                                }
838

839
                                // Check if the user is a citizen.
840
                                $isCitizen = Citizen::where('userid', $userId)->exists();
×
841
                                if ($isCitizen) {
×
842
                                        $statusDetails = [
×
843
                                                'status' => 'Citizen',
×
844
                                                'type' => 'CT',
×
845
                                        ];
×
846
                                }
847
                        }
848

849
                        return (object)$statusDetails;
×
850
                }
851

852

853
                public static function createSlug($id, $title) {
×
854
                        // Step 1: Concatenate
855
                        $combined = $id . '-' . $title;
×
856

857
                        // Step 2: Lowercase
858
                        $combined = strtolower($combined);
×
859

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

863
                        // Step 4: Trim whitespace
864
                        $combined = trim($combined);
×
865

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

869
                        // Step 6: Ensure uniqueness (not implemented here, depends on context)
870

871
                        return $combined;
×
872
                }
873

874

875

876
}
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