(object) $row, (array) ($answer['items'] ?? [])), (int) ($answer['total'] ?? 0), (int) ($answer['limit'] ?? 20), $page, ['path' => request()->url(), 'pageName' => 'pg', 'query' => request()->query()] ); } /** As categorias do site, que as telas de escrever e editar montam. */ private function categories(): array { return array_map(fn ($row) => (object) $row, (array) ($this->api->get('posts', ['action' => 'categories'])['items'] ?? [])); } /** Teto do envio de imagem, em kilobytes. */ private const UPLOAD_MAX = 10240; /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index($s = null) { if (Auth::check() === false) { Auth::logout(); return redirect()->route('login'); } // the search form posts a query string, the old links still use the route segment $s = $s ?: request('s'); $userLogged = User::where('id', '=', Auth::id())->get()->first(); $allNotifications = DB::table('notifications')->orderBy('id', 'desc')->limit('3')->get(); $siteData = SiteContext::current(); // the list shows the cover, and the file lives on the site server $serverData = DB::table('sites_servers')->where('id','=',session('serverID'))->get()->first(); $page = max(1, (int) request('pg', 1)); $answer = $this->api->get('posts', [ 'action' => 'list', 'page' => $page, 'limit' => 20, 's' => filter_var((string) $s, FILTER_SANITIZE_STRING), ]); $allPosts = $this->paginate($answer, $page); return view('site.posts', ['userLogged' => $userLogged , 'allNotifications' => $allNotifications, 'topHref' => 'site', 'siteData' => $siteData, 'serverData' => $serverData, 'allPosts' => $allPosts, 's' => $s]); } public function robot($arg) { list($start, $end) = explode("-", $arg); $start = (int) trim($start); $end = (int) trim($end); $svTime = date('Y-m-d H:i:s'); $listSchedulePosts = DB::select( "SELECT * FROM posts_schedules WHERE post_schedule <= ? LIMIT ?, ?", [$svTime, $start, $end] ); foreach($listSchedulePosts as $sch_posts){ DB::connection('mysql_site_' . $sch_posts->server_id) ->table('posts') ->where('id', $sch_posts->post_id) ->update([ 'status' => '1', 'created_at' => $sch_posts->post_schedule ]); DB::delete("DELETE FROM posts_schedules WHERE id = ?", [$sch_posts->id]); } } /** * Show the form for creating a new resource. * * @return \Illuminate\Http\Response */ public function create() { if (Auth::check() === false) { Auth::logout(); return redirect()->route('login'); } $userLogged = User::where('id', '=', Auth::id())->get()->first(); $allNotifications = DB::table('notifications')->orderBy('id', 'desc')->limit('3')->get(); $siteData = SiteContext::current(); // the cover thumbnail needs the base url even before anything is uploaded $serverData = DB::table('sites_servers')->where('id','=',session('serverID'))->get()->first(); $getCategories = $this->categories(); return view('site.postCreate', ['userLogged' => $userLogged , 'allNotifications' => $allNotifications, 'topHref' => 'site', 'siteData' => $siteData, 'serverData' => $serverData, 'getCategories' => $getCategories]); } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { if (Auth::check() === false) { Auth::logout(); return redirect()->route('login'); } $siteData = SiteContext::current(); if(!$request->title){ return redirect()->route('site.posts.c')->withInput()->with('error_title', __('site.page_post_controller_new_error_1')); } if(!isset($request->categories)){ return redirect()->route('site.posts.c')->withInput()->with('error_cat', __('site.page_post_controller_new_error_2')); } $impCats = implode(',',$request->categories); $status = filter_var($request->visibility, FILTER_SANITIZE_NUMBER_INT); $slug = Str::slug(filter_var($request->title, FILTER_SANITIZE_STRING)); /* a noticia e as categorias dela vao juntas, numa transacao do lado de la */ $answer = $this->api->post('posts', [ 'action' => 'create', 'title' => filter_var($request->title, FILTER_SANITIZE_STRING), 'slug' => $slug, 'feature_img' => ($request->feature_image ? $request->feature_image : null), 'content' => $request->content, 'short_text' => filter_var((string) $request->short_text, FILTER_SANITIZE_STRING) ?: null, 'feature_img_alt' => filter_var((string) $request->feature_img_alt, FILTER_SANITIZE_STRING) ?: null, 'status' => $status, 'allow_comments' => filter_var($request->allow_comments, FILTER_SANITIZE_NUMBER_INT), 'schedule_timestamp' => $request->schedule_time, 'categories_ids' => array_map('intval', (array) $request->categories), ]); if (($answer['ok'] ?? false) !== true) { return redirect()->route('site.posts.c')->withInput()->with('error', __('site.page_posts_api_error')); } $idInserted = (int) ($answer['id_row'] ?? 0); // Se for agendado, adiciona ao agendamento if ($status === '3') { $postId = (int) $idInserted; $serverId = (int) session('serverID'); $scheduleTime = $request->schedule_time; DB::select(" INSERT INTO posts_schedules (post_id, server_id, post_schedule, created_at) VALUES ($postId, $serverId, '$scheduleTime', NOW()) "); } UserLog::write('post.create', ['entity' => 'post', 'entity_id' => $idInserted, 'description' => $request->title]); return redirect()->route('site.posts.r')->with('success', __('site.page_post_controller_new_success')); } /** * Show the form for editing the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function edit($id) { if (Auth::check() === false) { Auth::logout(); return redirect()->route('login'); } if(!$id){ return redirect()->route('site.posts.r'); } $userLogged = User::where('id', '=', Auth::id())->get()->first(); $allNotifications = DB::table('notifications')->orderBy('id', 'desc')->limit('3')->get(); $siteData = SiteContext::current(); $serverData = DB::table('sites_servers')->where('id','=',session('serverID'))->get()->first(); $answer = $this->api->get('posts', ['action' => 'get', 'id_row' => (int) $id]); $dataPost = ($answer['post'] ?? null) ? (object) $answer['post'] : null; // a removed post, or one from another site, used to reach the view as null if(!$dataPost){ return redirect()->route('site.posts.r'); } $expCategories = array_map('intval', (array) ($answer['categories_ids'] ?? [])); $getCategories = $this->categories(); return view('site.postEdit', ['userLogged' => $userLogged , 'allNotifications' => $allNotifications, 'topHref' => 'site', 'siteData' => $siteData, 'serverData' => $serverData, 'dataPost' => $dataPost, 'expCategories' => $expCategories, 'getCategories' => $getCategories]); } /** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param int $id * @return \Illuminate\Http\Response */ public function update(Request $request, $id) { if (Auth::check() === false) { Auth::logout(); return redirect()->route('login'); } if(!$id){ return redirect()->route('site.posts.r'); } if(!$request->title){ return redirect()->route('site.posts.u', $id)->withInput()->with('error_title', __('site.page_post_controller_new_error_1')); } if(!isset($request->categories)){ return redirect()->route('site.posts.u', $id)->withInput()->with('error_cat', __('site.page_post_controller_new_error_2')); } //$impCats = implode(',',$request->categories); $serverData = DB::table('sites_servers')->where('id','=',session('serverID'))->get()->first(); $siteData = SiteContext::current(); $currentData = ($this->api->get('posts', ['action' => 'get', 'id_row' => (int) $id])['post'] ?? null); $currentData = $currentData ? (object) $currentData : null; if (! $currentData) { return redirect()->route('site.posts.r'); } /** * The old image is only deleted AFTER the update, at the end of the * method. Deleting first left the post pointing at a missing file * whenever the update failed, and a lost client image never comes back. */ $oldImage = $currentData->feature_img; $slug = Str::slug(filter_var($request->title, FILTER_SANITIZE_STRING)); config(['app.timezone' => $siteData->timezone ?? 'America/Sao_Paulo']); $update = [ 'title' => filter_var($request->title, FILTER_SANITIZE_STRING), 'slug' => $slug, 'feature_img' => ($request->fea_rmv == 'yes' ? null : ($request->feature_image ? $request->feature_image : $currentData->feature_img)), 'content' => $request->content, 'short_text' => filter_var((string) $request->short_text, FILTER_SANITIZE_STRING) ?: null, 'feature_img_alt' => filter_var((string) $request->feature_img_alt, FILTER_SANITIZE_STRING) ?: null, 'categories' => '99999999', 'status' => filter_var($request->visibility, FILTER_SANITIZE_NUMBER_INT), 'allow_comments' => filter_var($request->allow_comments, FILTER_SANITIZE_NUMBER_INT), 'updated_at' => now() ]; // sempre deleta para garantir que � excluido antes DB::delete("DELETE FROM posts_schedules WHERE post_id = ?", [$id]); // apos isso se for tipo agendamento vai adicionar o tempo e em seguida vai criar o registro para o robot processar. if ($request->visibility === '3') { $update['schedule_timestamp'] = $request->schedule_time; $postId = (int) $id; $serverId = (int) session('serverID'); $scheduleTime = $request->schedule_time; DB::select(" INSERT INTO posts_schedules (post_id, server_id, post_schedule, created_at) VALUES ($postId, $serverId, '$scheduleTime', NOW()) "); } /* a noticia e as categorias dela vao juntas, numa transacao do lado de la */ unset($update['categories'], $update['updated_at']); $saved = $this->api->post('posts', [ 'action' => 'update', 'id_row' => (int) $id, 'categories_ids' => array_map('intval', (array) $request->categories), ] + $update); if (($saved['ok'] ?? false) !== true) { return redirect()->route('site.posts.u', $id)->withInput()->with('error', __('site.page_posts_api_error')); } // written, so the old file can go without orphaning the post SiteMedia::remove($serverData->name ?? null, 'posts', $oldImage, $update['feature_img']); UserLog::write('post.update', ['entity' => 'post', 'entity_id' => $id, 'description' => $request->title]); return redirect()->route('site.posts.r')->with('success', __('site.page_post_controller_edit_success')); } /** * Remove the specified resource from storage. * * @param int $id * @return \Illuminate\Http\Response */ public function destroy($id) { if(Auth::check() === false){ Auth::logout(); return redirect()->route('login'); } $serverData = DB::table('sites_servers')->where('id','=',session('serverID'))->get()->first(); $verifyPost = DB::connection('mysql_site_' . session('serverID'))->table('posts')->where('site_id','=',session('siteID'))->where('id','=',$id)->get(); if(!$verifyPost->count() || !$id){ return redirect()->route('site.posts.r'); } DB::connection('mysql_site_' . session('serverID'))->table('posts')->where('id','=',$id)->delete(); // after the database: a failed file removal leaves an orphan on disk, // not a post the client cannot delete UserLog::write('post.delete', ['entity' => 'post', 'entity_id' => $id, 'description' => $verifyPost[0]->title]); SiteMedia::remove($serverData->name ?? null, 'posts', $verifyPost[0]->feature_img); DB::connection('mysql_site_' . session('serverID'))->table('featured')->where('type','=','3')->where('entry','=',$id)->delete(); // remove o agendamento de post caso tenha: DB::delete("DELETE FROM posts_schedules WHERE post_id = ?", [$id]); return redirect()->route('site.posts.r')->with('success', __('site.page_post_controller_delete_success')); } public function removeByIds(Request $request) { if(Auth::check() === false){ Auth::logout(); return redirect()->route('login'); } $ids = $request->input('ids', []); if (empty($ids)) { return response()->json([ 'success' => false, 'message' => 'Nenhuma postagem selecionada.' ]); } $hasOk = true; $removedIds = []; foreach($ids as $id){ $serverData = DB::table('sites_servers')->where('id','=',session('serverID'))->get()->first(); $verifyPost = DB::connection('mysql_site_' . session('serverID'))->table('posts')->where('site_id','=',session('siteID'))->where('id','=',$id)->get(); if(!$verifyPost->count()){ $hasOk = false; continue; } DB::connection('mysql_site_' . session('serverID'))->table('posts')->where('id','=',$id)->delete(); DB::delete("DELETE FROM posts_schedules WHERE post_id = ?", [$id]); // remove o agendamento de post caso tenha DB::connection('mysql_site_' . session('serverID'))->table('featured')->where('type','=','3')->where('entry','=',$id)->delete(); // database first: a failed file removal is an orphan, never a post // the client selected that stayed in the list SiteMedia::remove($serverData->name ?? null, 'posts', $verifyPost[0]->feature_img); $removedIds[] = (int) $id; } /** * One record for the whole operation, with the ids in the description. * A row per post would turn a 200 item cleanup into 200 records and * hide what actually happened: a single action. */ if ($removedIds) { UserLog::write('post.delete.bulk', [ 'entity' => 'post', 'description' => count($removedIds) . ' posts: ' . implode(',', array_slice($removedIds, 0, 40)) . (count($removedIds) > 40 ? '...' : ''), ]); } return response()->json([ 'success' => $hasOk, 'message' => $hasOk ? 'As postagens selecionadas foram removidas.' : 'Postagens removidas anteriormente.' ]); } public function integration() { if(Auth::check() === false){ Auth::logout(); return redirect()->route('login'); } return view('site.postIntegration'); } public function integrationImg() { if(Auth::check() === false){ Auth::logout(); return redirect()->route('login'); } $serverData = DB::table('sites_servers')->where('id','=',session('serverID'))->get()->first(); $siteData = SiteContext::current(); return view('site.postIntegrationImg',['siteData' => $siteData, 'serverData' => $serverData]); } /** * Table and label column per integrable type. Everything used to be five * near identical blocks that loaded the whole table into memory, which is * why a site with many videos took seconds to open the picker. */ private function integrationSources(): array { return [ 'album' => ['albums', 'name'], 'video' => ['videos', 'title'], 'form' => ['forms', 'name'], 'poll' => ['poll', 'name'], 'freeModule' => ['free_module', 'name'], ]; } /** * Placeholder card drawn for an integrated block inside the editor. * * It is built here instead of being a static file because the card shows * the name of the chosen item, and the editor only knows the type and the * id when it rebuilds the content of a saved post. */ public function integrationCard($type = null, $id = null) { if(Auth::check() === false){ Auth::logout(); return redirect()->route('login'); } $sources = $this->integrationSources(); if(!isset($sources[$type])){ abort(404); } $labels = [ 'album' => [__('site.page_integration_albums'), 'M3 5.5h18v13H3zM3 15l4.5-4.5 4 4 3-3L21 17'], 'poll' => [__('site.menu_item_5_sub_6'), 'M5 20V10M12 20V4M19 20v-7'], 'form' => [__('site.menu_item_5_sub_8'), 'M8 3.5h8a1.5 1.5 0 0 1 1.5 1.5v15a1.5 1.5 0 0 1-1.5 1.5H8A1.5 1.5 0 0 1 6.5 20V5A1.5 1.5 0 0 1 8 3.5zM9.5 8h5M9.5 12h5M9.5 16h3'], 'video' => [__('site.menu_item_3_sub_2'), 'M3.5 6.5h12a1.5 1.5 0 0 1 1.5 1.5v8a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 2 16V8a1.5 1.5 0 0 1 1.5-1.5zM17 10.5l4.5-2.5v8L17 13.5z'], 'freeModule' => [__('site.menu_item_3_sub_8'), 'M12 3l7 4v10l-7 4-7-4V7zM5 7l7 4 7-4M12 11v10'], ]; [$table, $column] = $sources[$type]; [$kind, $icon] = $labels[$type]; $row = DB::connection('mysql_site_' . session('serverID'))->table($table) ->where('site_id', '=', session('siteID'))->where('id', '=', (int) $id) ->select($column . ' as name')->first(); // the item may have been deleted after the post was written $name = $row->name ?? __('site.page_integration_missing'); return response($this->integrationSvg($kind, $name, $icon), 200, [ 'Content-Type' => 'image/svg+xml; charset=utf-8', 'Cache-Control' => 'no-store', ]); } private function integrationSvg(string $kind, string $name, string $icon): string { $font = "system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif"; // the card is 800 wide and the name starts at 92, so this is what fits $name = mb_strimwidth(trim($name), 0, 58, '...'); $kind = htmlspecialchars($kind, ENT_QUOTES, 'UTF-8'); $name = htmlspecialchars($name, ENT_QUOTES, 'UTF-8'); return << {$name} {$kind} SVG; } /** * Media library page for the editor picker. Paged and searchable so the * dialog opens at once no matter how many files the site has uploaded. */ public function integrationImgItems(Request $request) { if(Auth::check() === false){ Auth::logout(); return redirect()->route('login'); } $serverData = DB::table('sites_servers')->where('id','=',session('serverID'))->get()->first(); $perPage = 24; $page = max(1, (int) $request->input('page', 1)); $term = trim((string) $request->input('q')); $query = DB::connection('mysql_site_' . session('serverID'))->table('files_editor') ->where('site_id', '=', session('siteID')) ->select('id', 'source'); if($term !== ''){ $query->where('source', 'like', '%' . $term . '%'); } // one extra row tells the client there is another page, with no count query $rows = $query->orderBy('id', 'desc')->offset(($page - 1) * $perPage)->limit($perPage + 1)->get(); $hasMore = $rows->count() > $perPage; $items = $rows->take($perPage)->map(function ($row) use ($serverData) { return [ 'id' => $row->id, 'name' => basename($row->source), 'url' => 'https://' . ($serverData->name ?? '') . '/contents' . $row->source, ]; })->values(); return response()->json(['items' => $items, 'hasMore' => $hasMore], 200); } public function integrationItems(Request $request) { if(Auth::check() === false){ Auth::logout(); return redirect()->route('login'); } $sources = $this->integrationSources(); if(!isset($sources[$request->type])){ return response()->json(['items' => [], 'hasMore' => false], 200); } [$table, $column] = $sources[$request->type]; $perPage = 20; $page = max(1, (int) $request->input('page', 1)); $term = trim((string) $request->input('q')); $query = DB::connection('mysql_site_' . session('serverID'))->table($table) ->where('site_id', '=', session('siteID')) ->select('id', $column . ' as name'); if($term !== ''){ $query->where($column, 'like', '%' . $term . '%'); } // one extra row tells the client there is another page, with no count query $rows = $query->orderBy('id', 'desc')->offset(($page - 1) * $perPage)->limit($perPage + 1)->get(); $hasMore = $rows->count() > $perPage; return response()->json([ 'items' => $rows->take($perPage)->values(), 'hasMore' => $hasMore, ], 200); } public function uploadAsset(Request $request) { if (Auth::check() === false) { Auth::logout(); return redirect()->route('login'); } $siteData = SiteContext::current(); $serverData = DB::table('sites_servers')->where('id','=',session('serverID'))->get()->first(); // the folder is built remotely from this value, so it must not carry a path if(!preg_match('/^[a-z]+$/', (string) $request->type)){ return response()->json(['success' => false], 422); } /** * O arquivo nao era conferido aqui: o navegador dizia aceitar imagem e o * servidor repassava o que viesse. A lista e a mesma que os dropzones do * painel ja anunciam, e a resposta e json porque quem chama e o dropzone. */ $arquivo = $request->file('file'); if (! $arquivo || ! $arquivo->isValid()) { return response()->json(['success' => false, 'message' => __('site.site_file_missing')], 422); } $permitidas = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']; if (! in_array(strtolower((string) $arquivo->extension()), $permitidas, true)) { return response()->json([ 'success' => false, 'message' => __('site.site_file_type_error', ['list' => 'JPG, PNG, GIF, BMP, WEBP']), ], 422); } /* getimagesize recusa o arquivo que so tem nome de imagem */ if (@getimagesize($arquivo->path()) === false) { return response()->json([ 'success' => false, 'message' => __('site.site_file_type_error', ['list' => 'JPG, PNG, GIF, BMP, WEBP']), ], 422); } if ($arquivo->getSize() > self::UPLOAD_MAX * 1024) { return response()->json([ 'success' => false, 'message' => __('site.site_file_size_error', ['n' => self::UPLOAD_MAX / 1024]), ], 422); } /*$curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => 'https://'.$serverData->name.'/_apiBrasUpload/bras_beUpl.php', CURLOPT_RETURNTRANSFER => 1, CURLOPT_HEADER => true, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_VERBOSE => true, CURLOPT_TIMEOUT => 120, CURLOPT_POST => true, CURLOPT_POSTFIELDS => ['file'=>new \CURLFile($request->file('file')->path()),'image/jpeg', 'ext'=>$request->file('file')->extension(), 'site_id' => session('siteID'), 'md5' => md5($siteData->name), 'fld' =>$request->type], CURLOPT_HTTPHEADER => [ "Content-Type: multipart/form-data;" ], ]); $http_status = curl_getinfo($curl, CURLINFO_HTTP_CODE); $response = curl_exec($curl); if ($http_status != '200') { dd($http_status,$response); } curl_close($curl);*/ $post = [ 'file' => new \CURLFile($request->file('file')->path()), 'ext'=>$request->file('file')->extension(), 'site_id' => session('siteID'), 'md5' => md5($siteData->name), 'fld' =>$request->type ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://'.$serverData->name.'/_apiBrasUpload/bras_beUpl.php'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 45); curl_setopt($ch, CURLOPT_TIMEOUT, 45); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $post); $response = curl_exec($ch); $http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($response === false || $http_status != 200) { Log::warning('site upload failed', [ 'site_id' => session('siteID'), 'type' => $request->type, 'status' => $http_status, 'error' => curl_error($ch), ]); curl_close($ch); return response()->json(['success' => false], 502); } curl_close($ch); /*$client = new Client(); try { $response = $client->request("POST", 'https://'.$serverData->name.'/_apiBrasUpload/bras_beUpl.php', [ 'multipart' => [ [ 'name' => 'file', 'filename' => $request->file('file')->getClientOriginalName(), 'Mime-Type'=> $request->file('file')->getMimeType(), 'contents' => fopen($request->file('file')->path(), 'r'), ], [ 'name' => 'ext', 'contents' => $request->file('file')->extension() ], [ 'name' => 'site_id', 'contents' => session('siteID') ], [ 'name' => 'md5', 'contents' => md5($siteData->name) ], [ 'name' => 'fld', 'contents' => $request->type ], ] ]); } catch (\Exception $e) { dd($e); } $response = $response->getBody()->getContents();*/ if($request->type == 'background'){ DB::connection('mysql_site_' . session('serverID'))->table('sites')->where('id', '=', session('siteID'))->update(['bg_img' => $response]); $request->session()->flash('success', 'Imagem de fundo adicionada com sucesso!'); } if($request->type == 'media'){ DB::connection('mysql_site_' . session('serverID'))->table('files_editor')->insert([ 'site_id' => session('siteID'), 'source' => '/'.session('siteID').'/media/'.$response, 'type' => 'media', 'created_at' => NOW() ]); } echo $response; } public function uploadCanc(Request $request) { if (Auth::check() === false) { Auth::logout(); return redirect()->route('login'); } $serverData = DB::table('sites_servers')->where('id','=',session('serverID'))->get()->first(); $siteData = SiteContext::current(); /*$client = new Client(); try { $response = $client->request("POST", 'https://'.$serverData->name.'/_apiBrasUpload/bras_beRmv.php', [ 'multipart' => [ [ 'name' => 'chnk', 'contents' => 'E45DS87A6' ], [ 'name' => 'file', 'contents' => $request->fl ], [ 'name' => 'site_id', 'contents' => session('siteID') ], [ 'name' => 'fld', 'contents' => $request->type ], ] ]); } catch (\Exception $e) { echo $e->getResponse()->getStatusCode(); exit(); } */ // the folder is built remotely from this value, so it must not carry a path if(!preg_match('/^[a-z]+$/', (string) $request->type) || !$request->fl){ return response()->json(['success' => false], 422); } if($request->type == 'media'){ DB::connection('mysql_site_' . session('serverID'))->table('files_editor')->where('site_id','=',session('siteID')) ->where('source','=','/'.session('siteID').'/'.$request->type.'/'.$request->fl)->delete(); } SiteMedia::remove($serverData->name ?? null, $request->type, $request->fl); UserLog::write('site.file.delete', ['entity' => $request->type, 'description' => $request->fl]); return response()->json(['success' => true]); } }