AdminController.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  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')->findOrFail($message->user_id);
  215. return view('admin.messages.show', compact('message'));
  216. }
  217. public function messagesReply(Request $request, $id)
  218. {
  219. $this->validate($request, [
  220. 'message' => 'required|string|min:1|max:500',
  221. ]);
  222. $message = Contact::whereNull('responded_at')->findOrFail($id);
  223. $message->response = $request->input('message');
  224. $message->read_at = now();
  225. $message->responded_at = now();
  226. $message->save();
  227. Mail::to($message->user->email)->send(new AdminMessageResponse($message));
  228. return redirect('/i/admin/messages/home')->with('status', 'Sent response to '.$message->user->username);
  229. }
  230. public function messagesReplyPreview(Request $request, $id)
  231. {
  232. $this->validate($request, [
  233. 'message' => 'required|string|min:1|max:500',
  234. ]);
  235. $message = Contact::whereNull('read_at')->findOrFail($id);
  236. return new AdminMessageResponse($message);
  237. }
  238. public function messagesMarkRead(Request $request)
  239. {
  240. $this->validate($request, [
  241. 'id' => 'required|integer|min:1',
  242. ]);
  243. $id = $request->input('id');
  244. $message = Contact::findOrFail($id);
  245. if ($message->read_at) {
  246. return;
  247. }
  248. $message->read_at = now();
  249. $message->save();
  250. $request->session()->flash('status', 'Marked response from '.$message->user->username.' as read!');
  251. return ['status' => 200];
  252. }
  253. public function newsroomHome(Request $request)
  254. {
  255. $newsroom = Newsroom::latest()->paginate(10);
  256. return view('admin.newsroom.home', compact('newsroom'));
  257. }
  258. public function newsroomCreate(Request $request)
  259. {
  260. return view('admin.newsroom.create');
  261. }
  262. public function newsroomEdit(Request $request, $id)
  263. {
  264. $news = Newsroom::findOrFail($id);
  265. return view('admin.newsroom.edit', compact('news'));
  266. }
  267. public function newsroomDelete(Request $request, $id)
  268. {
  269. $news = Newsroom::findOrFail($id);
  270. $news->delete();
  271. return redirect('/i/admin/newsroom');
  272. }
  273. public function newsroomUpdate(Request $request, $id)
  274. {
  275. $this->validate($request, [
  276. 'title' => 'required|string|min:1|max:100',
  277. 'summary' => 'nullable|string|max:200',
  278. 'body' => 'nullable|string',
  279. ]);
  280. $changed = false;
  281. $changedFields = [];
  282. $slug = str_slug($request->input('title'));
  283. if (Newsroom::whereSlug($slug)->exists()) {
  284. $slug = $slug.'-'.str_random(4);
  285. }
  286. $news = Newsroom::findOrFail($id);
  287. $fields = [
  288. 'title' => 'string',
  289. 'summary' => 'string',
  290. 'body' => 'string',
  291. 'category' => 'string',
  292. 'show_timeline' => 'boolean',
  293. 'auth_only' => 'boolean',
  294. 'show_link' => 'boolean',
  295. 'force_modal' => 'boolean',
  296. 'published' => 'published',
  297. ];
  298. foreach ($fields as $field => $type) {
  299. switch ($type) {
  300. case 'string':
  301. if ($request->{$field} != $news->{$field}) {
  302. if ($field == 'title') {
  303. $news->slug = $slug;
  304. }
  305. $news->{$field} = $request->{$field};
  306. $changed = true;
  307. array_push($changedFields, $field);
  308. }
  309. break;
  310. case 'boolean':
  311. $state = $request->{$field} == 'on' ? true : false;
  312. if ($state != $news->{$field}) {
  313. $news->{$field} = $state;
  314. $changed = true;
  315. array_push($changedFields, $field);
  316. }
  317. break;
  318. case 'published':
  319. $state = $request->{$field} == 'on' ? true : false;
  320. $published = $news->published_at != null;
  321. if ($state != $published) {
  322. $news->published_at = $state ? now() : null;
  323. $changed = true;
  324. array_push($changedFields, $field);
  325. }
  326. break;
  327. }
  328. }
  329. if ($changed) {
  330. $news->save();
  331. }
  332. $redirect = $news->published_at ? $news->permalink() : $news->editUrl();
  333. return redirect($redirect);
  334. }
  335. public function newsroomStore(Request $request)
  336. {
  337. $this->validate($request, [
  338. 'title' => 'required|string|min:1|max:100',
  339. 'summary' => 'nullable|string|max:200',
  340. 'body' => 'nullable|string',
  341. ]);
  342. $changed = false;
  343. $changedFields = [];
  344. $slug = str_slug($request->input('title'));
  345. if (Newsroom::whereSlug($slug)->exists()) {
  346. $slug = $slug.'-'.str_random(4);
  347. }
  348. $news = new Newsroom;
  349. $fields = [
  350. 'title' => 'string',
  351. 'summary' => 'string',
  352. 'body' => 'string',
  353. 'category' => 'string',
  354. 'show_timeline' => 'boolean',
  355. 'auth_only' => 'boolean',
  356. 'show_link' => 'boolean',
  357. 'force_modal' => 'boolean',
  358. 'published' => 'published',
  359. ];
  360. foreach ($fields as $field => $type) {
  361. switch ($type) {
  362. case 'string':
  363. if ($request->{$field} != $news->{$field}) {
  364. if ($field == 'title') {
  365. $news->slug = $slug;
  366. }
  367. $news->{$field} = $request->{$field};
  368. $changed = true;
  369. array_push($changedFields, $field);
  370. }
  371. break;
  372. case 'boolean':
  373. $state = $request->{$field} == 'on' ? true : false;
  374. if ($state != $news->{$field}) {
  375. $news->{$field} = $state;
  376. $changed = true;
  377. array_push($changedFields, $field);
  378. }
  379. break;
  380. case 'published':
  381. $state = $request->{$field} == 'on' ? true : false;
  382. $published = $news->published_at != null;
  383. if ($state != $published) {
  384. $news->published_at = $state ? now() : null;
  385. $changed = true;
  386. array_push($changedFields, $field);
  387. }
  388. break;
  389. }
  390. }
  391. if ($changed) {
  392. $news->save();
  393. }
  394. $redirect = $news->published_at ? $news->permalink() : $news->editUrl();
  395. return redirect($redirect);
  396. }
  397. public function diagnosticsHome(Request $request)
  398. {
  399. return view('admin.diagnostics.home');
  400. }
  401. public function diagnosticsDecrypt(Request $request)
  402. {
  403. $this->validate($request, [
  404. 'payload' => 'required',
  405. ]);
  406. $key = 'exception_report:';
  407. $decrypted = decrypt($request->input('payload'));
  408. if (! starts_with($decrypted, $key)) {
  409. abort(403, 'Can only decrypt error diagnostics');
  410. }
  411. $res = [
  412. 'decrypted' => substr($decrypted, strlen($key)),
  413. ];
  414. return response()->json($res);
  415. }
  416. public function stories(Request $request)
  417. {
  418. $stories = Story::with('profile')->latest()->paginate(10);
  419. $stats = StoryService::adminStats();
  420. return view('admin.stories.home', compact('stories', 'stats'));
  421. }
  422. public function customEmojiHome(Request $request)
  423. {
  424. if (! (bool) config_cache('federation.custom_emoji.enabled')) {
  425. return view('admin.custom-emoji.not-enabled');
  426. }
  427. $this->validate($request, [
  428. 'sort' => 'sometimes|in:all,local,remote,duplicates,disabled,search',
  429. ]);
  430. if ($request->has('cc')) {
  431. Cache::forget('pf:admin:custom_emoji:stats');
  432. Cache::forget('pf:custom_emoji');
  433. return redirect(route('admin.custom-emoji'));
  434. }
  435. $sort = $request->input('sort') ?? 'all';
  436. if ($sort == 'search' && empty($request->input('q'))) {
  437. return redirect(route('admin.custom-emoji'));
  438. }
  439. $pg = config('database.default') == 'pgsql';
  440. $emojis = CustomEmoji::when($sort, function ($query, $sort) use ($request, $pg) {
  441. if ($sort == 'all') {
  442. if ($pg) {
  443. return $query->latest();
  444. } else {
  445. return $query->groupBy('shortcode')->latest();
  446. }
  447. } elseif ($sort == 'local') {
  448. return $query->latest()->where('domain', '=', config('pixelfed.domain.app'));
  449. } elseif ($sort == 'remote') {
  450. return $query->latest()->where('domain', '!=', config('pixelfed.domain.app'));
  451. } elseif ($sort == 'duplicates') {
  452. return $query->latest()->groupBy('shortcode')->havingRaw('count(*) > 1');
  453. } elseif ($sort == 'disabled') {
  454. return $query->latest()->whereDisabled(true);
  455. } elseif ($sort == 'search') {
  456. $q = $query
  457. ->latest()
  458. ->where('shortcode', 'like', '%'.$request->input('q').'%')
  459. ->orWhere('domain', 'like', '%'.$request->input('q').'%');
  460. if (! $request->has('dups')) {
  461. if (! $pg) {
  462. $q = $q->groupBy('shortcode');
  463. }
  464. }
  465. return $q;
  466. }
  467. })
  468. ->simplePaginate(10)
  469. ->withQueryString();
  470. $stats = Cache::remember('pf:admin:custom_emoji:stats', 43200, function () use ($pg) {
  471. $res = [
  472. 'total' => CustomEmoji::count(),
  473. 'active' => CustomEmoji::whereDisabled(false)->count(),
  474. 'remote' => CustomEmoji::where('domain', '!=', config('pixelfed.domain.app'))->count(),
  475. ];
  476. if ($pg) {
  477. $res['duplicate'] = CustomEmoji::select('shortcode')->groupBy('shortcode')->havingRaw('count(*) > 1')->count();
  478. } else {
  479. $res['duplicate'] = CustomEmoji::groupBy('shortcode')->havingRaw('count(*) > 1')->count();
  480. }
  481. return $res;
  482. });
  483. return view('admin.custom-emoji.home', compact('emojis', 'sort', 'stats'));
  484. }
  485. public function customEmojiToggleActive(Request $request, $id)
  486. {
  487. abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
  488. $emoji = CustomEmoji::findOrFail($id);
  489. $emoji->disabled = ! $emoji->disabled;
  490. $emoji->save();
  491. $key = CustomEmoji::CACHE_KEY.str_replace(':', '', $emoji->shortcode);
  492. Cache::forget($key);
  493. return redirect()->back();
  494. }
  495. public function customEmojiAdd(Request $request)
  496. {
  497. abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
  498. return view('admin.custom-emoji.add');
  499. }
  500. public function customEmojiStore(Request $request)
  501. {
  502. abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
  503. $this->validate($request, [
  504. 'shortcode' => [
  505. 'required',
  506. 'min:3',
  507. 'max:80',
  508. 'starts_with::',
  509. 'ends_with::',
  510. Rule::unique('custom_emoji')->where(function ($query) use ($request) {
  511. return $query->whereDomain(config('pixelfed.domain.app'))
  512. ->whereShortcode($request->input('shortcode'));
  513. }),
  514. ],
  515. 'emoji' => 'required|file|mimes:jpg,png|max:'.(config('federation.custom_emoji.max_size') / 1000),
  516. ]);
  517. $emoji = new CustomEmoji;
  518. $emoji->shortcode = $request->input('shortcode');
  519. $emoji->domain = config('pixelfed.domain.app');
  520. $emoji->save();
  521. $fileName = $emoji->id.'.'.$request->emoji->extension();
  522. $request->emoji->storePubliclyAs('public/emoji', $fileName);
  523. $emoji->media_path = 'emoji/'.$fileName;
  524. $emoji->save();
  525. Cache::forget('pf:custom_emoji');
  526. return redirect(route('admin.custom-emoji'));
  527. }
  528. public function customEmojiDelete(Request $request, $id)
  529. {
  530. abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
  531. $emoji = CustomEmoji::findOrFail($id);
  532. Storage::delete("public/{$emoji->media_path}");
  533. Cache::forget('pf:custom_emoji');
  534. $emoji->delete();
  535. return redirect(route('admin.custom-emoji'));
  536. }
  537. public function customEmojiShowDuplicates(Request $request, $id)
  538. {
  539. abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
  540. $emoji = CustomEmoji::orderBy('id')->whereDisabled(false)->whereShortcode($id)->firstOrFail();
  541. $emojis = CustomEmoji::whereShortcode($id)->where('id', '!=', $emoji->id)->cursorPaginate(10);
  542. return view('admin.custom-emoji.duplicates', compact('emoji', 'emojis'));
  543. }
  544. }