Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2007-2026 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.craftercms.studio.api.v2.exception.publish;

/**
* Exception thrown when a publish operation is attempted with an invalid publishing target.
*/
public class InvalidTargetException extends PublishException {
private final String[] validTargets;

public InvalidTargetException(String message, String... validTargets) {
super(message);
this.validTargets = validTargets;
}

public String[] getValidTargets() {
return validTargets;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2007-2024 Crafter Software Corporation. All Rights Reserved.
* Copyright (C) 2007-2026 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
Expand Down Expand Up @@ -49,6 +49,7 @@
import org.craftercms.studio.api.v2.exception.marketplace.PluginAlreadyInstalledException;
import org.craftercms.studio.api.v2.exception.marketplace.PluginInstallationException;
import org.craftercms.studio.api.v2.exception.publish.InvalidPackageStateException;
import org.craftercms.studio.api.v2.exception.publish.InvalidTargetException;
import org.craftercms.studio.api.v2.exception.publish.PackageAlreadyApprovedException;
import org.craftercms.studio.api.v2.exception.publish.PublishPackageNotFoundException;
import org.craftercms.studio.api.v2.exception.repository.InvalidRemoteException;
Expand Down Expand Up @@ -641,6 +642,17 @@ public Result handleException(HttpServletRequest request, PackageAlreadyApproved
return result;
}

@ExceptionHandler
@ResponseStatus(BAD_REQUEST)
public Result handleException(HttpServletRequest request, InvalidTargetException e) {
ApiResponse response = new ApiResponse(ApiResponse.INVALID_PUBLISH_TARGET);
handleExceptionInternal(request, e, response);
ResultOne<String[]> result = new ResultOne<>();
result.setResponse(response);
result.setEntity(RESULT_KEY_VALID_TARGETS, e.getValidTargets());
return result;
}

@ExceptionHandler
@ResponseStatus(NOT_FOUND)
public Result handleException(HttpServletRequest request, RepositoryNotFoundException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public final class ResultConstants {
public static final String RESULT_KEY_PUBLISH_STATUS = "publishingStatus";
public static final String RESULT_KEY_PUBLISH_HISTORY = "documents";
public static final String RESULT_KEY_HAS_INITIAL_PUBLISH = "hasInitialPublish";
public static final String RESULT_KEY_VALID_TARGETS = "validTargets";

/* Translation controller */
public static final String RESULT_KEY_CONFIG = "config";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.craftercms.studio.api.v2.event.publish.RequestPublishEvent;
import org.craftercms.studio.api.v2.event.workflow.WorkflowEvent;
import org.craftercms.studio.api.v2.exception.InvalidParametersException;
import org.craftercms.studio.api.v2.exception.publish.InvalidTargetException;
import org.craftercms.studio.api.v2.repository.GitContentRepository;
import org.craftercms.studio.api.v2.security.publish.PublishPackageAvailableActionResolver;
import org.craftercms.studio.api.v2.service.audit.AuditService;
Expand Down Expand Up @@ -553,11 +554,12 @@ public long requestPublish(final String siteId, final String publishingTarget, f
/**
* Routes the request to the appropriate method based on the site's publishing repo status.
*/
private long routePackageSubmission(final String siteId, final String publishingTarget,
protected long routePackageSubmission(final String siteId, final String publishingTarget,
final List<PublishRequestPath> paths, final List<String> commitIds,
final Instant schedule, final String title, final String comment,
final boolean requestApproval, final boolean publishAll)
throws ServiceLayerException, AuthenticationException {
validateTarget(siteId, publishingTarget);
Site site = siteService.getSite(siteId);
String lockKey = getSandboxRepoLockKey(site.getSiteId());
generalLockService.lock(lockKey);
Expand All @@ -580,6 +582,29 @@ private long routePackageSubmission(final String siteId, final String publishing
}
}

/**
* Validate the publishing target. If the target is not valid, an exception will be thrown.
*
* @param siteId the site id
* @param publishingTarget the publishing target to validate
* @throws SiteNotFoundException if the site is not found
* @throws InvalidTargetException if the publishing target is not valid for the site
*/
protected void validateTarget(String siteId, String publishingTarget) throws SiteNotFoundException, InvalidTargetException {
String liveTarget = servicesConfig.getLiveEnvironment(siteId);
if (!CS.equals(publishingTarget, liveTarget)) {
if (!servicesConfig.isStagingEnvironmentEnabled(siteId)) {
throw new InvalidTargetException(format("Invalid publishing target '%s'. The only valid target for site '%s' is: '%s'",
publishingTarget, siteId, liveTarget), liveTarget);
}
String stagingTarget = servicesConfig.getStagingEnvironment(siteId);
if (!CS.equals(publishingTarget, stagingTarget)) {
throw new InvalidTargetException(format("Invalid publishing target '%s'. Valid targets for site '%s' are: '%s' and '%s'",
publishingTarget, siteId, liveTarget, stagingTarget), liveTarget, stagingTarget);
}
}
}

@Override
public void setApplicationContext(@NonNull final ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2007-2025 Crafter Software Corporation. All Rights Reserved.
* Copyright (C) 2007-2026 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
Expand Down Expand Up @@ -113,6 +113,8 @@ public class ApiResponse {
"Check the current publish package state", StringUtils.EMPTY);
public static final ApiResponse EMPTY_CHANGESET = new ApiResponse(7010, "Empty changeset",
"Site repository already contains the specified content in the given path", StringUtils.EMPTY);
public static final ApiResponse INVALID_PUBLISH_TARGET = new ApiResponse(7011, "Invalid publish target",
"Check if you sent in the right publish target", StringUtils.EMPTY);

// 8000 - 9000
public static final ApiResponse PUBLISHING_DISABLED = new ApiResponse(8000, "Publishing is disabled",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright (C) 2007-2026 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.craftercms.studio.impl.v2.service.publish.internal;

import org.craftercms.studio.api.v1.exception.ServiceLayerException;
import org.craftercms.studio.api.v1.exception.security.AuthenticationException;
import org.craftercms.studio.api.v1.service.GeneralLockService;
import org.craftercms.studio.api.v1.service.configuration.ServicesConfig;
import org.craftercms.studio.api.v2.dal.Site;
import org.craftercms.studio.api.v2.exception.publish.InvalidTargetException;
import org.craftercms.studio.api.v2.service.site.SitesService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;

import static java.util.Collections.emptyList;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class PublishServiceInternalImplTest {

@Mock
private ServicesConfig servicesConfig;
@Mock
private SitesService sitesService;
@Mock
private GeneralLockService generalLockService;
@InjectMocks
@Spy
private PublishServiceInternalImpl service;

@Test(expected = InvalidTargetException.class)
public void testStagingFailsIfNotEnabled() throws AuthenticationException, ServiceLayerException {
String siteId = "site1";
when(servicesConfig.getLiveEnvironment(siteId)).thenReturn("live");

service.routePackageSubmission(siteId, "staging", emptyList(), emptyList(),
null, "Publish test", "Testing with invalid target", false, false);
}

@Test
public void testStagingPassIfEnabled() throws AuthenticationException, ServiceLayerException {
String siteId = "site1";
doReturn(4L).when(service).buildInitialPublishPackage(any(Site.class), anyString(), eq(false), anyString(), anyString());
Site site = mock(Site.class);
when(site.getSiteId()).thenReturn(siteId);
when(sitesService.getSite(siteId)).thenReturn(site);
when(servicesConfig.getLiveEnvironment(siteId)).thenReturn("live");
when(servicesConfig.getStagingEnvironment(siteId)).thenReturn("staging");
when(servicesConfig.isStagingEnvironmentEnabled(siteId)).thenReturn(true);

service.routePackageSubmission(siteId, "staging", emptyList(), emptyList(),
null, "Publish test", "Testing with invalid target", false, false);
verify(service).validateTarget(siteId, "staging");
}

@Test(expected = InvalidTargetException.class)
public void testInvalidTargetFails() throws AuthenticationException, ServiceLayerException {
String siteId = "site1";
when(servicesConfig.getLiveEnvironment(siteId)).thenReturn("live");

service.routePackageSubmission(siteId, "alive", emptyList(), emptyList(),
null, "Publish test", "Testing with invalid target", false, false);
}

@Test
public void testMatchingTargetPass() throws AuthenticationException, ServiceLayerException {
String siteId = "site1";
doReturn(4L).when(service).buildInitialPublishPackage(any(Site.class), anyString(), eq(false), anyString(), anyString());
Site site = mock(Site.class);
when(site.getSiteId()).thenReturn(siteId);
when(sitesService.getSite(siteId)).thenReturn(site);

when(servicesConfig.getLiveEnvironment(siteId)).thenReturn("thelivetarget");

service.routePackageSubmission(siteId, "thelivetarget", emptyList(), emptyList(),
null, "Publish test", "Testing with invalid target", false, false);

verify(service).validateTarget(siteId, "thelivetarget");
}
}