StatusController.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Jobs\ImageOptimizePipeline\ImageOptimize;
  4. use App\Jobs\StatusPipeline\NewStatusPipeline;
  5. use App\Jobs\StatusPipeline\StatusDelete;
  6. use App\Jobs\SharePipeline\SharePipeline;
  7. use App\Media;
  8. use App\Profile;
  9. use App\Status;
  10. use App\Transformer\ActivityPub\StatusTransformer;
  11. use App\Transformer\ActivityPub\Verb\Note;
  12. use App\User;
  13. use Auth, Cache;
  14. use Illuminate\Http\Request;
  15. use League\Fractal;
  16. use App\Util\Media\Filter;
  17. class StatusController extends Controller
  18. {
  19. public function show(Request $request, $username, int $id)
  20. {
  21. // $id = strlen($id) < 17 ? array_first(\Hashids::decode($id)) : $id;
  22. $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
  23. if($user->status != null) {
  24. return ProfileController::accountCheck($user);
  25. }
  26. $status = Status::whereProfileId($user->id)
  27. ->whereNotIn('visibility',['draft','direct'])
  28. ->findOrFail($id);
  29. if($status->uri) {
  30. $url = $status->uri;
  31. if(ends_with($url, '/activity')) {
  32. $url = str_replace('/activity', '', $url);
  33. }
  34. return redirect($url);
  35. }
  36. if($status->visibility == 'private' || $user->is_private) {
  37. if(!Auth::check()) {
  38. abort(403);
  39. }
  40. $pid = Auth::user()->profile;
  41. if($user->followedBy($pid) == false && $user->id !== $pid->id) {
  42. abort(403);
  43. }
  44. }
  45. if ($request->wantsJson() && config('pixelfed.activitypub_enabled')) {
  46. return $this->showActivityPub($request, $status);
  47. }
  48. $template = $status->in_reply_to_id ? 'status.reply' : 'status.show';
  49. return view($template, compact('user', 'status'));
  50. }
  51. public function showObject(Request $request, $username, int $id)
  52. {
  53. $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
  54. if($user->status != null) {
  55. return ProfileController::accountCheck($user);
  56. }
  57. $status = Status::whereProfileId($user->id)
  58. ->whereNotIn('visibility',['draft','direct'])
  59. ->findOrFail($id);
  60. if($status->uri) {
  61. $url = $status->uri;
  62. if(ends_with($url, '/activity')) {
  63. $url = str_replace('/activity', '', $url);
  64. }
  65. return redirect($url);
  66. }
  67. if($status->visibility == 'private' || $user->is_private) {
  68. if(!Auth::check()) {
  69. abort(403);
  70. }
  71. $pid = Auth::user()->profile;
  72. if($user->followedBy($pid) == false && $user->id !== $pid->id) {
  73. abort(403);
  74. }
  75. }
  76. return $this->showActivityPub($request, $status);
  77. }
  78. public function compose()
  79. {
  80. $this->authCheck();
  81. return view('status.compose');
  82. }
  83. public function store(Request $request)
  84. {
  85. $this->authCheck();
  86. $user = Auth::user();
  87. $size = Media::whereUserId($user->id)->sum('size') / 1000;
  88. $limit = (int) config('pixelfed.max_account_size');
  89. if ($size >= $limit) {
  90. return redirect()->back()->with('error', 'You have exceeded your storage limit. Please click <a href="#">here</a> for more info.');
  91. }
  92. $this->validate($request, [
  93. 'photo.*' => 'required|mimetypes:' . config('pixelfed.media_types').'|max:' . config('pixelfed.max_photo_size'),
  94. 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length'),
  95. 'cw' => 'nullable|string',
  96. 'filter_class' => 'nullable|alpha_dash|max:30',
  97. 'filter_name' => 'nullable|string',
  98. 'visibility' => 'required|string|min:5|max:10',
  99. ]);
  100. if (count($request->file('photo')) > config('pixelfed.max_album_length')) {
  101. return redirect()->back()->with('error', 'Too many files, max limit per post: '.config('pixelfed.max_album_length'));
  102. }
  103. $cw = $request->filled('cw') && $request->cw == 'on' ? true : false;
  104. $monthHash = hash('sha1', date('Y').date('m'));
  105. $userHash = hash('sha1', $user->id.(string) $user->created_at);
  106. $profile = $user->profile;
  107. $visibility = $this->validateVisibility($request->visibility);
  108. $cw = $profile->cw == true ? true : $cw;
  109. $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
  110. if(config('costar.enabled') == true) {
  111. $blockedKeywords = config('costar.keyword.block');
  112. if($blockedKeywords !== null) {
  113. $keywords = config('costar.keyword.block');
  114. foreach($keywords as $kw) {
  115. if(Str::contains($request->caption, $kw) == true) {
  116. abort(400, 'Invalid object');
  117. }
  118. }
  119. }
  120. }
  121. $status = new Status();
  122. $status->profile_id = $profile->id;
  123. $status->caption = strip_tags($request->caption);
  124. $status->is_nsfw = $cw;
  125. // TODO: remove deprecated visibility in favor of scope
  126. $status->visibility = $visibility;
  127. $status->scope = $visibility;
  128. $status->save();
  129. $photos = $request->file('photo');
  130. $order = 1;
  131. $mimes = [];
  132. $medias = 0;
  133. foreach ($photos as $k => $v) {
  134. $allowedMimes = explode(',', config('pixelfed.media_types'));
  135. if(in_array($v->getMimeType(), $allowedMimes) == false) {
  136. continue;
  137. }
  138. $filter_class = $request->input('filter_class');
  139. $filter_name = $request->input('filter_name');
  140. $storagePath = "public/m/{$monthHash}/{$userHash}";
  141. $path = $v->store($storagePath);
  142. $hash = \hash_file('sha256', $v);
  143. $media = new Media();
  144. $media->status_id = $status->id;
  145. $media->profile_id = $profile->id;
  146. $media->user_id = $user->id;
  147. $media->media_path = $path;
  148. $media->original_sha256 = $hash;
  149. $media->size = $v->getSize();
  150. $media->mime = $v->getMimeType();
  151. $media->filter_class = in_array($filter_class, Filter::classes()) ? $filter_class : null;
  152. $media->filter_name = in_array($filter_name, Filter::names()) ? $filter_name : null;
  153. $media->order = $order;
  154. $media->save();
  155. array_push($mimes, $media->mime);
  156. ImageOptimize::dispatch($media);
  157. $order++;
  158. $medias++;
  159. }
  160. if($medias == 0) {
  161. $status->delete();
  162. return;
  163. }
  164. $status->type = (new self)::mimeTypeCheck($mimes);
  165. $status->save();
  166. NewStatusPipeline::dispatch($status);
  167. // TODO: Send to subscribers
  168. return redirect($status->url());
  169. }
  170. public function delete(Request $request)
  171. {
  172. $this->authCheck();
  173. $this->validate($request, [
  174. 'item' => 'required|integer|min:1',
  175. ]);
  176. $status = Status::findOrFail($request->input('item'));
  177. if ($status->profile_id === Auth::user()->profile->id || Auth::user()->is_admin == true) {
  178. StatusDelete::dispatch($status);
  179. }
  180. if($request->wantsJson()) {
  181. return response()->json(['Status successfully deleted.']);
  182. } else {
  183. return redirect(Auth::user()->url());
  184. }
  185. }
  186. public function storeShare(Request $request)
  187. {
  188. $this->authCheck();
  189. $this->validate($request, [
  190. 'item' => 'required|integer',
  191. ]);
  192. $profile = Auth::user()->profile;
  193. $status = Status::withCount('shares')->findOrFail($request->input('item'));
  194. Cache::forget('transform:status:'.$status->url());
  195. $count = $status->shares_count;
  196. $exists = Status::whereProfileId(Auth::user()->profile->id)
  197. ->whereReblogOfId($status->id)
  198. ->count();
  199. if ($exists !== 0) {
  200. $shares = Status::whereProfileId(Auth::user()->profile->id)
  201. ->whereReblogOfId($status->id)
  202. ->get();
  203. foreach ($shares as $share) {
  204. $share->delete();
  205. $count--;
  206. }
  207. } else {
  208. $share = new Status();
  209. $share->profile_id = $profile->id;
  210. $share->reblog_of_id = $status->id;
  211. $share->in_reply_to_profile_id = $status->profile_id;
  212. $share->save();
  213. $count++;
  214. SharePipeline::dispatch($share);
  215. }
  216. if ($request->ajax()) {
  217. $response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count];
  218. } else {
  219. $response = redirect($status->url());
  220. }
  221. return $response;
  222. }
  223. public function showActivityPub(Request $request, $status)
  224. {
  225. $fractal = new Fractal\Manager();
  226. $resource = new Fractal\Resource\Item($status, new Note());
  227. $res = $fractal->createData($resource)->toArray();
  228. return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
  229. }
  230. public function edit(Request $request, $username, $id)
  231. {
  232. $this->authCheck();
  233. $user = Auth::user()->profile;
  234. $status = Status::whereProfileId($user->id)
  235. ->with(['media'])
  236. ->findOrFail($id);
  237. return view('status.edit', compact('user', 'status'));
  238. }
  239. public function editStore(Request $request, $username, $id)
  240. {
  241. $this->authCheck();
  242. $user = Auth::user()->profile;
  243. $status = Status::whereProfileId($user->id)
  244. ->with(['media'])
  245. ->findOrFail($id);
  246. $this->validate($request, [
  247. 'id' => 'required|integer|min:1',
  248. 'caption' => 'nullable',
  249. 'filter' => 'nullable|alpha_dash|max:30',
  250. ]);
  251. $id = $request->input('id');
  252. $caption = $request->input('caption');
  253. $filter = $request->input('filter');
  254. $media = Media::whereProfileId($user->id)
  255. ->whereStatusId($status->id)
  256. ->find($id);
  257. $changed = false;
  258. if ($media->caption != $caption) {
  259. $media->caption = $caption;
  260. $changed = true;
  261. }
  262. if ($media->filter_class != $filter) {
  263. $media->filter_class = $filter;
  264. $changed = true;
  265. }
  266. if ($changed === true) {
  267. $media->save();
  268. }
  269. return response()->json([], 200);
  270. }
  271. protected function authCheck()
  272. {
  273. if (Auth::check() == false) {
  274. abort(403);
  275. }
  276. }
  277. protected function validateVisibility($visibility)
  278. {
  279. $allowed = ['public', 'unlisted', 'private'];
  280. return in_array($visibility, $allowed) ? $visibility : 'public';
  281. }
  282. public static function mimeTypeCheck($mimes)
  283. {
  284. $allowed = explode(',', config('pixelfed.media_types'));
  285. $count = count($mimes);
  286. $photos = 0;
  287. $videos = 0;
  288. foreach($mimes as $mime) {
  289. if(in_array($mime, $allowed) == false && $mime !== 'video/mp4') {
  290. continue;
  291. }
  292. if(str_contains($mime, 'image/')) {
  293. $photos++;
  294. }
  295. if(str_contains($mime, 'video/')) {
  296. $videos++;
  297. }
  298. }
  299. if($photos == 1 && $videos == 0) {
  300. return 'photo';
  301. }
  302. if($videos == 1 && $photos == 0) {
  303. return 'video';
  304. }
  305. if($photos > 1 && $videos == 0) {
  306. return 'photo:album';
  307. }
  308. if($videos > 1 && $photos == 0) {
  309. return 'video:album';
  310. }
  311. if($photos >= 1 && $videos >= 1) {
  312. return 'photo:video:album';
  313. }
  314. }
  315. public function toggleVisibility(Request $request) {
  316. $this->authCheck();
  317. $this->validate($request, [
  318. 'item' => 'required|string|min:1|max:20',
  319. 'disableComments' => 'required|boolean'
  320. ]);
  321. $user = Auth::user();
  322. $id = $request->input('item');
  323. $state = $request->input('disableComments');
  324. $status = Status::findOrFail($id);
  325. if($status->profile_id != $user->profile->id && $user->is_admin == false) {
  326. abort(403);
  327. }
  328. $status->comments_disabled = $status->comments_disabled == true ? false : true;
  329. $status->save();
  330. return response()->json([200]);
  331. }
  332. }