jdarwood007 commited on 2023-04-13 14:52:52
Showing 8 changed files, with 1644 additions and 0 deletions.
... | ... |
@@ -0,0 +1,190 @@ |
1 |
+checks: |
|
2 |
+ php: |
|
3 |
+ variable_existence: true |
|
4 |
+ use_statement_alias_conflict: true |
|
5 |
+ unused_variables: true |
|
6 |
+ unused_properties: true |
|
7 |
+ unused_parameters: true |
|
8 |
+ unused_methods: true |
|
9 |
+ unreachable_code: true |
|
10 |
+ switch_fallthrough_commented: true |
|
11 |
+ simplify_boolean_return: true |
|
12 |
+ return_doc_comments: true |
|
13 |
+ return_doc_comment_if_not_inferrable: true |
|
14 |
+ require_scope_for_methods: true |
|
15 |
+ require_php_tag_first: true |
|
16 |
+ remove_extra_empty_lines: true |
|
17 |
+ property_assignments: true |
|
18 |
+ precedence_mistakes: true |
|
19 |
+ precedence_in_conditions: true |
|
20 |
+ parse_doc_comments: true |
|
21 |
+ parameter_non_unique: true |
|
22 |
+ parameter_doc_comments: true |
|
23 |
+ param_doc_comment_if_not_inferrable: true |
|
24 |
+ overriding_private_members: true |
|
25 |
+ no_trailing_whitespace: true |
|
26 |
+ no_short_open_tag: true |
|
27 |
+ no_property_on_interface: true |
|
28 |
+ no_non_implemented_abstract_methods: true |
|
29 |
+ no_short_method_names: |
|
30 |
+ minimum: '3' |
|
31 |
+ no_goto: true |
|
32 |
+ no_error_suppression: true |
|
33 |
+ no_debug_code: true |
|
34 |
+ more_specific_types_in_doc_comments: true |
|
35 |
+ missing_arguments: true |
|
36 |
+ method_calls_on_non_object: true |
|
37 |
+ instanceof_class_exists: true |
|
38 |
+ foreach_traversable: true |
|
39 |
+ fix_use_statements: |
|
40 |
+ remove_unused: true |
|
41 |
+ preserve_multiple: false |
|
42 |
+ preserve_blanklines: false |
|
43 |
+ order_alphabetically: false |
|
44 |
+ fix_line_ending: true |
|
45 |
+ fix_doc_comments: true |
|
46 |
+ encourage_shallow_comparison: true |
|
47 |
+ duplication: true |
|
48 |
+ deprecated_code_usage: true |
|
49 |
+ deadlock_detection_in_loops: true |
|
50 |
+ code_rating: true |
|
51 |
+ closure_use_not_conflicting: true |
|
52 |
+ closure_use_modifiable: true |
|
53 |
+ catch_class_exists: true |
|
54 |
+ avoid_duplicate_types: true |
|
55 |
+ avoid_closing_tag: false |
|
56 |
+ assignment_of_null_return: true |
|
57 |
+ argument_type_checks: true |
|
58 |
+ no_long_variable_names: |
|
59 |
+ maximum: '40' |
|
60 |
+ no_short_variable_names: |
|
61 |
+ minimum: '3' |
|
62 |
+ phpunit_assertions: true |
|
63 |
+ remove_php_closing_tag: false |
|
64 |
+ no_mixed_inline_html: false |
|
65 |
+ require_braces_around_control_structures: false |
|
66 |
+ psr2_control_structure_declaration: false |
|
67 |
+ avoid_superglobals: false |
|
68 |
+ security_vulnerabilities: false |
|
69 |
+ no_exit: false |
|
70 |
+coding_style: |
|
71 |
+ php: |
|
72 |
+ indentation: |
|
73 |
+ general: |
|
74 |
+ use_tabs: true |
|
75 |
+ size: 4 |
|
76 |
+ switch: |
|
77 |
+ indent_case: true |
|
78 |
+ spaces: |
|
79 |
+ general: |
|
80 |
+ linefeed_character: newline |
|
81 |
+ before_parentheses: |
|
82 |
+ function_declaration: false |
|
83 |
+ closure_definition: false |
|
84 |
+ function_call: false |
|
85 |
+ if: true |
|
86 |
+ for: true |
|
87 |
+ while: true |
|
88 |
+ switch: true |
|
89 |
+ catch: true |
|
90 |
+ array_initializer: false |
|
91 |
+ around_operators: |
|
92 |
+ assignment: true |
|
93 |
+ logical: true |
|
94 |
+ equality: true |
|
95 |
+ relational: true |
|
96 |
+ bitwise: true |
|
97 |
+ additive: true |
|
98 |
+ multiplicative: true |
|
99 |
+ shift: true |
|
100 |
+ unary_additive: false |
|
101 |
+ concatenation: true |
|
102 |
+ negation: false |
|
103 |
+ before_left_brace: |
|
104 |
+ class: true |
|
105 |
+ function: true |
|
106 |
+ if: true |
|
107 |
+ else: true |
|
108 |
+ for: true |
|
109 |
+ while: true |
|
110 |
+ do: true |
|
111 |
+ switch: true |
|
112 |
+ try: true |
|
113 |
+ catch: true |
|
114 |
+ finally: true |
|
115 |
+ before_keywords: |
|
116 |
+ else: true |
|
117 |
+ while: true |
|
118 |
+ catch: true |
|
119 |
+ finally: true |
|
120 |
+ within: |
|
121 |
+ brackets: false |
|
122 |
+ array_initializer: false |
|
123 |
+ grouping: false |
|
124 |
+ function_call: false |
|
125 |
+ function_declaration: false |
|
126 |
+ if: false |
|
127 |
+ for: false |
|
128 |
+ while: false |
|
129 |
+ switch: false |
|
130 |
+ catch: false |
|
131 |
+ type_cast: false |
|
132 |
+ ternary_operator: |
|
133 |
+ before_condition: true |
|
134 |
+ after_condition: true |
|
135 |
+ before_alternative: true |
|
136 |
+ after_alternative: true |
|
137 |
+ in_short_version: false |
|
138 |
+ other: |
|
139 |
+ before_comma: false |
|
140 |
+ after_comma: true |
|
141 |
+ before_semicolon: false |
|
142 |
+ after_semicolon: true |
|
143 |
+ after_type_cast: true |
|
144 |
+ braces: |
|
145 |
+ classes_functions: |
|
146 |
+ class: new-line |
|
147 |
+ function: new-line |
|
148 |
+ closure: new-line |
|
149 |
+ if: |
|
150 |
+ opening: new-line |
|
151 |
+ always: false |
|
152 |
+ else_on_new_line: true |
|
153 |
+ for: |
|
154 |
+ opening: new-line |
|
155 |
+ always: false |
|
156 |
+ while: |
|
157 |
+ opening: new-line |
|
158 |
+ always: false |
|
159 |
+ do_while: |
|
160 |
+ opening: undefined |
|
161 |
+ always: true |
|
162 |
+ while_on_new_line: true |
|
163 |
+ switch: |
|
164 |
+ opening: new-line |
|
165 |
+ try: |
|
166 |
+ opening: new-line |
|
167 |
+ catch_on_new_line: true |
|
168 |
+ finally_on_new_line: true |
|
169 |
+ upper_lower_casing: |
|
170 |
+ keywords: |
|
171 |
+ general: lower |
|
172 |
+ constants: |
|
173 |
+ true_false_null: lower |
|
174 |
+ |
|
175 |
+ |
|
176 |
+build: |
|
177 |
+ nodes: |
|
178 |
+ analysis: |
|
179 |
+ tests: |
|
180 |
+ override: |
|
181 |
+ - php-scrutinizer-run |
|
182 |
+ dependencies: |
|
183 |
+ after: |
|
184 |
+ - git clone https://github.com/WordPress/WordPress.git wordpress |
|
185 |
+ |
|
186 |
+filter: |
|
187 |
+ dependency_paths: |
|
188 |
+ - wordpress/ |
|
189 |
+ excluded_paths: |
|
190 |
+ - '*.min.js' |
... | ... |
@@ -0,0 +1,29 @@ |
1 |
+BSD 3-Clause License |
|
2 |
+ |
|
3 |
+Copyright (c) 2023, SleePy |
|
4 |
+All rights reserved. |
|
5 |
+ |
|
6 |
+Redistribution and use in source and binary forms, with or without |
|
7 |
+modification, are permitted provided that the following conditions are met: |
|
8 |
+ |
|
9 |
+1. Redistributions of source code must retain the above copyright notice, this |
|
10 |
+ list of conditions and the following disclaimer. |
|
11 |
+ |
|
12 |
+2. Redistributions in binary form must reproduce the above copyright notice, |
|
13 |
+ this list of conditions and the following disclaimer in the documentation |
|
14 |
+ and/or other materials provided with the distribution. |
|
15 |
+ |
|
16 |
+3. Neither the name of the copyright holder nor the names of its |
|
17 |
+ contributors may be used to endorse or promote products derived from |
|
18 |
+ this software without specific prior written permission. |
|
19 |
+ |
|
20 |
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
|
21 |
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
|
22 |
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
23 |
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
|
24 |
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
|
25 |
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
|
26 |
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
|
27 |
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
|
28 |
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
|
29 |
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
... | ... |
@@ -0,0 +1,1170 @@ |
1 |
+/* |
|
2 |
+ * Simple way to handle translation using a C# like Format for variable replacements. Used for i18n. |
|
3 |
+*/ |
|
4 |
+String.prototype.format = function() {return [...arguments].reduce((rt,cv,ci) => rt.replace("{" + ci + "}", cv), this);}; |
|
5 |
+txt = []; |
|
6 |
+ |
|
7 |
+/* Some variables we pass around to other functions*/ |
|
8 |
+usingArchive = 'zip'; |
|
9 |
+archiveData = null; |
|
10 |
+instructions = []; |
|
11 |
+ |
|
12 |
+/* Handles the event when we upload */ |
|
13 |
+async function uploadFileToJS(evt) |
|
14 |
+{ |
|
15 |
+ console.debug('Processing Uploaded file'); |
|
16 |
+ |
|
17 |
+ // Disable the event listners, so we can grab the SMF version data and repopulate data without triggering changes. |
|
18 |
+ disableVersionChangeListiners(); |
|
19 |
+ archiveData = null; |
|
20 |
+ await fetchSmfVersions().then(populateSmfVersions); |
|
21 |
+ |
|
22 |
+ // Reading the file data into something. |
|
23 |
+ let reader = new FileReader(); |
|
24 |
+ reader.onload = async function(evt) { |
|
25 |
+ console.debug('Waiting on File Reader'); |
|
26 |
+ if(evt.target.readyState != 2) |
|
27 |
+ return; |
|
28 |
+ if(evt.target.error) { |
|
29 |
+ throw new Error ('Unable to read uploaded file'); |
|
30 |
+ return; |
|
31 |
+ } |
|
32 |
+ |
|
33 |
+ let header = ''; |
|
34 |
+ const arr = (new Uint8Array(evt.target.result)).subarray(0, 4); |
|
35 |
+ for (let j = 0; j < arr.length; j++) { |
|
36 |
+ header += arr[j].toString(16); |
|
37 |
+ } |
|
38 |
+ |
|
39 |
+ // Zip |
|
40 |
+ if (header == '504b34') |
|
41 |
+ { |
|
42 |
+ console.debug('Sending to Zip Handler'); |
|
43 |
+ usingArchive = 'zip'; |
|
44 |
+ |
|
45 |
+ JSZip.loadAsync(evt.target.result) |
|
46 |
+ .then(processZipFile).catch(function (e) { |
|
47 |
+ document.getElementById('errorContainer').removeAttribute('hidden'); |
|
48 |
+ document.getElementById('errorContainer').innerHTML = e; |
|
49 |
+ }); |
|
50 |
+ } |
|
51 |
+ // Tar.gz |
|
52 |
+ /* |
|
53 |
+ 1f8b80: Unix compressed file |
|
54 |
+ 1f8b88: FAT/MS DOS file system |
|
55 |
+ */ |
|
56 |
+ else if (header == '1f8b80' || header == '1f8b88') |
|
57 |
+ { |
|
58 |
+ console.debug('Sending to Tgz Handler'); |
|
59 |
+ usingArchive = 'tgz'; |
|
60 |
+ |
|
61 |
+ // Can't do this the easy way.. |
|
62 |
+ const tgz = evt.target.result; |
|
63 |
+ const inf = pako.inflate(tgz); |
|
64 |
+ const ab = inf.buffer; |
|
65 |
+ const ut = await untar(ab); |
|
66 |
+ processTarGzFile(ut); |
|
67 |
+ } |
|
68 |
+ // 425a6839 == Tar.Bz2 |
|
69 |
+ else |
|
70 |
+ { |
|
71 |
+ console.debug('Unsure what file type we where handed', header); |
|
72 |
+ throw new Error ('Invalid usingArchive'); |
|
73 |
+ } |
|
74 |
+ }; |
|
75 |
+ |
|
76 |
+ // Send the received file data to our file reader. |
|
77 |
+ await reader.readAsArrayBuffer(evt.target.files[0]); |
|
78 |
+} |
|
79 |
+ |
|
80 |
+/* Handles receiving a file from a url. Due to browser security, needs to be local or have appropriate COORS headers to allow us to receive it */ |
|
81 |
+async function runParser(lvUrlFile) |
|
82 |
+{ |
|
83 |
+ console.debug('Loading File', lvUrlFile); |
|
84 |
+ |
|
85 |
+ // Disable the event listners, so we can grab the SMF version data and repopulate data without triggering changes. |
|
86 |
+ disableVersionChangeListiners(); |
|
87 |
+ await fetchSmfVersions().then(populateSmfVersions); |
|
88 |
+ |
|
89 |
+ if (lvUrlFile.endsWith('tar.gz') || lvUrlFile.endsWith('tgz')) |
|
90 |
+ { |
|
91 |
+ usingArchive = 'tgz'; |
|
92 |
+ console.debug('Smells like a tgz'); |
|
93 |
+ await fetch(lvUrlFile).then(res => res.arrayBuffer()) // Download gzipped tar file and get ArrayBuffer |
|
94 |
+ .then(pako.inflate) // Decompress gzip using pako |
|
95 |
+ .then(arr => arr.buffer) // Get ArrayBuffer from the Uint8Array pako returns |
|
96 |
+ .then(untar) // Untar |
|
97 |
+ .then(processTarGzFile); |
|
98 |
+ } |
|
99 |
+ else if (lvUrlFile.endsWith('zip')) |
|
100 |
+ { |
|
101 |
+ usingArchive = 'zip'; |
|
102 |
+ console.debug('Seems to be a zip'); |
|
103 |
+ |
|
104 |
+ await new JSZip.external.Promise(function (resolve, reject) { |
|
105 |
+ JSZipUtils.getBinaryContent(lvUrlFile, function(err, data) { |
|
106 |
+ if (err) { |
|
107 |
+ reject(err); |
|
108 |
+ } else { |
|
109 |
+ resolve(data); |
|
110 |
+ } |
|
111 |
+ }); |
|
112 |
+ }).then(function (data) { |
|
113 |
+ return JSZip.loadAsync(data); |
|
114 |
+ }).then(await processZipFile).catch(function (e) { |
|
115 |
+ console.debug('Error!', e); |
|
116 |
+ document.getElementById('errorContainer').removeAttribute('hidden'); |
|
117 |
+ document.getElementById('errorContainer').innerHTML = e; |
|
118 |
+ }); |
|
119 |
+ } |
|
120 |
+ else |
|
121 |
+ throw new Error ('Invalid usingArchive'); |
|
122 |
+} |
|
123 |
+ |
|
124 |
+/* Adds our event listner to changing the SMF version */ |
|
125 |
+function addVersionChangeListiners() |
|
126 |
+{ |
|
127 |
+ console.debug('Enable Version Change Listner'); |
|
128 |
+ document.getElementById('smfVersions').addEventListener('change', changeSmfVersion); |
|
129 |
+} |
|
130 |
+ |
|
131 |
+/* Disables our event listner to change the SMF Version */ |
|
132 |
+function disableVersionChangeListiners() |
|
133 |
+{ |
|
134 |
+ console.debug('Disable Version Change Listner'); |
|
135 |
+ document.getElementById('smfVersions').removeEventListener('change', changeSmfVersion); |
|
136 |
+} |
|
137 |
+ |
|
138 |
+/* Handles all logic we need to do in order to procoess the zip file */ |
|
139 |
+async function processZipFile(zip) |
|
140 |
+{ |
|
141 |
+ console.debug('Processing ZIP'); |
|
142 |
+ archiveData = zip; |
|
143 |
+ await processFile(); |
|
144 |
+} |
|
145 |
+ |
|
146 |
+/* Handles all logic we need to do in order to procoess the tar.gz file */ |
|
147 |
+async function processTarGzFile(tgz) |
|
148 |
+{ |
|
149 |
+ console.debug('Processing TGZ'); |
|
150 |
+ archiveData = tgz; |
|
151 |
+ await processFile(); |
|
152 |
+} |
|
153 |
+ |
|
154 |
+/* This processes the file, we have generic handlers and some functions will handle treating the zip/tar data differently as needed*/ |
|
155 |
+async function processFile() |
|
156 |
+{ |
|
157 |
+ // This extracts the info file from the root. If its in a sub folder, it won't find it. |
|
158 |
+ infoData = await fetchFileFromArchive('package-info.xml'); |
|
159 |
+ |
|
160 |
+ // Attempt to parse the info file as valid xml data. |
|
161 |
+ console.debug('Attempting to parse package-info.xml into XML Object'); |
|
162 |
+ parsedInfo = parseXml(infoData); |
|
163 |
+ if (typeof parsedInfo === 'undefined') |
|
164 |
+ throw new Error ('Unable to parse package-info.xml'); |
|
165 |
+ |
|
166 |
+ // Try to find our package name |
|
167 |
+ console.debug('Attempting to find Customization Name'); |
|
168 |
+ packageName = parsedInfo.getElementsByTagName('name')[0]?.innerHTML; |
|
169 |
+ |
|
170 |
+ // Can't find it, do not go any further. |
|
171 |
+ if (typeof packageName === 'undefined') |
|
172 |
+ throw new Error ('Unable to find Customization Name'); |
|
173 |
+ |
|
174 |
+ // Set up showing some information about this. |
|
175 |
+ setPackageName(packageName); |
|
176 |
+ showParserContainer(); |
|
177 |
+ |
|
178 |
+ // Ensure our event listners are disable, change our version drop down to list the versions we match better. |
|
179 |
+ console.debug('Locating possible versions'); |
|
180 |
+ disableVersionChangeListiners(); |
|
181 |
+ let possibleInstalls = parsedInfo.getElementsByTagName('install'); |
|
182 |
+ for(let i = 0; i < possibleInstalls.length; i++) |
|
183 |
+ processInstallXml(possibleInstalls[i]); |
|
184 |
+ |
|
185 |
+ // We can listen for events now. |
|
186 |
+ addVersionChangeListiners(); |
|
187 |
+ |
|
188 |
+ // Trigger a version change, which should have the best and newest match for this package now. |
|
189 |
+ document.getElementById('smfVersions').dispatchEvent(new Event('change')); |
|
190 |
+} |
|
191 |
+ |
|
192 |
+/* This function retrieves a file from the archive for use later */ |
|
193 |
+async function fetchFileFromArchive(fileName) |
|
194 |
+{ |
|
195 |
+ if (usingArchive == 'zip') |
|
196 |
+ { |
|
197 |
+ // The zip handler treats them as objects, but we need to ensure we can find files regardless of case, so we find the match, to find the real file name. |
|
198 |
+ let match = archiveData.files[Object.keys(archiveData.files).find(key => key.toLowerCase() === fileName.toLowerCase())]; |
|
199 |
+ |
|
200 |
+ // We can't continue without the file. |
|
201 |
+ if (match == undefined || match == null) |
|
202 |
+ throw new Error('Unable to find ' + fileName); |
|
203 |
+ |
|
204 |
+ // Now extract the info-file contents. |
|
205 |
+ realFileName = match.name; |
|
206 |
+ fileData = await archiveData.file(realFileName).async('string'); |
|
207 |
+ |
|
208 |
+ return fileData; |
|
209 |
+ } |
|
210 |
+ else if (usingArchive == 'tgz') |
|
211 |
+ { |
|
212 |
+ // The tar handler treats them as arrays. But we also need to handle case sensitivity, so find the index. |
|
213 |
+ let index = archiveData.map(function(e) { return e.name.toLowerCase(); }).indexOf(fileName.toLowerCase()) ?? -1; |
|
214 |
+ |
|
215 |
+ // We can't continue without the file. |
|
216 |
+ if (index < 0 || !archiveData[index]) |
|
217 |
+ throw new Error('Unable to find ' + fileName); |
|
218 |
+ |
|
219 |
+ // Get our file data from the index we have. |
|
220 |
+ fileData = await archiveData[index].readAsString(); |
|
221 |
+ |
|
222 |
+ return fileData; |
|
223 |
+ } |
|
224 |
+ else |
|
225 |
+ throw new Error ('Invalid usingArchive'); |
|
226 |
+} |
|
227 |
+ |
|
228 |
+/* Fetch SMF version data from a API, store it in localStage */ |
|
229 |
+async function fetchSmfVersions() |
|
230 |
+{ |
|
231 |
+ let smfVersions = []; |
|
232 |
+ const now = new Date(); |
|
233 |
+ |
|
234 |
+ // Try the cache. |
|
235 |
+ const cachedVersions = localStorage.getItem('smfVersions'); |
|
236 |
+ if (cachedVersions) |
|
237 |
+ { |
|
238 |
+ const item = JSON.parse(cachedVersions); |
|
239 |
+ |
|
240 |
+ // Cache is valid. |
|
241 |
+ if (item.expiry && now.getTime() < item.expiry) |
|
242 |
+ return item.value; |
|
243 |
+ } |
|
244 |
+ |
|
245 |
+ console.debug('Fetching SMF Version API'); |
|
246 |
+ try |
|
247 |
+ { |
|
248 |
+ await fetch(SmfVersionApiURL, { |
|
249 |
+ method:'GET', |
|
250 |
+ headers: { |
|
251 |
+ 'Content-Type': 'application/json' |
|
252 |
+ }, |
|
253 |
+ }) |
|
254 |
+ .then(res => res.json()) |
|
255 |
+ .then(out => smfVersions = out) |
|
256 |
+ .catch(err => { throw err }); |
|
257 |
+ |
|
258 |
+ // We have not always been semantic versioning, lets clean it up. |
|
259 |
+ smfVersions.data.forEach(function(data, index, theArray) { |
|
260 |
+ theArray[index] = semanticVersion(data); |
|
261 |
+ }); |
|
262 |
+ |
|
263 |
+ const item = { |
|
264 |
+ value: smfVersions, |
|
265 |
+ expiry: now.getTime() + 100 * 60 * 60, |
|
266 |
+ } |
|
267 |
+ localStorage.setItem('smfVersions', JSON.stringify(item)) |
|
268 |
+ |
|
269 |
+ return smfVersions; |
|
270 |
+ } |
|
271 |
+ catch |
|
272 |
+ { |
|
273 |
+ console.debug('Failed to fetch SMF Versions, attempt fallback'); |
|
274 |
+ |
|
275 |
+ const cachedVersions = localStorage.getItem('smfVersions'); |
|
276 |
+ const item = JSON.parse(cachedVersions) ?? null; |
|
277 |
+ |
|
278 |
+ // We failed, but do we have a cache to fall back on? |
|
279 |
+ if (item != null && item.value != null) |
|
280 |
+ return item.value; |
|
281 |
+ else |
|
282 |
+ throw new Error('Unable to fetch SMF Versions'); |
|
283 |
+ } |
|
284 |
+} |
|
285 |
+ |
|
286 |
+/* Populate the SMF Versions drop down */ |
|
287 |
+function populateSmfVersions(json) |
|
288 |
+{ |
|
289 |
+ console.debug('Populating SMF Versions'); |
|
290 |
+ |
|
291 |
+ if (typeof json === 'undefined' || typeof json.data === 'undefined') |
|
292 |
+ throw new Error ('Missing SMF Versions'); |
|
293 |
+ |
|
294 |
+ disableVersionChangeListiners(); |
|
295 |
+ |
|
296 |
+ // Clean out the existing data, this is supposed to be faster than innterHTML = ''; |
|
297 |
+ let pf = document.getElementById('preferedVersions'); |
|
298 |
+ if (pf && pf.hasChildNodes) |
|
299 |
+ while (pf.firstChild) |
|
300 |
+ pf.removeChild(pf.firstChild); |
|
301 |
+ let ot = document.getElementById('otherVersions'); |
|
302 |
+ if (ot && ot.hasChildNodes) |
|
303 |
+ while (ot.firstChild) |
|
304 |
+ ot.removeChild(ot.firstChild); |
|
305 |
+ |
|
306 |
+ // Sort, reverse and then put into the DDL |
|
307 |
+ json.data.sort(window.compareVersions.compareVersions).reverse().forEach(function (ver) { |
|
308 |
+ var option = document.createElement("option"); |
|
309 |
+ option.text = ver; |
|
310 |
+ option.value = ver; |
|
311 |
+ |
|
312 |
+ ot.appendChild(option); |
|
313 |
+ }); |
|
314 |
+ |
|
315 |
+ addVersionChangeListiners(); |
|
316 |
+} |
|
317 |
+ |
|
318 |
+/* Wrapper for DomParser to return a XML object */ |
|
319 |
+function parseXml(xmlString) |
|
320 |
+{ |
|
321 |
+ const parser = new DOMParser(); |
|
322 |
+ const xmlDoc = parser.parseFromString(xmlString,'text/xml'); |
|
323 |
+ |
|
324 |
+ if (!xmlDoc) |
|
325 |
+ throw new Error('Unable to Parse XML data'); |
|
326 |
+ return xmlDoc; |
|
327 |
+} |
|
328 |
+ |
|
329 |
+/* Show the Parser container */ |
|
330 |
+function showParserContainer() |
|
331 |
+{ |
|
332 |
+ console.debug('Showing the Parser Container'); |
|
333 |
+ document.getElementById('instructionsContainer').removeAttribute('hidden'); |
|
334 |
+ document.getElementById('smfVersions').removeAttribute('hidden'); |
|
335 |
+} |
|
336 |
+ |
|
337 |
+/* Hide the Parser container */ |
|
338 |
+function hideParserContainer() |
|
339 |
+{ |
|
340 |
+ console.debug('Hidding the Parser Container'); |
|
341 |
+ document.getElementById('instructionsContainer').setAttribute('hidden'); |
|
342 |
+ document.getElementById('smfVersions').setAttribute('hidden'); |
|
343 |
+} |
|
344 |
+ |
|
345 |
+/* Set our page title with the package name */ |
|
346 |
+function setPackageName(packageName) |
|
347 |
+{ |
|
348 |
+ console.debug('Set the Package Name', packageName); |
|
349 |
+ document.getElementById('packageName').innerHTML = packageName; |
|
350 |
+} |
|
351 |
+ |
|
352 |
+/* Processes the install XML data for the "for" attributes, finds the SMF versions supported, updating our prefered versions */ |
|
353 |
+function processInstallXml(xml) |
|
354 |
+{ |
|
355 |
+ console.debug('Processing Install XML data'); |
|
356 |
+ let setDefault = true; |
|
357 |
+ |
|
358 |
+ // Find a "for". It is valid to not have a for, as it implies it installs for any version of SMF. |
|
359 |
+ const forVersions = xml.getAttribute('for'); |
|
360 |
+ if (forVersions) |
|
361 |
+ { |
|
362 |
+ // The list is comma separated. |
|
363 |
+ const versions = forVersions.split(','); |
|
364 |
+ for (let j = 0; j < versions.length; j++) |
|
365 |
+ { |
|
366 |
+ // Fidn the first and last version in the string provided, even if just a single version. |
|
367 |
+ vr = findVersionRangeFromFor(versions[j]); |
|
368 |
+ |
|
369 |
+ // The newest match will be moved to Prefered versions. |
|
370 |
+ setPreferedVersion(vr.end, vr.start); |
|
371 |
+ |
|
372 |
+ // If we have not set a default, do it now. |
|
373 |
+ if (setDefault) |
|
374 |
+ { |
|
375 |
+ console.debug('Setting a default version'); |
|
376 |
+ const cn = document.getElementById('preferedVersions').getElementsByTagName('option')[0]?.value ?? null; |
|
377 |
+ |
|
378 |
+ if (cn != null) |
|
379 |
+ document.getElementById('smfVersions').value = cn; |
|
380 |
+ setDefault = false; |
|
381 |
+ } |
|
382 |
+ } |
|
383 |
+ } |
|
384 |
+} |
|
385 |
+ |
|
386 |
+/* Given a version string from a install for="", we find the newest and oldest verison in that range */ |
|
387 |
+function findVersionRangeFromFor(forString) |
|
388 |
+{ |
|
389 |
+ // No version, lets simplify it and say it matches anything. |
|
390 |
+ if (forString == '' || forString == null) |
|
391 |
+ { |
|
392 |
+ console.debug('No version specified from for'); |
|
393 |
+ return { |
|
394 |
+ 'start': '0.0.0-Alpha1', |
|
395 |
+ 'end': '99.99.99' |
|
396 |
+ }; |
|
397 |
+ } |
|
398 |
+ |
|
399 |
+ // Strip off 'SMF'; |
|
400 |
+ forString = forString.replace('SMF', ''); |
|
401 |
+ |
|
402 |
+ // No - nor *, must just be a single version, no other wildcards are supported. |
|
403 |
+ if (forString.indexOf('-') == -1 && forString.indexOf('*') == -1) |
|
404 |
+ { |
|
405 |
+ console.debug('No Range, single version', forString); |
|
406 |
+ return { |
|
407 |
+ 'start': semanticVersion(forString), |
|
408 |
+ 'end': semanticVersion(forString) |
|
409 |
+ }; |
|
410 |
+ } |
|
411 |
+ |
|
412 |
+ // If we have a * we need to split it up. "SMF 2.1.*" |
|
413 |
+ if (forString.indexOf('*') > -1) |
|
414 |
+ forString = forString.replace('.*', '.0') + '-' + forString.replace('.*', '.99'); |
|
415 |
+ |
|
416 |
+ // Now we have for sure a range, split it. "2.1.3-2.1.52" |
|
417 |
+ const es = forString.split('-'); |
|
418 |
+ |
|
419 |
+ // SMF consideres '2.0' to be '2.0.0-Alpha1' not '2.0.0' |
|
420 |
+ if (es[0].trim().match(/^\d\.\d$/, 'g')) |
|
421 |
+ es[0] = es[0].trim().replace(/^(\d)\.(\d)$/, '$1.$2.0-Alpha1', 'g'); |
|
422 |
+ |
|
423 |
+ console.debug('Finding Version Range For', forString, es); |
|
424 |
+ return { |
|
425 |
+ 'start': semanticVersion(es[0].trim()), |
|
426 |
+ 'end': semanticVersion(es[1].trim()) |
|
427 |
+ }; |
|
428 |
+} |
|
429 |
+ |
|
430 |
+/* Given a newest and oldest version, try to find the best match and move only that one to the prefered versions */ |
|
431 |
+function setPreferedVersion(end, start) |
|
432 |
+{ |
|
433 |
+ console.debug('Setting Prefered Versions', end, start); |
|
434 |
+ const pf = document.getElementById('preferedVersions'); |
|
435 |
+ const ot = document.getElementById('otherVersions'); |
|
436 |
+ const otOpts = ot.getElementsByTagName('option'); |
|
437 |
+ |
|
438 |
+ for(let i = 0; i < otOpts.length; i++) |
|
439 |
+ { |
|
440 |
+ if (compareVersions.compare(otOpts[i].value, start, '>=') && compareVersions.compare(otOpts[i].value, end, '<=')) |
|
441 |
+ { |
|
442 |
+ pf.appendChild(otOpts[i]); |
|
443 |
+ |
|
444 |
+ // Because we moved the node, move the counter back one. This is only really needed if we want to multi-match. |
|
445 |
+ --i; |
|
446 |
+ |
|
447 |
+ break; |
|
448 |
+ } |
|
449 |
+ } |
|
450 |
+} |
|
451 |
+ |
|
452 |
+/* When we change the SMF Version DDL, we trigger redoing parser data. This is also triggered when the package is first uploaded */ |
|
453 |
+async function changeSmfVersion(e) |
|
454 |
+{ |
|
455 |
+ let installXml = null; |
|
456 |
+ const selectedVersion = document.getElementById('smfVersions').value; |
|
457 |
+ |
|
458 |
+ console.debug('Changing SMF Version', selectedVersion); |
|
459 |
+ |
|
460 |
+ const possibleInstalls = parsedInfo.getElementsByTagName('install'); |
|
461 |
+ for (let i = 0; i < possibleInstalls.length; i++) |
|
462 |
+ { |
|
463 |
+ const forVersions = possibleInstalls[i].getAttribute('for'); |
|
464 |
+ |
|
465 |
+ const versions = forVersions.split(','); |
|
466 |
+ for (let j = 0; j < versions.length; j++) |
|
467 |
+ { |
|
468 |
+ vr = findVersionRangeFromFor(versions[j]); |
|
469 |
+ |
|
470 |
+ if (compareVersions.compare(selectedVersion, vr.start, '>=') && compareVersions.compare(selectedVersion, vr.end, '<=')) |
|
471 |
+ { |
|
472 |
+ installXml = i; |
|
473 |
+ break; |
|
474 |
+ } |
|
475 |
+ } |
|
476 |
+ |
|
477 |
+ if (installXml != null) |
|
478 |
+ break; |
|
479 |
+ } |
|
480 |
+ |
|
481 |
+ if (installXml == null) |
|
482 |
+ throw new Error ('No valid Install instructions for this SMF version found'); |
|
483 |
+ |
|
484 |
+ // Set the install xml we want to use and parse it. |
|
485 |
+ const seletedInstallNode = possibleInstalls[installXml]; |
|
486 |
+ await parseInstallNode(seletedInstallNode); |
|
487 |
+} |
|
488 |
+ |
|
489 |
+/* Process a install node for actions to be taken */ |
|
490 |
+async function parseInstallNode(installNode) |
|
491 |
+{ |
|
492 |
+ if (!installNode || !installNode.children) |
|
493 |
+ throw new Error ('Invalid Install instructions'); |
|
494 |
+ |
|
495 |
+ // Clear this out incase it wasn't. |
|
496 |
+ instructions = []; |
|
497 |
+ |
|
498 |
+ for (let i = 0; i < installNode.children.length; i++) { |
|
499 |
+ let thisNode = installNode.children[i]; |
|
500 |
+ |
|
501 |
+ switch (thisNode.tagName) |
|
502 |
+ { |
|
503 |
+ case 'readme': |
|
504 |
+ if (!instructions['readme']) |
|
505 |
+ instructions['readme'] = []; |
|
506 |
+ instructions['readme'].push(await parseReadmeNode(thisNode)); |
|
507 |
+ break; |
|
508 |
+ |
|
509 |
+ // Code and database are essentially the same, with the exception of database operations during package install are tracked. |
|
510 |
+ case 'code': |
|
511 |
+ case 'database': |
|
512 |
+ if (!instructions['code']) |
|
513 |
+ instructions['code'] = []; |
|
514 |
+ instructions['code'].push(await parseCodeNode(thisNode)); |
|
515 |
+ break; |
|
516 |
+ |
|
517 |
+ case 'create-dir': |
|
518 |
+ case 'create-file': |
|
519 |
+ case 'require-dir': |
|
520 |
+ case 'require-file': |
|
521 |
+ case 'move-dir': |
|
522 |
+ case 'move-file': |
|
523 |
+ case 'remove-dir': |
|
524 |
+ case 'remove-file': |
|
525 |
+ if (!instructions['fileop']) |
|
526 |
+ instructions['fileop'] = []; |
|
527 |
+ instructions['fileop'].push(await parseFileOpNode(thisNode, thisNode.tagName)); |
|
528 |
+ break; |
|
529 |
+ |
|
530 |
+ case 'hook': |
|
531 |
+ if (!instructions['hook']) |
|
532 |
+ instructions['hook'] = []; |
|
533 |
+ instructions['hook'].push(await parseHookNode(thisNode)); |
|
534 |
+ break; |
|
535 |
+ |
|
536 |
+ case 'credits': |
|
537 |
+ if (!instructions['credits']) |
|
538 |
+ instructions['credits'] = []; |
|
539 |
+ instructions['credits'].push(await parseCreditNode(thisNode)); |
|
540 |
+ break; |
|
541 |
+ |
|
542 |
+ case 'modification': |
|
543 |
+ if (!instructions['modification']) |
|
544 |
+ instructions['modification'] = []; |
|
545 |
+ instructions['modification'].push(await parseModificationNode(thisNode)); |
|
546 |
+ break; |
|
547 |
+ } |
|
548 |
+ } |
|
549 |
+ |
|
550 |
+ // We have build all the data, render it on the page. |
|
551 |
+ buildPage(); |
|
552 |
+} |
|
553 |
+ |
|
554 |
+/* Takes a <readme> node and parses out the data */ |
|
555 |
+async function parseReadmeNode(node) |
|
556 |
+{ |
|
557 |
+ let lang = 'english'; |
|
558 |
+ let text = ''; |
|
559 |
+ |
|
560 |
+ if (node.getAttribute('lang')) |
|
561 |
+ lang = node.getAttribute('lang'); |
|
562 |
+ |
|
563 |
+ if (node.getAttribute('type') && node.getAttribute('type') == 'inline') |
|
564 |
+ text = node.innerHTML; |
|
565 |
+ // We have a file. |
|
566 |
+ else |
|
567 |
+ { |
|
568 |
+ const fileName = node.innerHTML.trim(); |
|
569 |
+ text = await fetchFileFromArchive(fileName); |
|
570 |
+ } |
|
571 |
+ |
|
572 |
+ if (node.getAttribute('parsebbc') && node.getAttribute('parsebbc') === 'true') |
|
573 |
+ text = bbcParser.parse(text); |
|
574 |
+ |
|
575 |
+ return { |
|
576 |
+ lang: lang, |
|
577 |
+ text: text |
|
578 |
+ }; |
|
579 |
+} |
|
580 |
+ |
|
581 |
+/* Takes a <code> node and parses out the data */ |
|
582 |
+async function parseCodeNode(node) |
|
583 |
+{ |
|
584 |
+ let code = ''; |
|
585 |
+ let fileName = ''; |
|
586 |
+ |
|
587 |
+ if (node.getAttribute('type') && node.getAttribute('type') == 'inline') |
|
588 |
+ { |
|
589 |
+ fileName = 'inlineCode.php'; |
|
590 |
+ code = node.innerHTML; |
|
591 |
+ } |
|
592 |
+ // We have a file. |
|
593 |
+ else |
|
594 |
+ { |
|
595 |
+ fileName = node.innerHTML.trim(); |
|
596 |
+ code = await fetchFileFromArchive(fileName); |
|
597 |
+ } |
|
598 |
+ |
|
599 |
+ return { |
|
600 |
+ fileName: fileName, |
|
601 |
+ code: code |
|
602 |
+ }; |
|
603 |
+} |
|
604 |
+ |
|
605 |
+/* Takes various file based operation nodes and parses out the data */ |
|
606 |
+async function parseFileOpNode(node, op) |
|
607 |
+{ |
|
608 |
+ return { |
|
609 |
+ type: op, |
|
610 |
+ name: node.getAttribute('name') ?? '', |
|
611 |
+ destination: node.getAttribute('destination') ?? '', |
|
612 |
+ from: node.getAttribute('from') ?? '' |
|
613 |
+ }; |
|
614 |
+} |
|
615 |
+ |
|
616 |
+/* Takes a <hook> node and parses out the data */ |
|
617 |
+async function parseHookNode(node) |
|
618 |
+{ |
|
619 |
+ return { |
|
620 |
+ func: node.getAttribute('function'), |
|
621 |
+ name: node.getAttribute('hook') ?? node.innerHTML, |
|
622 |
+ include_file: node.getAttribute('file'), |
|
623 |
+ reverse: node.getAttribute('reverse') && node.getAttribute('reverse') == 'true' ? true : false, |
|
624 |
+ object: node.getAttribute('object') && node.getAttribute('object') == 'true' ? true : false |
|
625 |
+ }; |
|
626 |
+} |
|
627 |
+ |
|
628 |
+/* Takes a <credit> node and parses out the data */ |
|
629 |
+async function parseCreditNode(node) |
|
630 |
+{ |
|
631 |
+ return { |
|
632 |
+ title: node.innerHTML, |
|
633 |
+ url: node.getAttribute('url') ?? '', |
|
634 |
+ license: node.getAttribute('license') ?? '', |
|
635 |
+ licenseurl: node.getAttribute('licenseurl') ?? '', |
|
636 |
+ copyright: node.getAttribute('copyright') ?? '', |
|
637 |
+ version: parsedInfo.getElementsByTagName('version')[0]?.innerHTML ?? '' |
|
638 |
+ }; |
|
639 |
+} |
|
640 |
+ |
|
641 |
+/* Takes a <modification> node and parses out the data */ |
|
642 |
+async function parseModificationNode(node) |
|
643 |
+{ |
|
644 |
+ let edits = null; |
|
645 |
+ let reverse = false; |
|
646 |
+ const fileName = node.innerHTML.trim(); |
|
647 |
+ fileContents = await fetchFileFromArchive(fileName); |
|
648 |
+ |
|
649 |
+ if (node.getAttribute('reverse') && node.getAttribute('reverse') == 'true') |
|
650 |
+ reverse = true; |
|
651 |
+ |
|
652 |
+ // Only other format supported is boardmod. |
|
653 |
+ if (node.getAttribute('format') && node.getAttribute('format') == 'boardmod') |
|
654 |
+ { |
|
655 |
+ console.debug('BoardMod detected'); |
|
656 |
+ edits = parseBoardBoard(fileContents); |
|
657 |
+ } |
|
658 |
+ else |
|
659 |
+ { |
|
660 |
+ console.debug('Modification XML detected'); |
|
661 |
+ edits = parseModificationXML(fileContents); |
|
662 |
+ } |
|
663 |
+ |
|
664 |
+ return { |
|
665 |
+ edits: edits, |
|
666 |
+ reverse: reverse, |
|
667 |
+ }; |
|
668 |
+} |
|
669 |
+ |
|
670 |
+/* BoardMod is not used anymore, but its still supported */ |
|
671 |
+function parseBoardBoard(data) |
|
672 |
+{ |
|
673 |
+ let edits = []; |
|
674 |
+ let file = ''; |
|
675 |
+ let search = ''; |
|
676 |
+ let position = ''; |
|
677 |
+ let add = ''; |
|
678 |
+ |
|
679 |
+ // Match our board mod data. |
|
680 |
+ const matches = data.matchAll(/<(edit file|file|search|search for|add|add after|replace|add before|add above|above|before|below)>\n?(.*?)\n?<\/\1>/gims); |
|
681 |
+ for (const match of matches) |
|
682 |
+ { |
|
683 |
+ // Update our file data. |
|
684 |
+ if (match[1] == 'file' || match[1] == 'edit file') |
|
685 |
+ { |
|
686 |
+ file = match[2]; |
|
687 |
+ } |
|
688 |
+ // Found a search data. |
|
689 |
+ else if (file != '' && (match[1] == 'search' || match[1] == 'search for')) |
|
690 |
+ { |
|
691 |
+ search = match[2]; |
|
692 |
+ } |
|
693 |
+ // If we have file and search data, we can now match for the add/edit data. |
|
694 |
+ else if (file != '' && search != '') |
|
695 |
+ { |
|
696 |
+ // If its 'add before', 'before', 'add above' or 'above' means the add/edit code should come after. |
|
697 |
+ if (match[1].includes('before') || match[1].includes('above')) |
|
698 |
+ position = 'after'; |
|
699 |
+ // If its 'add after', or 'below', the add/edit code should come before. |
|
700 |
+ else if (match[1].includes('after') || match[1].includes('below')) |
|
701 |
+ position = 'before'; |
|
702 |
+ // If its 'add', 'replace', we are replacing the code. |
|
703 |
+ else |
|
704 |
+ position = 'replace'; |
|
705 |
+ |
|
706 |
+ edits.push({ |
|
707 |
+ file: file, |
|
708 |
+ search: search, |
|
709 |
+ position: position, |
|
710 |
+ add: match[2], |
|
711 |
+ fileSkipOnError: false, |
|
712 |
+ opSkipOnError: false |
|
713 |
+ }); |
|
714 |
+ |
|
715 |
+ // Reset our search. |
|
716 |
+ search = ''; |
|
717 |
+ } |
|
718 |
+ } |
|
719 |
+ |
|
720 |
+ return edits; |
|
721 |
+} |
|
722 |
+ |
|
723 |
+/* Parse Modification XML data */ |
|
724 |
+function parseModificationXML(data) |
|
725 |
+{ |
|
726 |
+ let edits = []; |
|
727 |
+ let file = ''; |
|
728 |
+ let search = ''; |
|
729 |
+ let position = ''; |
|
730 |
+ let add = ''; |
|
731 |
+ let fileSkipOnError = false; |
|
732 |
+ let opSkipOnError = false; |
|
733 |
+ let opUseRegex = false; |
|
734 |
+ let opSearchEOF = false; |
|
735 |
+ |
|
736 |
+ const parser = new DOMParser(); |
|
737 |
+ const xmlDoc = parser.parseFromString(data,'text/xml'); |
|
738 |
+ |
|
739 |
+ if (!xmlDoc) |
|
740 |
+ throw new Error('Unable to Parse XML data'); |
|
741 |
+ |
|
742 |
+ let fileEdits = xmlDoc.getElementsByTagName('file'); |
|
743 |
+ for (let i = 0; i < fileEdits.length; i++) |
|
744 |
+ { |
|
745 |
+ // Firstly find our file information. |
|
746 |
+ file = fileEdits[i].getAttribute('name'); |
|
747 |
+ |
|
748 |
+ // When the file has error="ignore" or error="skip", we are able to skip any errors locating changes for the entire file. |
|
749 |
+ fileSkipOnError = (fileEdits[i].getAttribute('error') == 'ignore' || fileEdits[i].getAttribute('error') == 'skip'); |
|
750 |
+ |
|
751 |
+ // No edits? Skip. |
|
752 |
+ if (fileEdits[i].getElementsByTagName('operation').length == 0) |
|
753 |
+ continue; |
|
754 |
+ |
|
755 |
+ let ops = fileEdits[i].getElementsByTagName('operation'); |
|
756 |
+ for (let j = 0; j < ops.length; j++) |
|
757 |
+ { |
|
758 |
+ position = ops[j].getElementsByTagName('search')[0]?.getAttribute('position'); |
|
759 |
+ |
|
760 |
+ // When the operation has error="ignore" or error="skip", we are able to skip any errors locating changes for just this operation. |
|
761 |
+ opSkipOnError = (ops[j].getAttribute('error') == 'ignore' || ops[j].getAttribute('error') == 'skip'); |
|
762 |
+ |
|
763 |
+ // Regex is supported for operations. However it is not reversed. |
|
764 |
+ opUseRegex = ops[j].getElementsByTagName('search')[0]?.getAttribute('regexp') == 'true'; |
|
765 |
+ add = HtmlEncode(ops[j].getElementsByTagName('add')[0]?.childNodes[0].data); |
|
766 |
+ |
|
767 |
+ // If the position is end, pretend its looking for the PHP closing tag to make it easier' |
|
768 |
+ search = position == 'end' ? '?' + '>' : HtmlEncode(ops[j].getElementsByTagName('search')[0]?.childNodes[0].data); |
|
769 |
+ opSearchEOF = position == 'end' ? true : false; |
|
770 |
+ |
|
771 |
+ /* We add a edit only if |
|
772 |
+ 1. Any of the following conditions. (OR) |
|
773 |
+ a. search string is not empty |
|
774 |
+ b. We are looking for the end of the file |
|
775 |
+ 2. We have a valid position. |
|
776 |
+ 3. We have a non empty add/edit string. |
|
777 |
+ */ |
|
778 |
+ if ((search.length > 0 || opSearchEOF) && position.length > 0 && add.length > 0) |
|
779 |
+ edits.push({ |
|
780 |
+ file: file, |
|
781 |
+ search: search, |
|
782 |
+ position: position, |
|
783 |
+ add: add, |
|
784 |
+ fileSkipOnError: fileSkipOnError, |
|
785 |
+ opSkipOnError: opSkipOnError, |
|
786 |
+ opUseRegex: opUseRegex, |
|
787 |
+ opSearchEOF : opSearchEOF |
|
788 |
+ }); |
|
789 |
+ } |
|
790 |
+ } |
|
791 |
+ |
|
792 |
+ return edits; |
|
793 |
+} |
|
794 |
+ |
|
795 |
+/* Take all of the data we have built and build HTML output */ |
|
796 |
+function buildPage() |
|
797 |
+{ |
|
798 |
+ console.debug('Building Page'); |
|
799 |
+ |
|
800 |
+ // Clean it out first, this is faster than innerHTML = '' according to the internet. |
|
801 |
+ let ic = document.getElementById('instructionsContainer'); |
|
802 |
+ if (ic && ic.hasChildNodes) |
|
803 |
+ while (ic.firstChild) |
|
804 |
+ ic.removeChild(ic.firstChild); |
|
805 |
+ |
|
806 |
+ /* |
|
807 |
+ The Order of Operations on the output are: |
|
808 |
+ Readme |
|
809 |
+ Modifications |
|
810 |
+ Hooks |
|
811 |
+ Code/Database |
|
812 |
+ File Operations |
|
813 |
+ Credits |
|
814 |
+ */ |
|
815 |
+ |
|
816 |
+ if (instructions.readme != null && instructions.readme.length > 0) |
|
817 |
+ buildPageReadme(); |
|
818 |
+ |
|
819 |
+ if (instructions.modification != null && instructions.modification.length > 0) |
|
820 |
+ buildPageModifications(); |
|
821 |
+ |
|
822 |
+ if (instructions.hook != null && instructions.hook.length > 0) |
|
823 |
+ buildPageHooks(); |
|
824 |
+ |
|
825 |
+ if (instructions.code != null && instructions.code.length > 0) |
|
826 |
+ buildPageCode(); |
|
827 |
+ |
|
828 |
+ if (instructions.fileop != null && instructions.fileop.length > 0) |
|
829 |
+ buildPageFileOperations(); |
|
830 |
+ |
|
831 |
+ if (instructions.credits != null && instructions.credits.length > 0) |
|
832 |
+ buildPageCredits(); |
|
833 |
+ |
|
834 |
+ // Leave this debugging info here, useful to know and call when working on it. |
|
835 |
+ //console.debug('instructions', instructions, instructions.modification[0].edits); |
|
836 |
+} |
|
837 |
+ |
|
838 |
+/* Prepare output for a readme section */ |
|
839 |
+function buildPageReadme() |
|
840 |
+{ |
|
841 |
+ const langs = instructions.readme.map(function(e) {return e.lang;}); |
|
842 |
+ const lang = 'english'; |
|
843 |
+ |
|
844 |
+ const index = instructions.readme.map(function(e) { return e.lang.toLowerCase(); }).indexOf(lang.toLowerCase()) ?? -1; |
|
845 |
+ const thisReadme = instructions.readme[index]; |
|
846 |
+ |
|
847 |
+ let template = document.getElementById('templateReadme').cloneNode(true); |
|
848 |
+ template.removeAttribute('hidden'); |
|
849 |
+ template.innerHTML = template.innerHTML.replace('{TITLE}', txt['title_readme']).replace('{CONTENT}', thisReadme.text); |
|
850 |
+ |
|
851 |
+ document.getElementById('instructionsContainer').appendChild(template); |
|
852 |
+} |
|
853 |
+ |
|
854 |
+/* Prepare output for a modification section */ |
|
855 |
+function buildPageModifications() |
|
856 |
+{ |
|
857 |
+ let templateTitle = document.getElementById('templateOperationsTitle').cloneNode(true); |
|
858 |
+ let fileName = ''; |
|
859 |
+ let templateFileTitle = null; |
|
860 |
+ let template = null; |
|
861 |
+ |
|
862 |
+ // Setup the title. |
|
863 |
+ templateTitle.removeAttribute('id'); |
|
864 |
+ templateTitle.removeAttribute('hidden'); |
|
865 |
+ templateTitle.innerHTML = templateTitle.innerHTML.replace('{TITLE}', txt['title_operations']); |
|
866 |
+ document.getElementById('instructionsContainer').appendChild(templateTitle); |
|
867 |
+ |
|
868 |
+ // Work through all modifications we do, which could be from multiple modification files. |
|
869 |
+ for (let i = 0; i < instructions.modification.length; i++) |
|
870 |
+ { |
|
871 |
+ const thisOperation = instructions.modification[i]; |
|
872 |
+ |
|
873 |
+ // Loop through all edits we do. |
|
874 |
+ for (let j = 0; j < thisOperation.edits.length; j++) |
|
875 |
+ { |
|
876 |
+ const thisEdit = thisOperation.edits[j]; |
|
877 |
+ |
|
878 |
+ // Set a new file name. |
|
879 |
+ if (fileName != thisEdit.file) |
|
880 |
+ { |
|
881 |
+ fileName = thisEdit.file; |
|
882 |
+ templateFileTitle = document.getElementById('templateOperationsTitle').cloneNode(true); |
|
883 |
+ templateFileTitle.removeAttribute('id'); |
|
884 |
+ templateFileTitle.removeAttribute('hidden'); |
|
885 |
+ templateFileTitle.innerHTML = templateFileTitle.innerHTML.replace('{TITLE}', fileName); |
|
886 |
+ document.getElementById('instructionsContainer').appendChild(templateFileTitle); |
|
887 |
+ |
|
888 |
+ if (thisEdit.fileSkipOnError == true) |
|
889 |
+ { |
|
890 |
+ templateFileTitle.querySelector('.alert').innerHTML = txt['file_edit_skip_error']; |
|
891 |
+ templateFileTitle.querySelector('.alert').removeAttribute('hidden'); |
|
892 |
+ } |
|
893 |
+ } |
|
894 |
+ |
|
895 |
+ // Start working on our template. |
|
896 |
+ template = document.getElementById('templateOperations').cloneNode(true); |
|
897 |
+ template.removeAttribute('id'); |
|
898 |
+ template.removeAttribute('hidden'); |
|
899 |
+ |
|
900 |
+ // The search section. |
|
901 |
+ template.querySelector('.searchContainer h5').innerHTML = thisEdit.position == 'end' ? txt['operation_end'] : txt['operation_search']; |
|
902 |
+ |
|
903 |
+ // If we are going in reverse, flip some logic around. |
|
904 |
+ if (thisOperation.reverse != null && thisOperation.reverse == true) |
|
905 |
+ { |
|
906 |
+ if (thisEdit.position == 'before') |
|
907 |
+ thisEdit.position = 'after'; |
|
908 |
+ else if (thisEdit.position == 'after') |
|
909 |
+ thisEdit.position = 'before'; |
|
910 |
+ |
|
911 |
+ template.querySelector('.addContainer h5').innerHTML = txt['operation_' + thisEdit.position]; |
|
912 |
+ template.querySelector('.operationSearch pre').innerHTML = thisEdit.add; |
|
913 |
+ template.querySelector('.operationAdd pre').innerHTML = thisEdit.search; |
|
914 |
+ } |
|
915 |
+ else |
|
916 |
+ { |
|
917 |
+ template.querySelector('.addContainer h5').innerHTML = thisEdit.position == 'end' ? txt['operation_before'] : txt['operation_' + thisEdit.position]; |
|
918 |
+ template.querySelector('.operationSearch pre').innerHTML = thisEdit.search; |
|
919 |
+ template.querySelector('.operationAdd pre').innerHTML = thisEdit.add; |
|
920 |
+ } |
|
921 |
+ |
|
922 |
+ // If we can skip this operation, add a notice. |
|
923 |
+ if (thisEdit.opSkipOnError == true) |
|
924 |
+ { |
|
925 |
+ template.querySelector('.alert.alert-warning').innerHTML = txt['edit_skip_error']; |
|
926 |
+ template.querySelector('.alert.alert-warning').removeAttribute('hidden'); |
|
927 |
+ } |
|
928 |
+ |
|
929 |
+ // If this is a regex search, they will need something better than notepad. |
|
930 |
+ if (thisEdit.opUseRegex == true) |
|
931 |
+ { |
|
932 |
+ template.querySelector('.alert.alert-info').innerHTML = txt['edit_uses_regex']; |
|
933 |
+ template.querySelector('.alert.alert-info').removeAttribute('hidden'); |
|
934 |
+ } |
|
935 |
+ |
|
936 |
+ document.getElementById('instructionsContainer').appendChild(template); |
|
937 |
+ } |
|
938 |
+ } |
|
939 |
+ |
|
940 |
+ // Make it so we can click the clipboard icon and copy the data. |
|
941 |
+ document.querySelectorAll('.operationSearchCopy,.operationAddCopy').forEach(el => { |
|
942 |
+ el.addEventListener('click', function(evt) { |
|
943 |
+ const CodeArea = this.parentNode.nextSibling.nextSibling.querySelector('pre'); |
|
944 |
+ const CurSelection = window.getSelection(); |
|
945 |
+ |
|
946 |
+ // Webkit based browsers support setBaseAndExtent. |
|
947 |
+ if (CurSelection.setBaseAndExtent) |
|
948 |
+ { |
|
949 |
+ CurSelection.setBaseAndExtent(CodeArea, 0, CodeArea, CodeArea.childNodes.length); |
|
950 |
+ } |
|
951 |
+ // Firefox and others. |
|
952 |
+ else |
|
953 |
+ { |
|
954 |
+ const curRange = document.createRange(); |
|
955 |
+ curRange.selectNodeContents(CodeArea); |
|
956 |
+ CurSelection.removeAllRanges(); |
|
957 |
+ CurSelection.addRange(curRange); |
|
958 |
+ } |
|
959 |
+ |
|
960 |
+ // Try to execute the copy command. Give up silently if it doesn't. |
|
961 |
+ try { |
|
962 |
+ document.execCommand('copy'); |
|
963 |
+ } catch (err) { |
|
964 |
+ } |
|
965 |
+ }); |
|
966 |
+ }); |
|
967 |
+} |
|
968 |
+ |
|
969 |
+/* Prepare output for a hooks section */ |
|
970 |
+function buildPageHooks() |
|
971 |
+{ |
|
972 |
+ let template = document.getElementById('templateHooks').cloneNode(true); |
|
973 |
+ template.removeAttribute('hidden'); |
|
974 |
+ |
|
975 |
+ // Loop through all data and just put it into a nice table. |
|
976 |
+ for (let i = 0; i < instructions.hook.length; i++) |
|
977 |
+ { |
|
978 |
+ const thisHook = instructions.hook[i]; |
|
979 |
+ |
|
980 |
+ let newTR = document.createElement('tr'); |
|
981 |
+ let newTD = document.createElement('td'); |
|
982 |
+ newTR.appendChild(newTD); |
|
983 |
+ |
|
984 |
+ if (thisHook.name == 'integrate_pre_include') |
|
985 |
+ newTD.innerHTML = txt['hook_pre_include'].format(thisHook.func); |
|
986 |
+ else |
|
987 |
+ newTD.innerHTML = txt['hook_add'].format(thisHook.func, thisHook.name); |
|
988 |
+ |
|
989 |
+ template.querySelector('table tbody').appendChild(newTR); |
|
990 |
+ } |
|
991 |
+ |
|
992 |
+ template.innerHTML = template.innerHTML.replace('{TITLE}', txt['title_hook']); |
|
993 |
+ document.getElementById('instructionsContainer').appendChild(template); |
|
994 |
+} |
|
995 |
+ |
|
996 |
+/* Prepare output for a code section */ |
|
997 |
+function buildPageCode() |
|
998 |
+{ |
|
999 |
+ let template = document.getElementById('templateCode').cloneNode(true); |
|
1000 |
+ template.removeAttribute('hidden'); |
|
1001 |
+ |
|
1002 |
+ // Loop through all data and just put it into a nice table. |
|
1003 |
+ for (let i = 0; i < instructions.code.length; i++) |
|
1004 |
+ { |
|
1005 |
+ const thisCode = instructions.code[i]; |
|
1006 |
+ |
|
1007 |
+ let newTR = document.createElement('tr'); |
|
1008 |
+ let newTD = document.createElement('td'); |
|
1009 |
+ newTR.appendChild(newTD); |
|
1010 |
+ |
|
1011 |
+ newTD.innerHTML = thisCode.fileName; |
|
1012 |
+ |
|
1013 |
+ // Build a container and link to download the code. |
|
1014 |
+ let downloadTD = document.createElement('td'); |
|
1015 |
+ newTR.appendChild(downloadTD); |
|
1016 |
+ downloadAnchor = document.createElement('a'); |
|
1017 |
+ downloadAnchor.innerHTML = txt['download']; |
|
1018 |
+ downloadAnchor.download = thisCode.fileName; |
|
1019 |
+ |
|
1020 |
+ const file = new Blob([Uint8Array.from(thisCode.code, c => c.charCodeAt(0))]) |
|
1021 |
+ const url = URL.createObjectURL(file) |
|
1022 |
+ downloadAnchor.href = url; |
|
1023 |
+ |
|
1024 |
+ downloadTD.appendChild(downloadAnchor); |
|
1025 |
+ |
|
1026 |
+ template.querySelector('table tbody').appendChild(newTR); |
|
1027 |
+ } |
|
1028 |
+ |
|
1029 |
+ template.innerHTML = template.innerHTML.replace('{TITLE}', txt['title_code']); |
|
1030 |
+ document.getElementById('instructionsContainer').appendChild(template); |
|
1031 |
+} |
|
1032 |
+ |
|
1033 |
+/* Prepare output for file operations section */ |
|
1034 |
+function buildPageFileOperations() |
|
1035 |
+{ |
|
1036 |
+ let template = document.getElementById('templateFileOperations').cloneNode(true); |
|
1037 |
+ template.removeAttribute('hidden'); |
|
1038 |
+ |
|
1039 |
+ // Loop through all data and just put it into a nice table. |
|
1040 |
+ for (let i = 0; i < instructions.fileop.length; i++) |
|
1041 |
+ { |
|
1042 |
+ const thisHook = instructions.fileop[i]; |
|
1043 |
+ |
|
1044 |
+ let newTR = document.createElement('tr'); |
|
1045 |
+ let newTD = document.createElement('td'); |
|
1046 |
+ newTR.appendChild(newTD); |
|
1047 |
+ |
|
1048 |
+ let msg = txt['file_operation_' + thisHook.type]; |
|
1049 |
+ if (thisHook.name != null && thisHook.destination != null) |
|
1050 |
+ msg = msg.format(thisHook.name, formatPath(thisHook.destination)); |
|
1051 |
+ else if (thisHook.name != null) |
|
1052 |
+ msg = msg.format(thisHook.name); |
|
1053 |
+ |
|
1054 |
+ newTD.innerHTML = msg; |
|
1055 |
+ template.querySelector('table tbody').appendChild(newTR); |
|
1056 |
+ } |
|
1057 |
+ |
|
1058 |
+ template.innerHTML = template.innerHTML.replace('{TITLE}', txt['title_fileop']); |
|
1059 |
+ document.getElementById('instructionsContainer').appendChild(template); |
|
1060 |
+} |
|
1061 |
+ |
|
1062 |
+/* Prepare output for credits section, SMF would normally insert this into a table to track. */ |
|
1063 |
+function buildPageCredits() |
|
1064 |
+{ |
|
1065 |
+ let template = document.getElementById('templateCredits').cloneNode(true); |
|
1066 |
+ template.removeAttribute('hidden'); |
|
1067 |
+ |
|
1068 |
+ // Loop through all data and just put it into a nice table. |
|
1069 |
+ for (let i = 0; i < instructions.credits.length; i++) |
|
1070 |
+ { |
|
1071 |
+ const thisCredit = instructions.credits[i]; |
|
1072 |
+ |
|
1073 |
+ let newTR = document.createElement('tr'); |
|
1074 |
+ let newTD = document.createElement('td'); |
|
1075 |
+ newTR.appendChild(newTD); |
|
1076 |
+ |
|
1077 |
+ let credit = ''; |
|
1078 |
+ if (thisCredit.url != null) |
|
1079 |
+ credit = '<a href=' + thisCredit.url + '" rel="noopener">' + (thisCredit.title ?? packageName) + ': ' + txt['credits_version'] + ' ' + thisCredit.version + '</a>'; |
|
1080 |
+ else |
|
1081 |
+ credit = (thisCredit.title ?? packageName) + ': ' + txt['credits_version'] + ' ' + thisCredit.version; |
|
1082 |
+ |
|
1083 |
+ if (thisCredit.licenseurl != null) |
|
1084 |
+ credit += ' | ' + txt['credits_license'] + '<a href="' + thisCredit.licenseurl + '">' + thisCredit.license + '</a>'; |
|
1085 |
+ else |
|
1086 |
+ credit += ' | ' + txt['credits_license'] + thisCredit.license; |
|
1087 |
+ |
|
1088 |
+ newTD.innerHTML = credit; |
|
1089 |
+ template.querySelector('table tbody').appendChild(newTR); |
|
1090 |
+ } |
|
1091 |
+ |
|
1092 |
+ template.innerHTML = template.innerHTML.replace('{TITLE}', txt['title_credits']); |
|
1093 |
+ document.getElementById('instructionsContainer').appendChild(template); |
|
1094 |
+} |
|
1095 |
+ |
|
1096 |
+/* SMF hasn't been consistent nor always following semantic versioning. So do some cleanup to help with issues parsing versions */ |
|
1097 |
+function semanticVersion(version) |
|
1098 |
+{ |
|
1099 |
+ // Straight forward simple replacements. |
|
1100 |
+ const replacements = [ |
|
1101 |
+ {k: ' Security Patch', v:'.1'} // 2.0 RC4 Security Patch |
|
1102 |
+ ]; |
|
1103 |
+ |
|
1104 |
+ // Some regular expression replacements. |
|
1105 |
+ const pregReplacments = [ |
|
1106 |
+ // 2.0 => 2.0.0 |
|
1107 |
+ {k: /^(\d)\.(\d)$/, v:'$1.$2.0'}, |
|
1108 |
+ |
|
1109 |
+ // 2.0 RC-1 => 2.0.0-RC1 |
|
1110 |
+ {k: /^(\d)\.(\d) RC(\s-)?(\d)$/, v:'$1.$2.0-RC$4'}, |
|
1111 |
+ |
|
1112 |
+ // 2.0 RC 2-1 => 2.0.0-RC2.1 |
|
1113 |
+ {k: /^(\d)\.(\d) RC(\s-)?(\d)[\.|-](\d)$/, v:'$1.$2.0-RC$4.$5'}, |
|
1114 |
+ |
|
1115 |
+ // 2.1 Beta 3 Public => 2.1.0-Beta3 |
|
1116 |
+ {k: /^(\d)\.(\d) Beta\s?(\d)( Public)?$/, v:'$1.$2.0-Beta$3'}, |
|
1117 |
+ |
|
1118 |
+ // 2.1 Beta 2.1 => 2.1.0-Beta2.1 |
|
1119 |
+ {k: /^(\d)\.(\d) Beta\s?(\d)[\.|-](\d)( Public)?$/, v:'$1.$2.0-Beta$3.$4'} |
|
1120 |
+ ]; |
|
1121 |
+ |
|
1122 |
+ // Do the simple ones first. |
|
1123 |
+ version = replacements.reduce((rt,cv,ci) => rt.replace(cv['k'],cv['v']), version); |
|
1124 |
+ |
|
1125 |
+ // Do our regular expressions. |
|
1126 |
+ version = pregReplacments.reduce(function (rt,cv,ci) { |
|
1127 |
+ let re = new RegExp(cv['k'], 'gi'); |
|
1128 |
+ return rt.replace(re, cv['v']); |
|
1129 |
+ }, version); |
|
1130 |
+ |
|
1131 |
+ return version; |
|
1132 |
+} |
|
1133 |
+ |
|
1134 |
+/* Take a string and replace some variables with something more logical to understand */ |
|
1135 |
+function formatPath(path) |
|
1136 |
+{ |
|
1137 |
+ const Replacements = [ |
|
1138 |
+ {k:'\\\\', v: '/'}, |
|
1139 |
+ {k:'$boarddir', v:'.'}, |
|
1140 |
+ {k:'$sourcedir', v:'./Sources'}, |
|
1141 |
+ {k:'$avatardir', v:'./avatars'}, |
|
1142 |
+ {k:'$avatars_dir', v:'./avatars'}, |
|
1143 |
+ {k:'$themedir', v:'./Themes/default'}, |
|
1144 |
+ {k:'$imagesdir', v:'./Themes/default/images'}, |
|
1145 |
+ {k:'$themes_dir', v:'./Themes'}, |
|
1146 |
+ {k:'$languagedir', v:'./Themes/default/languages'}, |
|
1147 |
+ {k:'$languages_dir', v:'./Themes/default/languages'}, |
|
1148 |
+ {k:'$smileysdir', v:'./Smileys'}, |
|
1149 |
+ {k:'$smileys_dir', v:'./Smileys'}, |
|
1150 |
+ ]; |
|
1151 |
+ |
|
1152 |
+ return Replacements.reduce((rt,cv,ci) => rt.replace(cv['k'],cv['v']), path); |
|
1153 |
+} |
|
1154 |
+ |
|
1155 |
+/* The simple JS BBC parser needs some help. */ |
|
1156 |
+async function setBBCodes() |
|
1157 |
+{ |
|
1158 |
+ bbcParser.add('\\[size=large\\]', '<span style="font-size: 120%;">'); |
|
1159 |
+ bbcParser.add('\\[\\/size\\]', '</span>'); |
|
1160 |
+ bbcParser.add('\n', '<br>'); |
|
1161 |
+ bbcParser.add('\\[code\\](.+?)\\[\\/code\\]', '<pre>$1</pre>'); |
|
1162 |
+} |
|
1163 |
+ |
|
1164 |
+/* Encode HTML entities made easy */ |
|
1165 |
+function HtmlEncode(s) |
|
1166 |
+{ |
|
1167 |
+ let el = document.createElement('div'); |
|
1168 |
+ el.innerText = el.textContent = s; |
|
1169 |
+ return el.innerHTML; |
|
1170 |
+} |
|
0 | 1171 |
\ No newline at end of file |
... | ... |
@@ -0,0 +1,5 @@ |
1 |
+This tool parses SMF Packages to show you the instructions that will be peformed. |
|
2 |
+ |
|
3 |
+This tool is built entirely in javascript and requires no server side utilities. |
|
4 |
+ |
|
5 |
+I built this as an expierement to see if I could parse packages entirely in the borwser, which is succesful. However due to CORS, its not possible to fetch remote files. You can fetch files using the FileName javascript variable. They must exist on the same domain, or the remote domain must allows CORS/XHR requests. |
|
0 | 6 |
\ No newline at end of file |
... | ... |
@@ -0,0 +1,9 @@ |
1 |
+# Security Policy |
|
2 |
+ |
|
3 |
+## Supported Versions |
|
4 |
+ |
|
5 |
+The current release is the only supported version. Please test against the latest release prior to submitting a security report. |
|
6 |
+ |
|
7 |
+## Reporting a Vulnerability |
|
8 |
+ |
|
9 |
+Use GitHub's Security reporting tool. |
|
0 | 10 |
\ No newline at end of file |
... | ... |
@@ -0,0 +1,191 @@ |
1 |
+<!DOCTYPE html> |
|
2 |
+<html> |
|
3 |
+<head> |
|
4 |
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8"> |
|
5 |
+ <title>SMF Package Parser</title> |
|
6 |
+ <meta name="robots" content="noindex, nofollow"> |
|
7 |
+ <meta name="googlebot" content="noindex, nofollow"> |
|
8 |
+ <meta name="viewport" content="width=device-width, initial-scale=1"> |
|
9 |
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous"> |
|
10 |
+ <link rel="modulepreload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"> |
|
11 |
+ <link rel="modulepreload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/jszip-utils@0.1.0/dist/jszip-utils.min.js"> |
|
12 |
+ <link rel="modulepreload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"> |
|
13 |
+ <link rel="modulepreload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/compare-versions@6.0.0-rc.1/lib/umd/index.min.js"> |
|
14 |
+ <link rel="modulepreload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/js-untar@2.0.0/build/dist/untar.min.js"> |
|
15 |
+ <link rel="modulepreload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/pako@2.1.0/+esm"> |
|
16 |
+ <link rel="modulepreload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/js-bbcode-parser@4.0.0/src/simple.min.js"> |
|
17 |
+ <link rel="modulepreload" href="./Parser.js"> |
|
18 |
+ <link rel="modulepreload" href="./languages/en-us.js"> |
|
19 |
+ |
|
20 |
+ <style> |
|
21 |
+ pre {tab-size: 4;} |
|
22 |
+ </style> |
|
23 |
+</head> |
|
24 |
+<body> |
|
25 |
+ <main class="container"> |
|
26 |
+ <header class="d-flex flex-wrap justify-content-center py-3 mb-4 border-bottom"> |
|
27 |
+ <form id="myform" class="me-md-auto"> |
|
28 |
+ <input id="myfile" name="files[]" multiple="" type="file" class="form-control" /> |
|
29 |
+ </form> |
|
30 |
+ |
|
31 |
+ <a href="#" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-body-emphasis text-decoration-none"> |
|
32 |
+ <span class="fs-4" id="packageName">Package Parser</span> |
|
33 |
+ </a> |
|
34 |
+ |
|
35 |
+ <form> |
|
36 |
+ <select id="smfVersions" class="form-select"> |
|
37 |
+ <optgroup id="preferedVersions" label="Prefered SMF Versions"></optgroup> |
|
38 |
+ <optgroup id="otherVersions" label="Other SMF Versions"></optgroup> |
|
39 |
+ </select> |
|
40 |
+ </form> |
|
41 |
+ </header> |
|
42 |
+ |
|
43 |
+ <div id="errorContainer" class="alert alert-danger" role="alert" hidden></div> |
|
44 |
+ |
|
45 |
+ <div id="instructionsContainer" hidden class="container"> |
|
46 |
+ </div> |
|
47 |
+ |
|
48 |
+ <div id="templateContainer"> |
|
49 |
+ <div id="templateReadme" class="container" hidden> |
|
50 |
+ <div class="d-flex flex-row"> |
|
51 |
+ <h2 class="p-2">{TITLE}</h2> |
|
52 |
+ <div class="p-2"><button type="button" class="btn" aria-label="Close"> |
|
53 |
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-expand" viewBox="0 0 16 16"> |
|
54 |
+ <path fill-rule="evenodd" d="M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708zm0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708z"/> |
|
55 |
+ </svg> |
|
56 |
+ </button></div> |
|
57 |
+ </div> |
|
58 |
+ <div id="collapseReadme" class="accordion-collapse collapse show" data-bs-parent=""> |
|
59 |
+ <div class="accordion-body bg-primary-subtle p-2 border">{CONTENT}</div> |
|
60 |
+ </div> |
|
61 |
+ <hr> |
|
62 |
+ </div> |
|
63 |
+ |
|
64 |
+ <div id="ciplboard" hidden> |
|
65 |
+ </div> |
|
66 |
+ |
|
67 |
+ <div id="templateOperationsTitle" class="container mt-3" hidden> |
|
68 |
+ <h3>{TITLE}</h3> |
|
69 |
+ <div class="alert alert-warning" role="alert" hidden></div> |
|
70 |
+ </div> |
|
71 |
+ <div id="templateOperations" class="container mb-3" hidden> |
|
72 |
+ <div class="alert alert-warning" role="alert" hidden></div> |
|
73 |
+ <div class="alert alert-info" role="alert" hidden></div> |
|
74 |
+ |
|
75 |
+ <div class="searchContainer d-flex"> |
|
76 |
+ <h5 class="titleSearch d-inline fs-4"></h5> |
|
77 |
+ <button class="operationSearchCopy d-inline btn pt-0"> |
|
78 |
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16"> |
|
79 |
+ <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/> |
|
80 |
+ <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/> |
|
81 |
+ </svg> |
|
82 |
+ </button> |
|
83 |
+ </div> |
|
84 |
+ <div class="operationSearch highlight text-bg-light p-3 pb-1 ms-3 border"> |
|
85 |
+ <pre></pre> |
|
86 |
+ </div> |
|
87 |
+ <div class="addContainer d-flex"> |
|
88 |
+ <h5 class="titleAdd d-inline fs-4"></h5> |
|
89 |
+ <button class="operationAddCopy d-inline btn pt-0"> |
|
90 |
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16"> |
|
91 |
+ <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/> |
|
92 |
+ <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/> |
|
93 |
+ </svg> |
|
94 |
+ </button> |
|
95 |
+ </div> |
|
96 |
+ <div class="operationAdd highlight text-bg-light p-3 pb-1 ms-3 border"> |
|
97 |
+ <pre></pre> |
|
98 |
+ </div> |
|
99 |
+ </div> |
|
100 |
+ |
|
101 |
+ <div id="templateHooks" class="container" hidden> |
|
102 |
+ <table class="table table-sm table-striped"> |
|
103 |
+ <thead class="table-dark"> |
|
104 |
+ <th>{TITLE}</th> |
|
105 |
+ </thead> |
|
106 |
+ <tbody> |
|
107 |
+ </tbody> |
|
108 |
+ </table> |
|
109 |
+ <hr> |
|
110 |
+ </div> |
|
111 |
+ |
|
112 |
+ <div id="templateCode" class="container" hidden> |
|
113 |
+ <table class="table table-sm table-striped"> |
|
114 |
+ <thead class="table-dark"> |
|
115 |
+ <th colspan="2">{TITLE}</th> |
|
116 |
+ </thead> |
|
117 |
+ <tbody> |
|
118 |
+ </tbody> |
|
119 |
+ </table> |
|
120 |
+ <hr> |
|
121 |
+ </div> |
|
122 |
+ |
|
123 |
+ <div id="templateFileOperations" class="container" hidden> |
|
124 |
+ <table class="table table-sm table-striped"> |
|
125 |
+ <thead class="table-dark"> |
|
126 |
+ <th>{TITLE}</th> |
|
127 |
+ </thead> |
|
128 |
+ <tbody> |
|
129 |
+ </tbody> |
|
130 |
+ </table> |
|
131 |
+ <hr> |
|
132 |
+ </div> |
|
133 |
+ |
|
134 |
+ <div id="templateCredits" class="container" hidden> |
|
135 |
+ <table class="table table-sm table-striped"> |
|
136 |
+ <thead class="table-dark"> |
|
137 |
+ <th>{TITLE}</th> |
|
138 |
+ </thead> |
|
139 |
+ <tbody> |
|
140 |
+ </tbody> |
|
141 |
+ </table> |
|
142 |
+ <hr> |
|
143 |
+ </div> |
|
144 |
+ |
|
145 |
+ </div> |
|
146 |
+ </main> |
|
147 |
+ |
|
148 |
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script> |
|
149 |
+ <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jszip-utils@0.1.0/dist/jszip-utils.min.js" crossorigin="anonymous"></script> |
|
150 |
+ <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js" crossorigin="anonymous"></script> |
|
151 |
+ <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/compare-versions@6.0.0-rc.1/lib/umd/index.min.js" crossorigin="anonymous"></script> |
|
152 |
+ <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/js-untar@2.0.0/build/dist/untar.min.js" crossorigin="anonymous"></script> |
|
153 |
+ <script type="module"> |
|
154 |
+ import pako from 'https://cdn.jsdelivr.net/npm/pako@2.1.0/+esm' |
|
155 |
+ window.pako = pako; |
|
156 |
+ </script> |
|
157 |
+ <script type="module"> |
|
158 |
+ import bbCodeParser from 'https://cdn.jsdelivr.net/npm/js-bbcode-parser@4.0.0/src/simple.min.js'; |
|
159 |
+ window.bbcParser = bbCodeParser; |
|
160 |
+ </script> |
|
161 |
+ |
|
162 |
+ <script type="text/javascript" src="./Parser.js"></script> |
|
163 |
+ <script type="text/javascript" src="./languages/en-us.js"></script> |
|
164 |
+ <script type="text/javascript"> |
|
165 |
+ const SmfVersionApiURL = 'https://custom.simplemachines.org/api.php?action=smf;sa=versions'; |
|
166 |
+ let FileName = ''; |
|
167 |
+ |
|
168 |
+ // Check if we have another language to support. |
|
169 |
+ const language = window.navigator.language.toLowerCase(); |
|
170 |
+ const supportedLanguages = []; // ['en-us'] |
|
171 |
+ if (supportedLanguages.includes(language)) |
|
172 |
+ { |
|
173 |
+ const i18n = document.createElement('script'); |
|
174 |
+ i18n.src = './languages/' + language + '.js'; |
|
175 |
+ document.getElementsByTagName('head')[0].appendChild(i18n); |
|
176 |
+ } |
|
177 |
+ |
|
178 |
+ // Wait for everything to load, then add a listener on attaching files. |
|
179 |
+ window.addEventListener('load', async (e) => { |
|
180 |
+ // Setup the BBC Codes for the parser. |
|
181 |
+ await setBBCodes(); |
|
182 |
+ |
|
183 |
+ // If we have a file we are defaulting to/passing in |
|
184 |
+ if (FileName != '') |
|
185 |
+ runParser(FileName); |
|
186 |
+ |
|
187 |
+ document.getElementById('myfile').addEventListener('change', uploadFileToJS); |
|
188 |
+ }); |
|
189 |
+ </script> |
|
190 |
+</body> |
|
191 |
+</html> |
|
0 | 192 |
\ No newline at end of file |
... | ... |
@@ -0,0 +1,39 @@ |
1 |
+txt['title_readme'] = 'Readme'; |
|
2 |
+txt['title_hook'] = 'Integration Hooks'; |
|
3 |
+txt['title_fileop'] = 'File Operations'; |
|
4 |
+txt['title_credits'] = 'Credits'; |
|
5 |
+txt['title_code'] = 'Code'; |
|
6 |
+txt['title_operations'] = 'File Edits'; |
|
7 |
+ |
|
8 |
+// Operations |
|
9 |
+txt['operation_search'] = 'Find:'; |
|
10 |
+txt['operation_before'] = 'Add Before:'; |
|
11 |
+txt['operation_after'] = 'Add After:'; |
|
12 |
+txt['operation_replace'] = 'Replace With:'; |
|
13 |
+txt['operation_end'] = 'Find (at the end of the file)'; |
|
14 |
+ |
|
15 |
+// Download |
|
16 |
+txt['download'] = 'Download'; |
|
17 |
+ |
|
18 |
+// Hooks |
|
19 |
+txt['hook_add'] = 'Add integration function(s) {0} to hook {1}'; |
|
20 |
+txt['hook_pre_include'] = 'Add file(s) {0} to hook integrate_pre_include'; |
|
21 |
+ |
|
22 |
+// File Operations |
|
23 |
+txt['file_operation_create-dir'] = 'Create a directory "{0}" in "{1}".'; |
|
24 |
+txt['file_operation_create-file'] = 'Create a blank file named "{0}" in "{1}".'; |
|
25 |
+txt['file_operation_require-dir'] = 'Move the included directory "{0}" to "{1}".'; |
|
26 |
+txt['file_operation_require-file'] = 'Move the included file "{0}" to "{1}".'; |
|
27 |
+txt['file_operation_move-dir'] = 'Move the directory "{0}" to "{1}".'; |
|
28 |
+txt['file_operation_move-file'] = 'Move the file "{0}" to "{1}".'; |
|
29 |
+txt['file_operation_remove-dir'] = 'Remove the directory "{0}".'; |
|
30 |
+txt['file_operation_remove-file'] = 'Remove the file "{0}'; |
|
31 |
+ |
|
32 |
+// Credits |
|
33 |
+txt['credits_version'] = 'Version: '; |
|
34 |
+txt['credits_license'] = 'License: '; |
|
35 |
+ |
|
36 |
+// Errors |
|
37 |
+txt['file_edit_skip_error'] = 'The operations for this file isn\'t vital to the installation of this mod.'; |
|
38 |
+txt['edit_skip_error'] = 'This operation isn\'t vital to the installation of this mod.'; |
|
39 |
+txt['edit_uses_regex'] = 'This operation uses regular expressions and requires a editor capable of searching with regular expressions'; |
|
0 | 40 |