diff --git a/app/Jobs/GithubAppPermissionJob.php b/app/Jobs/GithubAppPermissionJob.php
index d483fe4c2..7cd1b86ac 100644
--- a/app/Jobs/GithubAppPermissionJob.php
+++ b/app/Jobs/GithubAppPermissionJob.php
@@ -27,19 +27,28 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue
public function handle()
{
try {
- $github_access_token = generate_github_jwt_token($this->github_app);
+ $github_access_token = generateGithubJwt($this->github_app);
+
$response = Http::withHeaders([
'Authorization' => "Bearer $github_access_token",
'Accept' => 'application/vnd.github+json',
])->get("{$this->github_app->api_url}/app");
+
+ if (! $response->successful()) {
+ throw new \RuntimeException('Failed to fetch GitHub app permissions: '.$response->body());
+ }
+
$response = $response->json();
$permissions = data_get($response, 'permissions');
+
$this->github_app->contents = data_get($permissions, 'contents');
$this->github_app->metadata = data_get($permissions, 'metadata');
$this->github_app->pull_requests = data_get($permissions, 'pull_requests');
$this->github_app->administration = data_get($permissions, 'administration');
+
$this->github_app->save();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
+
} catch (\Throwable $e) {
send_internal_notification('GithubAppPermissionJob failed with: '.$e->getMessage());
throw $e;
diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php
index 370d00555..4a81d841f 100644
--- a/app/Livewire/Project/New/GithubPrivateRepository.php
+++ b/app/Livewire/Project/New/GithubPrivateRepository.php
@@ -105,7 +105,7 @@ class GithubPrivateRepository extends Component
$this->page = 1;
$this->selected_github_app_id = $github_app_id;
$this->github_app = GithubApp::where('id', $github_app_id)->first();
- $this->token = generate_github_installation_token($this->github_app);
+ $this->token = generateGithubInstallationToken($this->github_app);
$this->loadRepositoryByPage();
if ($this->repositories->count() < $this->total_repositories_count) {
while ($this->repositories->count() < $this->total_repositories_count) {
diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php
index 8f4f02f70..fc597748e 100644
--- a/app/Livewire/Source/Github/Change.php
+++ b/app/Livewire/Source/Github/Change.php
@@ -76,7 +76,7 @@ class Change extends Component
// Need administration:read:write permission
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#list-self-hosted-runners-for-a-repository
- // $github_access_token = generate_github_installation_token($this->github_app);
+ // $github_access_token = generateGithubInstallationToken($this->github_app);
// $repositories = Http::withToken($github_access_token)->get("{$this->github_app->api_url}/installation/repositories?per_page=100");
// $runners_by_repository = collect([]);
// $repositories = $repositories->json()['repositories'];
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 56faf6c31..289ef5b0f 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -999,7 +999,7 @@ class Application extends BaseModel
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$base_command = "{$base_command} {$this->source->html_url}/{$customRepository}";
} else {
- $github_access_token = generate_github_installation_token($this->source);
+ $github_access_token = generateGithubInstallationToken($this->source);
if ($exec_in_docker) {
$base_command = "{$base_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
@@ -1111,7 +1111,7 @@ class Application extends BaseModel
$commands->push($git_clone_command);
}
} else {
- $github_access_token = generate_github_installation_token($this->source);
+ $github_access_token = generateGithubInstallationToken($this->source);
if ($exec_in_docker) {
$git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git {$baseDir}";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 2ce94201c..329482230 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -3,37 +3,68 @@
namespace App\Providers;
use App\Models\PersonalAccessToken;
-use Illuminate\Support\Facades\Event;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\App;
+use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;
use Laravel\Sanctum\Sanctum;
+use Laravel\Telescope\TelescopeServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
- if ($this->app->environment('local')) {
- $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class);
+ if (App::isLocal()) {
+ $this->app->register(TelescopeServiceProvider::class);
}
}
public function boot(): void
{
- Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
- $event->extendSocialite('authentik', \SocialiteProviders\Authentik\Provider::class);
- });
- Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
+ $this->configureCommands();
+ $this->configureModels();
+ $this->configurePasswords();
+ $this->configureSanctumModel();
+ $this->configureGitHubHttp();
+ }
+ private function configureCommands(): void
+ {
+ if (App::isProduction()) {
+ DB::prohibitDestructiveCommands();
+ }
+ }
+
+ private function configureModels(): void
+ {
+ // Disabled because it's causing issues with the application
+ // Model::shouldBeStrict();
+ }
+
+ private function configurePasswords(): void
+ {
Password::defaults(function () {
- $rule = Password::min(8);
-
- return $this->app->isProduction()
- ? $rule->mixedCase()->letters()->numbers()->symbols()
- : $rule;
+ return App::isProduction()
+ ? Password::min(8)
+ ->mixedCase()
+ ->letters()
+ ->numbers()
+ ->symbols()
+ ->uncompromised()
+ : Password::min(8)->letters();
});
+ }
- Http::macro('github', function (string $api_url, ?string $github_access_token = null) {
+ private function configureSanctumModel(): void
+ {
+ Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
+ }
+
+ private function configureGitHubHttp(): void
+ {
+ Http::macro('GitHub', function (string $api_url, ?string $github_access_token = null) {
if ($github_access_token) {
return Http::withHeaders([
'X-GitHub-Api-Version' => '2022-11-28',
diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php
index 529ac82b1..3a3f6e7b2 100644
--- a/bootstrap/helpers/github.php
+++ b/bootstrap/helpers/github.php
@@ -12,77 +12,108 @@ use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Token\Builder;
-function generate_github_installation_token(GithubApp $source)
+function generateGithubToken(GithubApp $source, string $type)
{
- $signingKey = InMemory::plainText($source->privateKey->private_key);
- $algorithm = new Sha256;
- $tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
- $now = CarbonImmutable::now();
- $now = $now->setTime($now->format('H'), $now->format('i'));
- $issuedToken = $tokenBuilder
- ->issuedBy($source->app_id)
- ->issuedAt($now)
- ->expiresAt($now->modify('+10 minutes'))
- ->getToken($algorithm, $signingKey)
- ->toString();
- $token = Http::withHeaders([
- 'Authorization' => "Bearer $issuedToken",
- 'Accept' => 'application/vnd.github.machine-man-preview+json',
- ])->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens");
- if ($token->failed()) {
- throw new RuntimeException('Failed to get access token for '.$source->name.' with error: '.data_get($token->json(), 'message', 'no error message found'));
+ $response = Http::get("{$source->api_url}/zen");
+ $serverTime = CarbonImmutable::now()->setTimezone('UTC');
+ $githubTime = Carbon::parse($response->header('date'));
+ $timeDiff = abs($serverTime->diffInSeconds($githubTime));
+
+ if ($timeDiff > 50) {
+ throw new \Exception(
+ 'System time is out of sync with GitHub API time:
'.
+ '- System time: '.$serverTime->format('Y-m-d H:i:s').' UTC
'.
+ '- GitHub time: '.$githubTime->format('Y-m-d H:i:s').' UTC
'.
+ '- Difference: '.$timeDiff.' seconds
'.
+ 'Please synchronize your system clock.'
+ );
}
- return $token->json()['token'];
-}
-
-function generate_github_jwt_token(GithubApp $source)
-{
$signingKey = InMemory::plainText($source->privateKey->private_key);
$algorithm = new Sha256;
$tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
- $now = CarbonImmutable::now();
- $now = $now->setTime($now->format('H'), $now->format('i'));
+ $now = CarbonImmutable::now()->setTimezone('UTC');
+ $now = $now->setTime($now->format('H'), $now->format('i'), $now->format('s'));
- return $tokenBuilder
+ $jwt = $tokenBuilder
->issuedBy($source->app_id)
->issuedAt($now->modify('-1 minute'))
- ->expiresAt($now->modify('+10 minutes'))
+ ->expiresAt($now->modify('+8 minutes'))
->getToken($algorithm, $signingKey)
->toString();
+
+ return match ($type) {
+ 'jwt' => $jwt,
+ 'installation' => (function () use ($source, $jwt) {
+ $response = Http::withHeaders([
+ 'Authorization' => "Bearer $jwt",
+ 'Accept' => 'application/vnd.github.machine-man-preview+json',
+ ])->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens");
+
+ if (! $response->successful()) {
+ $error = data_get($response->json(), 'message', 'no error message found');
+ throw new RuntimeException("Failed to get installation token for {$source->name} with error: ".$error);
+ }
+
+ return $response->json()['token'];
+ })(),
+ default => throw new \InvalidArgumentException("Unsupported token type: {$type}")
+ };
+}
+
+function generateGithubInstallationToken(GithubApp $source)
+{
+ return generateGithubToken($source, 'installation');
+}
+
+function generateGithubJwt(GithubApp $source)
+{
+ return generateGithubToken($source, 'jwt');
}
function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', ?array $data = null, bool $throwError = true)
{
if (is_null($source)) {
- throw new \Exception('Not implemented yet.');
+ throw new \Exception('Source is required for API calls');
}
- if ($source->getMorphClass() === \App\Models\GithubApp::class) {
- if ($source->is_public) {
- $response = Http::github($source->api_url)->$method($endpoint);
+
+ if ($source->getMorphClass() !== GithubApp::class) {
+ throw new \InvalidArgumentException("Unsupported source type: {$source->getMorphClass()}");
+ }
+
+ if ($source->is_public) {
+ $response = Http::GitHub($source->api_url)->$method($endpoint);
+ } else {
+ $token = generateGithubInstallationToken($source);
+ if ($data && in_array(strtolower($method), ['post', 'patch', 'put'])) {
+ $response = Http::GitHub($source->api_url, $token)->$method($endpoint, $data);
} else {
- $github_access_token = generate_github_installation_token($source);
- if ($data && ($method === 'post' || $method === 'patch' || $method === 'put')) {
- $response = Http::github($source->api_url, $github_access_token)->$method($endpoint, $data);
- } else {
- $response = Http::github($source->api_url, $github_access_token)->$method($endpoint);
- }
+ $response = Http::GitHub($source->api_url, $token)->$method($endpoint);
}
}
- $json = $response->json();
- if ($response->failed() && $throwError) {
- ray($json);
- throw new \Exception("Failed to get data from {$source->name} with error:
".$json['message'].'
Rate Limit resets at: '.Carbon::parse((int) $response->header('X-RateLimit-Reset'))->format('Y-m-d H:i:s').'UTC');
+
+ if (! $response->successful() && $throwError) {
+ $resetTime = Carbon::parse((int) $response->header('X-RateLimit-Reset'))->format('Y-m-d H:i:s');
+ $errorMessage = data_get($response->json(), 'message', 'no error message found');
+ $remainingCalls = $response->header('X-RateLimit-Remaining', '0');
+
+ throw new \Exception(
+ 'GitHub API call failed:
'.
+ "Error: {$errorMessage}
".
+ 'Rate Limit Status:
'.
+ "- Remaining Calls: {$remainingCalls}
".
+ "- Reset Time: {$resetTime} UTC"
+ );
}
return [
'rate_limit_remaining' => $response->header('X-RateLimit-Remaining'),
'rate_limit_reset' => $response->header('X-RateLimit-Reset'),
- 'data' => collect($json),
+ 'data' => collect($response->json()),
];
}
-function get_installation_path(GithubApp $source)
+function getInstallationPath(GithubApp $source)
{
$github = GithubApp::where('uuid', $source->uuid)->first();
$name = str(Str::kebab($github->name));
@@ -90,7 +121,8 @@ function get_installation_path(GithubApp $source)
return "$github->html_url/$installation_path/$name/installations/new";
}
-function get_permissions_path(GithubApp $source)
+
+function getPermissionsPath(GithubApp $source)
{
$github = GithubApp::where('uuid', $source->uuid)->first();
$name = str(Str::kebab($github->name));
diff --git a/composer.json b/composer.json
index 055059a56..f01913b5f 100644
--- a/composer.json
+++ b/composer.json
@@ -47,6 +47,7 @@
"spatie/laravel-ray": "^1.37",
"spatie/laravel-schemaless-attributes": "^2.4",
"spatie/url": "^2.2",
+ "stevebauman/purify": "^6.2",
"stripe/stripe-php": "^16.2.0",
"symfony/yaml": "^7.1.6",
"visus/cuid2": "^4.1.0",
diff --git a/composer.lock b/composer.lock
index 457ec2066..2d7cf461d 100644
--- a/composer.lock
+++ b/composer.lock
@@ -1883,6 +1883,67 @@
],
"time": "2024-12-27T00:36:43+00:00"
},
+ {
+ "name": "ezyang/htmlpurifier",
+ "version": "v4.18.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ezyang/htmlpurifier.git",
+ "reference": "cb56001e54359df7ae76dc522d08845dc741621b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/cb56001e54359df7ae76dc522d08845dc741621b",
+ "reference": "cb56001e54359df7ae76dc522d08845dc741621b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
+ },
+ "require-dev": {
+ "cerdic/css-tidy": "^1.7 || ^2.0",
+ "simpletest/simpletest": "dev-master"
+ },
+ "suggest": {
+ "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
+ "ext-bcmath": "Used for unit conversion and imagecrash protection",
+ "ext-iconv": "Converts text to and from non-UTF-8 encodings",
+ "ext-tidy": "Used for pretty-printing HTML"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "library/HTMLPurifier.composer.php"
+ ],
+ "psr-0": {
+ "HTMLPurifier": "library/"
+ },
+ "exclude-from-classmap": [
+ "/library/HTMLPurifier/Language/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Edward Z. Yang",
+ "email": "admin@htmlpurifier.org",
+ "homepage": "http://ezyang.com"
+ }
+ ],
+ "description": "Standards compliant HTML filter written in PHP",
+ "homepage": "http://htmlpurifier.org/",
+ "keywords": [
+ "html"
+ ],
+ "support": {
+ "issues": "https://github.com/ezyang/htmlpurifier/issues",
+ "source": "https://github.com/ezyang/htmlpurifier/tree/v4.18.0"
+ },
+ "time": "2024-11-01T03:51:45+00:00"
+ },
{
"name": "firebase/php-jwt",
"version": "v6.10.2",
@@ -8330,6 +8391,72 @@
],
"time": "2024-03-08T11:35:19+00:00"
},
+ {
+ "name": "stevebauman/purify",
+ "version": "v6.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/stevebauman/purify.git",
+ "reference": "a449299a3d5f5f8ef177e626721b3f69143890a4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/stevebauman/purify/zipball/a449299a3d5f5f8ef177e626721b3f69143890a4",
+ "reference": "a449299a3d5f5f8ef177e626721b3f69143890a4",
+ "shasum": ""
+ },
+ "require": {
+ "ezyang/htmlpurifier": "^4.17",
+ "illuminate/contracts": "^7.0|^8.0|^9.0|^10.0|^11.0",
+ "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "orchestra/testbench": "^5.0|^6.0|^7.0|^8.0|^9.0",
+ "phpunit/phpunit": "^8.0|^9.0|^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "aliases": {
+ "Purify": "Stevebauman\\Purify\\Facades\\Purify"
+ },
+ "providers": [
+ "Stevebauman\\Purify\\PurifyServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Stevebauman\\Purify\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Steve Bauman",
+ "email": "steven_bauman@outlook.com"
+ }
+ ],
+ "description": "An HTML Purifier / Sanitizer for Laravel",
+ "keywords": [
+ "Purifier",
+ "clean",
+ "cleaner",
+ "html",
+ "laravel",
+ "purification",
+ "purify"
+ ],
+ "support": {
+ "issues": "https://github.com/stevebauman/purify/issues",
+ "source": "https://github.com/stevebauman/purify/tree/v6.2.2"
+ },
+ "time": "2024-09-24T12:27:10+00:00"
+ },
{
"name": "stripe/stripe-php",
"version": "v16.4.0",
diff --git a/config/purify.php b/config/purify.php
new file mode 100644
index 000000000..66dbbb568
--- /dev/null
+++ b/config/purify.php
@@ -0,0 +1,115 @@
+ 'default',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Config sets
+ |--------------------------------------------------------------------------
+ |
+ | Here you may configure various sets of configuration for differentiated use of HTMLPurifier.
+ | A specific set of configuration can be applied by calling the "config($name)" method on
+ | a Purify instance. Feel free to add/remove/customize these attributes as you wish.
+ |
+ | Documentation: http://htmlpurifier.org/live/configdoc/plain.html
+ |
+ | Core.Encoding The encoding to convert input to.
+ | HTML.Doctype Doctype to use during filtering.
+ | HTML.Allowed The allowed HTML Elements with their allowed attributes.
+ | HTML.ForbiddenElements The forbidden HTML elements. Elements that are listed in this
+ | string will be removed, however their content will remain.
+ | CSS.AllowedProperties The Allowed CSS properties.
+ | AutoFormat.AutoParagraph Newlines are converted in to paragraphs whenever possible.
+ | AutoFormat.RemoveEmpty Remove empty elements that contribute no semantic information to the document.
+ |
+ */
+
+ 'configs' => [
+
+ 'default' => [
+ 'Core.Encoding' => 'utf-8',
+ 'HTML.Doctype' => 'HTML 4.01 Transitional',
+ 'HTML.Allowed' => 'h1,h2,h3,h4,h5,h6,b,u,strong,i,em,s,del,a[href|title],ul,ol,li,p[style],br,span,img[width|height|alt|src],blockquote',
+ 'HTML.ForbiddenElements' => '',
+ 'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align',
+ 'AutoFormat.AutoParagraph' => false,
+ 'AutoFormat.RemoveEmpty' => false,
+ ],
+
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | HTMLPurifier definitions
+ |--------------------------------------------------------------------------
+ |
+ | Here you may specify a class that augments the HTML definitions used by
+ | HTMLPurifier. Additional HTML5 definitions are provided out of the box.
+ | When specifying a custom class, make sure it implements the interface:
+ |
+ | \Stevebauman\Purify\Definitions\Definition
+ |
+ | Note that these definitions are applied to every Purifier instance.
+ |
+ | Documentation: http://htmlpurifier.org/docs/enduser-customize.html
+ |
+ */
+
+ 'definitions' => Html5Definition::class,
+
+ /*
+ |--------------------------------------------------------------------------
+ | HTMLPurifier CSS definitions
+ |--------------------------------------------------------------------------
+ |
+ | Here you may specify a class that augments the CSS definitions used by
+ | HTMLPurifier. When specifying a custom class, make sure it implements
+ | the interface:
+ |
+ | \Stevebauman\Purify\Definitions\CssDefinition
+ |
+ | Note that these definitions are applied to every Purifier instance.
+ |
+ | CSS should be extending $definition->info['css-attribute'] = values
+ | See HTMLPurifier_CSSDefinition for further explanation
+ |
+ */
+
+ 'css-definitions' => null,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Serializer
+ |--------------------------------------------------------------------------
+ |
+ | The storage implementation where HTMLPurifier can store its serializer files.
+ | If the filesystem cache is in use, the path must be writable through the
+ | storage disk by the web server, otherwise an exception will be thrown.
+ |
+ */
+
+ 'serializer' => [
+ 'driver' => env('CACHE_STORE', env('CACHE_DRIVER', 'file')),
+ 'cache' => \Stevebauman\Purify\Cache\CacheDefinitionCache::class,
+ ],
+
+ // 'serializer' => [
+ // 'disk' => env('FILESYSTEM_DISK', 'local'),
+ // 'path' => 'purify',
+ // 'cache' => \Stevebauman\Purify\Cache\FilesystemDefinitionCache::class,
+ // ],
+
+];
diff --git a/resources/views/errors/500.blade.php b/resources/views/errors/500.blade.php
index 9e556cbdf..cc672a324 100644
--- a/resources/views/errors/500.blade.php
+++ b/resources/views/errors/500.blade.php
@@ -1,20 +1,20 @@
@extends('layouts.base')
-
500
-There has been an error, we are working on it. -
+500
+There has been an error with the following error message:
@if ($exception->getMessage() !== '') -Error: {{ $exception->getMessage() }}
-
+