forked from anegostudios/vsmoddb
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathedit-release.php
More file actions
358 lines (304 loc) · 13.5 KB
/
Copy pathedit-release.php
File metadata and controls
358 lines (304 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
<?php
if(DB_READONLY) showReadonlyPage();
if (empty($user)) showErrorPage(HTTP_UNAUTHORIZED);
if ($user['isBanned']) showErrorPage(HTTP_FORBIDDEN, 'You are currently banned.');
include($config['basepath'] . 'lib/edit-release.php');
$existingRelease = null;
$targetMod = null;
$pushedErrorForCurrentFile = false; // This is here so we can push the errors even if the file is not submitted, but dont duplicate the error message in case it is.
// /edit/release/?assetid=32 (edit existing release)
if(!empty($_REQUEST['assetid'])) {
$existingRelease = $con->getRow(<<<SQL
SELECT a.*, r.*,
createdBy.name AS createdByUsername,
lastEditedBy.name AS lastEditedByUsername,
rr.reason AS retractionReason,
lastRetractedBy.roleId IN (?,?) AS retractedByModerator
FROM modReleases r
JOIN assets a ON a.assetId = r.assetId
LEFT JOIN modReleaseRetractions rr ON rr.releaseId = r.releaseId
LEFT JOIN users createdBy ON createdBy.userId = a.createdbyuserid
LEFT JOIN users lastEditedBy ON lastEditedBy.userId = a.editedbyuserid
LEFT JOIN users lastRetractedBy ON lastRetractedBy.userId = rr.lastModifiedBy
WHERE r.assetId = ?
SQL, [ROLE_ADMIN, ROLE_MODERATOR, $_REQUEST['assetid']]);
if($existingRelease) {
$targetMod = $con->getRow(<<<SQL
SELECT a.*, m.*
FROM mods m
JOIN assets a ON a.assetId = m.assetId
WHERE m.modId = ?
SQL, [$existingRelease['modId']]);
}
}
// /edit/release/?modid=32 (add new release)
else if(!empty($_REQUEST['modid'])) {
$targetMod = $con->getRow(<<<SQL
SELECT a.*, m.*
FROM mods m
JOIN assets a ON a.assetId = m.assetId
WHERE m.modId = ?
SQL, [$_REQUEST['modid']]);
}
//NOTE(Rennorb): Do as little work as possible before this permission check, but don't unnecessarily split queries.
if(!$targetMod) showErrorPage(HTTP_NOT_FOUND, 'Target mod was not found.');
if(!canEditAsset($targetMod, $user)) showErrorPage(HTTP_FORBIDDEN);
//
// Actions
//
if($existingRelease && $existingRelease['retractionReason']) {
addMessage(MSG_CLASS_WARN.' permanent', "This release has been retracted!");
}
else if(!empty($_POST['save'])) {
validateActionToken();
$oldMsgCount = count($messages /* global */);
$newData = [];
//
// Validate
//
//TODO(Rennorb) @cleanup @correctness: Attach files on save instead of on upload.
if($existingRelease) {
$currentFiles = $con->getAll(<<<SQL
SELECT f.assetId, f.fileId, mpr.modIdentifier, mpr.modVersion
FROM files f
LEFT JOIN modPeekResults mpr ON mpr.fileId = f.fileId
WHERE f.assetId = ?
SQL, [$existingRelease['assetId']]);
}
else {
// hovering files
$currentFiles = $con->getAll(<<<SQL
SELECT f.assetId, f.fileId, mpr.modIdentifier, mpr.modVersion, mpr.errors
FROM files f
LEFT JOIN modPeekResults mpr ON mpr.fileId = f.fileId
WHERE f.assetId IS NULL AND f.assetTypeId = 2 AND f.userId = ?
SQL, [$user['userId']]);
if(!empty($currentFiles[0]['errors']) && ($targetMod['category'] & CATEGORY__MASK) === CATEGORY_GAME_MOD) {
addMessage(MSG_CLASS_ERROR, 'There are issues with the current file: '.$currentFiles[0]['errors'], true);
$pushedErrorForCurrentFile = true;
}
}
/** @var array{'assetId':int, 'fileId':int, 'modIdentifier':string|null, 'modVersion':string|null}[] $currentFiles */
//TODO(Rennorb) @cleanup: This exists for the case that the user used the "Browse" button instead of drag and drop, that doesn't immediately upload the file.
if(!empty($_FILES['newfile']) && $_FILES['newfile']['error'] != 4) {
if($currentFiles) {
addMessage(MSG_CLASS_ERROR, 'Only one file can be attached to a release.');
}
else {
$assetId = $existingRelease['assetId'] ?? 0;
$processedFile = processFileUpload($_FILES['newfile'], ASSETTYPE_RELEASE, $assetId, $targetMod['modId']);
if($processedFile['status'] === 'error') {
addMessage(MSG_CLASS_ERROR, 'Failed to process uploaded file: '.$processedFile['errormessage'], true);
}
else if(($targetMod['category'] & CATEGORY__MASK) === CATEGORY_GAME_MOD) {
if($processedFile['modparse'] === 'error') {
addMessage(MSG_CLASS_ERROR, 'Failed to parse modinfo: '.$processedFile['parsemsg'], true);
$pushedErrorForCurrentFile = true;
}
else {
$currentFiles[] = [
'assetId' => $assetId,
'fileId' => $processedFile['fileid'],
'modIdentifier' => $processedFile['modid'],
'modVersion' => $processedFile['modversion'],
];
}
}
else {
$currentFiles[] = [
'assetId' => $assetId,
'fileId' => $processedFile['fileid'],
];
}
}
}
if(!$currentFiles && (empty($_FILES['newfile']) || $_FILES['newfile']['error'] === 4)) { // Release needs a file, but don't emit the message if the parsing failed.
addMessage(MSG_CLASS_ERROR, 'Release is missing a file.');
}
if(isset($_POST['text'])) {
$newData['text'] = trimHtml(sanitizeHtml($_POST['text']));
$textLen = strlen($newData['text']);
if($textLen > 65535) { // TEXT column max length in assets.text
$sizeKb = floor($textLen / 1024);
$reason = "Excessive size ({$sizeKb}KB).";
if(str_contains($newData['text'], 'src="data:image')) $reason .= " You cannot paste large images directly. If you need a large image, upload it to an external site and link to that.";
addMessage(MSG_CLASS_ERROR, $reason);
}
}
$newCompatibleGameVersions = null;
if(($targetMod['category'] & CATEGORY__MASK) === CATEGORY_GAME_MOD) {
// Mods take modid and version from the attached file. We no longer allow manual entry.
if($currentFiles) {
$newData['identifier'] = $currentFiles[0]['modIdentifier'];
$newData['version'] = $currentFiles[0]['modVersion'];
if (in_array($newData['identifier'], ["game", "creative", "survival"])) { // Reserve special mod ids
addMessage(MSG_CLASS_ERROR, "This modid ('{$newData['identifier']}') is reserved.");
}
else {
$sqlIgnoreExistingRelease = $existingRelease ? "r.releaseId != {$existingRelease['releaseId']} AND" : ''; // @security $existingRelease['releaseId'] comes from the database and is numeric, therefore sql inert.
$inUseBy = $con->getRow(<<<SQL
SELECT a.*, r.modId, r.version, m.assetId as modAssetId, m.urlAlias
FROM modReleases r
JOIN assets a ON a.assetId = r.assetId
JOIN mods m ON m.modId = r.modId
WHERE $sqlIgnoreExistingRelease r.identifier = ? AND (r.modId != ? || r.version = ?)
LIMIT 1
SQL, [$newData['identifier'], $targetMod['modId'], $newData['version']]);
if ($inUseBy) {
if($inUseBy['modId'] == $targetMod['modId'] && $inUseBy['version'] == $newData['version']) {
$rv = formatSemanticVersion(intval($newData['version']));
addMessage(MSG_CLASS_ERROR, "This version ($rv) of the mod has already been released (<a href='/edit/release/?assetid={$inUseBy['assetId']}'>link</a>).");
}
else {
$mid = htmlspecialchars($newData['identifier']);
$mpath = formatModPath(['urlAlias' => $inUseBy['urlAlias'], 'assetId' => $inUseBy['modAssetId']]);
addMessage(MSG_CLASS_ERROR, "This modid ('$mid') is already in use by another mod (<a href='$mpath' target='_blank'>link</a>).");
}
}
}
}
// modversion is already validated by the ModPeek wrapper.
if(!empty($_POST['cgvs'])) {
$newCompatibleGameVersions = array_filter(array_map('compileSemanticVersion', $_POST['cgvs']));
}
if(!$newCompatibleGameVersions) {
addMessage(MSG_CLASS_ERROR, 'Missing compatible game versions.');
}
}
else {
// Non-mods must have their version manually entered.
if(empty($_POST['modversion'])) {
addMessage(MSG_CLASS_ERROR, 'Missing version field.');
}
else {
$version = compileSemanticVersion($_POST['modversion']);
if($version === false) {
addMessage(MSG_CLASS_ERROR, 'Malformed version.<br/>Version numbers must follow semantic versioning, formatted as <code>n.n.n[-{rc|pre|dev}.n]</code><br/>Examples: <code>1.0.1</code> or <code>1.5.2-rc.1</code>');
}
else {
$newData['version'] = $version;
}
}
}
if(count($messages /* global */) === $oldMsgCount) { // no errors occurred
//
// Save
//
if($existingRelease) {
$ok = updateRelease($targetMod, $existingRelease, $newData, $newCompatibleGameVersions, $currentFiles[0]);
if($ok) {
if(!empty($_POST['saveandback'])) forceRedirect(formatModPath($targetMod).'#tab-files');
else forceRedirectAfterPOST();
exit();
}
}
else { // adding a new release, no $existingRelease
$assetId = createNewRelease($targetMod, $newData, $newCompatibleGameVersions, $currentFiles[0]);
if($assetId) {
if(!empty($_POST['saveandback'])) forceRedirect(formatModPath($targetMod).'#tab-files');
else forceRedirect('/edit/release/?assetid='.$assetId);
exit();
}
}
}
}
//
// Prepare data for display
//
if($existingRelease) {
$files = $con->getAll(<<<SQL
SELECT f.*, i.hasThumbnail, CONCAT(ST_X(i.size), 'x', ST_Y(i.size)) AS imageSize
FROM files f
LEFT JOIN fileImageData i ON i.fileId = f.fileId
WHERE f.assetId = ?
SQL, [$existingRelease['assetId']]);
$compatibleGameVersions = $con->getCol('SELECT gameVersion FROM modReleaseCompatibleGameVersions WHERE releaseId = ?', $existingRelease['releaseId']);
$existingRelease['compatibleGameVersions'] = array_flip(array_map('intval', $compatibleGameVersions));
}
else {
// hovering files
$files = $con->getAll(<<<SQL
SELECT f.*, i.hasThumbnail, CONCAT(ST_X(i.size), 'x', ST_Y(i.size)) AS imageSize, mpr.modIdentifier, mpr.modVersion, mpr.rawDependencies, mpr.errors
FROM files f
LEFT JOIN modPeekResults mpr ON mpr.fileId = f.fileId
LEFT JOIN fileImageData i ON i.fileId = f.fileId
WHERE f.assetId IS NULL AND f.assetTypeId = 2 AND f.userId = ?
SQL, [$user['userId']]);
if(!$pushedErrorForCurrentFile && !empty($files[0]['errors'])) {
addMessage(MSG_CLASS_ERROR, 'There are issues with the current file: '.$files[0]['errors'], true);
}
}
foreach($files as &$file) {
$file['created'] = date('M jS Y, H:i:s', strtotime($file['created']));
$file['ext'] = substr($file['name'], strrpos($file['name'], '.')+1); // no clue why pathinfo doesnt work here
$file['url'] = maybeFormatDownloadTrackingUrlDependingOnFileExt($file);
}
unset($file);
$allGameVersions = $con->getAll('SELECT version FROM gameVersions ORDER BY version DESC');
foreach($allGameVersions as &$gameVersion) {
$gameVersion['version'] = intval($gameVersion['version']);
$gameVersion['name'] = formatSemanticVersion($gameVersion['version']);
}
unset($gameVersion);
$assetChangelog = $existingRelease ? $con->getAll(<<<SQL
SELECT ch.text, ch.lastModified, u.name AS username
FROM changelogs ch
JOIN users u ON u.userId = ch.userId
WHERE ch.assetId = ?
ORDER BY ch.created DESC
LIMIT 20
SQL, [$existingRelease['assetId']]) : [];
if(!$existingRelease) {
$existingRelease = [
'assetId' => 0,
'releaseId' => 0,
'text' => $_POST['text'] ?? '',
'numSaved' => 0,
'compatibleGameVersions' => empty($_POST['cgvs']) ? [] : array_flip(array_filter(array_map('compileSemanticVersion', $_POST['cgvs']))),
'retractionReason' => null,
];
if(($targetMod['category'] & CATEGORY__MASK) === CATEGORY_GAME_MOD) {
// Pre-select values from hovering file:
$existingRelease['identifier'] = $files ? $files[0]['modIdentifier'] : '';
$existingRelease['version'] = $files ? formatSemanticVersion(intval($files[0]['modVersion'])) : '';
if(!$existingRelease['compatibleGameVersions'] && $files && ($minCompat = findMinCompatibleGameVersion($files[0]['rawDependencies'])) && $minCompat !== null) {
$detectedCompat = [];
foreach($allGameVersions as $version) { // Order is descending, so the filter is trivial.
if($version['version'] >= $minCompat) $detectedCompat[] = $version['version'];
else break;
}
$existingRelease['compatibleGameVersions'] = array_flip($detectedCompat);
}
}
else {
$existingRelease['identifier'] = '';
$existingRelease['version'] = $_POST['modversion'] ?? '';
}
}
else {
$existingRelease['version'] = formatSemanticVersion(intval($existingRelease['version']));
}
$allGameVersionsTree = [];
foreach($allGameVersions as $gameVersion) {
$num = $gameVersion['version'];
$major = ($num & VERSION_MASK_MAJOR) >> 48;
$minor = ($num & VERSION_MASK_MINOR) >> 32;
$patch = ($num & VERSION_MASK_PATCH) >> 16;
$prerelease = $num & VERSION_MASK_PRERELEASE;
$allGameVersionsTree[$major][$minor][$patch][$prerelease] = $gameVersion;
}
$maxUploadSizeLimit = parseMaxUploadSizeFromIni();
$uploadSizeLimitOfThisMod = $targetMod['uploadLimitOverwrite'] !== null ? $targetMod['uploadLimitOverwrite'] : UPLOAD_LIMITS[ASSETTYPE_RELEASE]['individualSize'];
$view->assign('uploadSizeLimit', min($maxUploadSizeLimit, $uploadSizeLimitOfThisMod), null, true);
cspAllowTinyMceFull();
cspPushAllowedInlineHandlerHash('sha256-nTlTeikEEupAQmSPlHWFcoJvMdPCIBu+Zu+G64E7uC4='); // javascript:submitForm(0)
cspPushAllowedInlineHandlerHash('sha256-XKuSPEJjbu3T+mAY9wlP6dgYQ4xJL1rP4m3GrDwZ68c='); // javascript:submitForm(1)
cspReplaceAllowedFetchSources("{$_SERVER['HTTP_HOST']}/edit-deletefile {$_SERVER['HTTP_HOST']}/edit-uploadfile {$_SERVER['HTTP_HOST']}/api/v2/mods/{$targetMod['modId']}/releases/{$existingRelease['releaseId']}/retraction");
$view->assign('allGameVersionsTree', $allGameVersionsTree, null, true);
$view->assign('mod', $targetMod);
$view->assign('doFileValidation', ($targetMod["category"] & CATEGORY__MASK) === CATEGORY_GAME_MOD, null, true);
$view->assign('release', $existingRelease);
$view->assign('asset', ['assetId' => $existingRelease['assetId'], 'assetTypeId' => ASSETTYPE_RELEASE], null, true); //TODO(Rennorb) @cleanup: only here for the footer js / file upload code
$view->assign('files', $files);
$view->assign('assetChangelog', $assetChangelog);
$view->display('edit-release');