diff options
| -rw-r--r-- | crates/shirabe/src/command/diagnose_command.rs | 382 |
1 files changed, 205 insertions, 177 deletions
diff --git a/crates/shirabe/src/command/diagnose_command.rs b/crates/shirabe/src/command/diagnose_command.rs index 9c1fb2f8..0390cefb 100644 --- a/crates/shirabe/src/command/diagnose_command.rs +++ b/crates/shirabe/src/command/diagnose_command.rs @@ -36,15 +36,32 @@ use indexmap::IndexMap; use shirabe_php_shim::PhpClass as _; use shirabe_php_shim::{ AnyThrowable, Catch as _, CmpOp, InvalidArgumentException, PHP_EOL, PhpMixed, RuntimeException, - disk_free_space, file_exists, filter_var_boolean, hash, impl_php_class, implode, is_array, - is_string, php_regex, preg_match, rtrim, str_replace, strpos, strstr, strstr3, strtolower, - trim, version_compare, + disk_free_space, file_exists, filter_var_boolean, hash, impl_php_class, implode, php_regex, + preg_match, rtrim, str_replace, strpos, strstr, strstr3, strtolower, trim, version_compare, }; use shirabe_symfony_console::command::Command; use shirabe_symfony_console::input::InputInterface; use shirabe_symfony_console::output::OutputInterface; use shirabe_symfony_process::ExecutableFinder; +/// What a `check_*` method hands to `output_result`, mirroring PHP's `true|string|string[]`. +#[derive(Debug)] +enum CheckResult { + Ok, + /// A falsey PHP result: counted as an error, with no message of its own. + Failed, + Message(String), + Messages(Vec<String>), +} + +/// What `get_github_rate_limit` returns: PHP yields either the `resources.core` entry of +/// GitHub's response, or a non-array value that goes straight to `output_result`. +#[derive(Debug)] +enum GithubRateLimit { + Unavailable(CheckResult), + Core { limit: i64, remaining: i64 }, +} + #[derive(Debug)] pub struct DiagnoseCommand { base_command_data: BaseCommandData, @@ -76,7 +93,7 @@ impl DiagnoseCommand { command } - fn check_composer_schema(&self) -> anyhow::Result<PhpMixed> { + fn check_composer_schema(&self) -> anyhow::Result<CheckResult> { let validator = ConfigValidator::new(self.get_io().clone()); let (errors, _, warnings) = validator.validate(&Factory::get_composer_file()?, 0, 0); @@ -92,13 +109,19 @@ impl DiagnoseCommand { } } - return Ok(PhpMixed::String(rtrim(&output, Some(" \t\n\r\0\u{0B}")))); + return Ok(CheckResult::Message(rtrim( + &output, + Some(" \t\n\r\0\u{0B}"), + ))); } - Ok(PhpMixed::Bool(true)) + Ok(CheckResult::Ok) } - fn check_composer_lock_schema(&self, locker: &dyn LockerInterface) -> anyhow::Result<PhpMixed> { + fn check_composer_lock_schema( + &self, + locker: &dyn LockerInterface, + ) -> anyhow::Result<CheckResult> { let json = locker.get_json_file(); match json.validate_schema(JsonFile::LOCK_SCHEMA, None) { @@ -110,13 +133,13 @@ impl DiagnoseCommand { output.push_str(&format!("<error>{}</error>{}", error, PHP_EOL)); } - return Ok(PhpMixed::String(trim(&output, Some(" \t\n\r\0\u{0B}")))); + return Ok(CheckResult::Message(trim(&output, Some(" \t\n\r\0\u{0B}")))); } return Err(e); } } - Ok(PhpMixed::Bool(true)) + Ok(CheckResult::Ok) } fn check_git(&self) -> anyhow::Result<String> { @@ -170,13 +193,13 @@ impl DiagnoseCommand { &self, proto: &str, config: &std::rc::Rc<std::cell::RefCell<Config>>, - ) -> anyhow::Result<PhpMixed> { + ) -> anyhow::Result<CheckResult> { let result = self.check_connectivity_and_composer_network_http_enablement(); - if result.as_bool() != Some(true) { + if !matches!(result, CheckResult::Ok) { return Ok(result); } - let mut result_list: Vec<PhpMixed> = vec![]; + let mut result_list: Vec<String> = vec![]; let mut tls_warning: Option<String> = None; if proto == "https" && config.borrow().get("disable-tls").as_bool() == Some(true) { tls_warning = Some("<warning>Shirabe is configured to disable SSL/TLS protection. This will leave remote HTTPS requests vulnerable to Man-In-The-Middle attacks.</warning>".to_string()); @@ -198,15 +221,17 @@ impl DiagnoseCommand { let hints = HttpDownloader::get_exception_hints(&e).unwrap_or_default(); if !hints.is_empty() { for hint in hints { - result_list.push(PhpMixed::String(hint)); + result_list.push(hint); } } - result_list.push(PhpMixed::String(format!( + result_list.push(format!( "<error>[{}] {}</error>", - std::any::type_name_of_val(te), + AnyThrowable::of(e.as_ref()) + .expect("the catch above proved the error carries a \\Throwable") + .php_class_name(), te.get_message() - ))); + )); } else { return Err(e); } @@ -214,27 +239,27 @@ impl DiagnoseCommand { } if let Some(w) = tls_warning { - result_list.push(PhpMixed::String(w)); + result_list.push(w); } if !result_list.is_empty() { - return Ok(PhpMixed::List(result_list)); + return Ok(CheckResult::Messages(result_list)); } - Ok(PhpMixed::Bool(true)) + Ok(CheckResult::Ok) } fn check_composer_repo( &self, url: &str, config: &std::rc::Rc<std::cell::RefCell<Config>>, - ) -> anyhow::Result<PhpMixed> { + ) -> anyhow::Result<CheckResult> { let result = self.check_connectivity_and_composer_network_http_enablement(); - if result.as_bool() != Some(true) { + if !matches!(result, CheckResult::Ok) { return Ok(result); } - let mut result_list: Vec<PhpMixed> = vec![]; + let mut result_list: Vec<String> = vec![]; let mut tls_warning: Option<String> = None; if url.starts_with("https://") && config.borrow().get("disable-tls").as_bool() == Some(true) { @@ -255,15 +280,17 @@ impl DiagnoseCommand { let hints = HttpDownloader::get_exception_hints(&e).unwrap_or_default(); if !hints.is_empty() { for hint in hints { - result_list.push(PhpMixed::String(hint)); + result_list.push(hint); } } - result_list.push(PhpMixed::String(format!( + result_list.push(format!( "<error>[{}] {}</error>", - std::any::type_name_of_val(te), + AnyThrowable::of(e.as_ref()) + .expect("the catch above proved the error carries a \\Throwable") + .php_class_name(), te.get_message() - ))); + )); } else { return Err(e); } @@ -271,26 +298,33 @@ impl DiagnoseCommand { } if let Some(w) = tls_warning { - result_list.push(PhpMixed::String(w)); + result_list.push(w); } if !result_list.is_empty() { - return Ok(PhpMixed::List(result_list)); + return Ok(CheckResult::Messages(result_list)); } - Ok(PhpMixed::Bool(true)) + Ok(CheckResult::Ok) } - fn check_http_proxy(&self, proxy: &RequestProxy, protocol: &str) -> anyhow::Result<PhpMixed> { + fn check_http_proxy( + &self, + proxy: &RequestProxy, + protocol: &str, + ) -> anyhow::Result<CheckResult> { let result = self.check_connectivity_and_composer_network_http_enablement(); - if result.as_bool() != Some(true) { + if !matches!(result, CheckResult::Ok) { return Ok(result); } + // TODO(error-model): PHP catches every \Exception below and returns it, so `output_result` + // renders it as `<error>[class] message</error>` and the caller goes on to the next + // protocol. The errors below propagate instead, cutting the proxy checks short. let proxy_status = proxy.get_status(None).unwrap_or_default(); if proxy.is_excluded_by_no_proxy() { - return Ok(PhpMixed::String(format!( + return Ok(CheckResult::Message(format!( "<info>SKIP</> <comment>Because repo.packagist.org is {}</>", proxy_status ))); @@ -340,22 +374,22 @@ impl DiagnoseCommand { let provider = response.get_body().unwrap_or_default().to_string(); if hash("sha256", &provider) != hash_val.as_string().unwrap_or("") { - return Ok(PhpMixed::String(format!( + return Ok(CheckResult::Message(format!( "<warning>It seems that your proxy ({}) is modifying {} traffic on the fly</>", proxy_status, protocol ))); } } - Ok(PhpMixed::String(format!( + Ok(CheckResult::Message(format!( "<info>OK</> <comment>{}</>", proxy_status ))) } - fn check_github_oauth(&self, domain: &str, token: &str) -> anyhow::Result<PhpMixed> { + fn check_github_oauth(&self, domain: &str, token: &str) -> anyhow::Result<CheckResult> { let result = self.check_connectivity_and_composer_network_http_enablement(); - if result.as_bool() != Some(true) { + if !matches!(result, CheckResult::Ok) { return Ok(result); } @@ -385,12 +419,12 @@ impl DiagnoseCommand { let expiration = response.get_header("github-authentication-token-expiration"); if expiration.is_none() { - return Ok(PhpMixed::String( + return Ok(CheckResult::Message( "<info>OK</> <comment>does not expire</>".to_string(), )); } - Ok(PhpMixed::String(format!( + Ok(CheckResult::Message(format!( "<info>OK</> <comment>expires on {}</>", expiration.unwrap() ))) @@ -399,12 +433,12 @@ impl DiagnoseCommand { if let Some(te) = e.catch::<TransportException>() && te.get_code() == 401 { - return Ok(PhpMixed::String(format!( + return Ok(CheckResult::Message(format!( "<comment>The oauth token for {} seems invalid, run \"composer config --global --unset github-oauth.{}\" to remove it</comment>", domain, domain ))); } - Ok(PhpMixed::String(format!( + Ok(CheckResult::Message(format!( "<error>[{}] {}</error>", AnyThrowable::of(e.as_ref()) .expect("PHP reaches this only with a caught \\Throwable") @@ -415,10 +449,14 @@ impl DiagnoseCommand { } } - fn get_github_rate_limit(&self, domain: &str, token: Option<&str>) -> anyhow::Result<PhpMixed> { + fn get_github_rate_limit( + &self, + domain: &str, + token: Option<&str>, + ) -> anyhow::Result<GithubRateLimit> { let result = self.check_connectivity_and_composer_network_http_enablement(); - if result.as_bool() != Some(true) { - return Ok(result); + if !matches!(result, CheckResult::Ok) { + return Ok(GithubRateLimit::Unavailable(result)); } if let Some(t) = token { @@ -450,13 +488,17 @@ impl DiagnoseCommand { .and_then(|a| a.get("resources")) .and_then(|v| v.as_array()) .and_then(|a| a.get("core")) - .cloned() - .unwrap_or(PhpMixed::Null)) + .and_then(|core| core.as_array()) + .map(|core| GithubRateLimit::Core { + limit: core.get("limit").and_then(|v| v.as_int()).unwrap_or(0), + remaining: core.get("remaining").and_then(|v| v.as_int()).unwrap_or(0), + }) + .unwrap_or(GithubRateLimit::Unavailable(CheckResult::Failed))) } - fn check_disk_space(&self, config: &Config) -> PhpMixed { + fn check_disk_space(&self, config: &Config) -> CheckResult { if !shirabe_php_rpc::get_diagnostics().function_exists("disk_free_space") { - return PhpMixed::Bool(true); + return CheckResult::Ok; } let min_space_free: f64 = (1024 * 1024) as f64; @@ -469,20 +511,26 @@ impl DiagnoseCommand { let mut dir = home_dir.clone(); let df_home = disk_free_space(&home_dir); if df_home.map(|d| d < min_space_free).unwrap_or(false) { - return PhpMixed::String(format!("<error>The disk hosting {} is full</error>", dir)); + return CheckResult::Message(format!( + "<error>The disk hosting {} is full</error>", + dir + )); } dir = vendor_dir.clone(); let df_vendor = disk_free_space(&vendor_dir); if df_vendor.map(|d| d < min_space_free).unwrap_or(false) { - return PhpMixed::String(format!("<error>The disk hosting {} is full</error>", dir)); + return CheckResult::Message(format!( + "<error>The disk hosting {} is full</error>", + dir + )); } - PhpMixed::Bool(true) + CheckResult::Ok } - fn check_pub_keys(&self, config: &Config) -> anyhow::Result<PhpMixed> { + fn check_pub_keys(&self, config: &Config) -> anyhow::Result<CheckResult> { let home = config.get("home").as_string().unwrap_or("").to_string(); - let mut errors: Vec<PhpMixed> = vec![]; + let mut errors: Vec<String> = vec![]; let io = self.get_io(); if file_exists(format!("{}/keys.tags.pub", home)) @@ -497,9 +545,7 @@ impl DiagnoseCommand { Keys::fingerprint(&format!("{}/keys.tags.pub", home))? )); } else { - errors.push(PhpMixed::String( - "<error>Missing pubkey for tags verification</error>".to_string(), - )); + errors.push("<error>Missing pubkey for tags verification</error>".to_string()); } if file_exists(format!("{}/keys.dev.pub", home)) { @@ -508,30 +554,28 @@ impl DiagnoseCommand { Keys::fingerprint(&format!("{}/keys.dev.pub", home))? )); } else { - errors.push(PhpMixed::String( - "<error>Missing pubkey for dev verification</error>".to_string(), - )); + errors.push("<error>Missing pubkey for dev verification</error>".to_string()); } if !errors.is_empty() { - errors.push(PhpMixed::String( + errors.push( "<error>Run composer self-update --update-keys to set them up</error>".to_string(), - )); + ); } Ok(if !errors.is_empty() { - PhpMixed::List(errors) + CheckResult::Messages(errors) } else { - PhpMixed::Bool(true) + CheckResult::Ok }) } fn check_version( &self, config: &std::rc::Rc<std::cell::RefCell<Config>>, - ) -> anyhow::Result<PhpMixed> { + ) -> anyhow::Result<CheckResult> { let result = self.check_connectivity_and_composer_network_http_enablement(); - if result.as_bool() != Some(true) { + if !matches!(result, CheckResult::Ok) { return Ok(result); } @@ -542,14 +586,14 @@ impl DiagnoseCommand { let latest = match versions_util.get_latest(None) { Ok(Ok(l)) => l, Ok(Err(e)) => { - return Ok(PhpMixed::String(format!( + return Ok(CheckResult::Message(format!( "<error>[{}] {}</error>", "UnexpectedValueException", e.get_message() ))); } Err(e) => { - return Ok(PhpMixed::String(format!( + return Ok(CheckResult::Message(format!( "<error>[{}] {}</error>", AnyThrowable::of(e.as_ref()) .expect("PHP reaches this only with a caught \\Throwable") @@ -565,7 +609,7 @@ impl DiagnoseCommand { .unwrap_or("") .to_string(); if composer::VERSION != latest_version && composer::VERSION != "@package_version@" { - return Ok(PhpMixed::String(format!( + return Ok(CheckResult::Message(format!( "<comment>You are not running the latest {} version, run `composer self-update` to update ({} => {})</comment>", versions_util.get_channel()?, composer::VERSION, @@ -573,15 +617,15 @@ impl DiagnoseCommand { ))); } - Ok(PhpMixed::Bool(true)) + Ok(CheckResult::Ok) } fn check_composer_audit( &self, config: &std::rc::Rc<std::cell::RefCell<Config>>, - ) -> anyhow::Result<PhpMixed> { + ) -> anyhow::Result<CheckResult> { let result = self.check_connectivity_and_composer_network_http_enablement(); - if result.as_bool() != Some(true) { + if !matches!(result, CheckResult::Ok) { return Ok(result); } @@ -606,7 +650,7 @@ impl DiagnoseCommand { })?; let installed_json = JsonFile::new(path.to_string(), None, None)?; if !installed_json.exists() { - return Ok(PhpMixed::String( + return Ok(CheckResult::Message( "<warning>Could not find Composer's installed.json, this must be a non-standard Composer installation.</>".to_string(), )); } @@ -657,7 +701,7 @@ impl DiagnoseCommand { ) { Ok(r) => r, Err(e) => { - return Ok(PhpMixed::String(format!( + return Ok(CheckResult::Message(format!( "<highlight>Failed performing audit: {}</>", e ))); @@ -665,7 +709,7 @@ impl DiagnoseCommand { }; if result > 0 { - return Ok(PhpMixed::String(format!( + return Ok(CheckResult::Message(format!( "<highlight>Audit found some issues:</>{}{}", PHP_EOL, io.borrow() @@ -676,7 +720,7 @@ impl DiagnoseCommand { ))); } - Ok(PhpMixed::Bool(true)) + Ok(CheckResult::Ok) } fn get_curl_version(&self) -> String { @@ -735,65 +779,47 @@ impl DiagnoseCommand { "<error>missing, using php streams fallback, which reduces performance</error>".to_string() } - fn output_result(&self, result: PhpMixed) { - let prev_exit_code = self.exit_code.get(); + // PHP: $result instanceof \Exception → already converted to string at call sites here + fn output_result(&self, result: CheckResult) { let io = self.get_io(); - if result.as_bool() == Some(true) { - io.write("<info>OK</info>"); + let messages = match result { + CheckResult::Ok => { + io.write("<info>OK</info>"); - return; - } + return; + } + CheckResult::Failed => vec![], + // PHP treats '' and '0' as falsey, so such a message never reaches the output loop. + CheckResult::Message(message) if message.is_empty() || message == "0" => vec![], + CheckResult::Message(message) => vec![message], + CheckResult::Messages(messages) => messages, + }; - let mut had_error = false; + // falsey results should be considered as an error, even if there is nothing to output + let mut had_error = messages.is_empty(); let mut had_warning = false; - let mut result = result; - // PHP: $result instanceof \Exception → already converted to string at call sites here - if !result.as_bool().unwrap_or(true) && result.as_string().is_none() && !is_array(&result) { - // falsey results should be considered as an error, even if there is nothing to output - had_error = true; - } else { - let result_list: Vec<PhpMixed> = match &result { - PhpMixed::List(l) => l.clone(), - other => vec![other.clone()], - }; - for message in &result_list { - let s = message.as_string().unwrap_or(""); - if strpos(s, "<error>").is_some() { - had_error = true; - } else if strpos(s, "<warning>").is_some() { - had_warning = true; - } + for message in &messages { + if strpos(message, "<error>").is_some() { + had_error = true; + } else if strpos(message, "<warning>").is_some() { + had_warning = true; } - // re-wrap so the final output loop works the same - result = PhpMixed::List(result_list); } if had_error { io.write("<error>FAIL</error>"); + self.exit_code.set(self.exit_code.get().max(2)); } else if had_warning { io.write("<warning>WARNING</warning>"); + self.exit_code.set(self.exit_code.get().max(1)); } - if !result.as_bool().unwrap_or(false) { - // PHP: if ($result) — falsey skips; this branch matches truthy - } - if let Some(list) = result.as_list() { - for message in list { - io.write(&trim( - message.as_string().unwrap_or(""), - Some(" \t\n\r\0\u{0B}"), - )); - } - } - // Apply exit code updates after io borrow ends - if had_error { - self.exit_code.set(prev_exit_code.max(2)); - } else if had_warning { - self.exit_code.set(prev_exit_code.max(1)); + for message in &messages { + io.write(&trim(message, Some(" \t\n\r\0\u{0B}"))); } } - fn check_platform(&self) -> anyhow::Result<PhpMixed> { + fn check_platform(&self) -> anyhow::Result<CheckResult> { let mut output = String::new(); let mut display_ini_message = false; @@ -802,31 +828,32 @@ impl DiagnoseCommand { let diagnostics = shirabe_php_rpc::get_diagnostics(); - let mut errors: IndexMap<String, PhpMixed> = IndexMap::new(); - let mut warnings: IndexMap<String, PhpMixed> = IndexMap::new(); + // PHP stores `true` for an issue with no detail, and a version string otherwise. + let mut errors: IndexMap<String, Option<String>> = IndexMap::new(); + let mut warnings: IndexMap<String, Option<String>> = IndexMap::new(); if !diagnostics.function_exists("json_decode") { - errors.insert("json".to_string(), PhpMixed::Bool(true)); + errors.insert("json".to_string(), None); } if !diagnostics.extension_loaded("Phar") { - errors.insert("phar".to_string(), PhpMixed::Bool(true)); + errors.insert("phar".to_string(), None); } if !diagnostics.extension_loaded("filter") { - errors.insert("filter".to_string(), PhpMixed::Bool(true)); + errors.insert("filter".to_string(), None); } if !diagnostics.extension_loaded("hash") { - errors.insert("hash".to_string(), PhpMixed::Bool(true)); + errors.insert("hash".to_string(), None); } if !diagnostics.extension_loaded("iconv") && !diagnostics.extension_loaded("mbstring") { - errors.insert("iconv_mbstring".to_string(), PhpMixed::Bool(true)); + errors.insert("iconv_mbstring".to_string(), None); } if !filter_var_boolean(diagnostics.ini_get("allow_url_fopen").unwrap_or("")) { - errors.insert("allow_url_fopen".to_string(), PhpMixed::Bool(true)); + errors.insert("allow_url_fopen".to_string(), None); } if diagnostics.extension_loaded("ionCube Loader") @@ -834,36 +861,33 @@ impl DiagnoseCommand { { errors.insert( "ioncube".to_string(), - PhpMixed::String(diagnostics.ioncube_loader_version.clone()), + Some(diagnostics.ioncube_loader_version.clone()), ); } if diagnostics.php_version_id < 70205 { - errors.insert( - "php".to_string(), - PhpMixed::String(diagnostics.php_version.clone()), - ); + errors.insert("php".to_string(), Some(diagnostics.php_version.clone())); } if !diagnostics.extension_loaded("openssl") { - errors.insert("openssl".to_string(), PhpMixed::Bool(true)); + errors.insert("openssl".to_string(), None); } if diagnostics.extension_loaded("openssl") && diagnostics.openssl_version_number < 0x1000100f { - warnings.insert("openssl_version".to_string(), PhpMixed::Bool(true)); + warnings.insert("openssl_version".to_string(), None); } if !diagnostics.has_hhvm_version && !diagnostics.extension_loaded("apcu") && filter_var_boolean(diagnostics.ini_get("apc.enable_cli").unwrap_or("")) { - warnings.insert("apc_cli".to_string(), PhpMixed::Bool(true)); + warnings.insert("apc_cli".to_string(), None); } if !diagnostics.extension_loaded("zlib") { - warnings.insert("zlib".to_string(), PhpMixed::Bool(true)); + warnings.insert("zlib".to_string(), None); } if let Some(phpinfo_match) = preg_match( @@ -874,18 +898,18 @@ impl DiagnoseCommand { let configure = configure.as_str(); if configure.contains("--enable-sigchild") { - warnings.insert("sigchild".to_string(), PhpMixed::Bool(true)); + warnings.insert("sigchild".to_string(), None); } if configure.contains("--with-curlwrappers") { - warnings.insert("curlwrappers".to_string(), PhpMixed::Bool(true)); + warnings.insert("curlwrappers".to_string(), None); } } if filter_var_boolean(diagnostics.ini_get("xdebug.profiler_enabled").unwrap_or("")) { - warnings.insert("xdebug_profile".to_string(), PhpMixed::Bool(true)); + warnings.insert("xdebug_profile".to_string(), None); } else if diagnostics.xdebug_active { - warnings.insert("xdebug_loaded".to_string(), PhpMixed::Bool(true)); + warnings.insert("xdebug_loaded".to_string(), None); } if diagnostics.has_php_windows_version_build @@ -895,7 +919,7 @@ impl DiagnoseCommand { { warnings.insert( "onedrive".to_string(), - PhpMixed::String(diagnostics.php_version.clone()), + Some(diagnostics.php_version.clone()), ); } @@ -903,7 +927,7 @@ impl DiagnoseCommand { && !(filter_var_boolean(diagnostics.ini_get("uopz.disable").unwrap_or("")) || filter_var_boolean(diagnostics.ini_get("uopz.exit").unwrap_or(""))) { - warnings.insert("uopz".to_string(), PhpMixed::Bool(true)); + warnings.insert("uopz".to_string(), None); } let out_fn = |msg: &str, style: &str, output: &mut String| { @@ -936,7 +960,9 @@ impl DiagnoseCommand { "php" => format!( "{}Your PHP ({}) is too old, you must upgrade to PHP 7.2.5 or higher.", PHP_EOL, - current.as_string().unwrap_or("") + current + .as_deref() + .expect("checkPlatform stores the PHP version with the php error") ), "allow_url_fopen" => { display_ini_message = true; @@ -950,7 +976,9 @@ impl DiagnoseCommand { format!( "{}Your ionCube Loader extension ({}) is incompatible with Phar files.{}Upgrade to ionCube 4.0.9 or higher or remove this line (path may be different) from your `php.ini` to disable it:{} zend_extension = /usr/lib/php5/20090626+lfs/ioncube_loader_lin_5.3.so", PHP_EOL, - current.as_string().unwrap_or(""), + current.as_deref().expect( + "checkPlatform stores the loader version with the ioncube error" + ), PHP_EOL, PHP_EOL ) @@ -1031,7 +1059,9 @@ impl DiagnoseCommand { "onedrive" => format!( "The Windows OneDrive folder is not supported on PHP versions below 7.2.23 and 7.3.10.{}Upgrade your PHP ({}) to use this location with Shirabe.{}", PHP_EOL, - current.as_string().unwrap_or(""), + current.as_deref().expect( + "checkPlatform stores the PHP version with the onedrive warning" + ), PHP_EOL ), "uopz" => format!( @@ -1056,7 +1086,7 @@ impl DiagnoseCommand { let composer_ipresolve = Platform::get_env("COMPOSER_IPRESOLVE").unwrap_or_default(); if ["4".to_string(), "6".to_string()].contains(&composer_ipresolve) { - warnings.insert("ipresolve".to_string(), PhpMixed::Bool(true)); + warnings.insert("ipresolve".to_string(), None); out_fn( &format!( "The COMPOSER_IPRESOLVE env var is set to {} which may result in network failures below.", @@ -1068,52 +1098,52 @@ impl DiagnoseCommand { } Ok(if warnings.is_empty() && errors.is_empty() { - PhpMixed::Bool(true) + CheckResult::Ok } else { - PhpMixed::String(output) + CheckResult::Message(output) }) } /// Check if allow_url_fopen is ON - fn check_connectivity(&self) -> PhpMixed { + fn check_connectivity(&self) -> CheckResult { // PHP: if (!ini_get('allow_url_fopen')) — a missing setting, "" and "0" are all falsey. let allow_url_fopen = shirabe_php_rpc::get_diagnostics().ini_get("allow_url_fopen"); if !allow_url_fopen.is_some_and(|value| !value.is_empty() && value != "0") { - return PhpMixed::String( + return CheckResult::Message( "<info>SKIP</> <comment>Because allow_url_fopen is missing.</>".to_string(), ); } - PhpMixed::Bool(true) + CheckResult::Ok } - fn check_connectivity_and_composer_network_http_enablement(&self) -> PhpMixed { + fn check_connectivity_and_composer_network_http_enablement(&self) -> CheckResult { let result = self.check_connectivity(); - if result.as_bool() != Some(true) { + if !matches!(result, CheckResult::Ok) { return result; } let result = self.check_composer_network_http_enablement(); - if result.as_bool() != Some(true) { + if !matches!(result, CheckResult::Ok) { return result; } - PhpMixed::Bool(true) + CheckResult::Ok } /// Check if Composer network is enabled for HTTP/S - fn check_composer_network_http_enablement(&self) -> PhpMixed { + fn check_composer_network_http_enablement(&self) -> CheckResult { if Platform::get_env("COMPOSER_DISABLE_NETWORK") .map(|v| !v.is_empty() && v != "0") .unwrap_or(false) { - return PhpMixed::String( + return CheckResult::Message( "<info>SKIP</> <comment>Network is disabled by COMPOSER_DISABLE_NETWORK.</>" .to_string(), ); } - PhpMixed::Bool(true) + CheckResult::Ok } } @@ -1189,6 +1219,9 @@ impl Command for DiagnoseCommand { Factory::create_http_downloader(io.clone(), &config, indexmap::IndexMap::new())?, ))); + // TODO(distribution): PHP tests `__FILE__`, which starts with `phar:` when Composer runs + // from its phar. Shirabe ships as a native binary, so this never matches and the pubkey + // and version checks below never run. if strpos(file!(), "phar:") == Some(0) { io.write_no_newline("Checking pubkeys: "); let r = self.check_pub_keys(&config.borrow())?; @@ -1331,7 +1364,7 @@ impl Command for DiagnoseCommand { io.write_no_newline("Checking git settings: "); let r = self.check_git()?; - self.output_result(PhpMixed::String(r)); + self.output_result(CheckResult::Message(r)); io.write_no_newline("Checking http connectivity to packagist: "); let r = self.check_http("http", &config)?; @@ -1410,10 +1443,10 @@ impl Command for DiagnoseCommand { if let Some(_te) = e.catch::<TransportException>() { io.write_no_newline("Checking HTTP proxy: "); let status = self.check_connectivity_and_composer_network_http_enablement(); - self.output_result(if is_string(&status) { + self.output_result(if matches!(status, CheckResult::Message(_)) { status } else { - PhpMixed::String(format!( + CheckResult::Message(format!( "<error>[{}] {}</error>", AnyThrowable::of(e.as_ref()) .expect("PHP reaches this only with a caught \\Throwable") @@ -1441,29 +1474,24 @@ impl Command for DiagnoseCommand { } else { io.write_no_newline("Checking github.com rate limit: "); match self.get_github_rate_limit("github.com", None) { - Ok(rate) => { - if !is_array(&rate) { - self.output_result(rate); - } else if let Some(arr) = rate.as_array() { - let remaining = arr.get("remaining").and_then(|v| v.as_int()).unwrap_or(0); - let limit = arr.get("limit").and_then(|v| v.as_int()).unwrap_or(0); - if 10 > remaining { - io.write("<warning>WARNING</warning>"); - io.write(&format!( - "<comment>GitHub has a rate limit on their API. You currently have <options=bold>{}</options=bold> out of <options=bold>{}</options=bold> requests left.\nSee https://developer.github.com/v3/#rate-limiting and also\n https://getcomposer.org/doc/articles/troubleshooting.md#api-rate-limit-and-oauth-tokens</comment>", - remaining, limit, - )); - } else { - self.output_result(PhpMixed::Bool(true)); - } + Ok(GithubRateLimit::Unavailable(rate)) => self.output_result(rate), + Ok(GithubRateLimit::Core { limit, remaining }) => { + if 10 > remaining { + io.write("<warning>WARNING</warning>"); + io.write(&format!( + "<comment>GitHub has a rate limit on their API. You currently have <options=bold>{}</options=bold> out of <options=bold>{}</options=bold> requests left.\nSee https://developer.github.com/v3/#rate-limiting and also\n https://getcomposer.org/doc/articles/troubleshooting.md#api-rate-limit-and-oauth-tokens</comment>", + remaining, limit, + )); + } else { + self.output_result(CheckResult::Ok); } } Err(e) => { if let Some(te) = e.catch::<TransportException>() { if te.get_code() == 401 { - self.output_result(PhpMixed::String("<comment>The oauth token for github.com seems invalid, run \"composer config --global --unset github-oauth.github.com\" to remove it</comment>".to_string())); + self.output_result(CheckResult::Message("<comment>The oauth token for github.com seems invalid, run \"composer config --global --unset github-oauth.github.com\" to remove it</comment>".to_string())); } else { - self.output_result(PhpMixed::String(format!( + self.output_result(CheckResult::Message(format!( "<error>[{}] {}</error>", AnyThrowable::of(e.as_ref()) .expect("PHP reaches this only with a caught \\Throwable") @@ -1472,7 +1500,7 @@ impl Command for DiagnoseCommand { ))); } } else { - self.output_result(PhpMixed::String(format!( + self.output_result(CheckResult::Message(format!( "<error>[{}] {}</error>", AnyThrowable::of(e.as_ref()) .expect("PHP reaches this only with a caught \\Throwable") |
