1
0

AdminDirectoryController.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. <?php
  2. namespace App\Http\Controllers\Admin;
  3. use DB, Cache;
  4. use App\{
  5. DiscoverCategory,
  6. DiscoverCategoryHashtag,
  7. Hashtag,
  8. Media,
  9. Profile,
  10. Status,
  11. StatusHashtag,
  12. User
  13. };
  14. use App\Models\ConfigCache;
  15. use App\Services\AccountService;
  16. use App\Services\ConfigCacheService;
  17. use App\Services\StatusService;
  18. use Carbon\Carbon;
  19. use Illuminate\Http\Request;
  20. use Illuminate\Validation\Rule;
  21. use League\ISO3166\ISO3166;
  22. use Illuminate\Support\Str;
  23. use Illuminate\Support\Facades\Storage;
  24. use Illuminate\Support\Facades\Validator;
  25. use Illuminate\Support\Facades\Http;
  26. use App\Http\Controllers\PixelfedDirectoryController;
  27. trait AdminDirectoryController
  28. {
  29. public function directoryHome(Request $request)
  30. {
  31. return view('admin.directory.home');
  32. }
  33. public function directoryInitialData(Request $request)
  34. {
  35. $res = [];
  36. $res['countries'] = collect((new ISO3166)->all())->pluck('name');
  37. $res['admins'] = User::whereIsAdmin(true)
  38. ->where('2fa_enabled', true)
  39. ->get()->map(function($user) {
  40. return [
  41. 'uid' => (string) $user->id,
  42. 'pid' => (string) $user->profile_id,
  43. 'username' => $user->username,
  44. 'created_at' => $user->created_at
  45. ];
  46. });
  47. $config = ConfigCache::whereK('pixelfed.directory')->first();
  48. if($config) {
  49. $data = $config->v ? json_decode($config->v, true) : [];
  50. $res = array_merge($res, $data);
  51. }
  52. if(empty($res['summary'])) {
  53. $summary = ConfigCache::whereK('app.short_description')->pluck('v');
  54. $res['summary'] = $summary ? $summary[0] : null;
  55. }
  56. if(isset($res['banner_image']) && !empty($res['banner_image'])) {
  57. $res['banner_image'] = url(Storage::url($res['banner_image']));
  58. }
  59. if(isset($res['favourite_posts'])) {
  60. $res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) {
  61. return StatusService::get($id);
  62. })
  63. ->filter(function($post) {
  64. return $post && isset($post['account']);
  65. })
  66. ->values();
  67. }
  68. $res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : [];
  69. $res['open_registration'] = (bool) config_cache('pixelfed.open_registration');
  70. $res['oauth_enabled'] = (bool) config_cache('pixelfed.oauth_enabled') && file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key'));
  71. $res['activitypub_enabled'] = (bool) config_cache('federation.activitypub.enabled');
  72. $res['feature_config'] = [
  73. 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
  74. 'image_quality' => config_cache('pixelfed.image_quality'),
  75. 'optimize_image' => config_cache('pixelfed.optimize_image'),
  76. 'max_photo_size' => config_cache('pixelfed.max_photo_size'),
  77. 'max_caption_length' => config_cache('pixelfed.max_caption_length'),
  78. 'max_altext_length' => config_cache('pixelfed.max_altext_length'),
  79. 'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'),
  80. 'max_account_size' => config_cache('pixelfed.max_account_size'),
  81. 'max_album_length' => config_cache('pixelfed.max_album_length'),
  82. 'account_deletion' => config_cache('pixelfed.account_deletion'),
  83. ];
  84. if(config_cache('pixelfed.directory.testimonials')) {
  85. $testimonials = collect(json_decode(config_cache('pixelfed.directory.testimonials'),true))
  86. ->map(function($t) {
  87. return [
  88. 'profile' => AccountService::get($t['profile_id']),
  89. 'body' => $t['body']
  90. ];
  91. });
  92. $res['testimonials'] = $testimonials;
  93. }
  94. $validator = Validator::make($res['feature_config'], [
  95. 'media_types' => [
  96. 'required',
  97. function ($attribute, $value, $fail) {
  98. if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) {
  99. $fail('You must enable image/jpeg and image/png support.');
  100. }
  101. },
  102. ],
  103. 'image_quality' => 'required_if:optimize_image,true|integer|min:75|max:100',
  104. 'max_altext_length' => 'required|integer|min:1000|max:5000',
  105. 'max_photo_size' => 'required|integer|min:15000|max:100000',
  106. 'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
  107. 'max_album_length' => 'required|integer|min:4|max:20',
  108. 'account_deletion' => 'required|accepted',
  109. 'max_caption_length' => 'required|integer|min:500|max:10000'
  110. ]);
  111. $res['requirements_validator'] = $validator->errors();
  112. $res['is_eligible'] = $res['open_registration'] &&
  113. $res['oauth_enabled'] &&
  114. $res['activitypub_enabled'] &&
  115. count($res['requirements_validator']) === 0 &&
  116. $this->validVal($res, 'admin') &&
  117. $this->validVal($res, 'summary', null, 10) &&
  118. $this->validVal($res, 'favourite_posts', 3) &&
  119. $this->validVal($res, 'contact_email') &&
  120. $this->validVal($res, 'privacy_pledge') &&
  121. $this->validVal($res, 'location');
  122. $res['has_submitted'] = config_cache('pixelfed.directory.has_submitted') ?? false;
  123. $res['synced'] = config_cache('pixelfed.directory.is_synced') ?? false;
  124. $res['latest_response'] = config_cache('pixelfed.directory.latest_response') ?? null;
  125. $path = base_path('resources/lang');
  126. $langs = collect([]);
  127. foreach (new \DirectoryIterator($path) as $io) {
  128. $name = $io->getFilename();
  129. $skip = ['vendor'];
  130. if($io->isDot() || in_array($name, $skip)) {
  131. continue;
  132. }
  133. if($io->isDir()) {
  134. $langs->push(['code' => $name, 'name' => locale_get_display_name($name)]);
  135. }
  136. }
  137. $res['available_languages'] = $langs->sortBy('name')->values();
  138. $res['primary_locale'] = config('app.locale');
  139. $submissionState = Http::withoutVerifying()
  140. ->post('https://pixelfed.org/api/v1/directory/check-submission', [
  141. 'domain' => config('pixelfed.domain.app')
  142. ]);
  143. $res['submission_state'] = $submissionState->json();
  144. return $res;
  145. }
  146. protected function validVal($res, $val, $count = false, $minLen = false)
  147. {
  148. if(!isset($res[$val])) {
  149. return false;
  150. }
  151. if($count) {
  152. return count($res[$val]) >= $count;
  153. }
  154. if($minLen) {
  155. return strlen($res[$val]) >= $minLen;
  156. }
  157. return $res[$val];
  158. }
  159. public function directoryStore(Request $request)
  160. {
  161. $this->validate($request, [
  162. 'location' => 'string|min:1|max:53',
  163. 'summary' => 'string|nullable|max:140',
  164. 'admin_uid' => 'sometimes|nullable',
  165. 'contact_email' => 'sometimes|nullable|email:rfc,dns',
  166. 'favourite_posts' => 'array|max:12',
  167. 'favourite_posts.*' => 'distinct',
  168. 'privacy_pledge' => 'sometimes',
  169. 'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000'
  170. ]);
  171. $config = ConfigCache::firstOrNew([
  172. 'k' => 'pixelfed.directory'
  173. ]);
  174. $res = $config->v ? json_decode($config->v, true) : [];
  175. $res['summary'] = strip_tags($request->input('summary'));
  176. $res['favourite_posts'] = $request->input('favourite_posts');
  177. $res['admin'] = (string) $request->input('admin_uid');
  178. $res['contact_email'] = $request->input('contact_email');
  179. $res['privacy_pledge'] = (bool) $request->input('privacy_pledge');
  180. if($request->filled('location')) {
  181. $exists = (new ISO3166)->name($request->location);
  182. if($exists) {
  183. $res['location'] = $request->input('location');
  184. }
  185. }
  186. if($request->hasFile('banner_image')) {
  187. collect(Storage::files('public/headers'))
  188. ->filter(function($name) {
  189. $protected = [
  190. 'public/headers/.gitignore',
  191. 'public/headers/default.jpg',
  192. 'public/headers/missing.png'
  193. ];
  194. return !in_array($name, $protected);
  195. })
  196. ->each(function($name) {
  197. Storage::delete($name);
  198. });
  199. $path = $request->file('banner_image')->store('public/headers');
  200. $res['banner_image'] = $path;
  201. ConfigCacheService::put('app.banner_image', url(Storage::url($path)));
  202. Cache::forget('api:v1:instance-data-response-v1');
  203. }
  204. $config->v = json_encode($res);
  205. $config->save();
  206. ConfigCacheService::put('pixelfed.directory', $config->v);
  207. $updated = json_decode($config->v, true);
  208. if(isset($updated['banner_image'])) {
  209. $updated['banner_image'] = url(Storage::url($updated['banner_image']));
  210. }
  211. return $updated;
  212. }
  213. public function directoryHandleServerSubmission(Request $request)
  214. {
  215. $reqs = [];
  216. $reqs['feature_config'] = [
  217. 'open_registration' => config_cache('pixelfed.open_registration'),
  218. 'activitypub_enabled' => config_cache('federation.activitypub.enabled'),
  219. 'oauth_enabled' => config_cache('pixelfed.oauth_enabled'),
  220. 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
  221. 'image_quality' => config_cache('pixelfed.image_quality'),
  222. 'optimize_image' => config_cache('pixelfed.optimize_image'),
  223. 'max_photo_size' => config_cache('pixelfed.max_photo_size'),
  224. 'max_caption_length' => config_cache('pixelfed.max_caption_length'),
  225. 'max_altext_length' => config_cache('pixelfed.max_altext_length'),
  226. 'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'),
  227. 'max_account_size' => config_cache('pixelfed.max_account_size'),
  228. 'max_album_length' => config_cache('pixelfed.max_album_length'),
  229. 'account_deletion' => config_cache('pixelfed.account_deletion'),
  230. ];
  231. $validator = Validator::make($reqs['feature_config'], [
  232. 'open_registration' => 'required|accepted',
  233. 'activitypub_enabled' => 'required|accepted',
  234. 'oauth_enabled' => 'required|accepted',
  235. 'media_types' => [
  236. 'required',
  237. function ($attribute, $value, $fail) {
  238. if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) {
  239. $fail('You must enable image/jpeg and image/png support.');
  240. }
  241. },
  242. ],
  243. 'image_quality' => 'required_if:optimize_image,true|integer|min:75|max:100',
  244. 'max_altext_length' => 'required|integer|min:1000|max:5000',
  245. 'max_photo_size' => 'required|integer|min:15000|max:100000',
  246. 'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
  247. 'max_album_length' => 'required|integer|min:4|max:20',
  248. 'account_deletion' => 'required|accepted',
  249. 'max_caption_length' => 'required|integer|min:500|max:10000'
  250. ]);
  251. if(!$validator->validate()) {
  252. return response()->json($validator->errors(), 422);
  253. }
  254. ConfigCacheService::put('pixelfed.directory.submission-key', Str::random(random_int(40, 69)));
  255. ConfigCacheService::put('pixelfed.directory.submission-ts', now());
  256. $data = (new PixelfedDirectoryController())->buildListing();
  257. $res = Http::withoutVerifying()->post('https://pixelfed.org/api/v1/directory/submission', $data);
  258. return 200;
  259. }
  260. public function directoryDeleteBannerImage(Request $request)
  261. {
  262. $bannerImage = ConfigCache::whereK('app.banner_image')->first();
  263. $directory = ConfigCache::whereK('pixelfed.directory')->first();
  264. if(!$bannerImage && !$directory || empty($directory->v)) {
  265. return;
  266. }
  267. $directoryArr = json_decode($directory->v, true);
  268. $path = isset($directoryArr['banner_image']) ? $directoryArr['banner_image'] : false;
  269. $protected = [
  270. 'public/headers/.gitignore',
  271. 'public/headers/default.jpg',
  272. 'public/headers/missing.png'
  273. ];
  274. if(!$path || in_array($path, $protected)) {
  275. return;
  276. }
  277. if(Storage::exists($directoryArr['banner_image'])) {
  278. Storage::delete($directoryArr['banner_image']);
  279. }
  280. $directoryArr['banner_image'] = 'public/headers/default.jpg';
  281. $directory->v = $directoryArr;
  282. $directory->save();
  283. $bannerImage->v = url(Storage::url('public/headers/default.jpg'));
  284. $bannerImage->save();
  285. Cache::forget('api:v1:instance-data-response-v1');
  286. ConfigCacheService::put('pixelfed.directory', $directory);
  287. return $bannerImage->v;
  288. }
  289. public function directoryGetPopularPosts(Request $request)
  290. {
  291. $ids = Cache::remember('admin:api:popular_posts', 86400, function() {
  292. return Status::whereLocal(true)
  293. ->whereScope('public')
  294. ->whereType('photo')
  295. ->whereNull(['in_reply_to_id', 'reblog_of_id'])
  296. ->orderByDesc('likes_count')
  297. ->take(50)
  298. ->pluck('id');
  299. });
  300. $res = $ids->map(function($id) {
  301. return StatusService::get($id);
  302. })
  303. ->filter(function($post) {
  304. return $post && isset($post['account']);
  305. })
  306. ->values();
  307. return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
  308. }
  309. public function directoryGetAddPostByIdSearch(Request $request)
  310. {
  311. $this->validate($request, [
  312. 'q' => 'required|integer'
  313. ]);
  314. $id = $request->input('q');
  315. $status = Status::whereLocal(true)
  316. ->whereType('photo')
  317. ->whereNull(['in_reply_to_id', 'reblog_of_id'])
  318. ->findOrFail($id);
  319. $res = StatusService::get($status->id);
  320. return $res;
  321. }
  322. public function directoryDeleteTestimonial(Request $request)
  323. {
  324. $this->validate($request, [
  325. 'profile_id' => 'required',
  326. ]);
  327. $profile_id = $request->input('profile_id');
  328. $testimonials = ConfigCache::whereK('pixelfed.directory.testimonials')->firstOrFail();
  329. $existing = collect(json_decode($testimonials->v, true))
  330. ->filter(function($t) use($profile_id) {
  331. return $t['profile_id'] !== $profile_id;
  332. })
  333. ->values();
  334. ConfigCacheService::put('pixelfed.directory.testimonials', $existing);
  335. return $existing;
  336. }
  337. public function directorySaveTestimonial(Request $request)
  338. {
  339. $this->validate($request, [
  340. 'username' => 'required',
  341. 'body' => 'required|string|min:5|max:500'
  342. ]);
  343. $user = User::whereUsername($request->input('username'))->whereNull('status')->firstOrFail();
  344. $configCache = ConfigCache::firstOrCreate([
  345. 'k' => 'pixelfed.directory.testimonials'
  346. ]);
  347. $testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
  348. abort_if($testimonials->contains('profile_id', $user->profile_id), 422, 'Testimonial already exists');
  349. abort_if($testimonials->count() == 10, 422, 'You can only have 10 active testimonials');
  350. $testimonials->push([
  351. 'profile_id' => (string) $user->profile_id,
  352. 'username' => $request->input('username'),
  353. 'body' => $request->input('body')
  354. ]);
  355. $configCache->v = json_encode($testimonials->toArray());
  356. $configCache->save();
  357. ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v);
  358. $res = [
  359. 'profile' => AccountService::get($user->profile_id),
  360. 'body' => $request->input('body')
  361. ];
  362. return $res;
  363. }
  364. public function directoryUpdateTestimonial(Request $request)
  365. {
  366. $this->validate($request, [
  367. 'profile_id' => 'required',
  368. 'body' => 'required|string|min:5|max:500'
  369. ]);
  370. $profile_id = $request->input('profile_id');
  371. $body = $request->input('body');
  372. $user = User::whereProfileId($profile_id)->firstOrFail();
  373. $configCache = ConfigCache::firstOrCreate([
  374. 'k' => 'pixelfed.directory.testimonials'
  375. ]);
  376. $testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
  377. $updated = $testimonials->map(function($t) use($profile_id, $body) {
  378. if($t['profile_id'] == $profile_id) {
  379. $t['body'] = $body;
  380. }
  381. return $t;
  382. })
  383. ->values();
  384. $configCache->v = json_encode($updated);
  385. $configCache->save();
  386. ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v);
  387. return $updated;
  388. }
  389. }