AdminController.php 19 KB

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