External url for versions (#129)

* work

* significant progress

* some work

* some more work

* redirects & warnings

* safe host list

* external url download counter
This commit is contained in:
Jake Potrebic 2020-09-20 16:02:44 -07:00 committed by GitHub
parent 0946b4bb0f
commit 8e148a82b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 738 additions and 287 deletions

View File

@ -0,0 +1,66 @@
<template>
<div>
<div v-show="loading">
<i class="fas fa-spinner fa-spin"></i>
<span>Loading platforms for you...</span>
</div>
<div v-show="!loading && selectedPlatform == null">
<span v-for="platform in platforms" @click="select(platform)" style="cursor: pointer;">
<Tag :name="platform.name" :color="platform.tag" />
</span>
</div>
<div v-if="!loading && selectedPlatform != null">
<span @click="unselect">
<i class="fas fa-times-circle"></i>
</span>
<input type="hidden" name="platform" :value="selectedPlatform.id" form="form-publish">
<Tag :name="selectedPlatform.name" :color="selectedPlatform.tag" />
<div>
<template v-for="(v, index) in selectedPlatform.possibleVersions">
<label :for="'version-' + v" style="margin-left: 10px;">{{ v }}</label>
<input form="form-publish" :id="'version-' + v" type="checkbox" name="versions" :value="v">
</template>
</div>
</div>
</div>
</template>
<script>
import Tag from './components/Tag';
export default {
name: 'platform-choice',
components: {
Tag
},
data() {
return {
platforms: [],
loading: true,
selectedPlatform: null
}
},
methods: {
select: function(platform) {
this.selectedPlatform = platform;
},
unselect: function() {
this.selectedPlatform = null;
}
},
created() {
var self = this;
$.ajax({
url: '/api/v1/platforms',
dataType: 'json',
complete: function() {
self.loading = false;
},
success: function(platforms) {
self.platforms = platforms;
}
})
}
}
</script>

View File

@ -11,7 +11,7 @@
</div>
<div v-show="!loading">
<div class="list-group">
<a v-for="version in versions" :href="routes.Versions.show(htmlDecode(projectOwner), htmlDecode(projectSlug), version.name).absoluteURL()" class="list-group-item"
<a v-for="version in versions" :href="routes.Versions.show(htmlDecode(projectOwner), htmlDecode(projectSlug), version.name).absoluteURL()" class="list-group-item list-group-item-action"
:class="[classForVisibility(version.visibility)]">
<div class="container-fluid">
<div class="row">
@ -37,7 +37,12 @@
</div>
<div class="col-12">
<i class="far fa-fw fa-file"></i>
{{ formatSize(version.file_info.size_bytes) }}
<span v-if="version.file_info.size_bytes">
{{ formatSize(version.file_info.size_bytes) }}
</span>
<span v-else>
(external)
</span>
</div>
</div>
</div>

View File

@ -21,6 +21,7 @@ import {
faDownload,
faEdit,
faExclamationCircle,
faExclamationTriangle,
faExternalLinkAlt,
faEye,
faEyeSlash,
@ -92,6 +93,6 @@ library.add(fasStar, fasGem, faEye, faDownload, faServer, faComment, faWrench, f
faCheck, faReply, faSave, faTimes, faPencilAlt, faArrowLeft, faCog, faPlayCircle, faEdit, faKey, faCalendar, faFile,
faUpload, faPaperPlane, faPlusSquare, faSearch, farStar, faExternalLinkAlt, faMinusSquare, faBug, faFileArchive,
faTerminal, faStopCircle, faClipboard, faWindowClose, faSadTear, faUnlockAlt, farGem, faLink, farCheckCircle, faClock,
faInfo, fasCheckCircle, faTimesCircle, faEyeSlash, faUserTag, faTags);
faInfo, fasCheckCircle, faTimesCircle, faEyeSlash, faUserTag, faTags, faExclamationTriangle);
dom.watch();

View File

@ -0,0 +1,7 @@
import Vue from 'vue'
const root = require('../PlatformChoice.vue').default;
const app = new Vue({
el: '#platform-choice',
render: createElement => createElement(root)
});

View File

@ -120,10 +120,11 @@ function initModal() {
function initColorPicker() {
var modal = getModal();
// Initialize popover to stay opened when hovered over
modal.find(".color-picker").popover({
var colorPicker = modal.find(".color-picker");
colorPicker.popover({
html: true,
trigger: 'manual',
container: $(this).attr('id'),
container: colorPicker.attr('id'),
placement: 'right',
sanitize: false,
content: function() {

View File

@ -63,8 +63,9 @@ function reset() {
var bs = alert.find('.alert');
bs.removeClass('alert-danger').addClass('alert-info');
bs.find('[data-fa-i2svg]').attr('data-prefix', 'far');
bs.find('[data-fa-i2svg]').removeClass('fa-exclamation-circle').addClass('fa-file-archive').tooltip('destroy');
if (bs.find('[data-fa-i2svg]').data('ui-tooltip')) {
bs.find('[data-fa-i2svg]').removeClass('fa-exclamation-circle').addClass('fa-file-archive').tooltip('destroy');
}
return alert;
}
@ -78,6 +79,7 @@ $(function() {
}
$('#pluginFile').on('change', function() {
var alert = reset();
if (this.files.length === 0) {
$('#form-upload')[0].reset();
@ -104,6 +106,8 @@ $(function() {
alert.find('.file-size').text(filesize(this.files[0].size));
alert.fadeIn('slow');
$("#form-url-upload").css('display', 'none');
if(success) {
var alertInner = alert.find('.alert');
var button = alert.find('button');
@ -115,9 +119,10 @@ $(function() {
icon.addClass('fa-upload');
var newTitle = 'Upload plugin';
button.tooltip('hide')
.attr('data-original-title', newTitle)
.tooltip('fixTitle');
button
.tooltip('option', 'hide', false)
.data('original-title', newTitle)
.tooltip();
}
});
});

View File

@ -5,12 +5,15 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@ConfigurationProperties(prefix = "hangar.security")
public class HangarSecurityConfig {
private boolean secure = false;
private long unsafeDownloadMaxAge = 600000;
private List<String> safeDownloadHosts = List.of();
@NestedConfigurationProperty
public SecurityApiConfig api;
@ -125,6 +128,14 @@ public class HangarSecurityConfig {
this.unsafeDownloadMaxAge = unsafeDownloadMaxAge;
}
public List<String> getSafeDownloadHosts() {
return safeDownloadHosts;
}
public void setSafeDownloadHosts(List<String> safeDownloadHosts) {
this.safeDownloadHosts = safeDownloadHosts;
}
public SecurityApiConfig getApi() {
return api;
}

View File

@ -15,6 +15,7 @@ import io.papermc.hangar.db.model.ProjectsTable;
import io.papermc.hangar.db.model.UserProjectRolesTable;
import io.papermc.hangar.db.model.UsersTable;
import io.papermc.hangar.model.Category;
import io.papermc.hangar.model.Platform;
import io.papermc.hangar.model.Role;
import io.papermc.hangar.model.SsoSyncData;
import io.papermc.hangar.model.TagColor;
@ -209,11 +210,22 @@ public class Apiv1Controller extends HangarController {
@GetMapping("/v1/tags/{tagId}")
public ResponseEntity<ObjectNode> tagColor(@PathVariable("tagId") TagColor tag) {
ObjectNode tagColor = mapper.createObjectNode();
tagColor.set("id", mapper.valueToTree(tag.ordinal()));
tagColor.set("backgroundColor", mapper.valueToTree(tag.getBackground()));
tagColor.set("foregroundColor", mapper.valueToTree(tag.getForeground()));
return ResponseEntity.of(Optional.of(tagColor));
return ResponseEntity.of(Optional.of(writeTagColor(tag)));
}
@GetMapping("/v1/platforms")
public ResponseEntity<ArrayNode> platformList() {
ArrayNode platforms = mapper.createArrayNode();
for (Platform pl : Platform.getValues()) {
ObjectNode platformObj = mapper.createObjectNode()
.put("id", pl.ordinal())
.put("name", pl.getName())
.put("category", pl.getPlatformCategory().getName());
platformObj.set("possibleVersions", mapper.valueToTree(pl.getPossibleVersions()));
platformObj.set("tag", writeTagColor(pl.getTagColor()));
platforms.add(platformObj);
}
return ResponseEntity.ok(platforms);
}
@GetMapping("/v1/users")
@ -232,6 +244,13 @@ public class Apiv1Controller extends HangarController {
return ResponseEntity.ok((ObjectNode) userObj);
}
private ObjectNode writeTagColor(TagColor tagColor) {
return mapper.createObjectNode()
.put("id", tagColor.ordinal())
.put("background", tagColor.getBackground())
.put("foreground", tagColor.getForeground());
}
private ArrayNode writeUsers(List<UsersTable> usersTables) {
ArrayNode usersArray = mapper.createArrayNode();
List<Long> userIds = usersTables.stream().map(UsersTable::getId).collect(Collectors.toList());

View File

@ -50,7 +50,7 @@ public class ChannelsController extends HangarController {
@Secured("ROLE_USER")
@PostMapping("/{author}/{slug}/channels")
public ModelAndView create(@PathVariable String author, @PathVariable String slug, @RequestParam("channel-input") String channelId, @RequestParam("channel-color-input") Color channelColor) {
channelService.addProjectChannel(projectsTable.get().getId(), channelId, channelColor);
channelService.addProjectChannel(projectsTable.get().getId(), channelId, channelColor, false);
return Routes.CHANNELS_SHOW_LIST.getRedirect(author, slug);
}

View File

@ -19,7 +19,9 @@ import io.papermc.hangar.exceptions.HangarException;
import io.papermc.hangar.model.Color;
import io.papermc.hangar.model.DownloadType;
import io.papermc.hangar.model.NamedPermission;
import io.papermc.hangar.model.Platform;
import io.papermc.hangar.model.Visibility;
import io.papermc.hangar.model.generated.Dependency;
import io.papermc.hangar.model.generated.ReviewState;
import io.papermc.hangar.model.viewhelpers.ProjectData;
import io.papermc.hangar.model.viewhelpers.ScopedProjectData;
@ -31,6 +33,7 @@ import io.papermc.hangar.service.DownloadsService;
import io.papermc.hangar.service.StatsService;
import io.papermc.hangar.service.UserActionLogService;
import io.papermc.hangar.service.VersionService;
import io.papermc.hangar.service.plugindata.PluginFileWithData;
import io.papermc.hangar.service.pluginupload.PendingVersion;
import io.papermc.hangar.service.pluginupload.PluginUploadService;
import io.papermc.hangar.service.pluginupload.ProjectFiles;
@ -70,6 +73,7 @@ import org.springframework.web.util.WebUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URI;
import java.nio.file.Path;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
@ -77,6 +81,7 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.regex.Pattern;
@Controller
public class VersionsController extends HangarController {
@ -147,6 +152,9 @@ public class VersionsController extends HangarController {
public Object downloadJarById(@PathVariable String pluginId, @PathVariable String name, @RequestParam Optional<String> token) {
ProjectsTable project = projectsTable.get();
ProjectVersionsTable pvt = projectVersionsTable.get();
if (pvt.isExternal()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No jar for this version found");
}
if (token.isPresent()) {
confirmDownload0(DownloadType.JAR_FILE, token);
return sendJar(project, pvt, token.get(), true);
@ -217,6 +225,38 @@ public class VersionsController extends HangarController {
return _showCreator(author, slug, pendingVersion);
}
private final Pattern URL_PATTERN = Pattern.compile("^https?://[^\\s$.?#].[^\\s]*$");
@ProjectPermission(NamedPermission.CREATE_VERSION)
@UserLock(route = Routes.PROJECTS_SHOW, args = "{#author, #slug}")
@Secured("ROLE_USER")
@PostMapping(value = "/{author}/{slug}/versions/new/create", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ModelAndView create(@PathVariable String author, @PathVariable String slug, @RequestParam String externalUrl) {
ProjectData projData = projectData.get();
if (!URL_PATTERN.matcher(externalUrl).matches()) { // TODO check list of allowed hosts
ModelAndView mav = _showCreator(author, slug, null);
return fillModel(AlertUtil.showAlert(mav, AlertType.ERROR, "error.invalidUrl"));
}
ProjectChannelsTable channel = channelService.getFirstChannel(projData.getProject());
PendingVersion pendingVersion = new PendingVersion(
null,
null,
null,
projData.getProject().getId(),
null,
null,
null,
projData.getProjectOwner().getId(),
channel.getName(),
channel.getColor(),
null,
externalUrl,
false
);
return _showCreator(author, slug, pendingVersion);
}
@ProjectPermission(NamedPermission.CREATE_VERSION)
@UserLock(route = Routes.PROJECTS_SHOW, args = "{#author, #slug}")
@Secured("ROLE_USER")
@ -274,10 +314,13 @@ public class VersionsController extends HangarController {
@ProjectPermission(NamedPermission.CREATE_VERSION)
@UserLock(route = Routes.PROJECTS_SHOW, args = "{#author, #slug}")
@Secured("ROLE_USER")
@PostMapping(value = "/{author}/{slug}/versions/{version:.+}", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@PostMapping(value = "/{author}/{slug}/versions/publish", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ModelAndView publish(@PathVariable String author,
@PathVariable String slug,
@PathVariable("version") String versionName,
@RequestParam String versionString,
@RequestParam String externalUrl,
@RequestParam Platform platform,
@RequestParam(required = false) String versionDescription,
@RequestParam(defaultValue = "false") boolean unstable,
@RequestParam(defaultValue = "false") boolean recommended,
@RequestParam("channel-input") String channelInput,
@ -287,14 +330,35 @@ public class VersionsController extends HangarController {
@RequestParam(required = false) String content,
@RequestParam List<String> versions,
RedirectAttributes attributes) {
ProjectsTable project = projectsTable.get();
PendingVersion pendingVersion = new PendingVersion(
versionString,
List.of(new Dependency(platform.getDependencyId(), String.join(",", versions), true)),
versionDescription,
project.getId(),
null,
null,
null,
project.getOwnerId(),
channelInput,
channelColorInput,
null,
externalUrl,
forumPost
);
return _publish(author, slug, versionString, unstable, recommended, channelInput, channelColorInput, versions, forumPost, nonReviewed,content, pendingVersion, platform, attributes);
}
private ModelAndView _publish(String author, String slug, String versionName, boolean unstable, boolean recommended, String channelInput, Color channelColorInput, List<String> versions, boolean forumPost, boolean isNonReviewed, String content, PendingVersion pendingVersion, Platform platform, RedirectAttributes attributes) {
ProjectData projData = projectData.get();
Color channelColor = channelColorInput == null ? hangarConfig.channels.getColorDefault() : channelColorInput;
PendingVersion pendingVersion = cacheManager.getCache(CacheConfig.PENDING_VERSION_CACHE).get(projData.getProject().getId() + "/" + versionName, PendingVersion.class);
if (pendingVersion == null) {
AlertUtil.showAlert(attributes, AlertUtil.AlertType.ERROR, "error.plugin.timeout");
return Routes.VERSIONS_SHOW_CREATOR.getRedirect(author, slug);
}
if (versions.stream().anyMatch(s -> !pendingVersion.getPlugin().getPlatform().getPossibleVersions().contains(s))) {
if (versions.stream().anyMatch(s -> !platform.getPossibleVersions().contains(s))) {
AlertUtil.showAlert(attributes, AlertType.ERROR, "error.plugin.invalidVersion");
return Routes.VERSIONS_SHOW_CREATOR.getRedirect(author, slug);
}
@ -316,7 +380,7 @@ public class VersionsController extends HangarController {
AlertUtil.showAlert(attributes, AlertUtil.AlertType.ERROR, alertMsg, alertArgs);
return Routes.VERSIONS_SHOW_CREATOR.getRedirect(author, slug);
}
channel = channelService.addProjectChannel(projData.getProject().getId(), channelInput.trim(), channelColor);
channel = channelService.addProjectChannel(projData.getProject().getId(), channelInput.trim(), channelColor, isNonReviewed);
} else {
channel = channelOptional.get();
}
@ -326,7 +390,8 @@ public class VersionsController extends HangarController {
channel.getColor(),
forumPost,
content,
versions
versions,
platform
);
if (versionService.exists(newPendingVersion)) {
@ -354,6 +419,42 @@ public class VersionsController extends HangarController {
userActionLogService.version(request, LoggedActionType.VERSION_UPLOADED.with(VersionContext.of(projData.getProject().getId(), version.getId())), "published", "");
return Routes.VERSIONS_SHOW.getRedirect(author, slug, versionName);
}
@ProjectPermission(NamedPermission.CREATE_VERSION)
@UserLock(route = Routes.PROJECTS_SHOW, args = "{#author, #slug}")
@Secured("ROLE_USER")
@PostMapping(value = "/{author}/{slug}/versions/{version:.+}", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ModelAndView publish(@PathVariable String author,
@PathVariable String slug,
@PathVariable("version") String versionName,
@RequestParam(defaultValue = "false") boolean unstable,
@RequestParam(defaultValue = "false") boolean recommended,
@RequestParam("channel-input") String channelInput,
@RequestParam(value = "channel-color-input", required = false) Color channelColorInput,
@RequestParam(value = "non-reviewed", defaultValue = "false") boolean nonReviewed,
@RequestParam(value = "forum-post", defaultValue = "false") boolean forumPost,
@RequestParam(required = false) String content,
@RequestParam List<String> versions,
RedirectAttributes attributes) {
ProjectsTable project = projectsTable.get();
PendingVersion pendingVersion = cacheManager.getCache(CacheConfig.PENDING_VERSION_CACHE).get(project.getId() + "/" + versionName, PendingVersion.class);
return _publish(author,
slug,
versionName,
unstable,
recommended,
channelInput,
channelColorInput,
versions,
forumPost,
nonReviewed,
content,
pendingVersion,
Optional.ofNullable(pendingVersion).map(PendingVersion::getPlugin).map(PluginFileWithData::getPlatform).orElse(null),
attributes
);
}
@GetMapping("/{author}/{slug}/versions/{version:.*}")
@ -400,7 +501,7 @@ public class VersionsController extends HangarController {
}
@GetMapping("/{author}/{slug}/versions/{version}/confirm")
public Object showDownloadConfirm(@PathVariable String author, @PathVariable String slug, @PathVariable String version, @RequestParam(defaultValue = "0") DownloadType downloadType, @RequestParam Optional<Boolean> api, @RequestParam(required = false) String dummy) {
public Object showDownloadConfirm(@PathVariable String author, @PathVariable String slug, @PathVariable String version, @RequestParam(defaultValue = "0") DownloadType downloadType, @RequestParam(defaultValue = "false") boolean api, @RequestParam(required = false) String dummy) {
ProjectVersionsTable versionsTable = projectVersionsTable.get();
ProjectsTable project = projectsTable.get();
if (versionsTable.getReviewState() == ReviewState.REVIEWED) {
@ -424,15 +525,16 @@ public class VersionsController extends HangarController {
}
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"README.txt\"");
if (api.orElse(false)) {
if (api) {
removeAddWarnings(address, expiration, token);
headers.setContentType(MediaType.APPLICATION_JSON);
ObjectNode objectNode = mapper.createObjectNode();
objectNode.put("message", apiMsg);
objectNode.put("post", Routes.VERSIONS_CONFIRM_DOWNLOAD.getRouteUrl(author, slug, version, downloadType.ordinal() + "", token, null));
objectNode.put("url", Routes.VERSIONS_DOWNLOAD_JAR_BY_ID.getRouteUrl(project.getPluginId(), versionsTable.getVersionString(), token));
objectNode.put("curl", curlInstruction);
objectNode.put("token", token);
String downloadUrl = versionsTable.getExternalUrl() != null ? versionsTable.getExternalUrl() : Routes.VERSIONS_DOWNLOAD_JAR_BY_ID.getRouteUrl(project.getPluginId(), versionsTable.getVersionString(), token);
ObjectNode objectNode = mapper.createObjectNode()
.put("message", apiMsg)
.put("post", Routes.VERSIONS_CONFIRM_DOWNLOAD.getRouteUrl(author, slug, version, downloadType.ordinal() + "", token, null))
.put("url", downloadUrl)
.put("curl", curlInstruction)
.put("token", token);
return new ResponseEntity<>(objectNode.toPrettyString(), headers, HttpStatus.MULTIPLE_CHOICES);
} else {
Optional<String> userAgent = Optional.ofNullable(request.getHeader(HttpHeaders.USER_AGENT)).map(String::toLowerCase);
@ -467,8 +569,9 @@ public class VersionsController extends HangarController {
@PostMapping(value = "/{author}/{slug}/versions/{version}/confirm", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
@ResponseBody
public Object confirmDownload(@PathVariable String author, @PathVariable String slug, @PathVariable String version, @RequestParam(defaultValue = "0") DownloadType downloadType, @RequestParam Optional<String> token, @RequestParam(required = false) String dummy) {
if (projectVersionsTable.get().getReviewState() == ReviewState.REVIEWED) {
public ModelAndView confirmDownload(@PathVariable String author, @PathVariable String slug, @PathVariable String version, @RequestParam(defaultValue = "0") DownloadType downloadType, @RequestParam Optional<String> token, @RequestParam(required = false) String dummy) {
ProjectVersionsTable pvt = projectVersionsTable.get();
if (pvt.getReviewState() == ReviewState.REVIEWED) {
return Routes.PROJECTS_SHOW.getRedirect(author, slug);
}
ProjectVersionUnsafeDownloadsTable download;
@ -482,6 +585,8 @@ public class VersionsController extends HangarController {
return Routes.VERSIONS_DOWNLOAD.getRedirect(author, slug, version, token.orElse(null), "");
case JAR_FILE:
return Routes.VERSIONS_DOWNLOAD_JAR.getRedirect(author, slug, version, token.orElse(null), "");
case EXTERNAL_DOWNLOAD:
return new ModelAndView("redirect:" + pvt.getExternalUrl());
default:
throw new IllegalArgumentException();
}
@ -539,15 +644,16 @@ public class VersionsController extends HangarController {
}
private Object sendVersion(ProjectsTable project, ProjectVersionsTable version, String token, boolean confirm) {
boolean isSafeExternalHost = version.isExternal() && hangarConfig.security.getSafeDownloadHosts().contains(URI.create(version.getExternalUrl()).getHost());
boolean passed = checkConfirmation(version, token);
if (passed || confirm) {
if (passed || confirm || isSafeExternalHost) {
return _sendVersion(project, version);
} else {
return Routes.VERSIONS_SHOW_DOWNLOAD_CONFIRM.getRedirect(
project.getOwnerName(),
project.getSlug(),
version.getVersionString(),
DownloadType.UPLOADED_FILE.ordinal() + "",
(version.getExternalUrl() != null ? DownloadType.EXTERNAL_DOWNLOAD.ordinal() : DownloadType.UPLOADED_FILE.ordinal()) + "",
false + "",
"dummy"
);
@ -566,7 +672,7 @@ public class VersionsController extends HangarController {
response.addCookie(newCookie);
return true;
} else {
ProjectVersionDownloadWarningsTable warning = downloadWarningDao.get().find(token, version.getId(), RequestUtil.getRemoteInetAddress(request));
ProjectVersionDownloadWarningsTable warning = downloadWarningDao.get().findConfirmedWarning(token, version.getId(), RequestUtil.getRemoteInetAddress(request));
if (warning == null) {
return false;
@ -580,12 +686,14 @@ public class VersionsController extends HangarController {
}
}
private FileSystemResource _sendVersion(ProjectsTable project, ProjectVersionsTable version) {
private Object _sendVersion(ProjectsTable project, ProjectVersionsTable version) {
statsService.addVersionDownloaded(version);
if (version.getExternalUrl() != null) {
return new ModelAndView("redirect:" + version.getExternalUrl());
}
Path path = projectFiles.getVersionDir(project.getOwnerName(), project.getName(), version.getVersionString()).resolve(version.getFileName());
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + version.getFileName() + "\"");
statsService.addVersionDownloaded(version);
return new FileSystemResource(path);
}
@ -615,7 +723,7 @@ public class VersionsController extends HangarController {
}
private Object sendJar(ProjectsTable project, ProjectVersionsTable version, String token, boolean api) {
if (project.getVisibility() == Visibility.SOFTDELETE) {
if (project.getVisibility() == Visibility.SOFTDELETE || version.isExternal()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
} else {
boolean passed = checkConfirmation(version, token);

View File

@ -22,7 +22,7 @@ import java.util.Map;
@RegisterBeanMapper(ProjectChannelsTable.class)
public interface ProjectChannelDao {
@SqlUpdate("insert into project_channels (created_at, name, color, project_id) values (:now, :name, :color, :projectId)")
@SqlUpdate("insert into project_channels (created_at, name, color, project_id, is_non_reviewed) values (:now, :name, :color, :projectId, :isNonReviewed)")
@Timestamped
@GetGeneratedKeys
ProjectChannelsTable insert(@BindBean ProjectChannelsTable projectChannel);

View File

@ -24,12 +24,12 @@ public interface ProjectVersionDao {
@Timestamped
@GetGeneratedKeys
@SqlUpdate("INSERT INTO project_versions " +
"(created_at, version_string, dependencies, description, project_id, channel_id, file_size, hash, file_name, author_id, create_forum_post) VALUES " +
"(:now, :versionString, :dependencies, :description, :projectId, :channelId, :fileSize, :hash, :fileName, :authorId, :createForumPost)")
"(created_at, version_string, dependencies, description, project_id, channel_id, file_size, hash, file_name, author_id, create_forum_post, external_url) VALUES " +
"(:now, :versionString, :dependencies, :description, :projectId, :channelId, :fileSize, :hash, :fileName, :authorId, :createForumPost, :externalUrl)")
ProjectVersionsTable insert(@BindBean ProjectVersionsTable projectVersionsTable);
@SqlUpdate("UPDATE project_versions SET visibility = :visibility, reviewer_id = :reviewerId, approved_at = :approvedAt, description = :description, " +
"review_state = :reviewState " +
"review_state = :reviewState, external_url = :externalUrl " +
"WHERE id = :id")
void update(@BindBean ProjectVersionsTable projectVersionsTable);

View File

@ -25,7 +25,7 @@ public interface ProjectVersionDownloadWarningDao {
" AND version_id = :versionId" +
" AND address = :address" +
" AND is_confirmed IS TRUE")
ProjectVersionDownloadWarningsTable find(String token, long versionId, InetAddress address);
ProjectVersionDownloadWarningsTable findConfirmedWarning(String token, long versionId, InetAddress address);
@SqlQuery("SELECT pvdw.* FROM project_version_download_warnings pvdw " +
"WHERE pvdw.address = :address" +

View File

@ -19,11 +19,11 @@ public class ProjectChannelsTable {
//
}
public ProjectChannelsTable(String name, Color color, long projectId) {
public ProjectChannelsTable(String name, Color color, long projectId, boolean isNonReviewed) {
this.name = name;
this.color = color;
this.projectId = projectId;
this.isNonReviewed = false;
this.isNonReviewed = isNonReviewed;
}
public long getId() {

View File

@ -3,6 +3,7 @@ package io.papermc.hangar.db.model;
import io.papermc.hangar.model.Visibility;
import io.papermc.hangar.model.generated.ReviewState;
import org.jdbi.v3.core.annotation.Unmappable;
import org.jdbi.v3.core.enums.EnumByOrdinal;
import java.time.OffsetDateTime;
@ -17,7 +18,7 @@ public class ProjectVersionsTable {
private String description;
private long projectId;
private long channelId;
private long fileSize;
private Long fileSize;
private String hash;
private String fileName;
private Long reviewerId;
@ -27,8 +28,9 @@ public class ProjectVersionsTable {
private ReviewState reviewState = ReviewState.UNREVIEWED;
private boolean createForumPost = true;
private Long postId;
private String externalUrl;
public ProjectVersionsTable(String versionString, List<String> dependencies, String description, long projectId, long channelId, long fileSize, String hash, String fileName, long authorId, boolean createForumPost) {
public ProjectVersionsTable(String versionString, List<String> dependencies, String description, long projectId, long channelId, Long fileSize, String hash, String fileName, long authorId, boolean createForumPost, String externalUrl) {
this.versionString = versionString;
this.dependencies = dependencies;
this.description = description;
@ -39,6 +41,7 @@ public class ProjectVersionsTable {
this.fileName = fileName;
this.authorId = authorId;
this.createForumPost = createForumPost;
this.externalUrl = externalUrl;
}
public ProjectVersionsTable() { }
@ -106,11 +109,11 @@ public class ProjectVersionsTable {
}
public long getFileSize() {
public Long getFileSize() {
return fileSize;
}
public void setFileSize(long fileSize) {
public void setFileSize(Long fileSize) {
this.fileSize = fileSize;
}
@ -199,6 +202,20 @@ public class ProjectVersionsTable {
this.postId = postId;
}
public String getExternalUrl() {
return externalUrl;
}
public void setExternalUrl(String externalUrl) {
this.externalUrl = externalUrl;
}
@Unmappable
public boolean isExternal() {
return this.externalUrl != null && this.fileName == null;
}
@Override
public String toString() {
return "ProjectVersionsTable{" +

View File

@ -10,5 +10,10 @@ public enum DownloadType {
/**
* The download was for just the JAR file of the upload.
*/
JAR_FILE
JAR_FILE,
/**
* The download is on an external site.
*/
EXTERNAL_DOWNLOAD
}

View File

@ -3,8 +3,8 @@ package io.papermc.hangar.model;
public enum TagColor { // remember, once we push to production, the order of these enums cannot change
PAPER("#F7CF0D", "#333333"),
WATERFALL("#F7CF0D", "#FFFFFF"),
VELOCITY("#039BE5","#FFFFFF"),
WATERFALL("#F7CF0D", "#333333"),
VELOCITY("#039BE5","#333333"),
UNSTABLE("#FFDAB9", "#333333");

View File

@ -20,16 +20,17 @@ public class PendingVersion {
private final List<Dependency> dependencies;
private final String description;
private final long projectId;
private final long fileSize;
private final Long fileSize;
private final String hash;
private final String fileName;
private final long authorId;
private final String channelName;
private final Color channelColor;
private final PluginFileWithData plugin;
private final String externalUrl;
private final boolean createForumPost;
public PendingVersion(String versionString, List<Dependency> dependencies, String description, long projectId, long fileSize, String hash, String fileName, long authorId, String channelName, Color channelColor, PluginFileWithData plugin, boolean createForumPost) {
public PendingVersion(String versionString, List<Dependency> dependencies, String description, long projectId, Long fileSize, String hash, String fileName, long authorId, String channelName, Color channelColor, PluginFileWithData plugin, String externalUrl, boolean createForumPost) {
this.versionString = versionString;
this.dependencies = dependencies;
this.description = description;
@ -41,6 +42,7 @@ public class PendingVersion {
this.channelName = channelName;
this.channelColor = channelColor;
this.plugin = plugin;
this.externalUrl = externalUrl;
this.createForumPost = createForumPost;
}
@ -60,7 +62,7 @@ public class PendingVersion {
return projectId;
}
public long getFileSize() {
public Long getFileSize() {
return fileSize;
}
@ -88,6 +90,10 @@ public class PendingVersion {
return plugin;
}
public String getExternalUrl() {
return externalUrl;
}
public boolean isCreateForumPost() {
return createForumPost;
}
@ -96,8 +102,8 @@ public class PendingVersion {
return Platform.getGhostTags(-1L, dependencies);
}
public PendingVersion copy(String channelName, Color channelColor, boolean createForumPost, String description, List<String> versions) {
Optional<Dependency> optional = dependencies.stream().filter(d -> d.getPluginId().equals(plugin.getPlatform().getDependencyId())).findAny();
public PendingVersion copy(String channelName, Color channelColor, boolean createForumPost, String description, List<String> versions, Platform platform) {
Optional<Dependency> optional = dependencies.stream().filter(d -> d.getPluginId().equals(platform.getDependencyId())).findAny();
optional.ifPresent(dependency -> dependency.setVersion(String.join(",", versions))); // Should always be present, if not, there are other problems
return new PendingVersion(
versionString,
@ -111,7 +117,7 @@ public class PendingVersion {
channelName,
channelColor,
plugin,
createForumPost
externalUrl, createForumPost
);
}

View File

@ -80,6 +80,7 @@ public class PluginUploadService {
}
}
public PendingVersion processSubsequentPluginUpload(MultipartFile file, UsersTable owner, ProjectsTable project) throws HangarException {
PluginFileWithData plugin = processPluginUpload(file, owner);
// TODO not sure what to do w/plugin id, that isn't stored in the metadata for the file
@ -127,6 +128,7 @@ public class PluginUploadService {
channelName,
config.getChannels().getColorDefault(),
plugin,
null,
forumSync
);
}

View File

@ -21,12 +21,12 @@ public class ChannelFactory {
this.channelDao = channelDao;
}
public ProjectChannelsTable createChannel(long projectId, String channelName, Color color) {
public ProjectChannelsTable createChannel(long projectId, String channelName, Color color, boolean isNonReviewed) {
if (!hangarConfig.channels.isValidChannelName(channelName)) {
throw new HangarException("error.channel.invalidName", channelName);
}
ProjectChannelsTable channel = new ProjectChannelsTable(channelName, color, projectId);
ProjectChannelsTable channel = new ProjectChannelsTable(channelName, color, projectId, isNonReviewed);
channelDao.get().insert(channel);
return channel;
}

View File

@ -52,10 +52,10 @@ public class ChannelService {
return channelDao.get().getChannelsWithVersionCount(projectId);
}
public ProjectChannelsTable addProjectChannel(long projectId, String channelName, Color color) {
public ProjectChannelsTable addProjectChannel(long projectId, String channelName, Color color, boolean isNonReviewed) {
InvalidChannelCreationReason reason = channelDao.get().validateChannelCreation(projectId, channelName, color.getValue(), hangarConfig.projects.getMaxChannels());
checkInvalidChannelCreationReason(reason);
return channelFactory.createChannel(projectId, channelName, color);
return channelFactory.createChannel(projectId, channelName, color, isNonReviewed);
}
public void updateProjectChannel(long projectId, String oldChannel, String channelName, Color color) {

View File

@ -93,7 +93,7 @@ public class ProjectFactory {
String slug = StringUtils.slugify(name);
ProjectsTable projectsTable = new ProjectsTable(pluginId, name, slug, ownerUser.getName(), ownerUser.getUserId(), category, description, Visibility.NEW);
ProjectChannelsTable channelsTable = new ProjectChannelsTable(hangarConfig.channels.getNameDefault(), hangarConfig.channels.getColorDefault(), -1);
ProjectChannelsTable channelsTable = new ProjectChannelsTable(hangarConfig.channels.getNameDefault(), hangarConfig.channels.getColorDefault(), -1, false);
String content = "# " + name + "\n\n" + hangarConfig.pages.home.getMessage();
ProjectPagesTable pagesTable = new ProjectPage( -1, hangarConfig.pages.home.getName(), StringUtils.slugify(hangarConfig.pages.home.getName()), content, false, null);
@ -163,10 +163,13 @@ public class ProjectFactory {
pendingVersion.getHash(),
pendingVersion.getFileName(),
pendingVersion.getAuthorId(),
pendingVersion.isCreateForumPost()
pendingVersion.isCreateForumPost(),
pendingVersion.getExternalUrl()
));
if (pendingVersion.getPlugin() != null) {
pendingVersion.getPlugin().getData().createTags(version.getId(), versionService); // TODO not sure what this is for
}
pendingVersion.getPlugin().getData().createTags(version.getId(), versionService); // TODO not sure what this is for
Platform.createPlatformTags(versionService, version.getId(), Dependency.from(version.getDependencies()));
List<UsersTable> watchers = projectService.getProjectWatchers(project.getProject().getId(), 0, null);
@ -178,11 +181,13 @@ public class ProjectFactory {
new String[]{"notification.project.newVersion", project.getProject().getName(), version.getVersionString()},
project.getNamespace() + "/versions/" + version.getVersionString()
));
try {
uploadPlugin(project, pendingVersion.getPlugin(), version);
} catch (IOException e) {
versionService.deleteVersion(version.getId());
throw new HangarException("error.version.fileIOError");
if (pendingVersion.getPlugin() != null) {
try {
uploadPlugin(project, pendingVersion.getPlugin(), version);
} catch (IOException e) {
versionService.deleteVersion(version.getId());
throw new HangarException("error.version.fileIOError");
}
}

View File

@ -10,138 +10,134 @@ import static java.util.List.of;
public enum Routes {
SHOW_PROJECT_VISIBILITY("showProjectVisibility", "/admin/approval/projects", of(), of()),
ACTOR_COUNT("actorCount", "/pantopticon/actor-count", of(), of("timeoutMs")),
ACTOR_TREE("actorTree", "/pantopticon/actor-tree", of(), of("timeoutMs")),
UPDATE_USER("updateUser", "/admin/user/{user}/update", of("user"), of()),
SHOW_QUEUE("showQueue", "/admin/approval/versions", of(), of()),
SHOW_LOG("showLog", "/admin/log", of(), of("page", "userFilter", "projectFilter", "versionFilter", "pageFilter", "actionFilter", "subjectFilter")),
SHOW_PLATFORM_VERSIONS("showPlatformVersions", "/admin/versions", of(), of()),
UPDATE_PLATFORM_VERSIONS("updatePlatformVersions", "/admin/versions/{platform}", of("platform"), of()),
REMOVE_TRAIL("removeTrail", "/{path}/", of("path"), of()),
FAVICON_REDIRECT("faviconRedirect", "/favicon.ico", of(), of()),
SHOW_FLAGS("showFlags", "/admin/flags", of(), of()),
SITEMAP_INDEX("sitemapIndex", "/sitemap.xml", of(), of()),
GLOBAL_SITEMAP("globalSitemap", "/global-sitemap.xml", of(), of()),
SHOW_STATS("showStats", "/admin/stats", of(), of("from", "to")),
LINK_OUT("linkOut", "/linkout", of(), of("remoteUrl")),
SHOW_HEALTH("showHealth", "/admin/health", of(), of()),
SHOW_HOME("showHome", "/", of(), of()),
ROBOTS("robots", "/robots.txt", of(), of()),
SET_FLAG_RESOLVED("setFlagResolved", "/admin/flags/{id}/resolve/{resolved}", of("id", "resolved"), of()),
SWAGGER("swagger", "/api", of(), of()),
SHOW_ACTIVITIES("showActivities", "/admin/activities/{user}", of("user"), of()),
USER_ADMIN("userAdmin", "/admin/user/{user}", of("user"), of()),
JAVA_SCRIPT_ROUTES("javaScriptRoutes", "/javascriptRoutes", of(), of()),
SHOW_PROJECT_VISIBILITY("showProjectVisibility", Paths.SHOW_PROJECT_VISIBILITY, of(), of()),
ACTOR_COUNT("actorCount", Paths.ACTOR_COUNT, of(), of("timeoutMs")),
ACTOR_TREE("actorTree", Paths.ACTOR_TREE, of(), of("timeoutMs")),
UPDATE_USER("updateUser", Paths.UPDATE_USER, of("user"), of()),
SHOW_QUEUE("showQueue", Paths.SHOW_QUEUE, of(), of()),
SHOW_LOG("showLog", Paths.SHOW_LOG, of(), of("page", "userFilter", "projectFilter", "versionFilter", "pageFilter", "actionFilter", "subjectFilter")),
SHOW_PLATFORM_VERSIONS("showPlatformVersions", Paths.SHOW_PLATFORM_VERSIONS, of(), of()),
UPDATE_PLATFORM_VERSIONS("updatePlatformVersions", Paths.UPDATE_PLATFORM_VERSIONS, of("platform"), of()),
REMOVE_TRAIL("removeTrail", Paths.REMOVE_TRAIL, of("path"), of()),
FAVICON_REDIRECT("faviconRedirect", Paths.FAVICON_REDIRECT, of(), of()),
SHOW_FLAGS("showFlags", Paths.SHOW_FLAGS, of(), of()),
SITEMAP_INDEX("sitemapIndex", Paths.SITEMAP_INDEX, of(), of()),
GLOBAL_SITEMAP("globalSitemap", Paths.GLOBAL_SITEMAP, of(), of()),
SHOW_STATS("showStats", Paths.SHOW_STATS, of(), of("from", "to")),
LINK_OUT("linkOut", Paths.LINK_OUT, of(), of("remoteUrl")),
SHOW_HEALTH("showHealth", Paths.SHOW_HEALTH, of(), of()),
SHOW_HOME("showHome", Paths.SHOW_HOME, of(), of()),
ROBOTS("robots", Paths.ROBOTS, of(), of()),
SET_FLAG_RESOLVED("setFlagResolved", Paths.SET_FLAG_RESOLVED, of("id", "resolved"), of()),
SWAGGER("swagger", Paths.SWAGGER, of(), of()),
SHOW_ACTIVITIES("showActivities", Paths.SHOW_ACTIVITIES, of("user"), of()),
USER_ADMIN("userAdmin", Paths.USER_ADMIN, of("user"), of()),
JAVA_SCRIPT_ROUTES("javaScriptRoutes", Paths.JAVA_SCRIPT_ROUTES, of(), of()),
PROJECTS_RENAME("projects.rename", "/{author}/{slug}/manage/rename", of("author", "slug"), of()),
PROJECTS_SET_WATCHING("projects.setWatching", "/{author}/{slug}/watchers/{watching}", of("author", "slug", "watching"), of()),
PROJECTS_SHOW_SETTINGS("projects.showSettings", "/{author}/{slug}/manage", of("author", "slug"), of()),
PROJECTS_SET_INVITE_STATUS("projects.setInviteStatus", "/invite/{id}/{status}", of("id", "status"), of()),
PROJECTS_TOGGLE_STARRED("projects.toggleStarred", "/{author}/{slug}/stars/toggle", of("author", "slug"), of()),
PROJECTS_SHOW_CREATOR("projects.showCreator", "/new", of(), of()),
PROJECTS_SHOW_STARGAZERS("projects.showStargazers", "/{author}/{slug}/stars", of("author", "slug"), of("page")),
PROJECTS_SHOW_WATCHERS("projects.showWatchers", "/{author}/{slug}/watchers", of("author", "slug"), of("page")),
PROJECTS_UPLOAD_ICON("projects.uploadIcon", "/{author}/{slug}/icon", of("author", "slug"), of()),
PROJECTS_SHOW_ICON("projects.showIcon", "/{author}/{slug}/icon", of("author", "slug"), of()),
PROJECTS_SEND_FOR_APPROVAL("projects.sendForApproval", "/{author}/{slug}/manage/sendforapproval", of("author", "slug"), of()),
PROJECTS_SET_INVITE_STATUS_ON_BEHALF("projects.setInviteStatusOnBehalf", "/invite/{id}/{status}/{behalf}", of("id", "status", "behalf"), of()),
PROJECTS_DELETE("projects.delete", "/{author}/{slug}/manage/hardDelete", of("author", "slug"), of()),
PROJECTS_ADD_MESSAGE("projects.addMessage", "/{author}/{slug}/notes/addmessage", of("author", "slug"), of()),
PROJECTS_SHOW_PENDING_ICON("projects.showPendingIcon", "/{author}/{slug}/icon/pending", of("author", "slug"), of()),
PROJECTS_POST_DISCUSSION_REPLY("projects.postDiscussionReply", "/{author}/{slug}/discuss/reply", of("author", "slug"), of()),
PROJECTS_SHOW("projects.show", "/{author}/{slug}", of("author", "slug"), of()),
PROJECTS_SHOW_DISCUSSION("projects.showDiscussion", "/{author}/{slug}/discuss", of("author", "slug"), of()),
PROJECTS_SOFT_DELETE("projects.softDelete", "/{author}/{slug}/manage/delete", of("author", "slug"), of()),
PROJECTS_SHOW_FLAGS("projects.showFlags", "/{author}/{slug}/flags", of("author", "slug"), of()),
PROJECTS_FLAG("projects.flag", "/{author}/{slug}/flag", of("author", "slug"), of()),
PROJECTS_CREATE_PROJECT("projects.createProject", "/new", of(), of()),
PROJECTS_RESET_ICON("projects.resetIcon", "/{author}/{slug}/icon/reset", of("author", "slug"), of()),
PROJECTS_SAVE("projects.save", "/{author}/{slug}/manage/save", of("author", "slug"), of()),
PROJECTS_SHOW_NOTES("projects.showNotes", "/{author}/{slug}/notes", of("author", "slug"), of()),
PROJECTS_SET_VISIBLE("projects.setVisible", "/{author}/{slug}/visible/{visibility}", of("author", "slug", "visibility"), of()),
PROJECTS_REMOVE_MEMBER("projects.removeMember", "/{author}/{slug}/manage/members/remove", of("author", "slug"), of()),
VERSIONS_RESTORE("versions.restore", "/{author}/{slug}/versions/{version}/restore", of("author", "slug", "version"), of()),
VERSIONS_DOWNLOAD_RECOMMENDED_JAR("versions.downloadRecommendedJar", "/{author}/{slug}/versions/recommended/jar", of("author", "slug"), of("token")),
VERSIONS_PUBLISH("versions.publish", "/{author}/{slug}/versions/{version}", of("author", "slug", "version"), of()),
VERSIONS_SET_RECOMMENDED("versions.setRecommended", "/{author}/{slug}/versions/{version}/recommended", of("author", "slug", "version"), of()),
VERSIONS_DOWNLOAD("versions.download", "/{author}/{slug}/versions/{version}/download", of("author", "slug", "version"), of("token", "confirm")),
VERSIONS_SHOW_LOG("versions.showLog", "/{author}/{slug}/versionLog", of("author", "slug"), of("versionString")),
VERSIONS_SHOW("versions.show", "/{author}/{slug}/versions/{version}", of("author", "slug", "version"), of()),
VERSIONS_DOWNLOAD_JAR("versions.downloadJar", "/{author}/{slug}/versions/{version}/jar", of("author", "slug", "version"), of("token")),
VERSIONS_APPROVE("versions.approve", "/{author}/{slug}/versions/{version}/approve", of("author", "slug", "version"), of()),
VERSIONS_APPROVE_PARTIAL("versions.approvePartial", "/{author}/{slug}/versions/{version}/approvePartial", of("author", "slug", "version"), of()),
VERSIONS_SAVE_DESCRIPTION("versions.saveDescription", "/{author}/{slug}/versions/{version}/save", of("author", "slug", "version"), of()),
VERSIONS_DOWNLOAD_RECOMMENDED("versions.downloadRecommended", "/{author}/{slug}/versions/recommended/download", of("author", "slug"), of("token")),
VERSIONS_SHOW_LIST("versions.showList", "/{author}/{slug}/versions", of("author", "slug"), of()),
VERSIONS_DOWNLOAD_JAR_BY_ID("versions.downloadJarById", "/api/project/{pluginId}/versions/{name}/download", of("pluginId", "name"), of("token")),
VERSIONS_DOWNLOAD_RECOMMENDED_JAR_BY_ID("versions.downloadRecommendedJarById", "/api/project/{pluginId}/versions/recommended/download", of("pluginId"), of("token")),
VERSIONS_UPLOAD("versions.upload", "/{author}/{slug}/versions/new/upload", of("author", "slug"), of()),
VERSIONS_SOFT_DELETE("versions.softDelete", "/{author}/{slug}/versions/{version}/delete", of("author", "slug", "version"), of()),
VERSIONS_SHOW_DOWNLOAD_CONFIRM("versions.showDownloadConfirm", "/{author}/{slug}/versions/{version}/confirm", of("author", "slug", "version"), of("downloadType", "api", "dummy")),
VERSIONS_SHOW_CREATOR("versions.showCreator", "/{author}/{slug}/versions/new", of("author", "slug"), of()),
VERSIONS_DELETE("versions.delete", "/{author}/{slug}/versions/{version}/hardDelete", of("author", "slug", "version"), of()),
VERSIONS_SHOW_CREATOR_WITH_META("versions.showCreatorWithMeta", "/{author}/{slug}/versions/new/{version}", of("author", "slug", "version"), of()),
VERSIONS_CONFIRM_DOWNLOAD("versions.confirmDownload", "/{author}/{slug}/versions/{version}/confirm", of("author", "slug", "version"), of("downloadType", "token", "dummy")),
PAGES_SHOW_PREVIEW("pages.showPreview", "/pages/preview", of(), of()),
PAGES_SAVE("pages.save", "/{author}/{slug}/pages/{page}/edit", of("author", "slug", "page"), of()),
PAGES_SHOW_EDITOR("pages.showEditor", "/{author}/{slug}/pages/{page}/edit", of("author", "slug", "page"), of()),
PAGES_SHOW("pages.show", "/{author}/{slug}/pages/{page}", of("author", "slug", "page"), of()),
PAGES_DELETE("pages.delete", "/{author}/{slug}/pages/{page}/delete", of("author", "slug", "page"), of()),
USERS_SHOW_AUTHORS("users.showAuthors", "/authors", of(), of("sort", "page")),
USERS_SAVE_TAGLINE("users.saveTagline", "/{user}/settings/tagline", of("user"), of()),
USERS_SIGN_UP("users.signUp", "/signup", of(), of()),
USERS_SHOW_NOTIFICATIONS("users.showNotifications", "/notifications", of(), of("notificationFilter", "inviteFilter")),
USERS_SHOW_PROJECTS("users.showProjects", "/{user}", of("user"), of()),
USERS_VERIFY("users.verify", "/verify", of(), of("returnPath")),
USERS_MARK_NOTIFICATION_READ("users.markNotificationRead", "/notifications/read/{id}", of("id"), of()),
USERS_LOGIN("users.login", "/login", of(), of("sso", "sig", "returnUrl")),
USERS_SHOW_STAFF("users.showStaff", "/staff", of(), of("sort", "page")),
USERS_SET_LOCKED("users.setLocked", "/{user}/settings/lock/{locked}", of("user", "locked"), of("sso", "sig")),
USERS_MARK_PROMPT_READ("users.markPromptRead", "/prompts/read/{id}", of("id"), of()),
USERS_LOGOUT("users.logout", "/logout", of(), of()),
USERS_USER_SITEMAP("users.userSitemap", "/{user}/sitemap.xml", of("user"), of()),
USERS_EDIT_API_KEYS("users.editApiKeys", "/{user}/settings/apiKeys", of("user"), of()),
ORG_UPDATE_MEMBERS("org.updateMembers", "/organizations/{organization}/settings/members", of("organization"), of()),
ORG_UPDATE_AVATAR("org.updateAvatar", "/organizations/{organization}/settings/avatar", of("organization"), of()),
ORG_SET_INVITE_STATUS("org.setInviteStatus", "/organizations/invite/{id}/{status}", of("id", "status"), of()),
ORG_SHOW_CREATOR("org.showCreator", "/organizations/new", of(), of()),
ORG_CREATE("org.create", "/organizations/new", of(), of()),
ORG_REMOVE_MEMBER("org.removeMember", "/organizations/{organization}/settings/members/remove", of("organization"), of()),
REVIEWS_ADD_MESSAGE("reviews.addMessage", "/{author}/{slug}/versions/{version}/reviews/addmessage", of("author", "slug", "version"), of()),
REVIEWS_BACKLOG_TOGGLE("reviews.backlogToggle", "/{author}/{slug}/versions/{version}/reviews/reviewtoggle", of("author", "slug", "version"), of()),
REVIEWS_SHOW_REVIEWS("reviews.showReviews", "/{author}/{slug}/versions/{version}/reviews", of("author", "slug", "version"), of()),
REVIEWS_APPROVE_REVIEW("reviews.approveReview", "/{author}/{slug}/versions/{version}/reviews/approve", of("author", "slug", "version"), of()),
REVIEWS_EDIT_REVIEW("reviews.editReview", "/{author}/{slug}/versions/{version}/reviews/edit/{review}", of("author", "slug", "version", "review"), of()),
REVIEWS_STOP_REVIEW("reviews.stopReview", "/{author}/{slug}/versions/{version}/reviews/stop", of("author", "slug", "version"), of()),
REVIEWS_CREATE_REVIEW("reviews.createReview", "/{author}/{slug}/versions/{version}/reviews/init", of("author", "slug", "version"), of()),
REVIEWS_TAKEOVER_REVIEW("reviews.takeoverReview", "/{author}/{slug}/versions/{version}/reviews/takeover", of("author", "slug", "version"), of()),
REVIEWS_REOPEN_REVIEW("reviews.reopenReview", "/{author}/{slug}/versions/{version}/reviews/reopen", of("author", "slug", "version"), of()),
CHANNELS_DELETE("channels.delete", "/{author}/{slug}/channels/{channel}/delete", of("author", "slug", "channel"), of()),
CHANNELS_SAVE("channels.save", "/{author}/{slug}/channels/{channel}", of("author", "slug", "channel"), of()),
CHANNELS_SHOW_LIST("channels.showList", "/{author}/{slug}/channels", of("author", "slug"), of()),
CHANNELS_CREATE("channels.create", "/{author}/{slug}/channels", of("author", "slug"), of()),
APIV1_SHOW_VERSION("apiv1.showVersion", "/api/v1/projects/{pluginId}/versions/{name}", of("pluginId", "name"), of()),
APIV1_LIST_PROJECTS("apiv1.listProjects", "/api/v1/projects", of(), of("categories", "sort", "q", "limit", "offset")),
APIV1_LIST_USERS("apiv1.listUsers", "/api/v1/users", of(), of("limit", "offset")),
APIV1_LIST_VERSIONS("apiv1.listVersions", "/api/v1/projects/{pluginId}/versions", of("pluginId"), of("channels", "limit", "offset")),
APIV1_REVOKE_KEY("apiv1.revokeKey", "/api/v1/projects/{pluginId}/keys/revoke", of("pluginId"), of()),
APIV1_CREATE_KEY("apiv1.createKey", "/api/v1/projects/{pluginId}/keys/new", of("pluginId"), of()),
APIV1_SHOW_PROJECT("apiv1.showProject", "/api/v1/projects/{pluginId}", of("pluginId"), of()),
APIV1_SYNC_SSO("apiv1.syncSso", "/api/sync_sso", of(), of()),
APIV1_TAG_COLOR("apiv1.tagColor", "/api/v1/tags/{tagId}", of("tagId"), of()),
APIV1_SHOW_STATUS_Z("apiv1.showStatusZ", "/statusz", of(), of()),
APIV1_SHOW_USER("apiv1.showUser", "/api/v1/users/{user}", of("user"), of()),
APIV1_LIST_PAGES("apiv1.listPages", "/api/v1/projects/{pluginId}/pages", of("pluginId"), of("parentId")),
APIV1_DEPLOY_VERSION("apiv1.deployVersion", "/api/v1/projects/{pluginId}/versions/{name}", of("pluginId", "name"), of()),
APIV1_LIST_TAGS("apiv1.listTags", "/api/v1/projects/{plugin}/tags/{versionName}", of("plugin", "versionName"), of());
PROJECTS_RENAME("projects.rename", Paths.PROJECTS_RENAME, of("author", "slug"), of()),
PROJECTS_SET_WATCHING("projects.setWatching", Paths.PROJECTS_SET_WATCHING, of("author", "slug", "watching"), of()),
PROJECTS_SHOW_SETTINGS("projects.showSettings", Paths.PROJECTS_SHOW_SETTINGS, of("author", "slug"), of()),
PROJECTS_SET_INVITE_STATUS("projects.setInviteStatus", Paths.PROJECTS_SET_INVITE_STATUS, of("id", "status"), of()),
PROJECTS_TOGGLE_STARRED("projects.toggleStarred", Paths.PROJECTS_TOGGLE_STARRED, of("author", "slug"), of()),
PROJECTS_SHOW_CREATOR("projects.showCreator", Paths.PROJECTS_SHOW_CREATOR, of(), of()),
PROJECTS_SHOW_STARGAZERS("projects.showStargazers", Paths.PROJECTS_SHOW_STARGAZERS, of("author", "slug"), of("page")),
PROJECTS_SHOW_WATCHERS("projects.showWatchers", Paths.PROJECTS_SHOW_WATCHERS, of("author", "slug"), of("page")),
PROJECTS_UPLOAD_ICON("projects.uploadIcon", Paths.PROJECTS_UPLOAD_ICON, of("author", "slug"), of()),
PROJECTS_SHOW_ICON("projects.showIcon", Paths.PROJECTS_SHOW_ICON, of("author", "slug"), of()),
PROJECTS_SEND_FOR_APPROVAL("projects.sendForApproval", Paths.PROJECTS_SEND_FOR_APPROVAL, of("author", "slug"), of()),
PROJECTS_SET_INVITE_STATUS_ON_BEHALF("projects.setInviteStatusOnBehalf", Paths.PROJECTS_SET_INVITE_STATUS_ON_BEHALF, of("id", "status", "behalf"), of()),
PROJECTS_DELETE("projects.delete", Paths.PROJECTS_DELETE, of("author", "slug"), of()),
PROJECTS_ADD_MESSAGE("projects.addMessage", Paths.PROJECTS_ADD_MESSAGE, of("author", "slug"), of()),
PROJECTS_SHOW_PENDING_ICON("projects.showPendingIcon", Paths.PROJECTS_SHOW_PENDING_ICON, of("author", "slug"), of()),
PROJECTS_POST_DISCUSSION_REPLY("projects.postDiscussionReply", Paths.PROJECTS_POST_DISCUSSION_REPLY, of("author", "slug"), of()),
PROJECTS_SHOW("projects.show", Paths.PROJECTS_SHOW, of("author", "slug"), of()),
PROJECTS_SHOW_DISCUSSION("projects.showDiscussion", Paths.PROJECTS_SHOW_DISCUSSION, of("author", "slug"), of()),
PROJECTS_SOFT_DELETE("projects.softDelete", Paths.PROJECTS_SOFT_DELETE, of("author", "slug"), of()),
PROJECTS_SHOW_FLAGS("projects.showFlags", Paths.PROJECTS_SHOW_FLAGS, of("author", "slug"), of()),
PROJECTS_FLAG("projects.flag", Paths.PROJECTS_FLAG, of("author", "slug"), of()),
PROJECTS_CREATE_PROJECT("projects.createProject", Paths.PROJECTS_CREATE_PROJECT, of(), of()),
PROJECTS_RESET_ICON("projects.resetIcon", Paths.PROJECTS_RESET_ICON, of("author", "slug"), of()),
PROJECTS_SAVE("projects.save", Paths.PROJECTS_SAVE, of("author", "slug"), of()),
PROJECTS_SHOW_NOTES("projects.showNotes", Paths.PROJECTS_SHOW_NOTES, of("author", "slug"), of()),
PROJECTS_SET_VISIBLE("projects.setVisible", Paths.PROJECTS_SET_VISIBLE, of("author", "slug", "visibility"), of()),
PROJECTS_REMOVE_MEMBER("projects.removeMember", Paths.PROJECTS_REMOVE_MEMBER, of("author", "slug"), of()),
VERSIONS_RESTORE("versions.restore", Paths.VERSIONS_RESTORE, of("author", "slug", "version"), of()),
VERSIONS_DOWNLOAD_RECOMMENDED_JAR("versions.downloadRecommendedJar", Paths.VERSIONS_DOWNLOAD_RECOMMENDED_JAR, of("author", "slug"), of("token")),
VERSIONS_PUBLISH("versions.publish", Paths.VERSIONS_PUBLISH, of("author", "slug", "version"), of()),
VERSIONS_PUBLISH_URL("versions.publishUrl", Paths.VERSIONS_PUBLISH_URL, of("author", "slug"), of()),
VERSIONS_SET_RECOMMENDED("versions.setRecommended", Paths.VERSIONS_SET_RECOMMENDED, of("author", "slug", "version"), of()),
VERSIONS_DOWNLOAD("versions.download", Paths.VERSIONS_DOWNLOAD, of("author", "slug", "version"), of("token", "confirm")),
VERSIONS_SHOW_LOG("versions.showLog", Paths.VERSIONS_SHOW_LOG, of("author", "slug"), of("versionString")),
VERSIONS_SHOW("versions.show", Paths.VERSIONS_SHOW, of("author", "slug", "version"), of()),
VERSIONS_DOWNLOAD_JAR("versions.downloadJar", Paths.VERSIONS_DOWNLOAD_JAR, of("author", "slug", "version"), of("token")),
VERSIONS_APPROVE("versions.approve", Paths.VERSIONS_APPROVE, of("author", "slug", "version"), of()),
VERSIONS_APPROVE_PARTIAL("versions.approvePartial", Paths.VERSIONS_APPROVE_PARTIAL, of("author", "slug", "version"), of()),
VERSIONS_SAVE_DESCRIPTION("versions.saveDescription", Paths.VERSIONS_SAVE_DESCRIPTION, of("author", "slug", "version"), of()),
VERSIONS_DOWNLOAD_RECOMMENDED("versions.downloadRecommended", Paths.VERSIONS_DOWNLOAD_RECOMMENDED, of("author", "slug"), of("token")),
VERSIONS_SHOW_LIST("versions.showList", Paths.VERSIONS_SHOW_LIST, of("author", "slug"), of()),
VERSIONS_DOWNLOAD_JAR_BY_ID("versions.downloadJarById", Paths.VERSIONS_DOWNLOAD_JAR_BY_ID, of("pluginId", "name"), of("token")),
VERSIONS_DOWNLOAD_RECOMMENDED_JAR_BY_ID("versions.downloadRecommendedJarById", Paths.VERSIONS_DOWNLOAD_RECOMMENDED_JAR_BY_ID, of("pluginId"), of("token")),
VERSIONS_UPLOAD("versions.upload", Paths.VERSIONS_UPLOAD, of("author", "slug"), of()),
VERSIONS_CREATE_EXTERNAL_URL("versions.createExternalUrl", Paths.VERSIONS_CREATE_EXTERNAL_URL, of("author", "slug"), of()),
VERSIONS_SOFT_DELETE("versions.softDelete", Paths.VERSIONS_SOFT_DELETE, of("author", "slug", "version"), of()),
VERSIONS_SHOW_DOWNLOAD_CONFIRM("versions.showDownloadConfirm", Paths.VERSIONS_SHOW_DOWNLOAD_CONFIRM, of("author", "slug", "version"), of("downloadType", "api", "dummy")),
VERSIONS_SHOW_CREATOR("versions.showCreator", Paths.VERSIONS_SHOW_CREATOR, of("author", "slug"), of()),
VERSIONS_DELETE("versions.delete", Paths.VERSIONS_DELETE, of("author", "slug", "version"), of()),
VERSIONS_SHOW_CREATOR_WITH_META("versions.showCreatorWithMeta", Paths.VERSIONS_SHOW_CREATOR_WITH_META, of("author", "slug", "version"), of()),
VERSIONS_CONFIRM_DOWNLOAD("versions.confirmDownload", Paths.VERSIONS_CONFIRM_DOWNLOAD, of("author", "slug", "version"), of("downloadType", "token", "dummy")),
PAGES_SHOW_PREVIEW("pages.showPreview", Paths.PAGES_SHOW_PREVIEW, of(), of()),
PAGES_SAVE("pages.save", Paths.PAGES_SAVE, of("author", "slug", "page"), of()),
PAGES_SHOW_EDITOR("pages.showEditor", Paths.PAGES_SHOW_EDITOR, of("author", "slug", "page"), of()),
PAGES_SHOW("pages.show", Paths.PAGES_SHOW, of("author", "slug", "page"), of()),
PAGES_DELETE("pages.delete", Paths.PAGES_DELETE, of("author", "slug", "page"), of()),
USERS_SHOW_AUTHORS("users.showAuthors", Paths.USERS_SHOW_AUTHORS, of(), of("sort", "page")),
USERS_SAVE_TAGLINE("users.saveTagline", Paths.USERS_SAVE_TAGLINE, of("user"), of()),
USERS_SIGN_UP("users.signUp", Paths.USERS_SIGN_UP, of(), of()),
USERS_SHOW_NOTIFICATIONS("users.showNotifications", Paths.USERS_SHOW_NOTIFICATIONS, of(), of("notificationFilter", "inviteFilter")),
USERS_SHOW_PROJECTS("users.showProjects", Paths.USERS_SHOW_PROJECTS, of("user"), of()),
USERS_VERIFY("users.verify", Paths.USERS_VERIFY, of(), of("returnPath")),
USERS_MARK_NOTIFICATION_READ("users.markNotificationRead", Paths.USERS_MARK_NOTIFICATION_READ, of("id"), of()),
USERS_LOGIN("users.login", Paths.USERS_LOGIN, of(), of("sso", "sig", "returnUrl")),
USERS_SHOW_STAFF("users.showStaff", Paths.USERS_SHOW_STAFF, of(), of("sort", "page")),
USERS_SET_LOCKED("users.setLocked", Paths.USERS_SET_LOCKED, of("user", "locked"), of("sso", "sig")),
USERS_MARK_PROMPT_READ("users.markPromptRead", Paths.USERS_MARK_PROMPT_READ, of("id"), of()),
USERS_LOGOUT("users.logout", Paths.USERS_LOGOUT, of(), of()),
USERS_USER_SITEMAP("users.userSitemap", Paths.USERS_USER_SITEMAP, of("user"), of()),
USERS_EDIT_API_KEYS("users.editApiKeys", Paths.USERS_EDIT_API_KEYS, of("user"), of()),
ORG_UPDATE_MEMBERS("org.updateMembers", Paths.ORG_UPDATE_MEMBERS, of("organization"), of()),
ORG_UPDATE_AVATAR("org.updateAvatar", Paths.ORG_UPDATE_AVATAR, of("organization"), of()),
ORG_SET_INVITE_STATUS("org.setInviteStatus", Paths.ORG_SET_INVITE_STATUS, of("id", "status"), of()),
ORG_SHOW_CREATOR("org.showCreator", Paths.ORG_SHOW_CREATOR, of(), of()),
ORG_CREATE("org.create", Paths.ORG_CREATE, of(), of()),
ORG_REMOVE_MEMBER("org.removeMember", Paths.ORG_REMOVE_MEMBER, of("organization"), of()),
REVIEWS_ADD_MESSAGE("reviews.addMessage", Paths.REVIEWS_ADD_MESSAGE, of("author", "slug", "version"), of()),
REVIEWS_BACKLOG_TOGGLE("reviews.backlogToggle", Paths.REVIEWS_BACKLOG_TOGGLE, of("author", "slug", "version"), of()),
REVIEWS_SHOW_REVIEWS("reviews.showReviews", Paths.REVIEWS_SHOW_REVIEWS, of("author", "slug", "version"), of()),
REVIEWS_APPROVE_REVIEW("reviews.approveReview", Paths.REVIEWS_APPROVE_REVIEW, of("author", "slug", "version"), of()),
REVIEWS_EDIT_REVIEW("reviews.editReview", Paths.REVIEWS_EDIT_REVIEW, of("author", "slug", "version", "review"), of()),
REVIEWS_STOP_REVIEW("reviews.stopReview", Paths.REVIEWS_STOP_REVIEW, of("author", "slug", "version"), of()),
REVIEWS_CREATE_REVIEW("reviews.createReview", Paths.REVIEWS_CREATE_REVIEW, of("author", "slug", "version"), of()),
REVIEWS_TAKEOVER_REVIEW("reviews.takeoverReview", Paths.REVIEWS_TAKEOVER_REVIEW, of("author", "slug", "version"), of()),
REVIEWS_REOPEN_REVIEW("reviews.reopenReview", Paths.REVIEWS_REOPEN_REVIEW, of("author", "slug", "version"), of()),
CHANNELS_DELETE("channels.delete", Paths.CHANNELS_DELETE, of("author", "slug", "channel"), of()),
CHANNELS_SAVE("channels.save", Paths.CHANNELS_SAVE, of("author", "slug", "channel"), of()),
CHANNELS_SHOW_LIST("channels.showList", Paths.CHANNELS_SHOW_LIST, of("author", "slug"), of()),
CHANNELS_CREATE("channels.create", Paths.CHANNELS_CREATE, of("author", "slug"), of()),
APIV1_SHOW_VERSION("apiv1.showVersion", Paths.APIV1_SHOW_VERSION, of("pluginId", "name"), of()),
APIV1_LIST_PROJECTS("apiv1.listProjects", Paths.APIV1_LIST_PROJECTS, of(), of("categories", "sort", "q", "limit", "offset")),
APIV1_LIST_USERS("apiv1.listUsers", Paths.APIV1_LIST_USERS, of(), of("limit", "offset")),
APIV1_LIST_VERSIONS("apiv1.listVersions", Paths.APIV1_LIST_VERSIONS, of("pluginId"), of("channels", "limit", "offset")),
APIV1_REVOKE_KEY("apiv1.revokeKey", Paths.APIV1_REVOKE_KEY, of("pluginId"), of()),
APIV1_CREATE_KEY("apiv1.createKey", Paths.APIV1_CREATE_KEY, of("pluginId"), of()),
APIV1_SHOW_PROJECT("apiv1.showProject", Paths.APIV1_SHOW_PROJECT, of("pluginId"), of()),
APIV1_SYNC_SSO("apiv1.syncSso", Paths.APIV1_SYNC_SSO, of(), of()),
APIV1_TAG_COLOR("apiv1.tagColor", Paths.APIV1_TAG_COLOR, of("tagId"), of()),
APIV1_SHOW_STATUS_Z("apiv1.showStatusZ", Paths.APIV1_SHOW_STATUS_Z, of(), of()),
APIV1_SHOW_USER("apiv1.showUser", Paths.APIV1_SHOW_USER, of("user"), of()),
APIV1_LIST_PAGES("apiv1.listPages", Paths.APIV1_LIST_PAGES, of("pluginId"), of("parentId")),
APIV1_DEPLOY_VERSION("apiv1.deployVersion", Paths.APIV1_DEPLOY_VERSION, of("pluginId", "name"), of()),
APIV1_LIST_TAGS("apiv1.listTags", Paths.APIV1_LIST_TAGS, of("plugin", "versionName"), of()),
APIV1_LIST_PLATFORMS("apiv1.listPlatforms", Paths.APIV1_LIST_PLATFORMS, of(), of());
private static final Map<String, Routes> ROUTES = new HashMap<>();
@ -172,6 +168,10 @@ public enum Routes {
this.queryParams = queryParams.toArray(new String[0]);
}
public String getUrl() {
return url;
}
public String getRouteUrl(String... args) {
if ((pathParams.length + queryParams.length) != args.length) {
throw new RuntimeException("Args dont match for route " + name + " " + (pathParams.length + queryParams.length) + "!=" + args.length);
@ -216,4 +216,144 @@ public enum Routes {
public static ModelAndView getRedirectToUrl(String url) {
return new ModelAndView("redirect:" + url);
}
public static class Paths {
public static final String SHOW_PROJECT_VISIBILITY = "/admin/approval/projects";
public static final String ACTOR_COUNT = "/pantopticon/actor-count";
public static final String ACTOR_TREE = "/pantopticon/actor-tree";
public static final String UPDATE_USER = "/admin/user/{user}/update";
public static final String SHOW_QUEUE = "/admin/approval/versions";
public static final String SHOW_LOG = "/admin/log";
public static final String SHOW_PLATFORM_VERSIONS = "/admin/versions";
public static final String UPDATE_PLATFORM_VERSIONS = "/admin/versions/{platform}";
public static final String REMOVE_TRAIL = "/{path}/";
public static final String FAVICON_REDIRECT = "/favicon.ico";
public static final String SHOW_FLAGS = "/admin/flags";
public static final String SITEMAP_INDEX = "/sitemap.xml";
public static final String GLOBAL_SITEMAP = "/global-sitemap.xml";
public static final String SHOW_STATS = "/admin/stats";
public static final String LINK_OUT = "/linkout";
public static final String SHOW_HEALTH = "/admin/health";
public static final String SHOW_HOME = "/";
public static final String ROBOTS = "/robots.txt";
public static final String SET_FLAG_RESOLVED = "/admin/flags/{id}/resolve/{resolved}";
public static final String SWAGGER = "/api";
public static final String SHOW_ACTIVITIES = "/admin/activities/{user}";
public static final String USER_ADMIN = "/admin/user/{user}";
public static final String JAVA_SCRIPT_ROUTES = "/javascriptRoutes";
public static final String PROJECTS_RENAME = "/{author}/{slug}/manage/rename";
public static final String PROJECTS_SET_WATCHING = "/{author}/{slug}/watchers/{watching}";
public static final String PROJECTS_SHOW_SETTINGS = "/{author}/{slug}/manage";
public static final String PROJECTS_SET_INVITE_STATUS = "/invite/{id}/{status}";
public static final String PROJECTS_TOGGLE_STARRED = "/{author}/{slug}/stars/toggle";
public static final String PROJECTS_SHOW_CREATOR = "/new";
public static final String PROJECTS_SHOW_STARGAZERS = "/{author}/{slug}/stars";
public static final String PROJECTS_SHOW_WATCHERS = "/{author}/{slug}/watchers";
public static final String PROJECTS_UPLOAD_ICON = "/{author}/{slug}/icon";
public static final String PROJECTS_SHOW_ICON = "/{author}/{slug}/icon";
public static final String PROJECTS_SEND_FOR_APPROVAL = "/{author}/{slug}/manage/sendforapproval";
public static final String PROJECTS_SET_INVITE_STATUS_ON_BEHALF = "/invite/{id}/{status}/{behalf}";
public static final String PROJECTS_DELETE = "/{author}/{slug}/manage/hardDelete";
public static final String PROJECTS_ADD_MESSAGE = "/{author}/{slug}/notes/addmessage";
public static final String PROJECTS_SHOW_PENDING_ICON = "/{author}/{slug}/icon/pending";
public static final String PROJECTS_POST_DISCUSSION_REPLY = "/{author}/{slug}/discuss/reply";
public static final String PROJECTS_SHOW = "/{author}/{slug}";
public static final String PROJECTS_SHOW_DISCUSSION = "/{author}/{slug}/discuss";
public static final String PROJECTS_SOFT_DELETE = "/{author}/{slug}/manage/delete";
public static final String PROJECTS_SHOW_FLAGS = "/{author}/{slug}/flags";
public static final String PROJECTS_FLAG = "/{author}/{slug}/flag";
public static final String PROJECTS_CREATE_PROJECT = "/new";
public static final String PROJECTS_RESET_ICON = "/{author}/{slug}/icon/reset";
public static final String PROJECTS_SAVE = "/{author}/{slug}/manage/save";
public static final String PROJECTS_SHOW_NOTES = "/{author}/{slug}/notes";
public static final String PROJECTS_SET_VISIBLE = "/{author}/{slug}/visible/{visibility}";
public static final String PROJECTS_REMOVE_MEMBER = "/{author}/{slug}/manage/members/remove";
public static final String VERSIONS_RESTORE = "/{author}/{slug}/versions/{version}/restore";
public static final String VERSIONS_DOWNLOAD_RECOMMENDED_JAR = "/{author}/{slug}/versions/recommended/jar";
public static final String VERSIONS_PUBLISH = "/{author}/{slug}/versions/{version}";
public static final String VERSIONS_PUBLISH_URL = "/{author}/{slug}/versions/publish";
public static final String VERSIONS_SET_RECOMMENDED = "/{author}/{slug}/versions/{version}/recommended";
public static final String VERSIONS_DOWNLOAD = "/{author}/{slug}/versions/{version}/download";
public static final String VERSIONS_SHOW_LOG = "/{author}/{slug}/versionLog";
public static final String VERSIONS_SHOW = "/{author}/{slug}/versions/{version}";
public static final String VERSIONS_DOWNLOAD_JAR = "/{author}/{slug}/versions/{version}/jar";
public static final String VERSIONS_APPROVE = "/{author}/{slug}/versions/{version}/approve";
public static final String VERSIONS_APPROVE_PARTIAL = "/{author}/{slug}/versions/{version}/approvePartial";
public static final String VERSIONS_SAVE_DESCRIPTION = "/{author}/{slug}/versions/{version}/save";
public static final String VERSIONS_DOWNLOAD_RECOMMENDED = "/{author}/{slug}/versions/recommended/download";
public static final String VERSIONS_SHOW_LIST = "/{author}/{slug}/versions";
public static final String VERSIONS_DOWNLOAD_JAR_BY_ID = "/api/project/{pluginId}/versions/{name}/download";
public static final String VERSIONS_DOWNLOAD_RECOMMENDED_JAR_BY_ID = "/api/project/{pluginId}/versions/recommended/download";
public static final String VERSIONS_UPLOAD = "/{author}/{slug}/versions/new/upload";
public static final String VERSIONS_CREATE_EXTERNAL_URL = "/{author}/{slug}/versions/new/create";
public static final String VERSIONS_SOFT_DELETE = "/{author}/{slug}/versions/{version}/delete";
public static final String VERSIONS_SHOW_DOWNLOAD_CONFIRM = "/{author}/{slug}/versions/{version}/confirm";
public static final String VERSIONS_SHOW_CREATOR = "/{author}/{slug}/versions/new";
public static final String VERSIONS_DELETE = "/{author}/{slug}/versions/{version}/hardDelete";
public static final String VERSIONS_SHOW_CREATOR_WITH_META = "/{author}/{slug}/versions/new/{version}";
public static final String VERSIONS_CONFIRM_DOWNLOAD = "/{author}/{slug}/versions/{version}/confirm";
public static final String PAGES_SHOW_PREVIEW = "/pages/preview";
public static final String PAGES_SAVE = "/{author}/{slug}/pages/{page}/edit";
public static final String PAGES_SHOW_EDITOR = "/{author}/{slug}/pages/{page}/edit";
public static final String PAGES_SHOW = "/{author}/{slug}/pages/{page}";
public static final String PAGES_DELETE = "/{author}/{slug}/pages/{page}/delete";
public static final String USERS_SHOW_AUTHORS = "/authors";
public static final String USERS_SAVE_TAGLINE = "/{user}/settings/tagline";
public static final String USERS_SIGN_UP = "/signup";
public static final String USERS_SHOW_NOTIFICATIONS = "/notifications";
public static final String USERS_SHOW_PROJECTS = "/{user}";
public static final String USERS_VERIFY = "/verify";
public static final String USERS_MARK_NOTIFICATION_READ = "/notifications/read/{id}";
public static final String USERS_LOGIN = "/login";
public static final String USERS_SHOW_STAFF = "/staff";
public static final String USERS_SET_LOCKED = "/{user}/settings/lock/{locked}";
public static final String USERS_MARK_PROMPT_READ = "/prompts/read/{id}";
public static final String USERS_LOGOUT = "/logout";
public static final String USERS_USER_SITEMAP = "/{user}/sitemap.xml";
public static final String USERS_EDIT_API_KEYS = "/{user}/settings/apiKeys";
public static final String ORG_UPDATE_MEMBERS = "/organizations/{organization}/settings/members";
public static final String ORG_UPDATE_AVATAR = "/organizations/{organization}/settings/avatar";
public static final String ORG_SET_INVITE_STATUS = "/organizations/invite/{id}/{status}";
public static final String ORG_SHOW_CREATOR = "/organizations/new";
public static final String ORG_CREATE = "/organizations/new";
public static final String ORG_REMOVE_MEMBER = "/organizations/{organization}/settings/members/remove";
public static final String REVIEWS_ADD_MESSAGE = "/{author}/{slug}/versions/{version}/reviews/addmessage";
public static final String REVIEWS_BACKLOG_TOGGLE = "/{author}/{slug}/versions/{version}/reviews/reviewtoggle";
public static final String REVIEWS_SHOW_REVIEWS = "/{author}/{slug}/versions/{version}/reviews";
public static final String REVIEWS_APPROVE_REVIEW = "/{author}/{slug}/versions/{version}/reviews/approve";
public static final String REVIEWS_EDIT_REVIEW = "/{author}/{slug}/versions/{version}/reviews/edit/{review}";
public static final String REVIEWS_STOP_REVIEW = "/{author}/{slug}/versions/{version}/reviews/stop";
public static final String REVIEWS_CREATE_REVIEW = "/{author}/{slug}/versions/{version}/reviews/init";
public static final String REVIEWS_TAKEOVER_REVIEW = "/{author}/{slug}/versions/{version}/reviews/takeover";
public static final String REVIEWS_REOPEN_REVIEW = "/{author}/{slug}/versions/{version}/reviews/reopen";
public static final String CHANNELS_DELETE = "/{author}/{slug}/channels/{channel}/delete";
public static final String CHANNELS_SAVE = "/{author}/{slug}/channels/{channel}";
public static final String CHANNELS_SHOW_LIST = "/{author}/{slug}/channels";
public static final String CHANNELS_CREATE = "/{author}/{slug}/channels";
public static final String APIV1_SHOW_VERSION = "/api/v1/projects/{pluginId}/versions/{name}";
public static final String APIV1_LIST_PROJECTS = "/api/v1/projects";
public static final String APIV1_LIST_USERS = "/api/v1/users";
public static final String APIV1_LIST_VERSIONS = "/api/v1/projects/{pluginId}/versions";
public static final String APIV1_REVOKE_KEY = "/api/v1/projects/{pluginId}/keys/revoke";
public static final String APIV1_CREATE_KEY = "/api/v1/projects/{pluginId}/keys/new";
public static final String APIV1_SHOW_PROJECT = "/api/v1/projects/{pluginId}";
public static final String APIV1_SYNC_SSO = "/api/sync_sso";
public static final String APIV1_TAG_COLOR = "/api/v1/tags/{tagId}";
public static final String APIV1_SHOW_STATUS_Z = "/statusz";
public static final String APIV1_SHOW_USER = "/api/v1/users/{user}";
public static final String APIV1_LIST_PAGES = "/api/v1/projects/{pluginId}/pages";
public static final String APIV1_DEPLOY_VERSION = "/api/v1/projects/{pluginId}/versions/{name}";
public static final String APIV1_LIST_TAGS = "/api/v1/projects/{plugin}/tags/{versionName}";
public static final String APIV1_LIST_PLATFORMS = "/api/v1/platforms";
private Paths() { }
}
}

View File

@ -2,7 +2,6 @@ package io.papermc.hangar.util;
import io.papermc.hangar.config.hangar.HangarConfig;
import io.papermc.hangar.db.model.ProjectsTable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@ -39,6 +38,7 @@ public class TemplateHelper {
}
public String formatFileSize(Long size) {
if (size == null) return "";
if (size < 1024) {
return size + "B";
}

View File

@ -122,6 +122,8 @@ hangar:
max-failures: 5
timeout: 10s
reset: 5m
safe-download-hosts:
- "github.com"
################

View File

@ -0,0 +1,3 @@
ALTER TABLE project_versions ALTER COLUMN file_size DROP NOT NULL;
ALTER TABLE project_versions ALTER COLUMN hash DROP NOT NULL;
ALTER TABLE project_versions ALTER COLUMN file_name DROP NOT NULL;

View File

@ -35,6 +35,7 @@ general.name = Name
general.close = Close
general.continue = Continue
general.download = Download
general.downloadExternal = Download (External)
general.upload = Upload
general.more = More
general.sponsoredBy = Sponsored By
@ -80,6 +81,7 @@ error.minLength = Content too short.
error.maxLength = Content too long.
error.noFile = No file submitted.
error.invalidFile = That is an invalid file type.
error.invalidUrl = That URL is invalid.
error.nameUnavailable = That name is not available.
error.noLogin = Login is temporarily unavailable, please try again later.
error.loginFailed = Authentication failed.
@ -149,6 +151,7 @@ project.delete.info.uniqueid = WARNING: You or anybody else will not be abl
project.download.recommend = Download the latest recommended version
project.download.recommend.warn = This project's recommended version has not been reviewed by our moderation staff and may not be safe for download.
project.download.warn = This version has not been reviewed by our moderation staff and may not be safe for download.
project.download.external = External Download
project.flag = Flag
project.flag.plural = Flags
project.create = New Project
@ -238,6 +241,7 @@ version = Version
version.description = Description
version.filename = File name
version.fileSize = File size
version.externalUrl = URL
version.delete.cannotLast = Every project must have at least one version
version.delete.alreadyDeleted = This version has already been deleted
version.dependency.notOnOre = This plugin is not available for download on Hangar
@ -247,6 +251,7 @@ version.create.title = New project release
version.create.noDescription = No description given.
version.create.upload = Upload
version.create.selectFile = Select file
version.create.externalUrl = External download URL
version.create.publish = Publish
version.create.tos = By clicking "Publish" you are agreeing to Hangar's <a href="https://docs.spongepowered.org/stable/en/about/tos.html">Terms of Service</a>.
version.create.info = Release a new version for <strong>{0}</strong> (pluginid: {1}).
@ -264,6 +269,7 @@ version.download.confirm.title = Download Warning for
version.download.confirm.header = {1} {2} by {0}
version.download.confirm.disclaimer = We disclaim all responsibility for any harm to your server or system should you choose not to heed this warning.
version.download.confirm.reviewedChannel = This version has not been reviewed by our moderation staff yet and may not be safe to use.
version.download.confirm.externalUrl = This version download is on an external site.
version.download.confirmPartial.reviewedChannel = \
This version has only been partially reviewed by our moderation staff and may not be safe for use. \
While the core plugin has been reviewed, other resources like shaded libraries have not.

View File

@ -23,7 +23,7 @@
maxlength="${config.channels.maxNameLen}"/>
<input type="hidden" name="channel-color-input" class="channel-color-input" value="" />
<a href="#">
<span class="color-picker" data-toggle="popover" data-placement="right" data-trigger="hover">
<span id="channel-color-picker" class="color-picker" data-toggle="popover" data-placement="right" data-trigger="hover">
<i class="fas fa-circle channel-id" style=""></i>
</span>
</a>

View File

@ -15,6 +15,10 @@
<script type="text/javascript" src="<@hangar.url "javascripts/pluginUpload.js" />"></script>
<script type="text/javascript" src="<@hangar.url "javascripts/projectDetail.js" />"></script>
<script type="text/javascript" src="<@hangar.url "javascripts/versionCreateChannelNew.js" />"></script>
<#if pending?? && !pending.dependencies??>
<script type="text/javascript" src="<@hangar.url "build/platform-choice.js" />"></script>
</#if>
<script>
DEFAULT_COLOR = '${config.channels.colorDefault.hex}';
</script>
@ -44,30 +48,58 @@
<table class="plugin-meta-table">
<tr>
<td><strong><@spring.message "version" /></strong></td>
<td>${version.versionString}</td>
</tr>
<tr>
<td><strong><@spring.message "version.description" /></strong></td>
<td>
<#if version.description?has_content>
${version.description}
<#if version.versionString??>
${version.versionString}
<#else>
<#if projectDescription?has_content>
${projectDescription}
<#else>
<@spring.message "version.create.noDescription" />
</#if>
<div class="form-group">
<label for="version-string-input" class="sr-only">Version String</label>
<input id="version-string-input" class="form-control" type="text" form="form-publish" name="versionString" required placeholder="Version">
</div>
</#if>
</td>
</tr>
<tr>
<td><strong><@spring.message "version.filename" /></strong></td>
<td>${version.fileName}</td>
</tr>
<tr>
<td><strong><@spring.message "version.fileSize" /></strong></td>
<td>${utils.formatFileSize(version.fileSize)}</td>
<td><strong><@spring.message "version.description" /></strong></td>
<td>
<#if version.versionString??>
<#if version.description?has_content>
${version.description}
<#else>
<#if projectDescription?has_content>
${projectDescription}
<#else>
<@spring.message "version.create.noDescription" />
</#if>
</#if>
<#else>
<div class="form-group">
<label for="version-description-input" class="sr-only">Version Description</label>
<input type="text" form="form-publish" name="versionDescription" class="form-control" id="version-description-input">
</div>
</#if>
</td>
</tr>
<#if version.fileName?? && !version.externalUrl??>
<tr>
<td><strong><@spring.message "version.filename" /></strong></td>
<td>${version.fileName}</td>
</tr>
<tr>
<td><strong><@spring.message "version.fileSize" /></strong></td>
<td>${utils.formatFileSize(version.fileSize)}</td>
</tr>
<#else>
<tr>
<td><strong><@spring.message "version.externalUrl" /></strong></td>
<td>
<div class="form-group">
<label for="external-url-input" class="sr-only"></label>
<input id="external-url-input" class="form-control" type="text" value="${version.externalUrl}" name="externalUrl" form="form-publish" required>
</div>
</td>
</tr>
</#if>
<tr>
<td><strong>Channel</strong></td>
<td class="form-inline">
@ -80,49 +112,59 @@
</#list>
</select>
<a href="#">
<i id="channel-new" class="fas fa-plus" data-toggle="modal"
data-target="#channel-settings"></i>
<i id="channel-new" class="fas fa-plus" data-toggle="modal" data-target="#channel-settings"></i>
</a>
</td>
</tr>
<tr>
<td><strong>Platform</strong></td>
<td>
<div class="float-right" id="upload-platform-tags">
<#list version.dependenciesAsGhostTags as pair>
<@projectTag.tagTemplate @helper["io.papermc.hangar.model.viewhelpers.ViewTag"].fromVersionTag(pair.right) pair.left "form-publish" />
</#list>
</div>
<#if version.dependencies??>
<div class="float-right" id="upload-platform-tags">
<#list version.dependenciesAsGhostTags as pair>
<@projectTag.tagTemplate @helper["io.papermc.hangar.model.viewhelpers.ViewTag"].fromVersionTag(pair.right) pair.left "form-publish" />
</#list>
</div>
<#else>
<div id="platform-choice"></div>
</#if>
</td>
</tr>
<tr>
<td><strong><@spring.message "version.create.unstable" /></strong></td>
<td>
<label for="is-unstable-version" class="form-check-label">
<strong><@spring.message "version.create.unstable" /></strong>
</label>
</td>
<td class="rv">
<div class="checkbox-inline">
<input form="form-publish" name="unstable" type="checkbox" value="true"/>
<div class="form-check">
<input id="is-unstable-version" class="form-check-input" form="form-publish" name="unstable" type="checkbox" value="true">
</div>
<div class="clearfix"></div>
</td>
</tr>
<tr>
<td><strong>Recommended</strong></td>
<td>
<label for="is-recommended-version" class="form-check-label">
<strong>Recommended</strong>
</label>
</td>
<td class="rv">
<div class="checkbox-inline">
<input form="form-publish" name="recommended" type="checkbox" checked
value="true"/>
<div class="form-check">
<input id="is-recommended-version" class="form-check-input" form="form-publish" name="recommended" type="checkbox" checked value="true">
</div>
<div class="clearfix"></div>
</td>
</tr>
<tr>
<td><strong>Create forum post</strong></td>
<td>
<label for="create-forum-post-version" class="form-check-label"></label>
<strong>Create forum post</strong>
</td>
<td class="rv">
<div class="checkbox-inline">
<div class="form-check">
<#-- @ftlvariable name="forumSync" type="java.lang.Boolean" -->
<input form="form-publish" name="forum-post" type="checkbox"
<#if forumSync> checked </#if> value="true"/>
<input id="create-forum-post-version" class="form-check-input" form="form-publish" name="forum-post" type="checkbox" <#if forumSync> checked </#if> value="true">
</div>
<div class="clearfix"></div>
</td>
@ -149,30 +191,36 @@
</#if>
<@form.form action=Routes.VERSIONS_UPLOAD.getRouteUrl(ownerName, projectSlug) method="POST"
enctype="multipart/form-data" id="form-upload">
enctype="multipart/form-data" id="form-upload" class="form-inline">
<@csrf.formField />
<label class="btn btn-default float-left" for="pluginFile">
<label class="btn btn-info float-left" for="pluginFile">
<input id="pluginFile" name="pluginFile" type="file" style="display: none;" accept=".jar,.zip">
<@spring.message "version.create.selectFile" />
</label>
<@alertFile.alertFile />
</@form.form>
<#if pending??>
<#-- Ready to go! -->
<#assign version = pending>
<@form.form method="POST" action=Routes.VERSIONS_PUBLISH.getRouteUrl(ownerName, projectSlug, version.versionString)
id="form-publish" class="float-right">
<#if !pending??>
<@form.form action=Routes.VERSIONS_CREATE_EXTERNAL_URL.getRouteUrl(ownerName, projectSlug) method="POST" id="form-url-upload" class="form-inline">
<@csrf.formField />
<input type="hidden" class="channel-color-input" name="channel-color-input"
value="${config.channels.colorDefault.hex}" />
<div class="input-group float-right" style="width: 50%">
<input type="text" class="form-control" id="externalUrl" name="externalUrl" placeholder="<@spring.message "version.create.externalUrl" />" style="width: 70%">
<div class="input-group-append">
<button class="btn btn-info" type="submit">Create Version</button>
</div>
<div>
<input type="submit" name="create" value="<@spring.message "version.create.publish" />"
class="btn btn-primary" />
</div>
</@form.form>
<#else>
<#assign version = pending>
<#assign formAction>
<#if version.versionString??>${Routes.VERSIONS_PUBLISH.getRouteUrl(ownerName, projectSlug, version.versionString)}<#else>${Routes.VERSIONS_PUBLISH_URL.getRouteUrl(ownerName, projectSlug)}</#if>
</#assign>
<@form.form method="POST" action=formAction id="form-publish" class="float-right">
<@csrf.formField />
<input type="hidden" class="channel-color-input" name="channel-color-input" value="${config.channels.colorDefault.hex}">
<div><input type="submit" name="create" value="<@spring.message "version.create.publish" />" class="btn btn-primary"></div>
</@form.form>
</#if>
</div>

View File

@ -7,8 +7,8 @@
<#-- @ftlvariable name="isTargetChannelNonReviewed" type="java.lang.Boolean" -->
<#assign message><@spring.message "version.download.confirm.title" /> <@spring.messageArgs code="version.download.confirm.header" args=[project.ownerName, project.name, target.versionString] /></#assign>
<@base.base title=message>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<div class="col-12 col-sm-4 no-padding">
@ -22,14 +22,19 @@
<div class="alert alert-danger" style="margin-bottom: 10px">
<#assign ReviewState=@helper["io.papermc.hangar.model.generated.ReviewState"] />
<#-- @ftlvariable name="ReviewState" type="io.papermc.hangar.model.generated.ReviewState" -->
<#if isTargetChannelNonReviewed || target.reviewState == ReviewState.BACKLOG>
<@spring.message "version.download.confirm.nonReviewedChannel" />
<#elseif target.reviewState != ReviewState.PARTIALLY_REVIEWED>
<@spring.message "version.download.confirm.reviewedChannel" />
<#else>
<@spring.message "version.download.confirmPartial.reviewedChannel" />
</#if>
<#if isTargetChannelNonReviewed || target.reviewState == ReviewState.BACKLOG>
<@spring.message "version.download.confirm.nonReviewedChannel" />
<#elseif target.reviewState != ReviewState.UNREVIEWED>
<@spring.message "version.download.confirm.reviewedChannel" />
<#elseif target.reviewState != ReviewState.PARTIALLY_REVIEWED>
<@spring.message "version.download.confirmPartial.reviewedChannel" />
</#if>
</div>
<#if target.isExternal()>
<div class="alert alert-danger" style="margin-bottom: 10px">
<@spring.message "version.download.confirm.externalUrl" />
</div>
</#if>
<p>
<@spring.message "general.disclaimer" />:
<span class="text-italic"><@spring.message "version.download.confirm.disclaimer" /></span>

View File

@ -5,20 +5,6 @@
<#import "*/projects/view.ftlh" as projects />
<#import "*/utils/editor.ftlh" as editor />
<#--
@import controllers.sugar.Requests.OreRequest
@import models.viewhelper.{ScopedProjectData, VersionData}
@import ore.OreConfig
@import ore.data.Platform
@import ore.markdown.MarkdownRenderer
@import ore.models.project.{ReviewState, Visibility}
@import ore.permission.Permission
@import util.StringFormatterUtils._
@import util.syntax._
@import views.html.helper.{CSRF, form}
@import views.html.utils.editor
@(v: VersionData, sp: ScopedProjectData)(implicit messages: Messages, request: OreRequest[_], flash: Flash, config: OreConfig, renderer: MarkdownRenderer, assetsFinder: AssetsFinder)-->
<#assign ReviewState=@helper["io.papermc.hangar.model.generated.ReviewState"] />
<#assign Permission=@helper["io.papermc.hangar.model.Permission"] />
<#assign Visibility=@helper["io.papermc.hangar.model.Visibility"] />
@ -116,16 +102,20 @@
<div class="btn-group btn-download">
<a href="${Routes.VERSIONS_DOWNLOAD.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionString, "", "")}"
title="<@spring.message "project.download.recommend" />" data-toggle="tooltip"
data-placement="bottom" class="btn btn-primary">
<i class="fas fa-download"></i> <@spring.message "general.download" />
title="<@spring.message "project.download.recommend" />" data-toggle="tooltip"
data-placement="bottom" class="btn btn-primary">
<i class="fas fa-download"></i>
<#if v.v.externalUrl??>
<@spring.message "general.downloadExternal" />
<#else>
<@spring.message "general.download" />
</#if>
</a>
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a href="${Routes.VERSIONS_DOWNLOAD.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionString, "", "")}" class="dropdown-item"><@spring.message "general.download" /></a>
<a class="dropdown-item" href="${Routes.VERSIONS_DOWNLOAD.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionString, "", "")}"><@spring.message "general.download" /></a>
<a href="#" class="copy-url dropdown-item" data-clipboard-text="${config.baseUrl}${Routes.VERSIONS_DOWNLOAD.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionString, "", "")}">Copy URL</a>
</div>
</div>

View File

@ -26,7 +26,7 @@ Base template for Project overview.
</#assign>
<#assign metaVar>
<meta property="og:title" content="${p.project.ownerName} / ${p.project.name}" />
<meta property="og:title" content="${p.project.ownerName}/${p.project.name}" />
<meta property="og:type" content="website" />
<meta property="og:url" content="${Routes.PROJECTS_SHOW.getRouteUrl(p.project.ownerName, p.project.slug)}" />
<meta property="og:image" content="${p.iconUrl}" />

View File

@ -2,10 +2,9 @@
<#import "*/utils/hangar.ftlh" as hangar />
<#import "*/layout/base.ftlh" as base />
@import controllers.sugar.Requests.OreRequest
@import ore.OreConfig
@(title: String, recipient: String, body: String)(implicit messages: Messages, request: OreRequest[_], config: OreConfig, flash: Flash, assetsFinder: AssetsFinder)
<#-- @ftlvariable name="title" type="java.lang.String" -->
<#-- @ftlvariable name="recipient" type="java.lang.String" -->
<#-- @ftlvariable name="body" type="java.lang.String" -->
<#assign message><@spring.message title /></#assign>
<@base.base title=message scriptsEnabled=false showHeader=false showFooter=false>
<div class="container">