Merge pull request #2354 from pixelfed/staging

Add MediaBlocklist feature
This commit is contained in:
daniel 2020-07-26 22:25:22 -06:00 committed by GitHub
commit 351fe7035f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 268 additions and 71 deletions

View file

@ -13,6 +13,7 @@
- Add MediaPathService ([c54b29c5](https://github.com/pixelfed/pixelfed/commit/c54b29c5)) - Add MediaPathService ([c54b29c5](https://github.com/pixelfed/pixelfed/commit/c54b29c5))
- Add Media Tags ([711fc020](https://github.com/pixelfed/pixelfed/commit/711fc020)) - Add Media Tags ([711fc020](https://github.com/pixelfed/pixelfed/commit/711fc020))
- Add MediaTagService ([524c6d45](https://github.com/pixelfed/pixelfed/commit/524c6d45)) - Add MediaTagService ([524c6d45](https://github.com/pixelfed/pixelfed/commit/524c6d45))
- Add MediaBlocklist feature ([ba1f7e7e](https://github.com/pixelfed/pixelfed/commit/ba1f7e7e))
### Updated ### Updated
- Updated PostComponent, fix remote urls ([42716ccc](https://github.com/pixelfed/pixelfed/commit/42716ccc)) - 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 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 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 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) ## [v0.10.9 (2020-04-17)](https://github.com/pixelfed/pixelfed/compare/v0.10.8...v0.10.9)
### Added ### Added

View file

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

View file

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

View file

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

View file

@ -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');
}
}

View file

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

10
app/MediaBlocklist.php Normal file
View file

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

View file

@ -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;
}
}

View file

@ -10,32 +10,13 @@ class Nickname
$url = str_replace('acct:', '', $url); $url = str_replace('acct:', '', $url);
} }
if (!str_contains($url, '@') && filter_var($url, FILTER_VALIDATE_URL)) { if(starts_with($url, '@')) {
$parsed = parse_url($url); $url = substr($url, 1);
$username = str_replace(['/', '\\', '@'], '', $parsed['path']);
return ['domain' => $parsed['host'], 'username' => $username];
} }
$parts = explode('@', $url); $parts = explode('@', $url);
$username = null; $username = $parts[0];
$domain = null; $domain = $parts[1];
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;
}
}
return ['domain' => $domain, 'username' => $username]; return ['domain' => $domain, 'username' => $username];
} }

View file

@ -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');
}
}

View file

@ -4,11 +4,11 @@
<div class="title"> <div class="title">
<h3 class="font-weight-bold d-inline-block">Media</h3> <h3 class="font-weight-bold d-inline-block">Media</h3>
<span class="float-right"> <span class="float-right">
<a class="btn btn-{{request()->input('layout')!=='list'?'primary':'light'}} btn-sm" href="{{route('admin.media')}}"> <a class="btn btn-{{request()->input('layout')!=='banned'?'primary':'light'}} btn-sm font-weight-bold" href="{{route('admin.media')}}">
<i class="fas fa-th"></i> All
</a> </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])}}"> <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])}}">
<i class="fas fa-list"></i> Banned
</a> </a>
<div class="dropdown d-inline-block"> <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"> <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> </button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="filterDropdown" style="width: 300px;"> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="filterDropdown" style="width: 300px;">
<div class="dropdown-item"> <div class="dropdown-item">
<form> <form action="/i/admin/media/?page=1">
<input type="hidden" name="layout" value="{{request()->input('layout')}}"></input> <input type="hidden" name="layout" value=""></input>
<input type="hidden" name="page" value="{{request()->input('page')}}"></input> <input type="hidden" name="page" value="{{request()->input('page')}}"></input>
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input class="form-control" name="search" placeholder="Filter by username, mime type" autocomplete="off"></input> <input class="form-control" name="search" placeholder="Filter by username, mime type" autocomplete="off"></input>
@ -27,7 +27,7 @@
</div> </div>
</form> </form>
</div> </div>
<div class="dropdown-divider"></div> {{-- <div class="dropdown-divider"></div>
<p class="text-wrap p-1 p-md-3 text-center"> <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="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> <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="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="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> <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> {{-- <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=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> <a class="dropdown-item font-weight-light" href="?filter=remote&layout={{request()->input('layout')}}">Remote Media Only</a>
@ -54,49 +54,103 @@
<p class="h4 pb-3">Showing results for: <i>{{request()->input('search')}}</i></p> <p class="h4 pb-3">Showing results for: <i>{{request()->input('search')}}</i></p>
@endif @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"> <ul class="list-group">
@foreach($media as $status) @foreach($media as $b)
<li class="list-group-item"> <li class="list-group-item">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div> <div class="d-flex align-items-center">
<a class="font-weight-lighter small mr-3" href="/i/admin/media/show/{{$status->id}}">{{$status->id}}</a> <span class="mr-4 text-monospace small">
<a href="{{$status->url()}}"> {{$b->id}}
<img class="" src="{{$status->thumb()}}" width="60px" height="60px"> </span>
</a> <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>
<div> <div class="small font-weight-bold">
<p class="mb-0 small">status id: <a href="{{$status->status->url()}}" class="font-weight-bold">{{$status->status_id}}</a></p> {{$b->created_at->diffForHumans()}}
<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>
</div> </div>
<div> <div class="">
<p class="mb-0 small">mime: <span class="font-weight-bold">{{$status->mime}}</span></p> <form action="/i/admin/media/block/delete" method="post">
<p class="mb-0 small">content warning: <i class="fas {{$status->is_nsfw ? 'fa-check text-danger':'fa-times text-success'}}"></i></p> @csrf
<p class="mb-0 small"> <input type="hidden" name="id" value="{{$b->id}}">
remote media: <i class="fas {{$status->remote_media ? 'fa-check text-danger':'fa-times text-success'}}"></i></p> <button type="submit" class="btn btn-outline-danger">
</div> <i class="fas fa-trash-alt"></i>
<div> </button>
<a class="btn btn-outline-secondary btn-sm py-0" href="#">Actions</a> </form>
</div> </div>
</div> </div>
</li> </li>
@endforeach @endforeach
</ul> </ul>
<hr>
<div class="d-flex justify-content-center"> @elseif(request()->input('layout') == 'addbanned')
{{$media->appends(['layout'=>request()->layout])->links()}} <div class="row">
</div> <div class="col-12 col-md-6 offset-md-3">
@else <div class="card shadow-none border">
<div class="profile-timeline mt-5 row"> <div class="card-header font-weight-bold">Add Banned Media</div>
@foreach($media as $status) <div class="card-body">
<div class="col-12 col-md-4 mb-4"> <form method="post" action="/i/admin/media/block/add">
<a class="card" href="{{$status->status->url()}}"> @csrf
<img class="card-img-top" src="{{$status->thumb()}}" width="150px" height="150px"> <div class="form-group">
</a> <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>
@endforeach
</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 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="/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>
</div>
<div>
<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>
@endforeach
</ul>
<hr> <hr>
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
{{$media->appends(['layout'=>request()->layout])->links()}} {{$media->appends(['layout'=>request()->layout])->links()}}

View file

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

View file

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

View file

@ -278,6 +278,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
}); });
Route::get('redirect', 'SiteController@redirectUrl'); 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 () { Route::group(['prefix' => 'account'], function () {