diff --git a/.env.example b/.env.example index 36fe06e..e745063 100644 --- a/.env.example +++ b/.env.example @@ -20,7 +20,6 @@ LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug -# Only 'mysql' and 'pgsql' are supported due to a 'naturalsort' database function. DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 @@ -29,7 +28,8 @@ DB_USERNAME=root DB_PASSWORD= # This is only needed if you are running the app:import-woltlab-data command. -# For normal development you should just seed the database with fake data. +# For normal development you should just seed the database with fake data: +# `php artisan migrate:fresh --seed` DB_WOLTLAB_CONNECTION=mysql DB_WOLTLAB_HOST=127.0.0.1 DB_WOLTLAB_PORT=3306 diff --git a/app/Console/Commands/ImportWoltlabData.php b/app/Console/Commands/ImportWoltlabData.php index bf94538..d87dfe7 100644 --- a/app/Console/Commands/ImportWoltlabData.php +++ b/app/Console/Commands/ImportWoltlabData.php @@ -14,6 +14,8 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; +use League\HTMLToMarkdown\HtmlConverter; +use Stevebauman\Purify\Facades\Purify; class ImportWoltlabData extends Command { @@ -212,19 +214,19 @@ class ImportWoltlabData extends Command } $insertData[] = [ - 'hub_id' => $mod->fileID, + 'hub_id' => (int) $mod->fileID, 'user_id' => User::whereHubId($mod->userID)->value('id'), 'name' => $modContent ? $modContent->subject : '', 'slug' => $modContent ? Str::slug($modContent->subject) : '', 'teaser' => $modContent ? (strlen($modContent->teaser) > 100 ? Str::take($modContent->teaser, 97).'...' : $modContent->teaser) : '', - 'description' => $modContent ? $modContent->message : '', + 'description' => $this->convertModDescription($modContent?->message ?? ''), 'thumbnail' => $this->fetchModThumbnail($command, $curl, $mod->fileID, $mod->iconHash, $mod->iconExtension), 'license_id' => License::whereHubId($mod->licenseID)->value('id'), 'source_code_link' => $this->fetchSourceLinkValue($modOptions), - 'featured' => $mod->isFeatured, + 'featured' => (bool) $mod->isFeatured, 'contains_ai_content' => $this->fetchContainsAiContentValue($modOptions), 'contains_ads' => $this->fetchContainsAdsValue($modOptions), - 'disabled' => $mod->isDisabled, + 'disabled' => (bool) $mod->isDisabled, 'created_at' => Carbon::parse($mod->time, 'UTC'), 'updated_at' => Carbon::parse($mod->lastChangeTime, 'UTC'), ]; @@ -408,11 +410,11 @@ class ImportWoltlabData extends Command 'hub_id' => $version->versionID, 'mod_id' => $modId, 'version' => $version->versionNumber, - 'description' => $versionContent['description'] ?? '', + 'description' => $this->convertModDescription($versionContent['description'] ?? ''), 'link' => $version->downloadURL, 'spt_version_id' => SptVersion::whereHubId($versionLabel)->value('id'), 'virus_total_link' => $this->fetchVirusTotalLink($modOptions), - 'downloads' => (int) $version->downloads, + 'downloads' => max((int) $version->downloads, 0), // Ensure the value is at least 0 'disabled' => (bool) $version->isDisabled, 'created_at' => Carbon::parse($version->uploadTime, 'UTC'), 'updated_at' => Carbon::parse($version->uploadTime, 'UTC'), @@ -449,8 +451,10 @@ class ImportWoltlabData extends Command return ''; } - protected function updateDisabledPropertty(): void + protected function convertModDescription(string $description): string { - $this->output->newLine(); + // Alright, hear me out... Shut up. + $converter = new HtmlConverter(); + return $converter->convert(Purify::clean($description)); } } diff --git a/app/Models/Mod.php b/app/Models/Mod.php index 32a5aa0..e080ef7 100644 --- a/app/Models/Mod.php +++ b/app/Models/Mod.php @@ -76,10 +76,10 @@ class Mod extends Model ->orderByDesc( SptVersion::select('version') ->whereColumn('mod_versions.spt_version_id', 'spt_versions.id') - ->orderByDesc(DB::raw('naturalSort(version)')) + ->orderByDesc('version') ->take(1), ) - ->orderByDesc(DB::raw('naturalSort(version)')) + ->orderByDesc('version') ->take(1), ]) ->havingNotNull('latest_spt_version_id') diff --git a/composer.json b/composer.json index 9263bec..320df4f 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,9 @@ "laravel/sanctum": "^4.0", "laravel/scout": "^10.9", "laravel/tinker": "^2.9", - "livewire/livewire": "^3.0" + "league/html-to-markdown": "^5.1", + "livewire/livewire": "^3.0", + "stevebauman/purify": "^6.2" }, "require-dev": { "barryvdh/laravel-debugbar": "^3.13", diff --git a/composer.lock b/composer.lock index 70b6766..88a43f7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bea00e78566494d8d997f51c6871ca02", + "content-hash": "b42a2bd2686065b7db27628579fde13c", "packages": [ { "name": "bacon/bacon-qr-code", @@ -878,6 +878,67 @@ ], "time": "2023-10-06T06:47:41+00:00" }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.17.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/bbc513d79acf6691fa9cf10f192c90dd2957f18c", + "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c", + "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" + }, + "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.17.0" + }, + "time": "2023-11-17T15:01:25+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.3.0", @@ -2757,6 +2818,95 @@ }, "time": "2024-05-06T20:05:52+00:00" }, + { + "name": "league/html-to-markdown", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/html-to-markdown.git", + "reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/0b4066eede55c48f38bcee4fb8f0aa85654390fd", + "reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "mikehaertl/php-shellcommand": "^1.1.0", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^8.5 || ^9.2", + "scrutinizer/ocular": "^1.6", + "unleashedtech/php-coding-standard": "^2.7 || ^3.0", + "vimeo/psalm": "^4.22 || ^5.0" + }, + "bin": [ + "bin/html-to-markdown" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\HTMLToMarkdown\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + }, + { + "name": "Nick Cernis", + "email": "nick@cern.is", + "homepage": "http://modernnerd.net", + "role": "Original Author" + } + ], + "description": "An HTML-to-markdown conversion helper for PHP", + "homepage": "https://github.com/thephpleague/html-to-markdown", + "keywords": [ + "html", + "markdown" + ], + "support": { + "issues": "https://github.com/thephpleague/html-to-markdown/issues", + "source": "https://github.com/thephpleague/html-to-markdown/tree/5.1.1" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/html-to-markdown", + "type": "tidelift" + } + ], + "time": "2023-07-12T21:21:09+00:00" + }, { "name": "league/mime-type-detection", "version": "1.15.0", @@ -4576,6 +4726,72 @@ ], "time": "2024-03-06T16:03:49+00:00" }, + { + "name": "stevebauman/purify", + "version": "v6.2.0", + "source": { + "type": "git", + "url": "https://github.com/stevebauman/purify.git", + "reference": "303d23e5756a1fd0e9b34f14459ec6aa327a8412" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stevebauman/purify/zipball/303d23e5756a1fd0e9b34f14459ec6aa327a8412", + "reference": "303d23e5756a1fd0e9b34f14459ec6aa327a8412", + "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": { + "providers": [ + "Stevebauman\\Purify\\PurifyServiceProvider" + ], + "aliases": { + "Purify": "Stevebauman\\Purify\\Facades\\Purify" + } + } + }, + "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.0" + }, + "time": "2024-03-12T15:22:59+00:00" + }, { "name": "symfony/clock", "version": "v7.1.0", diff --git a/config/purify.php b/config/purify.php new file mode 100644 index 0000000..7a3d86d --- /dev/null +++ b/config/purify.php @@ -0,0 +1,112 @@ + '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 Strict', + 'HTML.Allowed' => 'h1,h2,h3,h4,h5,h6,b,strong,i,em,s,del,a[href|title],ul,ol,li,p,br,span,img[width|height|alt|src],blockquote', + 'HTML.ForbiddenElements' => '', + 'HTML.TargetBlank' => true, + 'CSS.AllowedProperties' => 'font-weight,font-style,text-decoration,color,background-color,text-align', + 'AutoFormat.RemoveEmpty.RemoveNbsp' => true, + 'AutoFormat.AutoParagraph' => false, + 'AutoFormat.RemoveEmpty' => true, + 'AutoFormat.RemoveSpansWithoutAttributes' => true, + 'URI.AllowedSchemes' => ['http' => true, 'https' => true, 'ftp' => true], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | 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_DRIVER', 'file'), + 'cache' => \Stevebauman\Purify\Cache\CacheDefinitionCache::class, + ], +]; diff --git a/database/migrations/2024_05_24_132251_create_natural_sort_function.php b/database/migrations/2024_05_24_132251_create_natural_sort_function.php deleted file mode 100644 index 1d879cb..0000000 --- a/database/migrations/2024_05_24_132251_create_natural_sort_function.php +++ /dev/null @@ -1,79 +0,0 @@ - @foreach ($mods as $mod) - +
diff --git a/resources/views/mod/show.blade.php b/resources/views/mod/show.blade.php index 49710e6..c1eb7b3 100644 --- a/resources/views/mod/show.blade.php +++ b/resources/views/mod/show.blade.php @@ -7,34 +7,68 @@
+
+
+
+
+ @if(empty($mod->thumbnail)) + {{ $mod->name }} + + @else + {{ $mod->name }} + @endif +
+
+

+ {{ $mod->name }} + + {{ $mod->latestSptVersion->version }} + +

+

{{ __('Created by') }} {{ $mod->user->name }}

+

{{ $mod->latestSptVersion->sptVersion->version }} {{ __('Compatible') }}

+

{{ $mod->total_downloads }} {{ __('Downloads') }}

+
+
+
-
-
-
- @if(empty($mod->thumbnail)) - {{ $mod->name }} - - @else - {{ $mod->name }} - @endif +
- +
@@ -60,20 +94,26 @@

@endif -
  • -

    {{ __('Latest VirusTotal Result') }}

    -

    - - {{ $mod->latestSptVersion->virus_total_link }} - -

    -
  • -
  • -

    {{ __('Includes advertising?') }}

    -
  • -
  • -

    {{ __('Includes AI generated content?') }}

    -
  • + @if($mod->latestSptVersion->virus_total_link) +
  • +

    {{ __('Latest VirusTotal Result') }}

    +

    + + {{ $mod->latestSptVersion->virus_total_link }} + +

    +
  • + @endif + @if($mod->contains_ads) +
  • +

    {{ __('Includes Advertising') }}

    +
  • + @endif + @if($mod->contains_ai_content) +
  • +

    {{ __('Includes AI Generated Content') }}

    +
  • + @endif