package jenkins.plugins.nodejs.configfiles;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
import com.cloudbees.plugins.credentials.common.StandardUsernameListBoxModel;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.cloudbees.plugins.credentials.domains.HostnameRequirement;
import hudson.Extension;
import hudson.Util;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Computer;
import hudson.model.Descriptor;
import hudson.model.ItemGroup;
import hudson.security.ACL;
import hudson.security.AccessControlled;
import hudson.util.FormValidation;
import hudson.util.FormValidation.Kind;
import hudson.util.ListBoxModel;
import jenkins.model.Jenkins;
/**
* Holder of all informations about a npm public/private registry.
* <p>
* This class keep all necessary information to access a npm registry that must
* be stored in a user config file.
* Typically information are:
* <ul>
* <li>the registry URL</li>
* <li>list of scope for the registry, used typical in private registry</li>
* <li>account credentials to access the registry</li>
* </ul>
*
* @author Nikolas Falco
* @since 1.0
*/
public class NPMRegistry extends AbstractDescribableImpl<NPMRegistry> implements Serializable {
private static final long serialVersionUID = -5199710867477461372L;
private final String url;
private final String scopes;
private final String credentialsId;
/**
* Default constructor.
*
* @param url url of a npm registry
* @param credentialsId credentials identifier
* @param scopes url-safe characters, no leading dots or underscores
*/
public NPMRegistry(@Nonnull String url, String credentialsId, String scopes) {
this.url = Util.fixEmpty(url);
this.credentialsId = Util.fixEmpty(credentialsId);
this.scopes = fixScope(Util.fixEmpty(scopes));
}
/**
* Default constructor used by jelly page for the optional block.
*
* @param url url of a npm registry
* @param credentialsId credentials identifier
* @param hasScopes if this registry was designed for a specific scope
* @param scopes url-safe characters, no leading dots or underscores
*/
@DataBoundConstructor
public NPMRegistry(@Nonnull String url, String credentialsId, boolean hasScopes, String scopes) {
this.url = Util.fixEmpty(url);
this.credentialsId = Util.fixEmpty(credentialsId);
this.scopes = hasScopes ? fixScope(Util.fixEmpty(scopes)) : null;
}
@Nullable
private String fixScope(final @Nullable String scope) {
if (scope != null && scope.startsWith("@")) {
return scope.substring(1);
}
return scope;
}
/**
* Get the registry URL
*
* @return the registry URL
*/
@Nullable
public String getUrl() {
return url;
}
/**
* Get list of scope for this registry.
* <p>
* The scope are not prefixed with {@literal @} character.
*
* @return a space separated list of scope.
*/
public String getScopes() {
return scopes;
}
public boolean isHasScopes() {
return scopes != null;
}
/**
* Provide a list of scope for this registry.
* <p>
* The scope are not prefixed with {@literal @} character.
*
* @return list of scope.
*/
public List<String> getScopesAsList() {
List<String> result = Collections.emptyList();
if (isHasScopes()) {
result = Arrays.asList(StringUtils.split(scopes));
}
return result;
}
/**
* Get list of scope for this registry.
* <p>
* The scope are not prefixed with {@literal @} character.
*
* @return a space separated list of scope.
*/
public String getCredentialsId() {
return credentialsId;
}
/**
* Perform the validation of current registry.
* <p>
* If validation pass then no {@link VerifyConfigProviderException} will be
* raised.
*
* @throws VerifyConfigProviderException
* in case this configuration is not valid.
*/
public void doVerify() throws VerifyConfigProviderException {
// recycle validations from descriptor
DescriptorImpl descriptor = new DescriptorImpl();
throwException(descriptor.doCheckUrl(getUrl()));
throwException(descriptor.doCheckScopes(isHasScopes(), getScopes()));
}
private void throwException(FormValidation form) throws VerifyConfigProviderException {
if (form.kind == Kind.ERROR) {
throw new VerifyConfigProviderException(form.getLocalizedMessage());
}
}
@Override
public String toString() {
return "url: " + url + (scopes != null ? " scopes: [" + scopes + "]" : "") + (credentialsId != null ? " credentialId: " + credentialsId : "");
}
@Extension
public static class DescriptorImpl extends Descriptor<NPMRegistry> {
public FormValidation doCheckScopes(@CheckForNull @QueryParameter final boolean hasScopes,
@CheckForNull @QueryParameter String scopes) {
scopes = Util.fixEmptyAndTrim(scopes);
if (hasScopes) {
if (scopes == null) {
return FormValidation.error("Scopes is empty");
}
StringTokenizer st = new StringTokenizer(scopes);
while (st.hasMoreTokens()) {
String aScope = st.nextToken();
if (aScope.startsWith("@")) {
if (aScope.length() == 1) {
return FormValidation.error("Invalid scope");
}
return FormValidation.warning("Remove the '@' character from scope");
}
}
}
return FormValidation.ok();
}
public FormValidation doCheckUrl(@CheckForNull @QueryParameter final String url) {
if (StringUtils.isBlank(url)) {
return FormValidation.error("Empty URL");
}
// test malformed URL
if (url.indexOf('$') == -1 && toURL(url) == null) {
return FormValidation.error("Invalid URL, should start with https://");
}
return FormValidation.ok();
}
public ListBoxModel doFillCredentialsIdItems(@AncestorInPath final ItemGroup<?> context,
@Nonnull @QueryParameter final String credentialsId,
@Nonnull @QueryParameter final String url) {
if (!hasPermission(context)) {
return new StandardUsernameListBoxModel().includeCurrentValue(credentialsId);
}
List<DomainRequirement> domainRequirements;
URL registryURL = toURL(url);
if (registryURL != null) {
domainRequirements = Collections.<DomainRequirement> singletonList(new HostnameRequirement(registryURL.getHost()));
} else {
domainRequirements = Collections.emptyList();
}
return new StandardUsernameListBoxModel()
.includeMatchingAs(ACL.SYSTEM, context, StandardUsernameCredentials.class, domainRequirements, CredentialsMatchers.always())
.includeCurrentValue(credentialsId);
}
private boolean hasPermission(final ItemGroup<?> context) {
AccessControlled controller = context instanceof AccessControlled ? (AccessControlled) context : Jenkins.getInstance();
return controller != null && controller.hasPermission(Computer.CONFIGURE);
}
@Override
public String getDisplayName() {
return "";
}
private static URL toURL(final String url) {
URL result = null;
String fixedURL = Util.fixEmptyAndTrim(url);
if (fixedURL != null) {
try {
return new URL(fixedURL);
} catch (MalformedURLException e) {
// no filter based on hostname
}
}
return result;
}
}
}