Merge with default

This commit is contained in:
René Pfeuffer
2020-01-28 07:37:55 +01:00
8 changed files with 181 additions and 2 deletions

View File

@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Set individual page title - Set individual page title
- Copy on write - Copy on write
- A new repository can be initialized with a branch (for git and mercurial) and custom files (README.md on default) - A new repository can be initialized with a branch (for git and mercurial) and custom files (README.md on default)
- Plugins are validated directly after download
### Changed ### Changed
- Stop fetching commits when it takes too long - Stop fetching commits when it takes too long
@@ -20,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Subversion revision 0 leads to error - Subversion revision 0 leads to error
- Create mock subject to satisfy legman - Create mock subject to satisfy legman
- Multiple versions of hibernate-validator caused problems when starting from plugins - Multiple versions of hibernate-validator caused problems when starting from plugins
- Page title is now set correctly
## 2.0.0-rc1 - 2019-12-02 ## 2.0.0-rc1 - 2019-12-02
### Added ### Added

View File

@@ -1,10 +1,12 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import org.apache.shiro.SecurityUtils;
import org.tmatesoft.svn.core.SVNCommitInfo; import org.tmatesoft.svn.core.SVNCommitInfo;
import org.tmatesoft.svn.core.SVNDepth; import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.SVNWCClient; import org.tmatesoft.svn.core.wc.SVNWCClient;
import org.tmatesoft.svn.core.wc.SVNWCUtil;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.SvnWorkDirFactory; import sonia.scm.repository.SvnWorkDirFactory;
@@ -38,6 +40,7 @@ public class SvnModifyCommand implements ModifyCommand {
private String commitChanges(SVNClientManager clientManager, File workingDirectory, String commitMessage) { private String commitChanges(SVNClientManager clientManager, File workingDirectory, String commitMessage) {
try { try {
clientManager.setAuthenticationManager(SVNWCUtil.createDefaultAuthenticationManager(getCurrentUserName(), new char[0]));
SVNCommitInfo svnCommitInfo = clientManager.getCommitClient().doCommit( SVNCommitInfo svnCommitInfo = clientManager.getCommitClient().doCommit(
new File[]{workingDirectory}, new File[]{workingDirectory},
false, false,
@@ -54,6 +57,14 @@ public class SvnModifyCommand implements ModifyCommand {
} }
} }
private String getCurrentUserName() {
if (SecurityUtils.getSubject() != null && SecurityUtils.getSubject().getPrincipal() != null) {
return SecurityUtils.getSubject().getPrincipal().toString();
} else {
return "SCM-Manager";
}
}
private void modifyWorkingDirectory(ModifyCommandRequest request, SVNClientManager clientManager, File workingDirectory) { private void modifyWorkingDirectory(ModifyCommandRequest request, SVNClientManager clientManager, File workingDirectory) {
for (ModifyCommandRequest.PartialRequest partialRequest : request.getRequests()) { for (ModifyCommandRequest.PartialRequest partialRequest : request.getRequests()) {
try { try {

View File

@@ -1,5 +1,8 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@@ -14,6 +17,8 @@ import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class SvnModifyCommandTest extends AbstractSvnCommandTestBase { public class SvnModifyCommandTest extends AbstractSvnCommandTestBase {
@@ -31,6 +36,18 @@ public class SvnModifyCommandTest extends AbstractSvnCommandTestBase {
svnModifyCommand = new SvnModifyCommand(context, createRepository(), workDirFactory); svnModifyCommand = new SvnModifyCommand(context, createRepository(), workDirFactory);
} }
@Before
public void initSecurityManager() {
Subject subject = mock(Subject.class);
when(subject.getPrincipal()).thenReturn("alThor");
ThreadContext.bind(subject);
}
@After
public void cleanUpSecurityManager() {
ThreadContext.unbindSubject();
}
@Test @Test
public void shouldRemoveFiles() { public void shouldRemoveFiles() {
ModifyCommandRequest request = new ModifyCommandRequest(); ModifyCommandRequest request = new ModifyCommandRequest();

View File

@@ -28,6 +28,13 @@ const PageActionContainer = styled.div`
`; `;
export default class Page extends React.Component<Props> { export default class Page extends React.Component<Props> {
componentDidUpdate() {
const { title } = this.props;
if (title && title !== document.title) {
document.title = title;
}
}
render() { render() {
const { error } = this.props; const { error } = this.props;
return ( return (

View File

@@ -19,11 +19,13 @@ class PluginInstaller {
private final SCMContextProvider context; private final SCMContextProvider context;
private final AdvancedHttpClient client; private final AdvancedHttpClient client;
private final SmpDescriptorExtractor smpDescriptorExtractor;
@Inject @Inject
public PluginInstaller(SCMContextProvider context, AdvancedHttpClient client) { public PluginInstaller(SCMContextProvider context, AdvancedHttpClient client, SmpDescriptorExtractor smpDescriptorExtractor) {
this.context = context; this.context = context;
this.client = client; this.client = client;
this.smpDescriptorExtractor = smpDescriptorExtractor;
} }
@SuppressWarnings("squid:S4790") // hashing should be safe @SuppressWarnings("squid:S4790") // hashing should be safe
@@ -34,6 +36,7 @@ class PluginInstaller {
Files.copy(input, file); Files.copy(input, file);
verifyChecksum(plugin, input.hash(), file); verifyChecksum(plugin, input.hash(), file);
verifyConditions(plugin, file);
return new PendingPluginInstallation(plugin.install(), file); return new PendingPluginInstallation(plugin.install(), file);
} catch (IOException ex) { } catch (IOException ex) {
cleanup(file); cleanup(file);
@@ -64,6 +67,20 @@ class PluginInstaller {
} }
} }
private void verifyConditions(AvailablePlugin plugin, Path file) throws IOException {
InstalledPluginDescriptor pluginDescriptor = smpDescriptorExtractor.extractPluginDescriptor(file);
if (!pluginDescriptor.getCondition().isSupported()) {
cleanup(file);
throw new PluginConditionFailedException(
pluginDescriptor.getCondition(),
String.format(
"could not load plugin %s, the plugin condition does not match",
plugin.getDescriptor().getInformation().getName()
)
);
}
}
private InputStream download(AvailablePlugin plugin) throws IOException { private InputStream download(AvailablePlugin plugin) throws IOException {
return client.get(plugin.getDescriptor().getUrl()).request().contentAsStream(); return client.get(plugin.getDescriptor().getUrl()).request().contentAsStream();
} }

View File

@@ -0,0 +1,28 @@
package sonia.scm.plugin;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
class SmpDescriptorExtractor {
InstalledPluginDescriptor extractPluginDescriptor(Path file) throws IOException {
try (ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(file), StandardCharsets.UTF_8)) {
ZipEntry nextEntry;
while ((nextEntry = zipInputStream.getNextEntry()) != null) {
if ("META-INF/scm/plugin.xml".equals(nextEntry.getName())) {
JAXBContext context = JAXBContext.newInstance(ScmModule.class, InstalledPluginDescriptor.class);
return (InstalledPluginDescriptor) context.createUnmarshaller().unmarshal(zipInputStream);
}
}
} catch (JAXBException e) {
throw new IOException("failed to read descriptor file META-INF/scm/plugin.xml from plugin", e);
}
throw new IOException("Missing plugin descriptor META-INF/scm/plugin.xml in download package");
}
}

View File

@@ -32,18 +32,23 @@ class PluginInstallerTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS) @Mock(answer = Answers.RETURNS_DEEP_STUBS)
private AdvancedHttpClient client; private AdvancedHttpClient client;
@Mock
private SmpDescriptorExtractor extractor;
@InjectMocks @InjectMocks
private PluginInstaller installer; private PluginInstaller installer;
private Path directory; private Path directory;
@BeforeEach @BeforeEach
void setUpContext(@TempDirectory.TempDir Path directory) { void setUpContext(@TempDirectory.TempDir Path directory) throws IOException {
this.directory = directory; this.directory = directory;
lenient().when(context.resolve(any())).then(ic -> { lenient().when(context.resolve(any())).then(ic -> {
Path arg = ic.getArgument(0); Path arg = ic.getArgument(0);
return directory.resolve(arg); return directory.resolve(arg);
}); });
InstalledPluginDescriptor supportedPlugin = createPluginDescriptor(true);
lenient().when(extractor.extractPluginDescriptor(any())).thenReturn(supportedPlugin);
} }
@Test @Test
@@ -105,6 +110,15 @@ class PluginInstallerTest {
assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist(); assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist();
} }
@Test
void shouldFailForUnsupportedPlugin() throws IOException {
mockContent("42");
InstalledPluginDescriptor supportedPlugin = createPluginDescriptor(false);
when(extractor.extractPluginDescriptor(any())).thenReturn(supportedPlugin);
assertThrows(PluginConditionFailedException.class, () -> installer.install(createGitPlugin()));
assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist();
}
private AvailablePlugin createPlugin(String name, String url, String checksum) { private AvailablePlugin createPlugin(String name, String url, String checksum) {
PluginInformation information = new PluginInformation(); PluginInformation information = new PluginInformation();
@@ -114,4 +128,11 @@ class PluginInstallerTest {
); );
return new AvailablePlugin(descriptor); return new AvailablePlugin(descriptor);
} }
private InstalledPluginDescriptor createPluginDescriptor(boolean supported) {
InstalledPluginDescriptor installedPluginDescriptor = mock(InstalledPluginDescriptor.class, RETURNS_DEEP_STUBS);
lenient().when(installedPluginDescriptor.getCondition().isSupported()).thenReturn(supported);
lenient().when(installedPluginDescriptor.getInformation().getId()).thenReturn("scm-git-plugin");
return installedPluginDescriptor;
}
} }

View File

@@ -0,0 +1,76 @@
package sonia.scm.plugin;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(TempDirectory.class)
class SmpDescriptorExtractorTest {
private static final String PLUGIN_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
"<plugin>\n" +
"\n" +
" <scm-version>2</scm-version>\n" +
"\n" +
" <information>\n" +
" <displayName>Test</displayName>\n" +
" <author>Cloudogu GmbH</author>\n" +
" <category>Testing</category>\n" +
" <name>scm-test-plugin</name>\n" +
"<version>2.0.0</version>\n" +
"<description>Collects information for support cases</description>\n" +
"</information>\n" +
"\n" +
" <conditions>\n" +
" <min-version>2.0.0-rc1</min-version>\n" +
" </conditions>\n" +
"\n" +
" <resources>\n" +
" <script>assets/scm-support-plugin.bundle.js</script>\n" +
" </resources>\n" +
"\n" +
"</plugin>\n";
@Test
void shouldExtractPluginXml(@TempDirectory.TempDir Path tempDir) throws IOException {
Path pluginFile = createZipFile(tempDir, "META-INF/scm/plugin.xml", PLUGIN_XML);
InstalledPluginDescriptor installedPluginDescriptor = new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile);
Assertions.assertThat(installedPluginDescriptor.getInformation().getName()).isEqualTo("scm-test-plugin");
}
@Test
void shouldFailWithoutPluginXml(@TempDirectory.TempDir Path tempDir) throws IOException {
Path pluginFile = createZipFile(tempDir, "META-INF/wrong/plugin.xml", PLUGIN_XML);
assertThrows(IOException.class, () -> new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile));
}
@Test
void shouldFailWithIllegalPluginXml(@TempDirectory.TempDir Path tempDir) throws IOException {
Path pluginFile = createZipFile(tempDir, "META-INF/scm/plugin.xml", "<not><parsable>content</parsable></not>");
assertThrows(IOException.class, () -> new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile));
}
Path createZipFile(Path tempDir, String internalFileName, String content) throws IOException {
Path pluginFile = tempDir.resolve("scm-test-plugin.smp");
ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(pluginFile), UTF_8);
zipOutputStream.putNextEntry(new ZipEntry(internalFileName));
zipOutputStream.write(content.getBytes(UTF_8));
zipOutputStream.closeEntry();
zipOutputStream.close();
return pluginFile;
}
}