//! ref: composer/src/Composer/Repository/Vcs/GitLabDriver.php use crate::io::io_interface; use anyhow::Result; use chrono::{DateTime, Utc}; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::{CaptureKey, Preg}; use shirabe_php_shim::{ InvalidArgumentException, LogicException, PhpMixed, RuntimeException, array_search_mixed, array_shift, ctype_alnum, empty, explode, extension_loaded, implode, in_array, is_array, is_string, ord, sprintf, strpos, strtolower, }; use crate::cache::Cache; use crate::config::Config; use crate::downloader::TransportException; use crate::io::IOInterface; use crate::json::JsonFile; use crate::repository::vcs::GitDriver; use crate::repository::vcs::VcsDriverBase; use crate::repository::vcs::VcsDriverInterface; use crate::util::GitLab; use crate::util::HttpDownloader; use crate::util::http::Response; /// Driver for GitLab API, use the Git driver for local checkouts. #[derive(Debug)] pub struct GitLabDriver { pub(crate) inner: VcsDriverBase, /// @phpstan-var 'https'|'http' scheme: String, namespace: String, repository: String, /// @var mixed[] Project data returned by GitLab API project: Option>, /// @var array Keeps commits returned by GitLab API as commit id => info commits: IndexMap>, /// @var array Map of tag name to identifier tags: Option>, /// @var array Map of branch name to identifier branches: Option>, /// Git Driver pub(crate) git_driver: Option, /// Protocol to force use of for repository URLs. /// @var string One of ssh, http pub(crate) protocol: String, /// Defaults to true unless we can make sure it is public /// @var bool defines whether the repo is private or not is_private: bool, /// @var bool true if the origin has a port number or a path component in it has_nonstandard_origin: bool, } impl GitLabDriver { pub const URL_REGEX: &'static str = r##"#^(?:(?Phttps?)://(?P.+?)(?::(?P[0-9]+))?/|git@(?P[^:]+):)(?P.+)/(?P[^/]+?)(?:\.git|/)?$#"##; /// Extracts information from the repository url. /// /// SSH urls use https by default. Set "secure-http": false on the repository config to use http instead. pub fn initialize(&mut self) -> Result<()> { let mut match_: IndexMap = IndexMap::new(); if !Preg::is_match_strict_groups3(Self::URL_REGEX, &self.inner.url, Some(&mut match_)) .unwrap_or(false) { return Err(InvalidArgumentException { message: sprintf( "The GitLab repository URL %s is invalid. It must be the HTTP URL of a GitLab project.", &[PhpMixed::String(self.inner.url.clone())], ), code: 0, } .into()); } let guessed_domain = match_ .get(&CaptureKey::ByName("domain".to_string())) .cloned() .filter(|s| !s.is_empty()) .unwrap_or_else(|| { match_ .get(&CaptureKey::ByName("domain2".to_string())) .cloned() .unwrap_or_default() }); let configured_domains = self.inner.config.borrow_mut().get("gitlab-domains"); let mut url_parts: Vec = explode( "/", &match_ .get(&CaptureKey::ByName("parts".to_string())) .cloned() .unwrap_or_default(), ); let scheme_match = match_ .get(&CaptureKey::ByName("scheme".to_string())) .cloned() .unwrap_or_default(); self.scheme = if in_array( PhpMixed::String(scheme_match.clone()), &PhpMixed::List(vec![ Box::new(PhpMixed::String("https".to_string())), Box::new(PhpMixed::String("http".to_string())), ]), true, ) { scheme_match } else if self .inner .repo_config .get("secure-http") .and_then(|v| v.as_bool()) == Some(false) { "http".to_string() } else { "https".to_string() }; let port = match_.get(&CaptureKey::ByName("port".to_string())).cloned(); let origin = Self::determine_origin( &configured_domains, guessed_domain, &mut url_parts, port.clone(), ); let origin = match origin { Some(o) => o, None => { return Err(LogicException { message: format!( "It should not be possible to create a gitlab driver with an unparsable origin URL ({})", self.inner.url ), code: 0, } .into()); } }; self.inner.origin_url = origin; let protocol_value = self.inner.config.borrow_mut().get("gitlab-protocol"); if let Some(protocol) = protocol_value .as_string() .filter(|_| is_string(&protocol_value)) { // https treated as a synonym for http. if !in_array( PhpMixed::String(protocol.to_string()), &PhpMixed::List(vec![ Box::new(PhpMixed::String("git".to_string())), Box::new(PhpMixed::String("http".to_string())), Box::new(PhpMixed::String("https".to_string())), ]), true, ) { return Err(RuntimeException { message: "gitlab-protocol must be one of git, http.".to_string(), code: 0, } .into()); } self.protocol = if protocol == "git" { "ssh".to_string() } else { "http".to_string() }; } if strpos(&self.inner.origin_url, ":").is_some() || strpos(&self.inner.origin_url, "/").is_some() { self.has_nonstandard_origin = true; } self.namespace = implode("/", &url_parts); self.repository = Preg::replace( r"#(\.git)$#", "", &match_ .get(&CaptureKey::ByName("repo".to_string())) .cloned() .unwrap_or_default(), ) .unwrap_or_default(); self.inner.cache = Some(Cache::new( self.inner.io.clone_box(), &format!( "{}/{}/{}/{}", self.inner .config .borrow_mut() .get("cache-repo-dir") .as_string() .unwrap_or(""), self.inner.origin_url, self.namespace, self.repository, ), None, None, false, )); self.inner.cache.as_mut().map(|c| { c.set_read_only( self.inner .config .borrow_mut() .get("cache-read-only") .as_bool() .unwrap_or(false), ) }); self.fetch_project()?; Ok(()) } /// Updates the HttpDownloader instance. /// Mainly useful for tests. /// /// @internal pub fn set_http_downloader( &mut self, http_downloader: std::rc::Rc>, ) { self.inner.http_downloader = http_downloader; } pub fn get_composer_information( &mut self, identifier: &str, ) -> Result>> { if let Some(ref mut git_driver) = self.git_driver { return git_driver.get_composer_information(identifier); } if !self.inner.info_cache.contains_key(identifier) { let composer = if self.inner.should_cache(identifier) && self .inner .cache .as_mut() .and_then(|c| c.read(identifier)) .is_some() { let res = self .inner .cache .as_mut() .and_then(|c| c.read(identifier)) .unwrap_or_default(); // TODO(phase-b): cached payload is wrapped to satisfy outer Option type Some( JsonFile::parse_json(Some(&res), None)? .as_array() .cloned() .map(|m| { m.into_iter() .map(|(k, v)| (k, *v)) .collect::>() }) .unwrap_or_default(), ) } else { let file_content = self.get_file_content("composer.json", identifier)?; let composer = VcsDriverBase::finish_base_composer_information( identifier, file_content, || self.get_change_date(identifier), )?; if self.inner.should_cache(identifier) { if let Some(ref composer_map) = composer { self.inner.cache.as_mut().map(|c| { c.write( identifier, &JsonFile::encode( &PhpMixed::Array( composer_map .clone() .into_iter() .map(|(k, v)| (k, Box::new(v))) .collect(), ), shirabe_php_shim::JSON_UNESCAPED_UNICODE | shirabe_php_shim::JSON_UNESCAPED_SLASHES, ), ) }); } } composer }; let mut composer = composer; if let Some(ref mut composer) = composer { // specials for gitlab (this data is only available if authentication is provided) if composer.contains_key("support") && !is_array(&composer.get("support").cloned().unwrap_or(PhpMixed::Null)) { composer.insert("support".to_string(), PhpMixed::Array(IndexMap::new())); } let project = self.project.clone().unwrap_or_default(); let has_web_url = project.contains_key("web_url"); let support_source_missing = !composer .get("support") .and_then(|v| v.as_array()) .map(|m| m.contains_key("source")) .unwrap_or(false); if support_source_missing && has_web_url { let label = array_search_mixed( &PhpMixed::String(identifier.to_string()), &PhpMixed::Array( self.get_tags()? .into_iter() .map(|(k, v)| (k, Box::new(PhpMixed::String(v)))) .collect(), ), true, ) .filter(|v| !matches!(v, PhpMixed::Bool(false) | PhpMixed::Null)) .or_else(|| { array_search_mixed( &PhpMixed::String(identifier.to_string()), &PhpMixed::Array( self.get_branches() .unwrap_or_default() .into_iter() .map(|(k, v)| (k, Box::new(PhpMixed::String(v)))) .collect(), ), true, ) }) .filter(|v| !matches!(v, PhpMixed::Bool(false) | PhpMixed::Null)) .unwrap_or_else(|| PhpMixed::String(identifier.to_string())); let label_str = label.as_string().unwrap_or(identifier).to_string(); let web_url = project .get("web_url") .and_then(|v| v.as_string()) .unwrap_or("") .to_string(); if let Some(support) = composer.get_mut("support").and_then(|v| match v { PhpMixed::Array(m) => Some(m), _ => None, }) { support.insert( "source".to_string(), Box::new(PhpMixed::String(sprintf( "%s/-/tree/%s", &[PhpMixed::String(web_url), PhpMixed::String(label_str)], ))), ); } } let issues_missing = !composer .get("support") .and_then(|v| v.as_array()) .map(|m| m.contains_key("issues")) .unwrap_or(false); let issues_enabled = !empty( &project .get("issues_enabled") .cloned() .unwrap_or(PhpMixed::Null), ); if issues_missing && issues_enabled && has_web_url { let web_url = project .get("web_url") .and_then(|v| v.as_string()) .unwrap_or("") .to_string(); if let Some(support) = composer.get_mut("support").and_then(|v| match v { PhpMixed::Array(m) => Some(m), _ => None, }) { support.insert( "issues".to_string(), Box::new(PhpMixed::String(sprintf( "%s/-/issues", &[PhpMixed::String(web_url)], ))), ); } } if !composer.contains_key("abandoned") && !empty(&project.get("archived").cloned().unwrap_or(PhpMixed::Null)) { composer.insert("abandoned".to_string(), PhpMixed::Bool(true)); } } self.inner .info_cache .insert(identifier.to_string(), composer); } Ok(self .inner .info_cache .get(identifier) .cloned() .unwrap_or(None)) } pub fn get_file_content(&mut self, file: &str, identifier: &str) -> Result> { if let Some(ref mut git_driver) = self.git_driver { return git_driver.get_file_content(file, identifier); } // Convert the root identifier to a cacheable commit id let mut identifier = identifier.to_string(); if !Preg::is_match(r"{[a-f0-9]{40}}i", &identifier).unwrap_or(false) { let branches = self.get_branches()?; if let Some(sha) = branches.get(&identifier) { identifier = sha.clone(); } } let resource = format!( "{}/repository/files/{}/raw?ref={}", self.get_api_url(), self.url_encode_all(file), identifier, ); let content = match self.get_contents(&resource, false) { Ok(response) => response.get_body().map(|s| s.to_string()), Err(e) => { if e.code != 404 { return Err(e.into()); } return Ok(None); } }; Ok(content) } pub fn get_change_date(&mut self, identifier: &str) -> Result>> { if let Some(ref mut git_driver) = self.git_driver { return git_driver.get_change_date(identifier); } if let Some(commit) = self.commits.get(identifier) { let committed_date = commit .get("committed_date") .and_then(|v| v.as_string()) .unwrap_or(""); return Ok(Some( DateTime::parse_from_rfc3339(committed_date) .map(|dt| dt.with_timezone(&Utc)) .unwrap_or_else(|_| Utc::now()), )); } Ok(None) } pub fn get_repository_url(&self) -> String { let project = self.project.clone().unwrap_or_default(); if !self.protocol.is_empty() { return project .get(&format!("{}_url_to_repo", self.protocol)) .and_then(|v| v.as_string()) .unwrap_or("") .to_string(); } if self.is_private { project .get("ssh_url_to_repo") .and_then(|v| v.as_string()) .unwrap_or("") .to_string() } else { project .get("http_url_to_repo") .and_then(|v| v.as_string()) .unwrap_or("") .to_string() } } pub fn get_url(&self) -> String { if let Some(ref git_driver) = self.git_driver { return git_driver.get_url(); } self.project .as_ref() .and_then(|p| p.get("web_url")) .and_then(|v| v.as_string()) .unwrap_or("") .to_string() } pub fn get_dist(&self, identifier: &str) -> Option> { let url = format!( "{}/repository/archive.zip?sha={}", self.get_api_url(), identifier ); let mut result = IndexMap::new(); result.insert("type".to_string(), PhpMixed::String("zip".to_string())); result.insert("url".to_string(), PhpMixed::String(url)); result.insert( "reference".to_string(), PhpMixed::String(identifier.to_string()), ); result.insert("shasum".to_string(), PhpMixed::String(String::new())); Some(result) } pub fn get_source(&self, identifier: &str) -> IndexMap { if let Some(ref git_driver) = self.git_driver { return git_driver .get_source(identifier) .into_iter() .map(|(k, v)| (k, PhpMixed::String(v))) .collect(); } let mut result = IndexMap::new(); result.insert("type".to_string(), PhpMixed::String("git".to_string())); result.insert( "url".to_string(), PhpMixed::String(self.get_repository_url()), ); result.insert( "reference".to_string(), PhpMixed::String(identifier.to_string()), ); result } pub fn get_root_identifier(&mut self) -> Result { if let Some(ref mut git_driver) = self.git_driver { return git_driver.get_root_identifier(); } Ok(self .project .as_ref() .and_then(|p| p.get("default_branch")) .and_then(|v| v.as_string()) .unwrap_or("") .to_string()) } pub fn get_branches(&mut self) -> Result> { if let Some(ref mut git_driver) = self.git_driver { return git_driver.get_branches(); } if self.branches.is_none() { self.branches = Some(self.get_references("branches")?); } Ok(self.branches.clone().unwrap_or_default()) } pub fn get_tags(&mut self) -> Result> { if let Some(ref mut git_driver) = self.git_driver { return git_driver.get_tags(); } if self.tags.is_none() { self.tags = Some(self.get_references("tags")?); } Ok(self.tags.clone().unwrap_or_default()) } /// @return string Base URL for GitLab API v3 pub fn get_api_url(&self) -> String { format!( "{}://{}/api/v4/projects/{}%2F{}", self.scheme, self.inner.origin_url, self.url_encode_all(&self.namespace), self.url_encode_all(&self.repository), ) } /// Urlencode all non alphanumeric characters. rawurlencode() can not be used as it does not encode `.` fn url_encode_all(&self, string: &str) -> String { let mut encoded = String::new(); let bytes: Vec = string.chars().collect(); for i in 0..bytes.len() { let character = bytes[i].to_string(); let final_character = if !ctype_alnum(&character) && !in_array( PhpMixed::String(character.clone()), &PhpMixed::List(vec![ Box::new(PhpMixed::String("-".to_string())), Box::new(PhpMixed::String("_".to_string())), ]), true, ) { format!("%{}", sprintf("%02X", &[PhpMixed::Int(ord(&character))])) } else { character }; encoded.push_str(&final_character); } encoded } /// @return string[] where keys are named references like tags or branches and the value a sha pub(crate) fn get_references(&mut self, r#type: &str) -> Result> { let per_page = 100; let mut resource: Option = Some(format!( "{}/repository/{}?per_page={}", self.get_api_url(), r#type, per_page )); let mut references: IndexMap = IndexMap::new(); loop { let response = self .get_contents(resource.as_deref().unwrap_or(""), false) .map_err(|e| anyhow::anyhow!("{}", e.message))?; let data = response.decode_json()?; if let PhpMixed::List(ref list) = data { for datum in list { if let PhpMixed::Array(ref datum_map) = **datum { let name = datum_map .get("name") .and_then(|v| v.as_string()) .unwrap_or("") .to_string(); let commit_id = datum_map .get("commit") .and_then(|v| v.as_array()) .and_then(|m| m.get("id")) .and_then(|v| v.as_string()) .unwrap_or("") .to_string(); references.insert(name, commit_id.clone()); // Keep the last commit date of a reference to avoid // unnecessary API call when retrieving the composer file. let commit_data = datum_map .get("commit") .and_then(|v| v.as_array()) .cloned() .unwrap_or_default() .into_iter() .map(|(k, v)| (k, *v)) .collect(); self.commits.insert(commit_id, commit_data); } } } let len = match data { PhpMixed::List(ref l) => l.len() as i64, PhpMixed::Array(ref a) => a.len() as i64, _ => 0, }; if len >= per_page { resource = self.get_next_page(&response); } else { resource = None; } if resource.is_none() { break; } } Ok(references) } pub(crate) fn fetch_project(&mut self) -> Result<()> { if self.project.is_some() { return Ok(()); } // we need to fetch the default branch from the api let resource = self.get_api_url(); let project = self .get_contents(&resource, true) .map_err(|e| anyhow::anyhow!("{}", e.message))? .decode_json()?; self.project = match project { PhpMixed::Array(m) => Some(m.into_iter().map(|(k, v)| (k, *v)).collect()), _ => None, }; let project = self.project.clone().unwrap_or_default(); if project.contains_key("visibility") { self.is_private = project .get("visibility") .and_then(|v| v.as_string()) .map(|s| s != "public") .unwrap_or(true); } else { // client is not authenticated, therefore repository has to be public self.is_private = false; } Ok(()) } /// @phpstan-impure /// /// @return true /// @throws \RuntimeException pub(crate) fn attempt_clone_fallback(&mut self) -> Result { let url = if !self.is_private { self.generate_public_url() } else { self.generate_ssh_url() }; // If this repository may be private and we // cannot ask for authentication credentials (because we // are not interactive) then we fallback to GitDriver. match self.setup_git_driver(&url) { Ok(()) => Ok(true), Err(e) => { self.git_driver = None; self.inner.io.write_error3( &format!( "Failed to clone the {} repository, try running in interactive mode so that you can enter your credentials", url ), true, io_interface::NORMAL, ); Err(e) } } } /// Generate an SSH URL pub(crate) fn generate_ssh_url(&self) -> String { if self.has_nonstandard_origin { return format!( "ssh://git@{}/{}/{}.git", self.inner.origin_url, self.namespace, self.repository ); } format!( "git@{}:{}/{}.git", self.inner.origin_url, self.namespace, self.repository ) } pub(crate) fn generate_public_url(&self) -> String { format!( "{}://{}/{}/{}.git", self.scheme, self.inner.origin_url, self.namespace, self.repository ) } pub(crate) fn setup_git_driver(&mut self, url: &str) -> Result<()> { let mut repo_config: IndexMap = IndexMap::new(); repo_config.insert("url".to_string(), PhpMixed::String(url.to_string())); let mut git_driver = GitDriver::new( repo_config, self.inner.io.clone_box(), self.inner.config.clone(), self.inner.http_downloader.clone(), self.inner.process.clone(), ); git_driver.initialize()?; self.git_driver = Some(git_driver); Ok(()) } pub(crate) fn get_contents( &mut self, url: &str, fetching_repo_data: bool, ) -> Result { let response_result = self.inner.get_contents(url); match response_result { Ok(response) => { if fetching_repo_data { let json = response .decode_json() .map_err(|e| TransportException::new(e.to_string(), 0))?; let json_map = match json { PhpMixed::Array(ref m) => m.clone(), _ => IndexMap::new(), }; // Accessing the API with a token with Guest (10) or Planner (15) access will return // more data than unauthenticated access but no default_branch data // accessing files via the API will then also fail if !json_map.contains_key("default_branch") && json_map.contains_key("permissions") { self.is_private = json_map .get("visibility") .and_then(|v| v.as_string()) .map(|s| s != "public") .unwrap_or(true); let mut more_than_guest_access = false; // Check both access levels (e.g. project, group) // - value will be null if no access is set // - value will be array with key access_level if set if let Some(permissions) = json_map.get("permissions").and_then(|v| v.as_array()) { for (_, permission) in permissions { if let Some(perm_map) = permission.as_array() { if let Some(level) = perm_map.get("access_level").and_then(|v| v.as_int()) { if level >= 20 { more_than_guest_access = true; } } } } } if !more_than_guest_access { self.inner.io.write_error3( "GitLab token with Guest or Planner only access detected", true, io_interface::NORMAL, ); self.attempt_clone_fallback() .map_err(|e| TransportException::new(e.to_string(), 0))?; let mut req = IndexMap::new(); req.insert("url".to_string(), PhpMixed::String("dummy".to_string())); return Ok(Response::new( req, Some(200), vec![], Some("null".to_string()), ) .unwrap() .unwrap()); } } // force auth as the unauthenticated version of the API is broken if !json_map.contains_key("default_branch") { // GitLab allows you to disable the repository inside a project to use a project only for issues and wiki if json_map .get("repository_access_level") .and_then(|v| v.as_string()) == Some("disabled") { return Err(TransportException::new( "The GitLab repository is disabled in the project".to_string(), 400, )); } if !empty( &*json_map .get("id") .cloned() .unwrap_or(Box::new(PhpMixed::Null)), ) { self.is_private = false; } return Err(TransportException::new( "GitLab API seems to not be authenticated as it did not return a default_branch" .to_string(), 401, )); } } Ok(response) } Err(e) => { let mut git_lab_util = GitLab::new( self.inner.io.clone_box(), self.inner.config.clone(), Some(self.inner.process.clone()), Some(self.inner.http_downloader.clone()), ) .map_err(|err| TransportException::new(err.to_string(), 0))?; match e.code { 401 | 404 => { // try to authorize only if we are fetching the main /repos/foo/bar data, otherwise it must be a real 404 if !fetching_repo_data { return Err(e); } if git_lab_util.authorize_oauth(&self.inner.origin_url) { return self.inner.get_contents(url); } if git_lab_util.is_oauth_expired(&self.inner.origin_url) && git_lab_util .authorize_oauth_refresh(&self.scheme, &self.inner.origin_url) .map_err(|err| TransportException::new(err.to_string(), 0))? { return self.inner.get_contents(url); } if !self.inner.io.is_interactive() { self.attempt_clone_fallback() .map_err(|err| TransportException::new(err.to_string(), 0))?; let mut req = IndexMap::new(); req.insert("url".to_string(), PhpMixed::String("dummy".to_string())); return Ok(Response::new( req, Some(200), vec![], Some("null".to_string()), ) .unwrap() .unwrap()); } self.inner.io.write_error3( &format!( "Failed to download {}/{}:{}", self.namespace, self.repository, e.message ), true, io_interface::NORMAL, ); git_lab_util.authorize_oauth_interactively( &self.scheme, &self.inner.origin_url, Some(&format!( "Your credentials are required to fetch private repository metadata ({})", self.inner.url )), ); self.inner.get_contents(url) } 403 => { if !self.inner.io.has_authentication(&self.inner.origin_url) && git_lab_util.authorize_oauth(&self.inner.origin_url) { return self.inner.get_contents(url); } if !self.inner.io.is_interactive() && fetching_repo_data { self.attempt_clone_fallback() .map_err(|err| TransportException::new(err.to_string(), 0))?; let mut req = IndexMap::new(); req.insert("url".to_string(), PhpMixed::String("dummy".to_string())); return Ok(Response::new( req, Some(200), vec![], Some("null".to_string()), ) .unwrap() .unwrap()); } Err(e) } _ => Err(e), } } } } /// Uses the config `gitlab-domains` to see if the driver supports the url for the /// repository given. pub fn supports(io: &dyn IOInterface, config: &Config, url: &str, _deep: bool) -> bool { let mut match_: IndexMap = IndexMap::new(); if !Preg::is_match_strict_groups3(Self::URL_REGEX, url, Some(&mut match_)).unwrap_or(false) { return false; } let scheme = match_ .get(&CaptureKey::ByName("scheme".to_string())) .cloned() .unwrap_or_default(); let guessed_domain = match_ .get(&CaptureKey::ByName("domain".to_string())) .cloned() .filter(|s| !s.is_empty()) .unwrap_or_else(|| { match_ .get(&CaptureKey::ByName("domain2".to_string())) .cloned() .unwrap_or_default() }); let mut url_parts: Vec = explode( "/", &match_ .get(&CaptureKey::ByName("parts".to_string())) .cloned() .unwrap_or_default(), ); if Self::determine_origin( &config.get("gitlab-domains"), guessed_domain, &mut url_parts, match_.get(&CaptureKey::ByName("port".to_string())).cloned(), ) .is_none() { return false; } if scheme == "https" && !extension_loaded("openssl") { io.write_error3( &format!( "Skipping GitLab driver for {} because the OpenSSL PHP extension is missing.", url ), true, io_interface::VERBOSE, ); return false; } true } /// Gives back the loaded /projects// result /// /// @return mixed[]|null pub fn get_repo_data(&mut self) -> Result>> { self.fetch_project()?; Ok(self.project.clone()) } pub(crate) fn get_next_page(&self, response: &Response) -> Option { let header = response.get_header("link").unwrap_or_default(); let links = explode(",", &header); for link in &links { let mut match_: IndexMap = IndexMap::new(); if Preg::is_match_strict_groups3(r#"{<(.+?)>; *rel="next"}"#, link, Some(&mut match_)) .unwrap_or(false) { return Some( match_ .get(&CaptureKey::ByIndex(1)) .cloned() .unwrap_or_default(), ); } } None } /// @param array $configuredDomains /// @param array $urlParts /// /// @return string|false fn determine_origin( configured_domains: &PhpMixed, guessed_domain: String, url_parts: &mut Vec, port_number: Option, ) -> Option { let mut guessed_domain = strtolower(&guessed_domain); if in_array( PhpMixed::String(guessed_domain.clone()), configured_domains, false, ) || (port_number.is_some() && in_array( PhpMixed::String(format!( "{}:{}", guessed_domain, port_number.as_deref().unwrap_or("") )), configured_domains, false, )) { if let Some(ref port) = port_number { return Some(format!("{}:{}", guessed_domain, port)); } return Some(guessed_domain); } if let Some(ref port) = port_number { guessed_domain.push_str(&format!(":{}", port)); } while let Some(part) = array_shift(url_parts) { guessed_domain.push_str(&format!("/{}", part)); if in_array( PhpMixed::String(guessed_domain.clone()), configured_domains, false, ) || (port_number.is_some() && in_array( PhpMixed::String( Preg::replace(r"{:\d+}", "", &guessed_domain).unwrap_or_default(), ), configured_domains, false, )) { return Some(guessed_domain); } } None } }