From f749a47804cd296a3059cd3f8079c62dbaa5fdc0 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 7 Aug 2026 07:26:48 +0900 Subject: refactor: merge split inherent impl blocks into one per type Enable clippy::multiple_inherent_impl and fix the 21 sites it reports. Types whose inherent methods were spread across two or three impl blocks now keep them in a single block; only the impl headers move, no method bodies change. Co-Authored-By: Claude Opus 5 (1M context) --- crates/shirabe/src/command/diagnose_command.rs | 1864 ++++++++++++------------ 1 file changed, 931 insertions(+), 933 deletions(-) (limited to 'crates/shirabe/src/command/diagnose_command.rs') diff --git a/crates/shirabe/src/command/diagnose_command.rs b/crates/shirabe/src/command/diagnose_command.rs index 1674e10e..8483eb56 100644 --- a/crates/shirabe/src/command/diagnose_command.rs +++ b/crates/shirabe/src/command/diagnose_command.rs @@ -77,604 +77,215 @@ impl DiagnoseCommand { .expect("DiagnoseCommand::configure uses static, valid metadata"); command } -} - -impl Command for DiagnoseCommand { - fn configure(&self) -> anyhow::Result<()> { - self.set_name("diagnose")?; - self.set_description("Diagnoses the system to identify common errors"); - self.set_help( - "The diagnose command checks common errors to help debugging problems.\n\n\ - The process exit code will be 1 in case of warnings and 2 for errors.\n\n\ - Read more at https://getcomposer.org/doc/03-cli.md#diagnose", - ); - Ok(()) - } - fn execute( - &self, - input: std::rc::Rc>, - output: std::rc::Rc>, - ) -> anyhow::Result { - let mut composer = self.try_composer(None, None); - let io: std::rc::Rc> = self.get_io().clone(); + fn check_composer_schema(&self) -> anyhow::Result { + let validator = ConfigValidator::new(self.get_io().clone()); + let (errors, _, warnings) = validator.validate(&Factory::get_composer_file()?, 0, 0); - let config: std::rc::Rc>; - if let Some(ref mut c) = composer { - let c = crate::composer::composer_full(c); - config = c.get_config(); + if !errors.is_empty() || !warnings.is_empty() { + let mut messages: IndexMap> = IndexMap::new(); + messages.insert("error".to_string(), errors); + messages.insert("warning".to_string(), warnings); - let command_event = CommandEvent::new6( - PluginEvents::COMMAND, - "diagnose", - input, - output, - vec![], - IndexMap::new(), - ); - c.get_event_dispatcher() - .borrow_mut() - .dispatch(Some(command_event.get_name()), None); - *self.process.borrow_mut() = Some( - c.get_loop() - .borrow() - .get_process_executor() - .map(std::rc::Rc::clone) - .unwrap_or_else(|| { - std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(Some( - io.clone(), - )))) - }), - ); - } else { - config = std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config(None, None)?)); + let mut output = String::new(); + for (style, msgs) in &messages { + for msg in msgs { + output.push_str(&format!("<{}>{}{}", style, msg, style, PHP_EOL)); + } + } - *self.process.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new( - ProcessExecutor::new(Some(io.clone())), - ))); + return Ok(PhpMixed::String(rtrim(&output, Some(" \t\n\r\0\u{0B}")))); } - let mut config_inner = IndexMap::new(); - config_inner.insert("secure-http".to_string(), PhpMixed::Bool(false)); - let mut secure_http_wrap: IndexMap = IndexMap::new(); - secure_http_wrap.insert("config".to_string(), PhpMixed::Array(config_inner)); - let config = config; - config - .borrow_mut() - .merge(&secure_http_wrap, Config::SOURCE_COMMAND); - let _ = config.borrow_mut().prohibit_url_by_config( - "http://repo.packagist.org", - Some(std::rc::Rc::new(std::cell::RefCell::new(NullIO::new()))), - &IndexMap::new(), - ); - *self.http_downloader.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new( - Factory::create_http_downloader(io.clone(), &config, indexmap::IndexMap::new())?, - ))); + Ok(PhpMixed::Bool(true)) + } - if strpos(file!(), "phar:") == Some(0) { - io.write_no_newline("Checking pubkeys: "); - let r = self.check_pub_keys(&config.borrow())?; - self.output_result(r); + fn check_composer_lock_schema(&self, locker: &dyn LockerInterface) -> anyhow::Result { + let json = locker.get_json_file(); - io.write_no_newline("Checking Composer version: "); - let r = self.check_version(&config)?; - self.output_result(r); + match json.validate_schema(JsonFile::LOCK_SCHEMA, None) { + Ok(_) => {} + Err(e) => { + if let Some(jve) = e.downcast_ref::() { + let mut output = String::new(); + for error in jve.get_errors() { + output.push_str(&format!("{}{}", error, PHP_EOL)); + } + + return Ok(PhpMixed::String(trim(&output, Some(" \t\n\r\0\u{0B}")))); + } + return Err(e); + } } - io.write(&format!( - "Composer version: {}", - composer::get_version() - )); + Ok(PhpMixed::Bool(true)) + } - io.write_no_newline("Checking Composer and its dependencies for vulnerabilities: "); - let r = self.check_composer_audit(&config)?; - self.output_result(r); + fn check_git(&self) -> String { + if !shirabe_php_rpc::get_diagnostics().function_exists("proc_open") { + return "proc_open is not available, git cannot be used".to_string(); + } - let platform_overrides = config + let mut output = String::new(); + let _ = self + .process + .borrow() + .as_ref() + .unwrap() .borrow_mut() - .get("platform") - .as_array() - .cloned() - .unwrap_or_default(); - let platform_overrides_unboxed: indexmap::IndexMap = - platform_overrides.into_iter().collect(); - let mut platform_repo = - PlatformRepository::new(vec![], platform_overrides_unboxed).unwrap(); - let php_pkg = ::find_package( - &mut platform_repo, - "php", - crate::repository::FindPackageConstraint::String("*".to_string()), - )? - .unwrap(); - let mut php_version = php_pkg.get_pretty_version(); - if let Some(cp) = php_pkg.as_complete() - && str_contains(&cp.get_description().unwrap_or_default(), "overridden") - { - php_version = format!( - "{} - {}", - php_version, - cp.get_description().unwrap_or_default() + .execute( + vec![ + "git".to_string(), + "config".to_string(), + "color.ui".to_string(), + ], + &mut output, + None, ); + if strtolower(&trim(&output, Some(" \t\n\r\0\u{0B}"))) == "always" { + return "Your git color.ui setting is set to always, this is known to create issues. Use \"git config --global color.ui true\" to set it correctly.".to_string(); } - io.write(&format!("PHP version: {}", php_version)); + let process = self.process.borrow(); + let git_version = Git::get_version(process.as_ref().unwrap()); + let git_version = match git_version { + Some(v) => v, + None => return "No git process found".to_string(), + }; - let diagnostics = shirabe_php_rpc::get_diagnostics(); + if version_compare("2.24.0", &git_version, ">") { + return format!( + "Your git version ({}) is too old and possibly will cause issues. Please upgrade to git 2.24 or above", + git_version + ); + } - if let Some(php_binary) = &diagnostics.php_binary { - io.write(&format!( - "PHP binary path: {}", - php_binary - )); + format!("OK git version {}", git_version) + } + + fn check_http( + &self, + proto: &str, + config: &std::rc::Rc>, + ) -> anyhow::Result { + let result = self.check_connectivity_and_composer_network_http_enablement(); + if result.as_bool() != Some(true) { + return Ok(result); } - io.write(&format!( - "OpenSSL version: {}", - match &diagnostics.openssl_version_text { - Some(text) => format!("{}", text), - None => "missing".to_string(), + let mut result_list: Vec = vec![]; + let mut tls_warning: Option = None; + if proto == "https" && config.borrow().get("disable-tls").as_bool() == Some(true) { + tls_warning = Some("Composer is configured to disable SSL/TLS protection. This will leave remote HTTPS requests vulnerable to Man-In-The-Middle attacks.".to_string()); + } + + match self + .http_downloader + .borrow() + .as_ref() + .unwrap() + .borrow_mut() + .get( + &format!("{}://repo.packagist.org/packages.json", proto), + IndexMap::new(), + ) { + Ok(_) => {} + Err(e) => { + if let Some(te) = e.downcast_ref::() { + 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(PhpMixed::String(format!( + "[{}] {}", + std::any::type_name_of_val(te), + te.message + ))); + } else { + return Err(e); + } } - )); - io.write(&format!("curl version: {}", self.get_curl_version())); + } - let finder = ExecutableFinder::new(); - let has_system_unzip = finder.find("unzip", None, &[]).is_some(); - let mut bin_7zip = String::new(); - let has_system_7zip = if finder - .find("7z", None, &["C:\\Program Files\\7-Zip".to_string()]) - .is_some() - { - bin_7zip = "7z".to_string(); - true - } else if !Platform::is_windows() && finder.find("7zz", None, &[]).is_some() { - bin_7zip = "7zz".to_string(); - true - } else if !Platform::is_windows() && finder.find("7za", None, &[]).is_some() { - bin_7zip = "7za".to_string(); - true - } else { - false - }; + if let Some(w) = tls_warning { + result_list.push(PhpMixed::String(w)); + } - io.write(&format!( - "zip: {}, {}, {}{}", - if diagnostics.extension_loaded("zip") { - "extension present" - } else { - "extension not loaded" - }, - if has_system_unzip { - "unzip present".to_string() - } else { - "unzip not available".to_string() - }, - if has_system_7zip { - format!("7-Zip present ({})", bin_7zip) - } else { - "7-Zip not available".to_string() - }, - if (has_system_7zip || has_system_unzip) && !diagnostics.function_exists("proc_open") { - ", proc_open is disabled or not present, unzip/7-z will not be usable" - } else { - "" - } - )); - - if let Some(ref mut c) = composer { - let c = crate::composer::composer_full(c); - io.write(&format!( - "Active plugins: {}", - implode( - ", ", - &c.get_plugin_manager().borrow().get_registered_plugins() - ) - )); - - io.write_no_newline("Checking composer.json: "); - let r = self.check_composer_schema()?; - self.output_result(r); - - if c.get_locker().borrow_mut().is_locked() { - io.write_no_newline("Checking composer.lock: "); - let locker = c.get_locker().clone(); - let locker = locker.borrow(); - let r = self.check_composer_lock_schema(&*locker)?; - self.output_result(r); - } + if !result_list.is_empty() { + return Ok(PhpMixed::List(result_list)); } - io.write_no_newline("Checking platform settings: "); - let r = self.check_platform()?; - self.output_result(r); - - io.write_no_newline("Checking git settings: "); - let r = self.check_git(); - self.output_result(PhpMixed::String(r)); - - io.write_no_newline("Checking http connectivity to packagist: "); - let r = self.check_http("http", &config)?; - self.output_result(r); - - io.write_no_newline("Checking https connectivity to packagist: "); - let r = self.check_http("https", &config)?; - self.output_result(r); + Ok(PhpMixed::Bool(true)) + } - let repositories = config.borrow().get_repositories(); - for repo in repositories { - let repo_arr = repo.1.as_array().cloned().unwrap_or_default(); - if repo_arr.get("type").and_then(|v| v.as_string()) == Some("composer") - && repo_arr.get("url").is_some() - { - let repo_arr_unboxed: indexmap::IndexMap = repo_arr - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - let composer_repo = ComposerRepository::new( - repo_arr_unboxed, - self.get_io().clone(), - &config.borrow(), - self.http_downloader.borrow().clone().unwrap(), - None, - ) - .unwrap(); - // PHP: ReflectionMethod($composerRepo, 'getPackagesJsonUrl') - // We surface the same internal call by directly invoking the equivalent method. - // TODO(plugin): support reflection-based access if plugin code requires it. - let url = composer_repo.get_packages_json_url(); - if !str_starts_with(&url, "http") { - continue; - } - if str_starts_with(&url, "https://repo.packagist.org") { - continue; - } - io.write_no_newline(&format!( - "Checking connectivity to {}: ", - repo_arr - .get("url") - .and_then(|v| v.as_string()) - .unwrap_or("") - )); - let r = self.check_composer_repo(&url, &config)?; - self.output_result(r); - } + fn check_composer_repo( + &self, + url: &str, + config: &std::rc::Rc>, + ) -> anyhow::Result { + let result = self.check_connectivity_and_composer_network_http_enablement(); + if result.as_bool() != Some(true) { + return Ok(result); } - let protos: Vec<&str> = if config.borrow_mut().get("disable-tls").as_bool() == Some(true) { - vec!["http"] - } else { - vec!["http", "https"] - }; - let proxy_check_result: anyhow::Result<(), anyhow::Error> = (|| -> anyhow::Result<()> { - for proto in &protos { - // Compute the proxy under a short-lived lock: `check_http_proxy` below transitively - // re-enters `ProxyManager::get_instance()` (via HttpDownloader -> CurlDownloader / - // RemoteFilesystem), and `std::sync::Mutex` is not reentrant, so the guard must not - // still be held when that call happens. - let proxy = ProxyManager::get_instance() - .as_ref() - .unwrap() - .get_proxy_for_request(&format!("{}://repo.packagist.org", proto)) - .map_err(|e| anyhow::anyhow!(e))?; - if !proxy.get_status(None)?.is_empty() { - let r#type = if proxy.is_secure() { "HTTPS" } else { "HTTP" }; - io.write_no_newline(&format!("Checking {} proxy with {}: ", r#type, proto)); - let r = self.check_http_proxy(&proxy, proto)?; - self.output_result(r); - } - } - Ok(()) - })(); - if let Err(e) = proxy_check_result { - if let Some(_te) = e.downcast_ref::() { - io.write_no_newline("Checking HTTP proxy: "); - let status = self.check_connectivity_and_composer_network_http_enablement(); - self.output_result(if is_string(&status) { - status - } else { - PhpMixed::String(format!("[{}] {}", get_class_err(&e), e)) - }); - } else { - return Err(e); - } + let mut result_list: Vec = vec![]; + let mut tls_warning: Option = None; + if str_starts_with(url, "https://") + && config.borrow().get("disable-tls").as_bool() == Some(true) + { + tls_warning = Some("Composer is configured to disable SSL/TLS protection. This will leave remote HTTPS requests vulnerable to Man-In-The-Middle attacks.".to_string()); } - let oauth = config + match self + .http_downloader + .borrow() + .as_ref() + .unwrap() .borrow_mut() - .get("github-oauth") - .as_array() - .cloned() - .unwrap_or_default(); - if oauth.len() as i64 > 0 { - for (domain, token) in &oauth { - io.write_no_newline(&format!("Checking {} oauth access: ", domain)); - let r = self.check_github_oauth(domain, token.as_string().unwrap_or(""))?; - self.output_result(r); - } - } 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"); - io.write(&format!( - "GitHub has a rate limit on their API. You currently have {} out of {} 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", - remaining, limit, - )); - } else { - self.output_result(PhpMixed::Bool(true)); - } - } - } - Err(e) => { - if let Some(te) = e.downcast_ref::() { - if te.get_code() == 401 { - self.output_result(PhpMixed::String("The oauth token for github.com seems invalid, run \"composer config --global --unset github-oauth.github.com\" to remove it".to_string())); - } else { - self.output_result(PhpMixed::String(format!( - "[{}] {}", - get_class_err(&e), - e - ))); + .get(url, IndexMap::new()) + { + Ok(_) => {} + Err(e) => { + if let Some(te) = e.downcast_ref::() { + let hints = HttpDownloader::get_exception_hints(&e).unwrap_or_default(); + if !hints.is_empty() { + for hint in hints { + result_list.push(PhpMixed::String(hint)); } - } else { - self.output_result(PhpMixed::String(format!( - "[{}] {}", - get_class_err(&e), - e - ))); } + + result_list.push(PhpMixed::String(format!( + "[{}] {}", + std::any::type_name_of_val(te), + te.message + ))); + } else { + return Err(e); } } } - io.write_no_newline("Checking disk free space: "); - let r = self.check_disk_space(&config.borrow()); - self.output_result(r); - - Ok(self.exit_code.get()) - } + if let Some(w) = tls_warning { + result_list.push(PhpMixed::String(w)); + } - fn initialize( - &self, - input: std::rc::Rc>, - output: std::rc::Rc>, - ) -> anyhow::Result<()> { - base_command_initialize(self, input, output) - } + if !result_list.is_empty() { + return Ok(PhpMixed::List(result_list)); + } - fn complete( - &self, - input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, - suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, - ) -> anyhow::Result<()> { - crate::command::base_command::base_command_complete(self, input, suggestions) + Ok(PhpMixed::Bool(true)) } - shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); -} + fn check_http_proxy(&self, proxy: &RequestProxy, protocol: &str) -> anyhow::Result { + let result = self.check_connectivity_and_composer_network_http_enablement(); + if result.as_bool() != Some(true) { + return Ok(result); + } -impl BaseCommand for DiagnoseCommand { - fn base_command_data(&self) -> &crate::command::BaseCommandData { - &self.base_command_data - } - - crate::delegate_base_command_trait_impls_to_inner!(base_command_data); -} - -impl DiagnoseCommand { - fn check_composer_schema(&self) -> anyhow::Result { - let validator = ConfigValidator::new(self.get_io().clone()); - let (errors, _, warnings) = validator.validate(&Factory::get_composer_file()?, 0, 0); - - if !errors.is_empty() || !warnings.is_empty() { - let mut messages: IndexMap> = IndexMap::new(); - messages.insert("error".to_string(), errors); - messages.insert("warning".to_string(), warnings); - - let mut output = String::new(); - for (style, msgs) in &messages { - for msg in msgs { - output.push_str(&format!("<{}>{}{}", style, msg, style, PHP_EOL)); - } - } - - return Ok(PhpMixed::String(rtrim(&output, Some(" \t\n\r\0\u{0B}")))); - } - - Ok(PhpMixed::Bool(true)) - } - - fn check_composer_lock_schema(&self, locker: &dyn LockerInterface) -> anyhow::Result { - let json = locker.get_json_file(); - - match json.validate_schema(JsonFile::LOCK_SCHEMA, None) { - Ok(_) => {} - Err(e) => { - if let Some(jve) = e.downcast_ref::() { - let mut output = String::new(); - for error in jve.get_errors() { - output.push_str(&format!("{}{}", error, PHP_EOL)); - } - - return Ok(PhpMixed::String(trim(&output, Some(" \t\n\r\0\u{0B}")))); - } - return Err(e); - } - } - - Ok(PhpMixed::Bool(true)) - } - - fn check_git(&self) -> String { - if !shirabe_php_rpc::get_diagnostics().function_exists("proc_open") { - return "proc_open is not available, git cannot be used".to_string(); - } - - let mut output = String::new(); - let _ = self - .process - .borrow() - .as_ref() - .unwrap() - .borrow_mut() - .execute( - vec![ - "git".to_string(), - "config".to_string(), - "color.ui".to_string(), - ], - &mut output, - None, - ); - if strtolower(&trim(&output, Some(" \t\n\r\0\u{0B}"))) == "always" { - return "Your git color.ui setting is set to always, this is known to create issues. Use \"git config --global color.ui true\" to set it correctly.".to_string(); - } - - let process = self.process.borrow(); - let git_version = Git::get_version(process.as_ref().unwrap()); - let git_version = match git_version { - Some(v) => v, - None => return "No git process found".to_string(), - }; - - if version_compare("2.24.0", &git_version, ">") { - return format!( - "Your git version ({}) is too old and possibly will cause issues. Please upgrade to git 2.24 or above", - git_version - ); - } - - format!("OK git version {}", git_version) - } - - fn check_http( - &self, - proto: &str, - config: &std::rc::Rc>, - ) -> anyhow::Result { - let result = self.check_connectivity_and_composer_network_http_enablement(); - if result.as_bool() != Some(true) { - return Ok(result); - } - - let mut result_list: Vec = vec![]; - let mut tls_warning: Option = None; - if proto == "https" && config.borrow().get("disable-tls").as_bool() == Some(true) { - tls_warning = Some("Composer is configured to disable SSL/TLS protection. This will leave remote HTTPS requests vulnerable to Man-In-The-Middle attacks.".to_string()); - } - - match self - .http_downloader - .borrow() - .as_ref() - .unwrap() - .borrow_mut() - .get( - &format!("{}://repo.packagist.org/packages.json", proto), - IndexMap::new(), - ) { - Ok(_) => {} - Err(e) => { - if let Some(te) = e.downcast_ref::() { - 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(PhpMixed::String(format!( - "[{}] {}", - std::any::type_name_of_val(te), - te.message - ))); - } else { - return Err(e); - } - } - } - - if let Some(w) = tls_warning { - result_list.push(PhpMixed::String(w)); - } - - if !result_list.is_empty() { - return Ok(PhpMixed::List(result_list)); - } - - Ok(PhpMixed::Bool(true)) - } - - fn check_composer_repo( - &self, - url: &str, - config: &std::rc::Rc>, - ) -> anyhow::Result { - let result = self.check_connectivity_and_composer_network_http_enablement(); - if result.as_bool() != Some(true) { - return Ok(result); - } - - let mut result_list: Vec = vec![]; - let mut tls_warning: Option = None; - if str_starts_with(url, "https://") - && config.borrow().get("disable-tls").as_bool() == Some(true) - { - tls_warning = Some("Composer is configured to disable SSL/TLS protection. This will leave remote HTTPS requests vulnerable to Man-In-The-Middle attacks.".to_string()); - } - - match self - .http_downloader - .borrow() - .as_ref() - .unwrap() - .borrow_mut() - .get(url, IndexMap::new()) - { - Ok(_) => {} - Err(e) => { - if let Some(te) = e.downcast_ref::() { - 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(PhpMixed::String(format!( - "[{}] {}", - std::any::type_name_of_val(te), - te.message - ))); - } else { - return Err(e); - } - } - } - - if let Some(w) = tls_warning { - result_list.push(PhpMixed::String(w)); - } - - if !result_list.is_empty() { - return Ok(PhpMixed::List(result_list)); - } - - Ok(PhpMixed::Bool(true)) - } - - fn check_http_proxy(&self, proxy: &RequestProxy, protocol: &str) -> anyhow::Result { - let result = self.check_connectivity_and_composer_network_http_enablement(); - if result.as_bool() != Some(true) { - return Ok(result); - } - - let proxy_status = proxy.get_status(None).unwrap_or_default(); + let proxy_status = proxy.get_status(None).unwrap_or_default(); if proxy.is_excluded_by_no_proxy() { return Ok(PhpMixed::String(format!( @@ -1066,448 +677,835 @@ impl DiagnoseCommand { return "disabled via disable_functions, using php streams fallback, which reduces performance".to_string(); } - let version = shirabe_php_rpc::get_diagnostics() - .curl - .as_ref() - .expect("the diagnose payload carries curl details while the extension is loaded"); - let libz_version = version - .libz_version - .as_deref() - .filter(|v| !v.is_empty()) - .unwrap_or("missing"); - let brotli_version = version - .brotli_version - .as_deref() - .filter(|v| !v.is_empty()) - .unwrap_or("missing"); - let ssl_version = version - .ssl_version - .as_deref() - .filter(|v| !v.is_empty()) - .unwrap_or("missing"); - let has_zstd = match (version.features, version.version_zstd) { - (Some(features), Some(zstd)) => features & zstd != 0, - _ => false, - }; - let mut http_versions = "1.0, 1.1".to_string(); - if let (Some(features), Some(http2)) = (version.features, version.version_http2) - && version.has_http_version_2_0 - && http2 & features != 0 - { - http_versions.push_str(", 2"); - } - if let (Some(features), Some(http3)) = (version.features, version.version_http3) - && features & http3 != 0 - { - http_versions.push_str(", 3"); - } + let version = shirabe_php_rpc::get_diagnostics() + .curl + .as_ref() + .expect("the diagnose payload carries curl details while the extension is loaded"); + let libz_version = version + .libz_version + .as_deref() + .filter(|v| !v.is_empty()) + .unwrap_or("missing"); + let brotli_version = version + .brotli_version + .as_deref() + .filter(|v| !v.is_empty()) + .unwrap_or("missing"); + let ssl_version = version + .ssl_version + .as_deref() + .filter(|v| !v.is_empty()) + .unwrap_or("missing"); + let has_zstd = match (version.features, version.version_zstd) { + (Some(features), Some(zstd)) => features & zstd != 0, + _ => false, + }; + let mut http_versions = "1.0, 1.1".to_string(); + if let (Some(features), Some(http2)) = (version.features, version.version_http2) + && version.has_http_version_2_0 + && http2 & features != 0 + { + http_versions.push_str(", 2"); + } + if let (Some(features), Some(http3)) = (version.features, version.version_http3) + && features & http3 != 0 + { + http_versions.push_str(", 3"); + } + + return format!( + "{} libz {} brotli {} zstd {} ssl {} HTTP {}", + version.version, + libz_version, + brotli_version, + if has_zstd { "supported" } else { "missing" }, + ssl_version, + http_versions, + ); + } + + "missing, using php streams fallback, which reduces performance".to_string() + } + + fn output_result(&self, result: PhpMixed) { + let prev_exit_code = self.exit_code.get(); + let io = self.get_io(); + if result.as_bool() == Some(true) { + io.write("OK"); + + return; + } + + let mut had_error = false; + 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 = 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, "").is_some() { + had_error = true; + } else if strpos(s, "").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("FAIL"); + } else if had_warning { + io.write("WARNING"); + } + + 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)); + } + } + + fn check_platform(&self) -> anyhow::Result { + let mut output = String::new(); + let mut display_ini_message = false; + + let mut ini_message = format!("{}{}{}", PHP_EOL, PHP_EOL, IniHelper::get_message()); + ini_message.push_str(&format!("{}If you can not modify the ini file, you can also run `php -d option=value` to modify ini values on the fly. You can use -d multiple times.", PHP_EOL)); + + let diagnostics = shirabe_php_rpc::get_diagnostics(); + + let mut errors: IndexMap = IndexMap::new(); + let mut warnings: IndexMap = IndexMap::new(); + + if !diagnostics.function_exists("json_decode") { + errors.insert("json".to_string(), PhpMixed::Bool(true)); + } + + if !diagnostics.extension_loaded("Phar") { + errors.insert("phar".to_string(), PhpMixed::Bool(true)); + } + + if !diagnostics.extension_loaded("filter") { + errors.insert("filter".to_string(), PhpMixed::Bool(true)); + } + + if !diagnostics.extension_loaded("hash") { + errors.insert("hash".to_string(), PhpMixed::Bool(true)); + } + + if !diagnostics.extension_loaded("iconv") && !diagnostics.extension_loaded("mbstring") { + errors.insert("iconv_mbstring".to_string(), PhpMixed::Bool(true)); + } + + if !filter_var_boolean(diagnostics.ini_get("allow_url_fopen").unwrap_or("")) { + errors.insert("allow_url_fopen".to_string(), PhpMixed::Bool(true)); + } + + if diagnostics.extension_loaded("ionCube Loader") + && diagnostics.ioncube_loader_iversion < 40009 + { + errors.insert( + "ioncube".to_string(), + PhpMixed::String(diagnostics.ioncube_loader_version.clone()), + ); + } + + if diagnostics.php_version_id < 70205 { + errors.insert( + "php".to_string(), + PhpMixed::String(diagnostics.php_version.clone()), + ); + } + + if !diagnostics.extension_loaded("openssl") { + errors.insert("openssl".to_string(), PhpMixed::Bool(true)); + } + + if diagnostics.extension_loaded("openssl") + && diagnostics.openssl_version_number < 0x1000100f + { + warnings.insert("openssl_version".to_string(), PhpMixed::Bool(true)); + } + + 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)); + } + + if !diagnostics.extension_loaded("zlib") { + warnings.insert("zlib".to_string(), PhpMixed::Bool(true)); + } + + let mut phpinfo_match: IndexMap = IndexMap::new(); + if Preg::is_match3( + php_regex!("{Configure Command(?: *| *=> *)(.*?)(?:|$)}m"), + &diagnostics.phpinfo_general, + Some(&mut phpinfo_match), + ) { + let configure = phpinfo_match + .get(&CaptureKey::ByIndex(1)) + .cloned() + .unwrap_or_default(); + let configure = configure.as_str(); + + if str_contains(configure, "--enable-sigchild") { + warnings.insert("sigchild".to_string(), PhpMixed::Bool(true)); + } + + if str_contains(configure, "--with-curlwrappers") { + warnings.insert("curlwrappers".to_string(), PhpMixed::Bool(true)); + } + } + + if filter_var_boolean(diagnostics.ini_get("xdebug.profiler_enabled").unwrap_or("")) { + warnings.insert("xdebug_profile".to_string(), PhpMixed::Bool(true)); + } else if diagnostics.xdebug_active { + // PHP: XdebugHandler::isXdebugActive(). As with IniHelper::get_all, the port of that + // method in shirabe_external_packages cannot reach the PHP RPC bridge (the dependency + // would cycle), so the real runtime is queried through the diagnose payload instead. + warnings.insert("xdebug_loaded".to_string(), PhpMixed::Bool(true)); + } + + if diagnostics.has_php_windows_version_build + && (version_compare(&diagnostics.php_version, "7.2.23", "<") + || (version_compare(&diagnostics.php_version, "7.3.0", ">=") + && version_compare(&diagnostics.php_version, "7.3.10", "<"))) + { + warnings.insert( + "onedrive".to_string(), + PhpMixed::String(diagnostics.php_version.clone()), + ); + } + + if diagnostics.extension_loaded("uopz") + && !(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)); + } + + let out_fn = |msg: &str, style: &str, output: &mut String| { + output.push_str(&format!("<{}>{}{}", style, msg, style, PHP_EOL)); + }; + + if !errors.is_empty() { + for (error, current) in &errors { + let text = match error.as_str() { + "json" => format!( + "{}The json extension is missing.{}Install it or recompile php without --disable-json", + PHP_EOL, PHP_EOL + ), + "phar" => format!( + "{}The phar extension is missing.{}Install it or recompile php without --disable-phar", + PHP_EOL, PHP_EOL + ), + "filter" => format!( + "{}The filter extension is missing.{}Install it or recompile php without --disable-filter", + PHP_EOL, PHP_EOL + ), + "hash" => format!( + "{}The hash extension is missing.{}Install it or recompile php without --disable-hash", + PHP_EOL, PHP_EOL + ), + "iconv_mbstring" => format!( + "{}The iconv OR mbstring extension is required and both are missing.{}Install either of them or recompile php without --disable-iconv", + PHP_EOL, PHP_EOL + ), + "php" => format!( + "{}Your PHP ({}) is too old, you must upgrade to PHP 7.2.5 or higher.", + PHP_EOL, + current.as_string().unwrap_or("") + ), + "allow_url_fopen" => { + display_ini_message = true; + format!( + "{}The allow_url_fopen setting is incorrect.{}Add the following to the end of your `php.ini`:{} allow_url_fopen = On", + PHP_EOL, PHP_EOL, PHP_EOL + ) + } + "ioncube" => { + display_ini_message = true; + 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(""), + PHP_EOL, + PHP_EOL + ) + } + "openssl" => format!( + "{}The openssl extension is missing, which means that secure HTTPS transfers are impossible.{}If possible you should enable it or recompile php with --with-openssl", + PHP_EOL, PHP_EOL + ), + other => { + return Err(InvalidArgumentException { + message: format!( + "DiagnoseCommand: Unknown error type \"{}\". Please report at https://github.com/composer/composer/issues/new.", + other, + ), + code: 0, + } + .into()); + } + }; + out_fn(&text, "error", &mut output); + } + + output.push_str(PHP_EOL); + } + + if !warnings.is_empty() { + for (warning, current) in &warnings { + let text = match warning.as_str() { + "apc_cli" => { + display_ini_message = true; + format!( + "The apc.enable_cli setting is incorrect.{}Add the following to the end of your `php.ini`:{} apc.enable_cli = Off", + PHP_EOL, PHP_EOL + ) + } + "zlib" => { + display_ini_message = true; + format!( + "The zlib extension is not loaded, this can slow down Composer a lot.{}If possible, enable it or recompile php with --with-zlib{}", + PHP_EOL, PHP_EOL + ) + } + "sigchild" => format!( + "PHP was compiled with --enable-sigchild which can cause issues on some platforms.{}Recompile it without this flag if possible, see also:{} https://bugs.php.net/bug.php?id=22999", + PHP_EOL, PHP_EOL + ), + "curlwrappers" => format!( + "PHP was compiled with --with-curlwrappers which will cause issues with HTTP authentication and GitHub.{} Recompile it without this flag if possible", + PHP_EOL + ), + "openssl_version" => { + // Attempt to parse version number out, fallback to whole string value. + let openssl_version_text = + diagnostics.openssl_version_text.clone().unwrap_or_default(); + let openssl_trimmed = trim( + &strstr(&openssl_version_text, " ").unwrap_or_default(), + Some(" \t\n\r\0\u{0B}"), + ); + let mut openssl_version = + strstr3(&openssl_trimmed, " ", true).unwrap_or_default(); + if openssl_version.is_empty() { + openssl_version = openssl_version_text; + } + + format!( + "The OpenSSL library ({}) used by PHP does not support TLSv1.2 or TLSv1.1.{}If possible you should upgrade OpenSSL to version 1.0.1 or above.", + openssl_version, PHP_EOL + ) + } + "xdebug_loaded" => format!( + "The xdebug extension is loaded, this can slow down Composer a little.{} Disabling it when using Composer is recommended.", + PHP_EOL + ), + "xdebug_profile" => { + display_ini_message = true; + format!( + "The xdebug.profiler_enabled setting is enabled, this can slow down Composer a lot.{}Add the following to the end of your `php.ini` to disable it:{} xdebug.profiler_enabled = 0", + PHP_EOL, PHP_EOL + ) + } + "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 Composer.{}", + PHP_EOL, + current.as_string().unwrap_or(""), + PHP_EOL + ), + "uopz" => format!( + "The uopz extension ignores exit calls and may not work with all Composer commands.{}Disabling it when using Composer is recommended.", + PHP_EOL + ), + other => { + return Err(InvalidArgumentException { + message: format!( + "DiagnoseCommand: Unknown warning type \"{}\". Please report at https://github.com/composer/composer/issues/new.", + other, + ), + code: 0, + } + .into()); + } + }; + out_fn(&text, "comment", &mut output); + } + } + + if display_ini_message { + out_fn(&ini_message, "comment", &mut output); + } - return format!( - "{} libz {} brotli {} zstd {} ssl {} HTTP {}", - version.version, - libz_version, - brotli_version, - if has_zstd { "supported" } else { "missing" }, - ssl_version, - http_versions, + 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)); + out_fn( + &format!( + "The COMPOSER_IPRESOLVE env var is set to {} which may result in network failures below.", + Platform::get_env("COMPOSER_IPRESOLVE").unwrap_or_default() + ), + "comment", + &mut output, ); } - "missing, using php streams fallback, which reduces performance".to_string() + Ok(if warnings.is_empty() && errors.is_empty() { + PhpMixed::Bool(true) + } else { + PhpMixed::String(output) + }) } - fn output_result(&self, result: PhpMixed) { - let prev_exit_code = self.exit_code.get(); - let io = self.get_io(); - if result.as_bool() == Some(true) { - io.write("OK"); - - return; + /// Check if allow_url_fopen is ON + fn check_connectivity(&self) -> PhpMixed { + // 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( + "SKIP Because allow_url_fopen is missing.".to_string(), + ); } - let mut had_error = false; - 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 = 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, "").is_some() { - had_error = true; - } else if strpos(s, "").is_some() { - had_warning = true; - } - } - // re-wrap so the final output loop works the same - result = PhpMixed::List(result_list); - } + PhpMixed::Bool(true) + } - if had_error { - io.write("FAIL"); - } else if had_warning { - io.write("WARNING"); + fn check_connectivity_and_composer_network_http_enablement(&self) -> PhpMixed { + let result = self.check_connectivity(); + if result.as_bool() != Some(true) { + return result; } - 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}"), - )); - } + let result = self.check_composer_network_http_enablement(); + if result.as_bool() != Some(true) { + return result; } - // 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)); + + PhpMixed::Bool(true) + } + + /// Check if Composer network is enabled for HTTP/S + fn check_composer_network_http_enablement(&self) -> PhpMixed { + if Platform::get_env("COMPOSER_DISABLE_NETWORK") + .map(|v| !v.is_empty() && v != "0") + .unwrap_or(false) + { + return PhpMixed::String( + "SKIP Network is disabled by COMPOSER_DISABLE_NETWORK." + .to_string(), + ); } + + PhpMixed::Bool(true) } +} - fn check_platform(&self) -> anyhow::Result { - let mut output = String::new(); - let mut display_ini_message = false; +impl Command for DiagnoseCommand { + fn configure(&self) -> anyhow::Result<()> { + self.set_name("diagnose")?; + self.set_description("Diagnoses the system to identify common errors"); + self.set_help( + "The diagnose command checks common errors to help debugging problems.\n\n\ + The process exit code will be 1 in case of warnings and 2 for errors.\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#diagnose", + ); + Ok(()) + } - let mut ini_message = format!("{}{}{}", PHP_EOL, PHP_EOL, IniHelper::get_message()); - ini_message.push_str(&format!("{}If you can not modify the ini file, you can also run `php -d option=value` to modify ini values on the fly. You can use -d multiple times.", PHP_EOL)); + fn execute( + &self, + input: std::rc::Rc>, + output: std::rc::Rc>, + ) -> anyhow::Result { + let mut composer = self.try_composer(None, None); + let io: std::rc::Rc> = self.get_io().clone(); - let diagnostics = shirabe_php_rpc::get_diagnostics(); + let config: std::rc::Rc>; + if let Some(ref mut c) = composer { + let c = crate::composer::composer_full(c); + config = c.get_config(); - let mut errors: IndexMap = IndexMap::new(); - let mut warnings: IndexMap = IndexMap::new(); + let command_event = CommandEvent::new6( + PluginEvents::COMMAND, + "diagnose", + input, + output, + vec![], + IndexMap::new(), + ); + c.get_event_dispatcher() + .borrow_mut() + .dispatch(Some(command_event.get_name()), None); + *self.process.borrow_mut() = Some( + c.get_loop() + .borrow() + .get_process_executor() + .map(std::rc::Rc::clone) + .unwrap_or_else(|| { + std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(Some( + io.clone(), + )))) + }), + ); + } else { + config = std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config(None, None)?)); - if !diagnostics.function_exists("json_decode") { - errors.insert("json".to_string(), PhpMixed::Bool(true)); + *self.process.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new( + ProcessExecutor::new(Some(io.clone())), + ))); } + let mut config_inner = IndexMap::new(); + config_inner.insert("secure-http".to_string(), PhpMixed::Bool(false)); + let mut secure_http_wrap: IndexMap = IndexMap::new(); + secure_http_wrap.insert("config".to_string(), PhpMixed::Array(config_inner)); + let config = config; + config + .borrow_mut() + .merge(&secure_http_wrap, Config::SOURCE_COMMAND); + let _ = config.borrow_mut().prohibit_url_by_config( + "http://repo.packagist.org", + Some(std::rc::Rc::new(std::cell::RefCell::new(NullIO::new()))), + &IndexMap::new(), + ); - if !diagnostics.extension_loaded("Phar") { - errors.insert("phar".to_string(), PhpMixed::Bool(true)); - } + *self.http_downloader.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new( + Factory::create_http_downloader(io.clone(), &config, indexmap::IndexMap::new())?, + ))); - if !diagnostics.extension_loaded("filter") { - errors.insert("filter".to_string(), PhpMixed::Bool(true)); - } + if strpos(file!(), "phar:") == Some(0) { + io.write_no_newline("Checking pubkeys: "); + let r = self.check_pub_keys(&config.borrow())?; + self.output_result(r); - if !diagnostics.extension_loaded("hash") { - errors.insert("hash".to_string(), PhpMixed::Bool(true)); + io.write_no_newline("Checking Composer version: "); + let r = self.check_version(&config)?; + self.output_result(r); } - if !diagnostics.extension_loaded("iconv") && !diagnostics.extension_loaded("mbstring") { - errors.insert("iconv_mbstring".to_string(), PhpMixed::Bool(true)); - } + io.write(&format!( + "Composer version: {}", + composer::get_version() + )); - if !filter_var_boolean(diagnostics.ini_get("allow_url_fopen").unwrap_or("")) { - errors.insert("allow_url_fopen".to_string(), PhpMixed::Bool(true)); - } + io.write_no_newline("Checking Composer and its dependencies for vulnerabilities: "); + let r = self.check_composer_audit(&config)?; + self.output_result(r); - if diagnostics.extension_loaded("ionCube Loader") - && diagnostics.ioncube_loader_iversion < 40009 + let platform_overrides = config + .borrow_mut() + .get("platform") + .as_array() + .cloned() + .unwrap_or_default(); + let platform_overrides_unboxed: indexmap::IndexMap = + platform_overrides.into_iter().collect(); + let mut platform_repo = + PlatformRepository::new(vec![], platform_overrides_unboxed).unwrap(); + let php_pkg = ::find_package( + &mut platform_repo, + "php", + crate::repository::FindPackageConstraint::String("*".to_string()), + )? + .unwrap(); + let mut php_version = php_pkg.get_pretty_version(); + if let Some(cp) = php_pkg.as_complete() + && str_contains(&cp.get_description().unwrap_or_default(), "overridden") { - errors.insert( - "ioncube".to_string(), - PhpMixed::String(diagnostics.ioncube_loader_version.clone()), + php_version = format!( + "{} - {}", + php_version, + cp.get_description().unwrap_or_default() ); } - if diagnostics.php_version_id < 70205 { - errors.insert( - "php".to_string(), - PhpMixed::String(diagnostics.php_version.clone()), - ); - } + io.write(&format!("PHP version: {}", php_version)); - if !diagnostics.extension_loaded("openssl") { - errors.insert("openssl".to_string(), PhpMixed::Bool(true)); - } + let diagnostics = shirabe_php_rpc::get_diagnostics(); - if diagnostics.extension_loaded("openssl") - && diagnostics.openssl_version_number < 0x1000100f - { - warnings.insert("openssl_version".to_string(), PhpMixed::Bool(true)); + if let Some(php_binary) = &diagnostics.php_binary { + io.write(&format!( + "PHP binary path: {}", + php_binary + )); } - if !diagnostics.has_hhvm_version - && !diagnostics.extension_loaded("apcu") - && filter_var_boolean(diagnostics.ini_get("apc.enable_cli").unwrap_or("")) + io.write(&format!( + "OpenSSL version: {}", + match &diagnostics.openssl_version_text { + Some(text) => format!("{}", text), + None => "missing".to_string(), + } + )); + io.write(&format!("curl version: {}", self.get_curl_version())); + + let finder = ExecutableFinder::new(); + let has_system_unzip = finder.find("unzip", None, &[]).is_some(); + let mut bin_7zip = String::new(); + let has_system_7zip = if finder + .find("7z", None, &["C:\\Program Files\\7-Zip".to_string()]) + .is_some() { - warnings.insert("apc_cli".to_string(), PhpMixed::Bool(true)); - } + bin_7zip = "7z".to_string(); + true + } else if !Platform::is_windows() && finder.find("7zz", None, &[]).is_some() { + bin_7zip = "7zz".to_string(); + true + } else if !Platform::is_windows() && finder.find("7za", None, &[]).is_some() { + bin_7zip = "7za".to_string(); + true + } else { + false + }; - if !diagnostics.extension_loaded("zlib") { - warnings.insert("zlib".to_string(), PhpMixed::Bool(true)); - } + io.write(&format!( + "zip: {}, {}, {}{}", + if diagnostics.extension_loaded("zip") { + "extension present" + } else { + "extension not loaded" + }, + if has_system_unzip { + "unzip present".to_string() + } else { + "unzip not available".to_string() + }, + if has_system_7zip { + format!("7-Zip present ({})", bin_7zip) + } else { + "7-Zip not available".to_string() + }, + if (has_system_7zip || has_system_unzip) && !diagnostics.function_exists("proc_open") { + ", proc_open is disabled or not present, unzip/7-z will not be usable" + } else { + "" + } + )); - let mut phpinfo_match: IndexMap = IndexMap::new(); - if Preg::is_match3( - php_regex!("{Configure Command(?: *| *=> *)(.*?)(?:|$)}m"), - &diagnostics.phpinfo_general, - Some(&mut phpinfo_match), - ) { - let configure = phpinfo_match - .get(&CaptureKey::ByIndex(1)) - .cloned() - .unwrap_or_default(); - let configure = configure.as_str(); + if let Some(ref mut c) = composer { + let c = crate::composer::composer_full(c); + io.write(&format!( + "Active plugins: {}", + implode( + ", ", + &c.get_plugin_manager().borrow().get_registered_plugins() + ) + )); - if str_contains(configure, "--enable-sigchild") { - warnings.insert("sigchild".to_string(), PhpMixed::Bool(true)); - } + io.write_no_newline("Checking composer.json: "); + let r = self.check_composer_schema()?; + self.output_result(r); - if str_contains(configure, "--with-curlwrappers") { - warnings.insert("curlwrappers".to_string(), PhpMixed::Bool(true)); + if c.get_locker().borrow_mut().is_locked() { + io.write_no_newline("Checking composer.lock: "); + let locker = c.get_locker().clone(); + let locker = locker.borrow(); + let r = self.check_composer_lock_schema(&*locker)?; + self.output_result(r); } } - if filter_var_boolean(diagnostics.ini_get("xdebug.profiler_enabled").unwrap_or("")) { - warnings.insert("xdebug_profile".to_string(), PhpMixed::Bool(true)); - } else if diagnostics.xdebug_active { - // PHP: XdebugHandler::isXdebugActive(). As with IniHelper::get_all, the port of that - // method in shirabe_external_packages cannot reach the PHP RPC bridge (the dependency - // would cycle), so the real runtime is queried through the diagnose payload instead. - warnings.insert("xdebug_loaded".to_string(), PhpMixed::Bool(true)); - } + io.write_no_newline("Checking platform settings: "); + let r = self.check_platform()?; + self.output_result(r); - if diagnostics.has_php_windows_version_build - && (version_compare(&diagnostics.php_version, "7.2.23", "<") - || (version_compare(&diagnostics.php_version, "7.3.0", ">=") - && version_compare(&diagnostics.php_version, "7.3.10", "<"))) - { - warnings.insert( - "onedrive".to_string(), - PhpMixed::String(diagnostics.php_version.clone()), - ); - } + io.write_no_newline("Checking git settings: "); + let r = self.check_git(); + self.output_result(PhpMixed::String(r)); - if diagnostics.extension_loaded("uopz") - && !(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)); - } + io.write_no_newline("Checking http connectivity to packagist: "); + let r = self.check_http("http", &config)?; + self.output_result(r); - let out_fn = |msg: &str, style: &str, output: &mut String| { - output.push_str(&format!("<{}>{}{}", style, msg, style, PHP_EOL)); - }; + io.write_no_newline("Checking https connectivity to packagist: "); + let r = self.check_http("https", &config)?; + self.output_result(r); - if !errors.is_empty() { - for (error, current) in &errors { - let text = match error.as_str() { - "json" => format!( - "{}The json extension is missing.{}Install it or recompile php without --disable-json", - PHP_EOL, PHP_EOL - ), - "phar" => format!( - "{}The phar extension is missing.{}Install it or recompile php without --disable-phar", - PHP_EOL, PHP_EOL - ), - "filter" => format!( - "{}The filter extension is missing.{}Install it or recompile php without --disable-filter", - PHP_EOL, PHP_EOL - ), - "hash" => format!( - "{}The hash extension is missing.{}Install it or recompile php without --disable-hash", - PHP_EOL, PHP_EOL - ), - "iconv_mbstring" => format!( - "{}The iconv OR mbstring extension is required and both are missing.{}Install either of them or recompile php without --disable-iconv", - PHP_EOL, PHP_EOL - ), - "php" => format!( - "{}Your PHP ({}) is too old, you must upgrade to PHP 7.2.5 or higher.", - PHP_EOL, - current.as_string().unwrap_or("") - ), - "allow_url_fopen" => { - display_ini_message = true; - format!( - "{}The allow_url_fopen setting is incorrect.{}Add the following to the end of your `php.ini`:{} allow_url_fopen = On", - PHP_EOL, PHP_EOL, PHP_EOL - ) - } - "ioncube" => { - display_ini_message = true; - 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(""), - PHP_EOL, - PHP_EOL - ) - } - "openssl" => format!( - "{}The openssl extension is missing, which means that secure HTTPS transfers are impossible.{}If possible you should enable it or recompile php with --with-openssl", - PHP_EOL, PHP_EOL - ), - other => { - return Err(InvalidArgumentException { - message: format!( - "DiagnoseCommand: Unknown error type \"{}\". Please report at https://github.com/composer/composer/issues/new.", - other, - ), - code: 0, - } - .into()); - } - }; - out_fn(&text, "error", &mut output); + let repositories = config.borrow().get_repositories(); + for repo in repositories { + let repo_arr = repo.1.as_array().cloned().unwrap_or_default(); + if repo_arr.get("type").and_then(|v| v.as_string()) == Some("composer") + && repo_arr.get("url").is_some() + { + let repo_arr_unboxed: indexmap::IndexMap = repo_arr + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + let composer_repo = ComposerRepository::new( + repo_arr_unboxed, + self.get_io().clone(), + &config.borrow(), + self.http_downloader.borrow().clone().unwrap(), + None, + ) + .unwrap(); + // PHP: ReflectionMethod($composerRepo, 'getPackagesJsonUrl') + // We surface the same internal call by directly invoking the equivalent method. + // TODO(plugin): support reflection-based access if plugin code requires it. + let url = composer_repo.get_packages_json_url(); + if !str_starts_with(&url, "http") { + continue; + } + if str_starts_with(&url, "https://repo.packagist.org") { + continue; + } + io.write_no_newline(&format!( + "Checking connectivity to {}: ", + repo_arr + .get("url") + .and_then(|v| v.as_string()) + .unwrap_or("") + )); + let r = self.check_composer_repo(&url, &config)?; + self.output_result(r); } + } - output.push_str(PHP_EOL); + let protos: Vec<&str> = if config.borrow_mut().get("disable-tls").as_bool() == Some(true) { + vec!["http"] + } else { + vec!["http", "https"] + }; + let proxy_check_result: anyhow::Result<(), anyhow::Error> = (|| -> anyhow::Result<()> { + for proto in &protos { + // Compute the proxy under a short-lived lock: `check_http_proxy` below transitively + // re-enters `ProxyManager::get_instance()` (via HttpDownloader -> CurlDownloader / + // RemoteFilesystem), and `std::sync::Mutex` is not reentrant, so the guard must not + // still be held when that call happens. + let proxy = ProxyManager::get_instance() + .as_ref() + .unwrap() + .get_proxy_for_request(&format!("{}://repo.packagist.org", proto)) + .map_err(|e| anyhow::anyhow!(e))?; + if !proxy.get_status(None)?.is_empty() { + let r#type = if proxy.is_secure() { "HTTPS" } else { "HTTP" }; + io.write_no_newline(&format!("Checking {} proxy with {}: ", r#type, proto)); + let r = self.check_http_proxy(&proxy, proto)?; + self.output_result(r); + } + } + Ok(()) + })(); + if let Err(e) = proxy_check_result { + if let Some(_te) = e.downcast_ref::() { + io.write_no_newline("Checking HTTP proxy: "); + let status = self.check_connectivity_and_composer_network_http_enablement(); + self.output_result(if is_string(&status) { + status + } else { + PhpMixed::String(format!("[{}] {}", get_class_err(&e), e)) + }); + } else { + return Err(e); + } } - if !warnings.is_empty() { - for (warning, current) in &warnings { - let text = match warning.as_str() { - "apc_cli" => { - display_ini_message = true; - format!( - "The apc.enable_cli setting is incorrect.{}Add the following to the end of your `php.ini`:{} apc.enable_cli = Off", - PHP_EOL, PHP_EOL - ) - } - "zlib" => { - display_ini_message = true; - format!( - "The zlib extension is not loaded, this can slow down Composer a lot.{}If possible, enable it or recompile php with --with-zlib{}", - PHP_EOL, PHP_EOL - ) - } - "sigchild" => format!( - "PHP was compiled with --enable-sigchild which can cause issues on some platforms.{}Recompile it without this flag if possible, see also:{} https://bugs.php.net/bug.php?id=22999", - PHP_EOL, PHP_EOL - ), - "curlwrappers" => format!( - "PHP was compiled with --with-curlwrappers which will cause issues with HTTP authentication and GitHub.{} Recompile it without this flag if possible", - PHP_EOL - ), - "openssl_version" => { - // Attempt to parse version number out, fallback to whole string value. - let openssl_version_text = - diagnostics.openssl_version_text.clone().unwrap_or_default(); - let openssl_trimmed = trim( - &strstr(&openssl_version_text, " ").unwrap_or_default(), - Some(" \t\n\r\0\u{0B}"), - ); - let mut openssl_version = - strstr3(&openssl_trimmed, " ", true).unwrap_or_default(); - if openssl_version.is_empty() { - openssl_version = openssl_version_text; + let oauth = config + .borrow_mut() + .get("github-oauth") + .as_array() + .cloned() + .unwrap_or_default(); + if oauth.len() as i64 > 0 { + for (domain, token) in &oauth { + io.write_no_newline(&format!("Checking {} oauth access: ", domain)); + let r = self.check_github_oauth(domain, token.as_string().unwrap_or(""))?; + self.output_result(r); + } + } 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"); + io.write(&format!( + "GitHub has a rate limit on their API. You currently have {} out of {} 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", + remaining, limit, + )); + } else { + self.output_result(PhpMixed::Bool(true)); } - - format!( - "The OpenSSL library ({}) used by PHP does not support TLSv1.2 or TLSv1.1.{}If possible you should upgrade OpenSSL to version 1.0.1 or above.", - openssl_version, PHP_EOL - ) } - "xdebug_loaded" => format!( - "The xdebug extension is loaded, this can slow down Composer a little.{} Disabling it when using Composer is recommended.", - PHP_EOL - ), - "xdebug_profile" => { - display_ini_message = true; - format!( - "The xdebug.profiler_enabled setting is enabled, this can slow down Composer a lot.{}Add the following to the end of your `php.ini` to disable it:{} xdebug.profiler_enabled = 0", - PHP_EOL, PHP_EOL - ) - } - "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 Composer.{}", - PHP_EOL, - current.as_string().unwrap_or(""), - PHP_EOL - ), - "uopz" => format!( - "The uopz extension ignores exit calls and may not work with all Composer commands.{}Disabling it when using Composer is recommended.", - PHP_EOL - ), - other => { - return Err(InvalidArgumentException { - message: format!( - "DiagnoseCommand: Unknown warning type \"{}\". Please report at https://github.com/composer/composer/issues/new.", - other, - ), - code: 0, + } + Err(e) => { + if let Some(te) = e.downcast_ref::() { + if te.get_code() == 401 { + self.output_result(PhpMixed::String("The oauth token for github.com seems invalid, run \"composer config --global --unset github-oauth.github.com\" to remove it".to_string())); + } else { + self.output_result(PhpMixed::String(format!( + "[{}] {}", + get_class_err(&e), + e + ))); } - .into()); + } else { + self.output_result(PhpMixed::String(format!( + "[{}] {}", + get_class_err(&e), + e + ))); } - }; - out_fn(&text, "comment", &mut output); + } } } - if display_ini_message { - out_fn(&ini_message, "comment", &mut output); - } - - 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)); - out_fn( - &format!( - "The COMPOSER_IPRESOLVE env var is set to {} which may result in network failures below.", - Platform::get_env("COMPOSER_IPRESOLVE").unwrap_or_default() - ), - "comment", - &mut output, - ); - } + io.write_no_newline("Checking disk free space: "); + let r = self.check_disk_space(&config.borrow()); + self.output_result(r); - Ok(if warnings.is_empty() && errors.is_empty() { - PhpMixed::Bool(true) - } else { - PhpMixed::String(output) - }) + Ok(self.exit_code.get()) } - /// Check if allow_url_fopen is ON - fn check_connectivity(&self) -> PhpMixed { - // 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( - "SKIP Because allow_url_fopen is missing.".to_string(), - ); - } - - PhpMixed::Bool(true) + fn initialize( + &self, + input: std::rc::Rc>, + output: std::rc::Rc>, + ) -> anyhow::Result<()> { + base_command_initialize(self, input, output) } - fn check_connectivity_and_composer_network_http_enablement(&self) -> PhpMixed { - let result = self.check_connectivity(); - if result.as_bool() != Some(true) { - return result; - } - - let result = self.check_composer_network_http_enablement(); - if result.as_bool() != Some(true) { - return result; - } - - PhpMixed::Bool(true) + fn complete( + &self, + input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput, + suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions, + ) -> anyhow::Result<()> { + crate::command::base_command::base_command_complete(self, input, suggestions) } - /// Check if Composer network is enabled for HTTP/S - fn check_composer_network_http_enablement(&self) -> PhpMixed { - if Platform::get_env("COMPOSER_DISABLE_NETWORK") - .map(|v| !v.is_empty() && v != "0") - .unwrap_or(false) - { - return PhpMixed::String( - "SKIP Network is disabled by COMPOSER_DISABLE_NETWORK." - .to_string(), - ); - } + shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); +} - PhpMixed::Bool(true) +impl BaseCommand for DiagnoseCommand { + fn base_command_data(&self) -> &crate::command::BaseCommandData { + &self.base_command_data } + + crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } -- cgit v1.3.1