AdminController.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Contact;
  4. use App\Http\Controllers\Admin\AdminAutospamController;
  5. use App\Http\Controllers\Admin\AdminDirectoryController;
  6. use App\Http\Controllers\Admin\AdminDiscoverController;
  7. use App\Http\Controllers\Admin\AdminHashtagsController;
  8. use App\Http\Controllers\Admin\AdminInstanceController;
  9. use App\Http\Controllers\Admin\AdminMediaController;
  10. use App\Http\Controllers\Admin\AdminReportController;
  11. use App\Http\Controllers\Admin\AdminSettingsController;
  12. use App\Http\Controllers\Admin\AdminUserController;
  13. use App\Instance;
  14. use App\Mail\AdminMessageResponse;
  15. use App\Models\CustomEmoji;
  16. use App\Newsroom;
  17. use App\OauthClient;
  18. use App\Profile;
  19. use App\Services\AccountService;
  20. use App\Services\AdminStatsService;
  21. use App\Services\ConfigCacheService;
  22. use App\Services\StatusService;
  23. use App\Services\StoryService;
  24. use App\Status;
  25. use App\Story;
  26. use App\User;
  27. use Cache;
  28. use DB;
  29. use Illuminate\Http\Request;
  30. use Illuminate\Validation\Rule;
  31. use Mail;
  32. use Storage;
  33. class AdminController extends Controller
  34. {
  35. use AdminAutospamController,
  36. AdminDirectoryController,
  37. AdminDiscoverController,
  38. AdminHashtagsController,
  39. AdminInstanceController,
  40. AdminMediaController,
  41. AdminReportController,
  42. AdminSettingsController,
  43. AdminUserController;
  44. public function __construct()
  45. {
  46. $this->middleware('admin');
  47. $this->middleware('dangerzone');
  48. $this->middleware('twofactor');
  49. }
  50. public function home()
  51. {
  52. return view('admin.home');
  53. }
  54. public function customCss()
  55. {
  56. return view('admin.settings.customcss');
  57. }
  58. public function saveCustomCss(Request $request)
  59. {
  60. $this->validate($request, [
  61. 'css' => 'sometimes|max:5000',
  62. 'show' => 'sometimes',
  63. ]);
  64. ConfigCacheService::put('uikit.custom.css', $request->input('css'));
  65. ConfigCacheService::put('uikit.show_custom.css', $request->boolean('show'));
  66. return view('admin.settings.customcss');
  67. }
  68. public function stats()
  69. {
  70. $data = AdminStatsService::get();
  71. return view('admin.stats', compact('data'));
  72. }
  73. public function getStats()
  74. {
  75. return AdminStatsService::summary();
  76. }
  77. public function getAccounts()
  78. {
  79. $users = User::orderByDesc('id')->cursorPaginate(10);
  80. $res = [
  81. 'next_page_url' => $users->nextPageUrl(),
  82. 'data' => $users->map(function ($user) {
  83. $account = AccountService::get($user->profile_id, true);
  84. if (! $account) {
  85. return [
  86. 'id' => $user->profile_id,
  87. 'username' => $user->username,
  88. 'status' => 'deleted',
  89. 'avatar' => '/storage/avatars/default.jpg',
  90. 'created_at' => $user->created_at,
  91. ];
  92. }
  93. $account['user_id'] = $user->id;
  94. return $account;
  95. })
  96. ->filter(function ($user) {
  97. return $user;
  98. }),
  99. ];
  100. return $res;
  101. }
  102. public function getPosts()
  103. {
  104. $posts = DB::table('statuses')
  105. ->orderByDesc('id')
  106. ->cursorPaginate(10);
  107. $res = [
  108. 'next_page_url' => $posts->nextPageUrl(),
  109. 'data' => $posts->map(function ($post) {
  110. $status = StatusService::get($post->id, false);
  111. if (! $status) {
  112. return ['id' => $post->id, 'created_at' => $post->created_at];
  113. }
  114. return $status;
  115. }),
  116. ];
  117. return $res;
  118. }
  119. public function getInstances()
  120. {
  121. return Instance::orderByDesc('id')->cursorPaginate(10);
  122. }
  123. public function statuses(Request $request)
  124. {
  125. $statuses = Status::orderBy('id', 'desc')->cursorPaginate(10);
  126. $data = $statuses->map(function ($status) {
  127. return StatusService::get($status->id, false);
  128. })
  129. ->filter(function ($s) {
  130. return $s;
  131. })
  132. ->toArray();
  133. return view('admin.statuses.home', compact('statuses', 'data'));
  134. }
  135. public function showStatus(Request $request, $id)
  136. {
  137. $status = Status::findOrFail($id);
  138. return view('admin.statuses.show', compact('status'));
  139. }
  140. public function profiles(Request $request)
  141. {
  142. $this->validate($request, [
  143. 'search' => 'nullable|string|max:250',
  144. 'filter' => [
  145. 'nullable',
  146. 'string',
  147. Rule::in(['all', 'local', 'remote']),
  148. ],
  149. ]);
  150. $search = $request->input('search');
  151. $filter = $request->input('filter');
  152. $limit = 12;
  153. $profiles = Profile::select('id', 'username')
  154. ->whereNull('status')
  155. ->when($search, function ($q, $search) {
  156. return $q->where('username', 'like', "%$search%");
  157. })->when($filter, function ($q, $filter) {
  158. if ($filter == 'local') {
  159. return $q->whereNull('domain');
  160. }
  161. if ($filter == 'remote') {
  162. return $q->whereNotNull('domain');
  163. }
  164. return $q;
  165. })->orderByDesc('id')
  166. ->simplePaginate($limit);
  167. return view('admin.profiles.home', compact('profiles'));
  168. }
  169. public function profileShow(Request $request, $id)
  170. {
  171. $profile = Profile::findOrFail($id);
  172. $user = $profile->user;
  173. return view('admin.profiles.edit', compact('profile', 'user'));
  174. }
  175. public function appsHome(Request $request)
  176. {
  177. $filter = $request->input('filter');
  178. if ($filter == 'revoked') {
  179. $apps = OauthClient::with('user')
  180. ->whereNotNull('user_id')
  181. ->whereRevoked(true)
  182. ->orderByDesc('id')
  183. ->paginate(10);
  184. } else {
  185. $apps = OauthClient::with('user')
  186. ->whereNotNull('user_id')
  187. ->orderByDesc('id')
  188. ->paginate(10);
  189. }
  190. return view('admin.apps.home', compact('apps'));
  191. }
  192. public function messagesHome(Request $request)
  193. {
  194. $this->validate($request, [
  195. 'sort' => 'sometimes|string|in:all,open,closed',
  196. ]);
  197. $sort = $request->input('sort', 'open');
  198. $messages = Contact::when($sort, function ($query, $sort) {
  199. if ($sort === 'open') {
  200. $query->whereNull('read_at');
  201. }
  202. if ($sort === 'closed') {
  203. $query->whereNotNull('read_at');
  204. }
  205. })
  206. ->orderByDesc('id')
  207. ->paginate(10)
  208. ->withQueryString();
  209. return view('admin.messages.home', compact('messages', 'sort'));
  210. }
  211. public function messagesShow(Request $request, $id)
  212. {
  213. $message = Contact::findOrFail($id);
  214. $user = User::whereNull('status')->find($message->user_id);
  215. if(!$user) {
  216. $message->read_at = now();
  217. $message->save();
  218. return redirect('/i/admin/messages/home')->with('status', 'Redirected from message sent from a deleted account');
  219. }
  220. return view('admin.messages.show', compact('message'));
  221. }
  222. public function messagesReply(Request $request, $id)
  223. {
  224. $this->validate($request, [
  225. 'message' => 'required|string|min:1|max:500',
  226. ]);
  227. if(config('mail.default') === 'log') {
  228. return redirect('/i/admin/messages/home')->with('error', 'Mail driver not configured, please setup before you can sent email.');
  229. }
  230. $message = Contact::whereNull('responded_at')->findOrFail($id);
  231. $user = User::whereNull('status')->find($message->user_id);
  232. if(!$user) {
  233. $message->read_at = now();
  234. $message->save();
  235. return redirect('/i/admin/messages/home')->with('status', 'Redirected from message sent from a deleted account');
  236. }
  237. $message->response = $request->input('message');
  238. $message->read_at = now();
  239. $message->responded_at = now();
  240. $message->save();
  241. Mail::to($message->user->email)->send(new AdminMessageResponse($message));
  242. return redirect('/i/admin/messages/home')->with('status', 'Sent response to '.$message->user->username);
  243. }
  244. public function messagesReplyPreview(Request $request, $id)
  245. {
  246. $this->validate($request, [
  247. 'message' => 'required|string|min:1|max:500',
  248. ]);
  249. if(config('mail.default') === 'log') {
  250. return redirect('/i/admin/messages/home')->with('error', 'Mail driver not configured, please setup before you can sent email.');
  251. }
  252. $message = Contact::whereNull('read_at')->findOrFail($id);
  253. $user = User::whereNull('status')->find($message->user_id);
  254. if(!$user) {
  255. $message->read_at = now();
  256. $message->save();
  257. return redirect('/i/admin/messages/home')->with('error', 'Redirected from message sent from a deleted account');
  258. }
  259. return new AdminMessageResponse($message);
  260. }
  261. public function messagesMarkRead(Request $request)
  262. {
  263. $this->validate($request, [
  264. 'id' => 'required|integer|min:1',
  265. ]);
  266. $id = $request->input('id');
  267. $message = Contact::findOrFail($id);
  268. $user = User::whereNull('status')->find($message->user_id);
  269. if(!$user) {
  270. $message->read_at = now();
  271. $message->save();
  272. return redirect('/i/admin/messages/home')->with('error', 'Redirected from message sent from a deleted account');
  273. }
  274. if ($message->read_at) {
  275. return;
  276. }
  277. $message->read_at = now();
  278. $message->save();
  279. $request->session()->flash('status', 'Marked response from '.$message->user->username.' as read!');
  280. return ['status' => 200];
  281. }
  282. public function newsroomHome(Request $request)
  283. {
  284. $newsroom = Newsroom::latest()->paginate(10);
  285. return view('admin.newsroom.home', compact('newsroom'));
  286. }
  287. public function newsroomCreate(Request $request)
  288. {
  289. return view('admin.newsroom.create');
  290. }
  291. public function newsroomEdit(Request $request, $id)
  292. {
  293. $news = Newsroom::findOrFail($id);
  294. return view('admin.newsroom.edit', compact('news'));
  295. }
  296. public function newsroomDelete(Request $request, $id)
  297. {
  298. $news = Newsroom::findOrFail($id);
  299. $news->delete();
  300. return redirect('/i/admin/newsroom');
  301. }
  302. public function newsroomUpdate(Request $request, $id)
  303. {
  304. $this->validate($request, [
  305. 'title' => 'required|string|min:1|max:100',
  306. 'summary' => 'nullable|string|max:200',
  307. 'body' => 'nullable|string',
  308. ]);
  309. $changed = false;
  310. $changedFields = [];
  311. $slug = str_slug($request->input('title'));
  312. if (Newsroom::whereSlug($slug)->exists()) {
  313. $slug = $slug.'-'.str_random(4);
  314. }
  315. $news = Newsroom::findOrFail($id);
  316. $fields = [
  317. 'title' => 'string',
  318. 'summary' => 'string',
  319. 'body' => 'string',
  320. 'category' => 'string',
  321. 'show_timeline' => 'boolean',
  322. 'auth_only' => 'boolean',
  323. 'show_link' => 'boolean',
  324. 'force_modal' => 'boolean',
  325. 'published' => 'published',
  326. ];
  327. foreach ($fields as $field => $type) {
  328. switch ($type) {
  329. case 'string':
  330. if ($request->{$field} != $news->{$field}) {
  331. if ($field == 'title') {
  332. $news->slug = $slug;
  333. }
  334. $news->{$field} = $request->{$field};
  335. $changed = true;
  336. array_push($changedFields, $field);
  337. }
  338. break;
  339. case 'boolean':
  340. $state = $request->{$field} == 'on' ? true : false;
  341. if ($state != $news->{$field}) {
  342. $news->{$field} = $state;
  343. $changed = true;
  344. array_push($changedFields, $field);
  345. }
  346. break;
  347. case 'published':
  348. $state = $request->{$field} == 'on' ? true : false;
  349. $published = $news->published_at != null;
  350. if ($state != $published) {
  351. $news->published_at = $state ? now() : null;
  352. $changed = true;
  353. array_push($changedFields, $field);
  354. }
  355. break;
  356. }
  357. }
  358. if ($changed) {
  359. $news->save();
  360. }
  361. $redirect = $news->published_at ? $news->permalink() : $news->editUrl();
  362. return redirect($redirect);
  363. }
  364. public function newsroomStore(Request $request)
  365. {
  366. $this->validate($request, [
  367. 'title' => 'required|string|min:1|max:100',
  368. 'summary' => 'nullable|string|max:200',
  369. 'body' => 'nullable|string',
  370. ]);
  371. $changed = false;
  372. $changedFields = [];
  373. $slug = str_slug($request->input('title'));
  374. if (Newsroom::whereSlug($slug)->exists()) {
  375. $slug = $slug.'-'.str_random(4);
  376. }
  377. $news = new Newsroom;
  378. $fields = [
  379. 'title' => 'string',
  380. 'summary' => 'string',
  381. 'body' => 'string',
  382. 'category' => 'string',
  383. 'show_timeline' => 'boolean',
  384. 'auth_only' => 'boolean',
  385. 'show_link' => 'boolean',
  386. 'force_modal' => 'boolean',
  387. 'published' => 'published',
  388. ];
  389. foreach ($fields as $field => $type) {
  390. switch ($type) {
  391. case 'string':
  392. if ($request->{$field} != $news->{$field}) {
  393. if ($field == 'title') {
  394. $news->slug = $slug;
  395. }
  396. $news->{$field} = $request->{$field};
  397. $changed = true;
  398. array_push($changedFields, $field);
  399. }
  400. break;
  401. case 'boolean':
  402. $state = $request->{$field} == 'on' ? true : false;
  403. if ($state != $news->{$field}) {
  404. $news->{$field} = $state;
  405. $changed = true;
  406. array_push($changedFields, $field);
  407. }
  408. break;
  409. case 'published':
  410. $state = $request->{$field} == 'on' ? true : false;
  411. $published = $news->published_at != null;
  412. if ($state != $published) {
  413. $news->published_at = $state ? now() : null;
  414. $changed = true;
  415. array_push($changedFields, $field);
  416. }
  417. break;
  418. }
  419. }
  420. if ($changed) {
  421. $news->save();
  422. }
  423. $redirect = $news->published_at ? $news->permalink() : $news->editUrl();
  424. return redirect($redirect);
  425. }
  426. public function diagnosticsHome(Request $request)
  427. {
  428. return view('admin.diagnostics.home');
  429. }
  430. public function diagnosticsDecrypt(Request $request)
  431. {
  432. $this->validate($request, [
  433. 'payload' => 'required',
  434. ]);
  435. $key = 'exception_report:';
  436. $decrypted = decrypt($request->input('payload'));
  437. if (! starts_with($decrypted, $key)) {
  438. abort(403, 'Can only decrypt error diagnostics');
  439. }
  440. $res = [
  441. 'decrypted' => substr($decrypted, strlen($key)),
  442. ];
  443. return response()->json($res);
  444. }
  445. public function stories(Request $request)
  446. {
  447. $stories = Story::with('profile')->latest()->paginate(10);
  448. $stats = StoryService::adminStats();
  449. return view('admin.stories.home', compact('stories', 'stats'));
  450. }
  451. public function customEmojiHome(Request $request)
  452. {
  453. if (! (bool) config_cache('federation.custom_emoji.enabled')) {
  454. return view('admin.custom-emoji.not-enabled');
  455. }
  456. $this->validate($request, [
  457. 'sort' => 'sometimes|in:all,local,remote,duplicates,disabled,search',
  458. ]);
  459. if ($request->has('cc')) {
  460. Cache::forget('pf:admin:custom_emoji:stats');
  461. Cache::forget('pf:custom_emoji');
  462. return redirect(route('admin.custom-emoji'));
  463. }
  464. $sort = $request->input('sort') ?? 'all';
  465. if ($sort == 'search' && empty($request->input('q'))) {
  466. return redirect(route('admin.custom-emoji'));
  467. }
  468. $pg = config('database.default') == 'pgsql';
  469. $emojis = CustomEmoji::when($sort, function ($query, $sort) use ($request, $pg) {
  470. if ($sort == 'all') {
  471. if ($pg) {
  472. return $query->latest();
  473. } else {
  474. return $query->groupBy('shortcode')->latest();
  475. }
  476. } elseif ($sort == 'local') {
  477. return $query->latest()->where('domain', '=', config('pixelfed.domain.app'));
  478. } elseif ($sort == 'remote') {
  479. return $query->latest()->where('domain', '!=', config('pixelfed.domain.app'));
  480. } elseif ($sort == 'duplicates') {
  481. return $query->latest()->groupBy('shortcode')->havingRaw('count(*) > 1');
  482. } elseif ($sort == 'disabled') {
  483. return $query->latest()->whereDisabled(true);
  484. } elseif ($sort == 'search') {
  485. $q = $query
  486. ->latest()
  487. ->where('shortcode', 'like', '%'.$request->input('q').'%')
  488. ->orWhere('domain', 'like', '%'.$request->input('q').'%');
  489. if (! $request->has('dups')) {
  490. if (! $pg) {
  491. $q = $q->groupBy('shortcode');
  492. }
  493. }
  494. return $q;
  495. }
  496. })
  497. ->simplePaginate(10)
  498. ->withQueryString();
  499. $stats = Cache::remember('pf:admin:custom_emoji:stats', 43200, function () use ($pg) {
  500. $res = [
  501. 'total' => CustomEmoji::count(),
  502. 'active' => CustomEmoji::whereDisabled(false)->count(),
  503. 'remote' => CustomEmoji::where('domain', '!=', config('pixelfed.domain.app'))->count(),
  504. ];
  505. if ($pg) {
  506. $res['duplicate'] = CustomEmoji::select('shortcode')->groupBy('shortcode')->havingRaw('count(*) > 1')->count();
  507. } else {
  508. $res['duplicate'] = CustomEmoji::groupBy('shortcode')->havingRaw('count(*) > 1')->count();
  509. }
  510. return $res;
  511. });
  512. return view('admin.custom-emoji.home', compact('emojis', 'sort', 'stats'));
  513. }
  514. public function customEmojiToggleActive(Request $request, $id)
  515. {
  516. abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
  517. $emoji = CustomEmoji::findOrFail($id);
  518. $emoji->disabled = ! $emoji->disabled;
  519. $emoji->save();
  520. $key = CustomEmoji::CACHE_KEY.str_replace(':', '', $emoji->shortcode);
  521. Cache::forget($key);
  522. return redirect()->back();
  523. }
  524. public function customEmojiAdd(Request $request)
  525. {
  526. abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
  527. return view('admin.custom-emoji.add');
  528. }
  529. public function customEmojiStore(Request $request)
  530. {
  531. abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
  532. $this->validate($request, [
  533. 'shortcode' => [
  534. 'required',
  535. 'min:3',
  536. 'max:80',
  537. 'starts_with::',
  538. 'ends_with::',
  539. Rule::unique('custom_emoji')->where(function ($query) use ($request) {
  540. return $query->whereDomain(config('pixelfed.domain.app'))
  541. ->whereShortcode($request->input('shortcode'));
  542. }),
  543. ],
  544. 'emoji' => 'required|file|mimes:jpg,png|max:'.(config('federation.custom_emoji.max_size') / 1000),
  545. ]);
  546. $emoji = new CustomEmoji;
  547. $emoji->shortcode = $request->input('shortcode');
  548. $emoji->domain = config('pixelfed.domain.app');
  549. $emoji->save();
  550. $fileName = $emoji->id.'.'.$request->emoji->extension();
  551. $request->emoji->storePubliclyAs('public/emoji', $fileName);
  552. $emoji->media_path = 'emoji/'.$fileName;
  553. $emoji->save();
  554. Cache::forget('pf:custom_emoji');
  555. return redirect(route('admin.custom-emoji'));
  556. }
  557. public function customEmojiDelete(Request $request, $id)
  558. {
  559. abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
  560. $emoji = CustomEmoji::findOrFail($id);
  561. Storage::delete("public/{$emoji->media_path}");
  562. Cache::forget('pf:custom_emoji');
  563. $emoji->delete();
  564. return redirect(route('admin.custom-emoji'));
  565. }
  566. public function customEmojiShowDuplicates(Request $request, $id)
  567. {
  568. abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
  569. $emoji = CustomEmoji::orderBy('id')->whereDisabled(false)->whereShortcode($id)->firstOrFail();
  570. $emojis = CustomEmoji::whereShortcode($id)->where('id', '!=', $emoji->id)->cursorPaginate(10);
  571. return view('admin.custom-emoji.duplicates', compact('emoji', 'emojis'));
  572. }
  573. }