Преглед изворни кода

Merge pull request #2354 from pixelfed/staging

Add MediaBlocklist feature
daniel пре 5 година
родитељ
комит
351fe7035f

+ 3 - 0
CHANGELOG.md

@@ -13,6 +13,7 @@
 - Add MediaPathService ([c54b29c5](https://github.com/pixelfed/pixelfed/commit/c54b29c5))
 - Add Media Tags ([711fc020](https://github.com/pixelfed/pixelfed/commit/711fc020))
 - Add MediaTagService ([524c6d45](https://github.com/pixelfed/pixelfed/commit/524c6d45))
+- Add MediaBlocklist feature ([ba1f7e7e](https://github.com/pixelfed/pixelfed/commit/ba1f7e7e))
 
 ### Updated
 - Updated PostComponent, fix remote urls ([42716ccc](https://github.com/pixelfed/pixelfed/commit/42716ccc))
@@ -75,6 +76,8 @@
 - Updated ComposeModal.vue, add 451 http code warning. ([b213dcda](https://github.com/pixelfed/pixelfed/commit/b213dcda))
 - Updated Profile.vue, add empty follower modal placeholder. ([b542a3c5](https://github.com/pixelfed/pixelfed/commit/b542a3c5))
 - Updated private profiles, add context menu to mute, block or report. ([487c4ffc](https://github.com/pixelfed/pixelfed/commit/487c4ffc))
+- Updated webfinger util, fix bug preventing username with dots. ([c2d194af](https://github.com/pixelfed/pixelfed/commit/c2d194af))
+- Updated upload endpoints with MediaBlocklist checks. ([597378bf](https://github.com/pixelfed/pixelfed/commit/597378bf))
 
 ## [v0.10.9 (2020-04-17)](https://github.com/pixelfed/pixelfed/compare/v0.10.8...v0.10.9)
 ### Added

+ 10 - 4
app/Http/Controllers/Admin/AdminMediaController.php

@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
 use DB, Cache;
 use App\{
 	Media,
+	MediaBlocklist,
 	Profile,
 	Status
 };
@@ -21,8 +22,8 @@ trait AdminMediaController
 				'nullable',
 				'string',
 				'min:1',
-				'max:4',
-				Rule::in(['grid','list'])
+				'max:13',
+				Rule::in(['grid','list', 'banned', 'addbanned'])
 			],
 			'search' => 'nullable|string|min:1|max:20'
 		]);
@@ -34,9 +35,14 @@ trait AdminMediaController
 				->whereIn('profile_id', $profiles)
 				->orWhere('mime', $request->input('search'))
 				->paginate(12);
-		} else {
-			$media = Media::whereHas('status')->with('status')->orderby('id', 'desc')->paginate(12);
+			return view('admin.media.home', compact('media'));
 		}
+
+		if($request->input('layout') == 'banned') {
+			$media = MediaBlocklist::latest()->paginate(12);
+			return view('admin.media.home', compact('media'));
+		}
+		$media = Media::whereHas('status')->with('status')->orderby('id', 'desc')->paginate(12);
 		return view('admin.media.home', compact('media'));
 	}
 

+ 5 - 1
app/Http/Controllers/Api/ApiV1Controller.php

@@ -48,9 +48,11 @@ use App\Jobs\VideoPipeline\{
 use App\Services\{
     NotificationService,
     MediaPathService,
-    SearchApiV2Service
+    SearchApiV2Service,
+    MediaBlocklistService
 };
 
+
 class ApiV1Controller extends Controller 
 {
 	protected $fractal;
@@ -1046,6 +1048,8 @@ class ApiV1Controller extends Controller
         $path = $photo->store($storagePath);
         $hash = \hash_file('sha256', $photo);
 
+        abort_if(MediaBlocklistService::exists($hash) == true, 451);
+
         $media = new Media();
         $media->status_id = null;
         $media->profile_id = $profile->id;

+ 3 - 0
app/Http/Controllers/Api/BaseApiController.php

@@ -36,6 +36,7 @@ use App\Jobs\VideoPipeline\{
 };
 use App\Services\NotificationService;
 use App\Services\MediaPathService;
+use App\Services\MediaBlocklistService;
 
 class BaseApiController extends Controller
 {
@@ -247,6 +248,8 @@ class BaseApiController extends Controller
         $path = $photo->store($storagePath);
         $hash = \hash_file('sha256', $photo);
 
+        abort_if(MediaBlocklistService::exists($hash) == true, 451);
+
         $media = new Media();
         $media->status_id = null;
         $media->profile_id = $profile->id;

+ 50 - 0
app/Http/Controllers/MediaBlocklistController.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use App\MediaBlocklist;
+
+class MediaBlocklistController extends Controller
+{
+    public function __construct()
+    {
+    	$this->middleware('auth');
+    	$this->middleware('admin');
+    }
+
+    public function add(Request $request)
+    {
+    	$this->validate($request, [
+    		'hash' => 'required|string|size:64',
+    		'name' => 'nullable|string',
+    		'description' => 'nullable|string|max:500',
+    	]);
+
+    	$hash = $request->input('hash');
+    	abort_if(preg_match("/^([a-f0-9]{64})$/", $hash) !== 1, 400);
+
+    	$name = $request->input('name');
+    	$description = $request->input('description');
+
+    	$mb = new MediaBlocklist;
+    	$mb->sha256 = $hash;
+    	$mb->name = $name;
+    	$mb->description = $description;
+    	$mb->save();
+
+    	return redirect('/i/admin/media?layout=banned');
+    }
+
+    public function delete(Request $request)
+    {
+        $this->validate($request, [
+            'id'    => 'required|integer'
+        ]);
+
+        $media = MediaBlocklist::findOrFail($request->input('id'));
+        $media->delete();
+
+        return redirect('/i/admin/media?layout=banned');
+    }
+}

+ 1 - 1
app/Http/Middleware/TwoFactorAuth.php

@@ -21,7 +21,7 @@ class TwoFactorAuth
             $enabled = (bool) $user->{'2fa_enabled'};
             if($enabled != false) {
                 $checkpoint = 'i/auth/checkpoint';
-                if($request->session()->has('2fa.session.active') !== true && !$request->is($checkpoint))
+                if($request->session()->has('2fa.session.active') !== true && !$request->is($checkpoint) && !$request->is('logout'))
                 {
                     return redirect('/i/auth/checkpoint');
                 } elseif($request->session()->has('2fa.attempts') && (int) $request->session()->get('2fa.attempts') > 3) {

+ 10 - 0
app/MediaBlocklist.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App;
+
+use Illuminate\Database\Eloquent\Model;
+
+class MediaBlocklist extends Model
+{
+    //
+}

+ 44 - 0
app/Services/MediaBlocklistService.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace App\Services;
+
+use Cache;
+use Illuminate\Support\Facades\File;
+use App\Media;
+use App\MediaBlocklist;
+
+class MediaBlocklistService
+{
+	public static function get()
+	{
+		return MediaBlocklist::whereActive(true)
+			->pluck('sha256')
+			->toArray();
+	}
+
+	public static function exists($hash)
+	{
+		$hashes = self::get();
+		return in_array($hash, $hashes) == true;
+	}
+
+	public static function remove($hash)
+	{
+		if(!self::exists($hash)) {
+			return;
+		}
+		MediaBlocklist::whereSha256($hash)->delete();
+		return;
+	}
+
+	public static function add($hash, $metadata)
+	{
+		$m = new MediaBlocklist;
+		$m->sha256 = $hash;
+		$m->active = true;
+		$m->metadata = json_encode($metadata);
+		$m->save();
+
+		return $m;
+	}
+}

+ 5 - 24
app/Util/Lexer/Nickname.php

@@ -10,32 +10,13 @@ class Nickname
             $url = str_replace('acct:', '', $url);
         }
 
-        if (!str_contains($url, '@') && filter_var($url, FILTER_VALIDATE_URL)) {
-            $parsed = parse_url($url);
-            $username = str_replace(['/', '\\', '@'], '', $parsed['path']);
-
-            return ['domain' => $parsed['host'], 'username' => $username];
+        if(starts_with($url, '@')) {
+            $url = substr($url, 1);
         }
-        $parts = explode('@', $url);
-        $username = null;
-        $domain = null;
-
-        foreach ($parts as $part) {
 
-        // skip empty array slices
-            if (empty($part)) {
-                continue;
-            }
-
-            // if slice contains . assume its a domain
-            if (str_contains($part, '.')) {
-                $domain = filter_var($part, FILTER_VALIDATE_URL) ?
-                    parse_url($part, PHP_URL_HOST) :
-                    $part;
-            } else {
-                $username = $part;
-            }
-        }
+        $parts = explode('@', $url);
+        $username = $parts[0];
+        $domain = $parts[1];
 
         return ['domain' => $domain, 'username' => $username];
     }

+ 37 - 0
database/migrations/2020_07_25_230100_create_media_blocklists_table.php

@@ -0,0 +1,37 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateMediaBlocklistsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('media_blocklists', function (Blueprint $table) {
+            $table->id();
+            $table->string('sha256')->nullable()->unique()->index();
+            $table->string('sha512')->nullable()->unique()->index();
+            $table->string('name')->nullable();
+            $table->text('description')->nullable();
+            $table->boolean('active')->default(true)->index();
+            $table->json('metadata')->nullable();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('media_blocklists');
+    }
+}

+ 85 - 31
resources/views/admin/media/home.blade.php

@@ -4,11 +4,11 @@
 <div class="title">
 	<h3 class="font-weight-bold d-inline-block">Media</h3>
 	<span class="float-right">
-		<a class="btn btn-{{request()->input('layout')!=='list'?'primary':'light'}} btn-sm" href="{{route('admin.media')}}">
-			<i class="fas fa-th"></i>
+		<a class="btn btn-{{request()->input('layout')!=='banned'?'primary':'light'}} btn-sm font-weight-bold" href="{{route('admin.media')}}">
+			All
 		</a>
-		<a class="btn btn-{{request()->input('layout')=='list'?'primary':'light'}} btn-sm mr-3" href="{{route('admin.media',['layout'=>'list', 'page' => request()->input('page') ?? 1])}}">
-			<i class="fas fa-list"></i>
+		<a class="btn btn-{{request()->input('layout')=='banned'?'primary':'light'}} btn-sm mr-3 font-weight-bold" href="{{route('admin.media',['layout'=>'banned', 'page' => request()->input('page') ?? 1])}}">
+			Banned
 		</a>
 		<div class="dropdown d-inline-block">
 			<button class="btn btn-light btn-sm dropdown-toggle font-weight-bold" type="button" id="filterDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@@ -16,8 +16,8 @@
 			</button>
 			<div class="dropdown-menu dropdown-menu-right" aria-labelledby="filterDropdown" style="width: 300px;">
 				<div class="dropdown-item">
-					<form>
-						<input type="hidden" name="layout" value="{{request()->input('layout')}}"></input>
+					<form action="/i/admin/media/?page=1">
+						<input type="hidden" name="layout" value=""></input>
 						<input type="hidden" name="page" value="{{request()->input('page')}}"></input>
 						<div class="input-group input-group-sm">
 							<input class="form-control" name="search" placeholder="Filter by username, mime type" autocomplete="off"></input>
@@ -27,7 +27,7 @@
 						</div>
 					</form>
 				</div>
-				<div class="dropdown-divider"></div>
+				{{-- <div class="dropdown-divider"></div>
 				<p class="text-wrap p-1 p-md-3 text-center">
 					<a class="badge badge-primary p-2 mb-2 btn-filter" href="#" data-filter="cw" data-filter-state="true" data-toggle="tooltip" title="Show Content Warning media">CW</a> 
 					<a class="badge badge-primary p-2 mb-2 btn-filter" href="#" data-filter="remote" data-filter-state="true" data-toggle="tooltip" title="Show remote media">Remote Media</a> 
@@ -37,7 +37,7 @@
 					<a class="badge badge-light p-2 mb-2 btn-filter" href="#" data-filter="banned" data-filter-state="false" data-toggle="tooltip" title="Show banned media">Banned</a> 
 					<a class="badge badge-light p-2 mb-2 btn-filter" href="#" data-filter="reported" data-filter-state="false" data-toggle="tooltip" title="Show reported media">Reported</a> 
 					<a class="badge badge-light p-2 mb-2 btn-filter" href="#" data-filter="unlisted" data-filter-state="false" data-toggle="tooltip" title="Show unlisted media">Unlisted</a> 
-				</p>
+				</p> --}}
 				{{-- <div class="dropdown-divider"></div>
 				<a class="dropdown-item font-weight-light" href="?filter=local&layout={{request()->input('layout')}}">Local Media Only</a>
 				<a class="dropdown-item font-weight-light" href="?filter=remote&layout={{request()->input('layout')}}">Remote Media Only</a>
@@ -54,30 +54,98 @@
 <p class="h4 pb-3">Showing results for: <i>{{request()->input('search')}}</i></p>
 @endif
 
-@if(request()->input('layout') == 'list')
+@if(request()->input('layout') == 'banned')
+<p class="text-right">
+	<a class="btn btn-primary py-0 px-5" href="/i/admin/media/?layout=addbanned">Add Banned Media</a>
+</p>
+<ul class="list-group">
+	@foreach($media as $b)
+	<li class="list-group-item">
+		<div class="d-flex justify-content-between align-items-center">
+			<div class="d-flex align-items-center">
+				<span class="mr-4 text-monospace small">
+					{{$b->id}}
+				</span>
+				<span class="d-inline-block">
+					<p class="mb-0 small text-monospace">{{$b->sha256}}</p>
+					<p class="mb-0 font-weight-bold">{{$b->name ?? 'Untitled'}}</p>
+					<p class="mb-0 small">{{$b->description ?? 'No description'}}</p>
+				</span>
+			</div>
+			<div class="small font-weight-bold">
+				{{$b->created_at->diffForHumans()}}
+			</div>
+			<div class="">
+				<form action="/i/admin/media/block/delete" method="post">
+					@csrf
+					<input type="hidden" name="id" value="{{$b->id}}">
+					<button type="submit" class="btn btn-outline-danger">
+						<i class="fas fa-trash-alt"></i>
+					</button>
+				</form>
+			</div>
+		</div>
+	</li>
+	@endforeach
+</ul>
+
+@elseif(request()->input('layout') == 'addbanned')
+<div class="row">
+	<div class="col-12 col-md-6 offset-md-3">
+		<div class="card shadow-none border">
+			<div class="card-header font-weight-bold">Add Banned Media</div>
+			<div class="card-body">
+				<form method="post" action="/i/admin/media/block/add">
+					@csrf
+					<div class="form-group">
+						<label for="input3" class="text-muted font-weight-bold">SHA256 Hash</label>
+						<input type="text" class="form-control" id="input3" aria-describedby="input3Help" name="hash">
+						<small id="input3Help" class="form-text text-muted">Required</small>
+					</div>
+					<hr>
+					<div class="form-group">
+						<label for="input1" class="text-muted font-weight-bold">Name</label>
+						<input type="text" class="form-control" id="input1" aria-describedby="input1Help" name="name">
+						<small id="input1Help" class="form-text text-muted">Optional</small>
+					</div>
+					<div class="form-group">
+						<label for="input2" class="text-muted font-weight-bold">Description</label>
+						<textarea class="form-control" id="input2" aria-describedby="input2Help" rows="3" name="description"></textarea>
+						<small id="input2Help" class="form-text text-muted">Optional</small>
+					</div>
+					<hr>
+					<button type="submit" class="btn btn-primary btn-block font-weight-bold">Ban</button>
+				</form>
+			</div>
+		</div>
+		
+	</div>
+</div>
+
+@else
+
 <ul class="list-group">
 	@foreach($media as $status)
 	<li class="list-group-item">
 		<div class="d-flex justify-content-between align-items-center">
 			<div>
-				<a class="font-weight-lighter small mr-3" href="/i/admin/media/show/{{$status->id}}">{{$status->id}}</a>
+				<a class="font-weight-lighter small mr-3 text-monospace" href="/i/admin/media/show/{{$status->id}}">{{$status->id}}</a>
 				<a href="{{$status->url()}}">
 					<img class="" src="{{$status->thumb()}}" width="60px" height="60px">
 				</a>
 			</div>
 			<div>
-				<p class="mb-0 small">status id: <a href="{{$status->status->url()}}" class="font-weight-bold">{{$status->status_id}}</a></p>
-				<p class="mb-0 small">username: <a href="{{$status->profile->url()}}" class="font-weight-bold">{{$status->profile->username}}</a></p>
-				<p class="mb-0 small">size: <span class="filesize font-weight-bold" data-size="{{$status->size}}">0</span></p>
+				<p class="mb-0 small">status id: <a href="/p/{{\App\Services\HashidService::encode($status->status_id)}}" class="font-weight-bold text-monospace">{{$status->status_id}}</a></p>
+				<p class="mb-0 small">profile id: <a href="/i/admin/profiles/edit/{{$status->profile_id}}" class="font-weight-bold text-monospace">{{$status->profile_id}}</a></p>
 			</div>
 			<div>
+				<p class="mb-0 small">size: <span class="filesize font-weight-bold" data-size="{{$status->size}}">0</span></p>
 				<p class="mb-0 small">mime: <span class="font-weight-bold">{{$status->mime}}</span></p>
-				<p class="mb-0 small">content warning:  <i class="fas {{$status->is_nsfw  ? 'fa-check text-danger':'fa-times text-success'}}"></i></p>
-				<p class="mb-0 small">
-					remote media: <i class="fas {{$status->remote_media ? 'fa-check text-danger':'fa-times text-success'}}"></i></p>
 			</div>
 			<div>
-				<a class="btn btn-outline-secondary btn-sm py-0" href="#">Actions</a>
+				<p class="mb-0 small">content warning:  <i class="fas {{$status->is_nsfw  ? 'fa-check text-danger':'fa-times text-dark'}}"></i></p>
+				<p class="mb-0 small">
+					remote media: <i class="fas {{$status->remote_media ? 'fa-check text-danger':'fa-times text-dark'}}"></i></p>
 			</div>
 		</div>
 	</li>
@@ -87,20 +155,6 @@
 <div class="d-flex justify-content-center">
 	{{$media->appends(['layout'=>request()->layout])->links()}}
 </div>
-@else
-<div class="profile-timeline mt-5 row">
-	@foreach($media as $status)
-	<div class="col-12 col-md-4 mb-4">
-		<a class="card" href="{{$status->status->url()}}">
-			<img class="card-img-top" src="{{$status->thumb()}}" width="150px" height="150px">
-		</a>
-	</div>
-	@endforeach
-</div>
-<hr>
-<div class="d-flex justify-content-center">
-	{{$media->appends(['layout'=>request()->layout])->links()}}
-</div>
 @endif
 @endsection
 

+ 4 - 1
resources/views/admin/media/show.blade.php

@@ -8,7 +8,7 @@
 <hr>
 <div class="row">
 	<div class="col-12 col-md-8 offset-md-2">
-		<div class="card">
+		<div class="card shadow-none border">
 			<img class="card-img-top" src="{{$media->thumb()}}">
 			<ul class="list-group list-group-flush">
 				<li class="list-group-item d-flex justify-content-between">
@@ -26,6 +26,9 @@
 				</li>
 			</ul>
 		</div>
+		<p class="mt-3 small text-muted">
+			SHA256 Hash: <span class="text-monospace text-dark">{{$media->original_sha256}}</span>
+		</p>
 	</div>
 </div>
 @endsection

+ 1 - 1
resources/views/auth/checkpoint.blade.php

@@ -41,7 +41,7 @@
                 </div>
             </div>
             <div class="d-flex justify-content-between pt-4 small">
-                <a class="text-lighter text-decoration-none" href="/{{Auth::user()->username}}">Logged in as: <span class="font-weight-bold text-muted">{{Auth::user()->username}}</span></a>
+                <span class="text-lighter text-decoration-none">Logged in as: <span class="font-weight-bold text-muted">{{Auth::user()->username}}</span></span>
                 <span>
                     <a class="text-decoration-none text-muted font-weight-bold" href="{{ route('logout') }}" onclick="event.preventDefault();document.getElementById('logout-form').submit();">Logout</a>
                     <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">

+ 2 - 0
routes/web.php

@@ -278,6 +278,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
         });
 
         Route::get('redirect', 'SiteController@redirectUrl');
+        Route::post('admin/media/block/add', 'MediaBlocklistController@add');
+        Route::post('admin/media/block/delete', 'MediaBlocklistController@delete');
     });
 
     Route::group(['prefix' => 'account'], function () {