freescout/freescout-dist/app/Http/Controllers/SystemController.php

417 lines
15 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Option;
use App\User;
use App\FailedJob;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Symfony\Component\Console\Output\BufferedOutput;
class SystemController extends Controller
{
public static $latest_version_error = '';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth', ['except' => [
'cron'
]]);
}
/**
* System status.
*/
public function status(Request $request)
{
// PHP extensions.
$php_extensions = \Helper::checkRequiredExtensions();
// Functions.
$functions = \Helper::checkRequiredFunctions();
// Permissions.
$permissions = [];
foreach (config('installer.permissions') as $perm_path => $perm_value) {
$path = base_path($perm_path);
$value = '';
if (file_exists($path)) {
$value = substr(sprintf('%o', fileperms($path)), -4);
}
$permissions[$perm_path] = [
'status' => \Helper::isFolderWritable($path),
'value' => $value,
];
}
// Check if cache files are writable.
$non_writable_cache_file = '';
if (function_exists('shell_exec')) {
$non_writable_cache_file = shell_exec('find '.base_path('storage/framework/cache/data/').' -type f | xargs -I {} sh -c \'[ ! -w "{}" ] && echo {}\' 2>&1 | head -n 1');
$non_writable_cache_file = trim($non_writable_cache_file ?? '');
// Leave only one line (in case head -n 1 does not work)
$non_writable_cache_file = preg_replace("#[\r\n].+#m", '', $non_writable_cache_file);
if (!strstr($non_writable_cache_file, base_path('storage/framework/cache/data/'))) {
$non_writable_cache_file = '';
}
}
// Check if public symlink exists, if not, try to create.
$public_symlink_exists = true;
$public_path = public_path('storage');
$public_test = $public_path.DIRECTORY_SEPARATOR.'.gitignore';
if (!file_exists($public_test) || !file_get_contents($public_test)) {
\File::delete($public_path);
\Artisan::call('storage:link');
if (!file_exists($public_test) || !file_get_contents($public_test)) {
$public_symlink_exists = false;
}
}
// Check if .env is writable.
$env_is_writable = is_writable(base_path('.env'));
// Jobs
$queued_jobs = \App\Job::orderBy('created_at', 'desc')->get();
$failed_jobs = \App\FailedJob::orderBy('failed_at', 'desc')->get();
$failed_queues = $failed_jobs->pluck('queue')->unique();
// Commands
$commands_list = [
'freescout:fetch-emails' => 'freescout:fetch-emails',
\Helper::getWorkerIdentifier() => 'queue:work'
];
foreach ($commands_list as $command_identifier => $command_name) {
$status_texts = [];
// Check if command is running now
if (function_exists('shell_exec')) {
$running_commands = 0;
try {
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep '{$command_identifier}'"));
$pids = [];
foreach ($processes as $process) {
$process = trim($process);
preg_match("/^[\S]+\s+([\d]+)\s+/", $process, $m);
if (empty($m)) {
// Another format (used in Docker image).
// 1713 nginx 0:00 /usr/bin/php82...
preg_match("/^([\d]+)\s+[\S]+\s+/", $process, $m);
}
if (!preg_match("/(sh \-c|grep )/", $process) && !empty($m[1])) {
$running_commands++;
$pids[] = $m[1];
}
}
} catch (\Exception $e) {
// Do nothing
}
if ($running_commands == 1) {
$commands[] = [
'name' => $command_name,
'status' => 'success',
'status_text' => __('Running'),
];
continue;
} elseif ($running_commands > 1) {
// queue:work command is stopped by settings a cache key
if ($command_name == 'queue:work') {
\Helper::queueWorkerRestart();
$commands[] = [
'name' => $command_name,
'status' => 'error',
'status_text' => __(':number commands were running at the same time. Commands have been restarted', ['number' => $running_commands]),
];
} else {
unset($pids[0]);
$commands[] = [
'name' => $command_name,
'status' => 'error',
'status_text' => __(':number commands are running at the same time. Please stop extra commands by executing the following console command:', ['number' => $running_commands]).' kill '.implode(' | kill ', $pids),
];
}
continue;
}
}
// Check last run
$option_name = str_replace('freescout_', '', preg_replace('/[^a-zA-Z0-9]/', '_', $command_name));
$date_text = '?';
$last_run = Option::get($option_name.'_last_run');
if ($last_run) {
$date = Carbon::createFromTimestamp($last_run);
$date_text = User::dateFormat($date);
}
$status_texts[] = __('Last run:').' '.$date_text;
$date_text = '?';
$last_successful_run = Option::get($option_name.'_last_successful_run');
if ($last_successful_run) {
$date_ = Carbon::createFromTimestamp($last_successful_run);
$date_text = User::dateFormat($date);
}
$status_texts[] = __('Last successful run:').' '.$date_text;
$status = 'error';
if ($last_successful_run && $last_run && (int) $last_successful_run >= (int) $last_run) {
unset($status_texts[0]);
$status = 'success';
}
// If queue:work is not running, clear cache to let it start if something is wrong with the mutex
if ($command_name == 'queue:work' && !$last_successful_run) {
$status_texts[] = __('Try to :%a_start%clear cache:%a_end% to force command to start.', ['%a_start%' => '<a href="'.route('system.tools').'" target="_blank">', '%a_end%' => '</a>']);
// This sometimes makes Status page open as non logged in user.
//\Artisan::call('freescout:clear-cache', ['--doNotGenerateVars' => true]);
}
$commands[] = [
'name' => $command_name,
'status' => $status,
'status_text' => implode(' ', $status_texts),
];
}
// Check new version if enabled
$new_version_available = false;
if (!\Config::get('app.disable_updating')) {
$latest_version = \Cache::remember('latest_version', 15, function () {
try {
return \Updater::getVersionAvailable();
} catch (\Exception $e) {
SystemController::$latest_version_error = $e->getMessage();
return '';
}
});
if ($latest_version && version_compare($latest_version, \Config::get('app.version'), '>')) {
$new_version_available = true;
}
} else {
$latest_version = \Config::get('app.version');
}
// Detect missing migrations.
$migrations_output = \Helper::runCommand('migrate:status');
preg_match_all("#\| N \| ([^\|]+)\|#", $migrations_output, $migrations_m);
$missing_migrations = $migrations_m[1] ?? [];
return view('system/status', [
'commands' => $commands,
'queued_jobs' => $queued_jobs,
'failed_jobs' => $failed_jobs,
'failed_queues' => $failed_queues,
'php_extensions' => $php_extensions,
'functions' => $functions,
'permissions' => $permissions,
'new_version_available' => $new_version_available,
'latest_version' => $latest_version,
'latest_version_error' => SystemController::$latest_version_error,
'public_symlink_exists' => $public_symlink_exists,
'env_is_writable' => $env_is_writable,
'non_writable_cache_file' => $non_writable_cache_file,
'missing_migrations' => $missing_migrations,
'invalid_symlinks' => \App\Module::checkSymlinks(),
]);
}
public function action(Request $request)
{
switch ($request->action) {
case 'cancel_job':
\App\Job::where('id', $request->job_id)->delete();
\Session::flash('flash_success_floating', __('Done'));
break;
case 'retry_job':
\App\Job::where('id', $request->job_id)->update(['available_at' => time()]);
sleep(1);
\Session::flash('flash_success_floating', __('Done'));
break;
case 'delete_failed_jobs':
\App\FailedJob::where('queue', $request->failed_queue)->delete();
\Session::flash('flash_success_floating', __('Failed jobs deleted'));
break;
case 'retry_failed_jobs':
$jobs = \App\FailedJob::where('queue', $request->failed_queue)->get();
foreach ($jobs as $job) {
\Artisan::call('queue:retry', ['id' => $job->id]);
}
\Session::flash('flash_success_floating', __('Failed jobs restarted'));
break;
}
return redirect()->route('system');
}
/**
* System tools.
*/
public function tools(Request $request)
{
$output = \Cache::get('tools_execute_output');
if ($output) {
\Cache::forget('tools_execute_output');
}
return view('system/tools', [
'output' => $output,
]);
}
/**
* Execute tools action.
*
* @param Request $request [description]
*
* @return [type] [description]
*/
public function toolsExecute(Request $request)
{
$outputLog = new BufferedOutput();
switch ($request->action) {
case 'clear_cache':
\Artisan::call('freescout:clear-cache', [], $outputLog);
break;
case 'fetch_emails':
$params = [];
$params['--days'] = (int)$request->days;
$params['--unseen'] = (int)$request->unseen;
\Artisan::call('freescout:fetch-emails', $params, $outputLog);
break;
case 'migrate_db':
\Artisan::call('migrate', ['--force' => true], $outputLog);
break;
case 'logout_users':
\Artisan::call('freescout:logout-users', [], $outputLog);
break;
}
$output = $outputLog->fetch();
unset($outputLog);
if ($output) {
// \Session::flash does not work after BufferedOutput
\Cache::forever('tools_execute_output', $output);
}
return redirect()->route('system.tools')->withInput($request->input());
}
/**
* Ajax.
*/
public function ajax(Request $request)
{
$response = [
'status' => 'error',
'msg' => '', // this is error message
];
switch ($request->action) {
case 'update':
try {
$status = \Updater::update();
// Artisan::output()
} catch (\Exception $e) {
$response['msg'] = __('Error occurred. Please try again or try another :%a_start%update method:%a_end%', ['%a_start%' => '<a href="'.config('app.freescout_url').'/docs/update/" target="_blank">', '%a_end%' => '</a>']);
$response['msg'] .= '<br/><br/>'.$e->getMessage();
\Helper::logException($e);
}
if (!$response['msg'] && $status) {
// Adding session flash is useless as cache is cleared
$response['msg_success'] = __('Application successfully updated');
$response['status'] = 'success';
}
break;
case 'check_updates':
if (!\Config::get('app.disable_updating')) {
try {
$response['new_version_available'] = \Updater::isNewVersionAvailable(config('app.version'));
$response['status'] = 'success';
} catch (\Exception $e) {
$response['msg'] = __('Error occurred').': '.$e->getMessage();
}
if (!$response['msg'] && !$response['new_version_available']) {
// Adding session flash is useless as cache is cleated
$response['msg_success'] = __('You have the latest version installed');
}
} else {
$response['msg_success'] = __('You have the latest version installed');
}
break;
default:
$response['msg'] = 'Unknown action';
break;
}
if ($response['status'] == 'error' && empty($response['msg'])) {
$response['msg'] = 'Unknown error occurred';
}
return \Response::json($response);
}
/**
* Web Cron.
*/
public function cron(Request $request)
{
if (empty($request->hash) || $request->hash != \Helper::getWebCronHash()) {
abort(404);
}
$outputLog = new BufferedOutput();
\Artisan::call('schedule:run', [], $outputLog);
$output = $outputLog->fetch();
return response($output, 200)->header('Content-Type', 'text/plain');
}
/**
* Ajax HTML.
*/
public function ajaxHtml(Request $request)
{
switch ($request->action) {
case 'job_details':
$job = \App\FailedJob::find($request->param);
if (!$job) {
abort(404);
}
$html = '';
$payload = json_decode($job->payload, true);
if (!empty($payload['data']['command'])) {
$html .= '<pre>'.print_r(unserialize($payload['data']['command']), 1).'</pre>';
}
$html .= '<pre>'.$job->exception.'</pre>';
return response($html);
}
abort(404);
}
}