Jeremy D commited on 2023-03-29 17:46:45
Showing 13 changed files, with 1248 additions and 34 deletions.
* Add ability to build archives Fixes #2 * EOL * Hide dev tools in mobile menu
... | ... |
@@ -6,7 +6,7 @@ |
6 | 6 |
* @author SleePy <sleepy @ simplemachines (dot) org> |
7 | 7 |
* @copyright 2022 |
8 | 8 |
* @license 3-Clause BSD https://opensource.org/licenses/BSD-3-Clause |
9 |
- * @version 1.0 |
|
9 |
+ * @version 1.1 |
|
10 | 10 |
*/ |
11 | 11 |
class DevTools |
12 | 12 |
{ |
... | ... |
@@ -42,7 +42,7 @@ class DevTools |
42 | 42 |
$this->{$f} = &$GLOBALS[$f]; |
43 | 43 |
|
44 | 44 |
$this->loadLanguage(['DevTools', 'Admin']); |
45 |
- $this->loadSources(['DevToolsPackages', 'DevToolsHooks', 'Subs-Menu']); |
|
45 |
+ $this->loadSources(['DevToolsPackages', 'DevToolsHooks', 'DevToolsFile', 'Subs-Menu']); |
|
46 | 46 |
} |
47 | 47 |
|
48 | 48 |
/* |
... | ... |
@@ -63,6 +63,9 @@ class DevTools |
63 | 63 |
// Fixes a minor bug where the content isn't sized right. |
64 | 64 |
addInlineCss(' |
65 | 65 |
div#devtools_menu .half_content { width: 49%;} |
66 |
+ @media (max-width: 855px) { |
|
67 |
+ li a#devtools_menu_top {display: none;} |
|
68 |
+ } |
|
66 | 69 |
'); |
67 | 70 |
} |
68 | 71 |
|
... | ... |
@@ -179,6 +182,7 @@ class DevTools |
179 | 182 |
'index' => 'action_index', |
180 | 183 |
'packages' => 'action_packages', |
181 | 184 |
'hooks' => 'action_hooks', |
185 |
+ 'files' => 'action_files', |
|
182 | 186 |
]; |
183 | 187 |
|
184 | 188 |
$this->{$this->getAreaAction($areas, 'packages')}(); |
... | ... |
@@ -221,6 +225,23 @@ class DevTools |
221 | 225 |
$this->setSubTemplate($this->getSubAction($subActions, 'list')); |
222 | 226 |
} |
223 | 227 |
|
228 |
+ /* |
|
229 |
+ * When the area=files, this chooses the sub action we want to work with. |
|
230 |
+ */ |
|
231 |
+ private function action_files(): void |
|
232 |
+ { |
|
233 |
+ $subActions = [ |
|
234 |
+ 'list' => 'filesIndex', |
|
235 |
+ 'archive' => 'downloadArchive', |
|
236 |
+ ]; |
|
237 |
+ |
|
238 |
+ if (!isset($this->context['instances']['DevToolsFiles'])) |
|
239 |
+ $this->context['instances']['DevToolsFiles'] = new DevToolsFiles; |
|
240 |
+ |
|
241 |
+ $this->context['instances']['DevToolsFiles']->{$this->getSubAction($subActions, 'list')}(); |
|
242 |
+ $this->setSubTemplate($this->getSubAction($subActions, 'list')); |
|
243 |
+ } |
|
244 |
+ |
|
224 | 245 |
/* |
225 | 246 |
* Loads a sub template. If we specify the second parameter, we will also load the template file. |
226 | 247 |
* |
... | ... |
@@ -363,6 +384,10 @@ class DevTools |
363 | 384 |
'text' => 'hooks_title_list', |
364 | 385 |
'url' => $this->scripturl . '?action=devtools;area=hooks', |
365 | 386 |
], |
387 |
+ 'files' => [ |
|
388 |
+ 'text' => 'files_title_list', |
|
389 |
+ 'url' => $this->scripturl . '?action=devtools;area=files', |
|
390 |
+ ], |
|
366 | 391 |
]; |
367 | 392 |
|
368 | 393 |
$this->context['devtools_buttons'][$active]['active'] ?? null; |
... | ... |
@@ -387,7 +412,7 @@ class DevTools |
387 | 412 |
public function loadSources(array $sources): void |
388 | 413 |
{ |
389 | 414 |
array_map(function($rs) { |
390 |
- require_once($GLOBALS['sourcedir'] . '/' . strtr($rs, ['DevTools' => 'DevTools-']) . '.php'); |
|
415 |
+ require_once($GLOBALS['sourcedir'] . '/' . strtr($rs, ['DevTools' => 'DevTools/DevTools-']) . '.php'); |
|
391 | 416 |
}, $sources); |
392 | 417 |
} |
393 | 418 |
|
... | ... |
@@ -56,3 +56,9 @@ function template_hooksIndex() |
56 | 56 |
{ |
57 | 57 |
template_show_list('hooks_list'); |
58 | 58 |
} |
59 |
+ |
|
60 |
+/* This just calls the template for showing a list on our hooks */ |
|
61 |
+function template_filesIndex() |
|
62 |
+{ |
|
63 |
+ template_show_list('packages_lists_modification'); |
|
64 |
+} |
|
59 | 65 |
\ No newline at end of file |
... | ... |
@@ -0,0 +1,334 @@ |
1 |
+<?php |
|
2 |
+ |
|
3 |
+/** |
|
4 |
+ * The class for DevTools Hooks. |
|
5 |
+ * @package DevTools |
|
6 |
+ * @author SleePy <sleepy @ simplemachines (dot) org> |
|
7 |
+ * @copyright 2023 |
|
8 |
+ * @license 3-Clause BSD https://opensource.org/licenses/BSD-3-Clause |
|
9 |
+ * @version 1.1 |
|
10 |
+*/ |
|
11 |
+abstract class DevToolsFileBase |
|
12 |
+{ |
|
13 |
+ /* |
|
14 |
+ * The filename we will use upon downloading. |
|
15 |
+ */ |
|
16 |
+ protected string $fileName; |
|
17 |
+ |
|
18 |
+ /* |
|
19 |
+ * The physical path to the download file. |
|
20 |
+ */ |
|
21 |
+ protected string $physicalDownloadFile; |
|
22 |
+ |
|
23 |
+ /* |
|
24 |
+ * The directory will be compressing. |
|
25 |
+ */ |
|
26 |
+ protected string $directory; |
|
27 |
+ |
|
28 |
+ /* |
|
29 |
+ * Anything we are excluding. |
|
30 |
+ */ |
|
31 |
+ protected array $exclusions = []; |
|
32 |
+ |
|
33 |
+ /* |
|
34 |
+ * Our temp working directory. |
|
35 |
+ */ |
|
36 |
+ private ?string $temp_dir = null; |
|
37 |
+ |
|
38 |
+ /* |
|
39 |
+ * The data file we are looking for inside packages. |
|
40 |
+ */ |
|
41 |
+ protected string $packageInfoName = 'package-info.xml'; |
|
42 |
+ |
|
43 |
+ /* |
|
44 |
+ * Sets the file name to use on download. |
|
45 |
+ * |
|
46 |
+ * @param string $fileName Filename to use. |
|
47 |
+ * @return self return our own class |
|
48 |
+ */ |
|
49 |
+ public function setFileName(string $fileName): self |
|
50 |
+ { |
|
51 |
+ $this->fileName = $fileName; |
|
52 |
+ |
|
53 |
+ return $this; |
|
54 |
+ } |
|
55 |
+ |
|
56 |
+ /* |
|
57 |
+ * Sets the directory we will be compressing. |
|
58 |
+ * |
|
59 |
+ * @param string $directory directory to use. |
|
60 |
+ * @return self return our own class |
|
61 |
+ */ |
|
62 |
+ public function setDirectory(string $directory): self |
|
63 |
+ { |
|
64 |
+ $this->directory = $directory; |
|
65 |
+ |
|
66 |
+ return $this; |
|
67 |
+ } |
|
68 |
+ |
|
69 |
+ /* |
|
70 |
+ * Set a single exclusion. |
|
71 |
+ * |
|
72 |
+ * @param string $exclusion What we will be excluding. Can use * wildcards |
|
73 |
+ * @return self return our own class |
|
74 |
+ */ |
|
75 |
+ public function setExclusion(string $exclusion): self |
|
76 |
+ { |
|
77 |
+ $this->exclusions[] = $exclusion; |
|
78 |
+ |
|
79 |
+ return $this; |
|
80 |
+ } |
|
81 |
+ |
|
82 |
+ /* |
|
83 |
+ * Sets multiple exclusions. |
|
84 |
+ * |
|
85 |
+ * @param array $exclusions What we will be excluding. Can use * wildcards |
|
86 |
+ * @return self return our own class |
|
87 |
+ */ |
|
88 |
+ public function setExclusions(array $exclusions): self |
|
89 |
+ { |
|
90 |
+ $this->exclusions += $exclusions; |
|
91 |
+ |
|
92 |
+ return $this; |
|
93 |
+ } |
|
94 |
+ |
|
95 |
+ /* |
|
96 |
+ * Retreive the working file name. |
|
97 |
+ * This searches for a valid temp directory we can work with. |
|
98 |
+ * |
|
99 |
+ * @return string the working file name |
|
100 |
+ */ |
|
101 |
+ public function GetWorkingFile(): string |
|
102 |
+ { |
|
103 |
+ $tempdir = $this->FindWorkingTempDirectory(); |
|
104 |
+ |
|
105 |
+ return $tempdir . 'DevToolsTempArchive'; |
|
106 |
+ } |
|
107 |
+ |
|
108 |
+ /* |
|
109 |
+ * Find a working temp directory. |
|
110 |
+ * Most of this is borrowed from Subs-Admin.php sm_temp_dir. |
|
111 |
+ * |
|
112 |
+ * @return string A valid temp directory |
|
113 |
+ */ |
|
114 |
+ private function FindWorkingTempDirectory(): string |
|
115 |
+ { |
|
116 |
+ global $cachedir; |
|
117 |
+ |
|
118 |
+ // Already did this. |
|
119 |
+ if (!empty($this->temp_dir)) |
|
120 |
+ return $this->temp_dir; |
|
121 |
+ |
|
122 |
+ // Temp Directory options order. |
|
123 |
+ $temp_dir_options = array( |
|
124 |
+ 0 => 'sys_get_temp_dir', |
|
125 |
+ 1 => 'upload_tmp_dir', |
|
126 |
+ 2 => 'session.save_path', |
|
127 |
+ 3 => 'cachedir' |
|
128 |
+ ); |
|
129 |
+ |
|
130 |
+ // Determine if we should detect a restriction and what restrictions that may be. |
|
131 |
+ $open_base_dir = ini_get('open_basedir'); |
|
132 |
+ $restriction = !empty($open_base_dir) ? explode(':', $open_base_dir) : false; |
|
133 |
+ |
|
134 |
+ // Prevent any errors as we search. |
|
135 |
+ $old_error_reporting = error_reporting(0); |
|
136 |
+ |
|
137 |
+ // Search for a working temp directory. |
|
138 |
+ foreach ($temp_dir_options as $id_temp => $temp_option) |
|
139 |
+ { |
|
140 |
+ switch ($temp_option) { |
|
141 |
+ case 'cachedir': |
|
142 |
+ $possible_temp = rtrim($cachedir, '/'); |
|
143 |
+ break; |
|
144 |
+ |
|
145 |
+ case 'session.save_path': |
|
146 |
+ $possible_temp = rtrim(ini_get('session.save_path'), '/'); |
|
147 |
+ break; |
|
148 |
+ |
|
149 |
+ case 'upload_tmp_dir': |
|
150 |
+ $possible_temp = rtrim(ini_get('upload_tmp_dir'), '/'); |
|
151 |
+ break; |
|
152 |
+ |
|
153 |
+ default: |
|
154 |
+ $possible_temp = sys_get_temp_dir(); |
|
155 |
+ break; |
|
156 |
+ } |
|
157 |
+ |
|
158 |
+ // Check if we have a restriction preventing this from working. |
|
159 |
+ if ($restriction) |
|
160 |
+ { |
|
161 |
+ foreach ($restriction as $dir) |
|
162 |
+ { |
|
163 |
+ if (strpos($possible_temp, $dir) !== false && is_writable($possible_temp)) |
|
164 |
+ { |
|
165 |
+ $this->temp_dir = $possible_temp; |
|
166 |
+ break; |
|
167 |
+ } |
|
168 |
+ } |
|
169 |
+ } |
|
170 |
+ // No restrictions, but need to check for writable status. |
|
171 |
+ elseif (is_writable($possible_temp)) |
|
172 |
+ { |
|
173 |
+ $this->temp_dir = $possible_temp; |
|
174 |
+ break; |
|
175 |
+ } |
|
176 |
+ } |
|
177 |
+ |
|
178 |
+ // Fall back to sys_get_temp_dir even though it won't work, so we have something. |
|
179 |
+ if (empty($this->temp_dir)) |
|
180 |
+ $this->temp_dir = sys_get_temp_dir(); |
|
181 |
+ |
|
182 |
+ // Fix the path. |
|
183 |
+ $this->temp_dir = substr($this->temp_dir, -1) === '/' ? $this->temp_dir : $this->temp_dir . '/'; |
|
184 |
+ |
|
185 |
+ // Put things back. |
|
186 |
+ error_reporting($old_error_reporting); |
|
187 |
+ |
|
188 |
+ return $this->temp_dir; |
|
189 |
+ } |
|
190 |
+ |
|
191 |
+ /* |
|
192 |
+ * Delete the working file. |
|
193 |
+ * |
|
194 |
+ * @param ?string $file If provided we will delete this file, otherwise we get the working directory file. |
|
195 |
+ * @return bool If we can't unlink the file or a error occurs, return false. |
|
196 |
+ */ |
|
197 |
+ protected function DeleteWorkingFile(?string $file): bool |
|
198 |
+ { |
|
199 |
+ try |
|
200 |
+ { |
|
201 |
+ return unlink($file ?? $this->GetWorkingFile()); |
|
202 |
+ } |
|
203 |
+ catch (Exception $e) |
|
204 |
+ { |
|
205 |
+ return false; |
|
206 |
+ } |
|
207 |
+ } |
|
208 |
+ |
|
209 |
+ /* |
|
210 |
+ * Actually downloads a file. At this point we output the binary data and exit. |
|
211 |
+ * Parts of this is borrowed from SMF's showAttachment function. |
|
212 |
+ * |
|
213 |
+ * @calls: $sourcedir/Load.php:isBrowser |
|
214 |
+ * @return void Output is generated. |
|
215 |
+ */ |
|
216 |
+ public function downloadArchive(): void |
|
217 |
+ { |
|
218 |
+ $filesize = filesize($this->physicalDownloadFile); |
|
219 |
+ |
|
220 |
+ header('pragma: '); |
|
221 |
+ if (!isBrowser('gecko')) |
|
222 |
+ header('content-transfer-encoding: binary'); |
|
223 |
+ header('expires: ' . gmdate('D, d M Y H:i:s', time() * 60)); |
|
224 |
+ header('last-modified: ' . gmdate('D, d M Y H:i:s', time())); |
|
225 |
+ header('accept-ranges: bytes'); |
|
226 |
+ header('connection: close'); |
|
227 |
+ header('content-type: ' . (isBrowser('ie') || isBrowser('opera') ? 'application/octetstream' : 'application/octet-stream')); |
|
228 |
+ header('content-disposition: attachment; filename="' . $this->fileName . '"'); |
|
229 |
+ header('cache-control: max-age=' . (60) . ', private'); |
|
230 |
+ |
|
231 |
+ header("content-length: " . $filesize); |
|
232 |
+ |
|
233 |
+ if ($filesize > 4194304) |
|
234 |
+ { |
|
235 |
+ // Forcibly end any output buffering going on. |
|
236 |
+ while (@ob_get_level() > 0) |
|
237 |
+ @ob_end_clean(); |
|
238 |
+ |
|
239 |
+ header_remove('content-encoding'); |
|
240 |
+ |
|
241 |
+ $fp = fopen($this->GetWorkingFile(), 'rb'); |
|
242 |
+ while (!feof($fp)) |
|
243 |
+ { |
|
244 |
+ echo fread($fp, 8192); |
|
245 |
+ flush(); |
|
246 |
+ } |
|
247 |
+ fclose($fp); |
|
248 |
+ } |
|
249 |
+ |
|
250 |
+ // On some of the less-bright hosts, readfile() is disabled. It's just a faster, more byte safe, version of what's in the if. |
|
251 |
+ elseif (@readfile($this->physicalDownloadFile) === null) |
|
252 |
+ echo file_get_contents($this->physicalDownloadFile); |
|
253 |
+ |
|
254 |
+ $this->cleanupArchives(); |
|
255 |
+ die(); |
|
256 |
+ } |
|
257 |
+ |
|
258 |
+ /* |
|
259 |
+ * Searches our working directory for any additional temp files and delete them. |
|
260 |
+ * |
|
261 |
+ * @return void Nothing is returned |
|
262 |
+ */ |
|
263 |
+ public function cleanupArchives(): void |
|
264 |
+ { |
|
265 |
+ $files = scandir($this->FindWorkingTempDirectory()); |
|
266 |
+ |
|
267 |
+ if (!empty($files)) |
|
268 |
+ foreach ($files as $f) |
|
269 |
+ if (strpos($f, 'DevToolsTempArchive') === 0) |
|
270 |
+ unlink($this->FindWorkingTempDirectory() . '/' . $f); |
|
271 |
+ } |
|
272 |
+} |
|
273 |
+ |
|
274 |
+/* |
|
275 |
+ * The interface for the file handler. |
|
276 |
+*/ |
|
277 |
+interface DevToolsFileInterface |
|
278 |
+{ |
|
279 |
+ public static function IsSupported(): bool; |
|
280 |
+ |
|
281 |
+ /* |
|
282 |
+ * Sets the file name to use on download. |
|
283 |
+ * |
|
284 |
+ * @param string $fileName Filename to use. |
|
285 |
+ * @return self return our own class |
|
286 |
+ */ |
|
287 |
+ public function setFileName(string $fileName); |
|
288 |
+ |
|
289 |
+ /* |
|
290 |
+ * Sets the directory we will be compressing. |
|
291 |
+ * |
|
292 |
+ * @param string $directory directory to use. |
|
293 |
+ * @return self return our own class |
|
294 |
+ */ |
|
295 |
+ public function setDirectory(string $directory); |
|
296 |
+ |
|
297 |
+ /* |
|
298 |
+ * Set a single exclusion. |
|
299 |
+ * |
|
300 |
+ * @param string $exclusion What we will be excluding. Can use * wildcards |
|
301 |
+ * @return self return our own class |
|
302 |
+ */ |
|
303 |
+ public function setExclusion(string $exclusion); |
|
304 |
+ |
|
305 |
+ /* |
|
306 |
+ * Sets multiple exclusions. |
|
307 |
+ * |
|
308 |
+ * @param array $exclusions What we will be excluding. Can use * wildcards |
|
309 |
+ * @return self return our own class |
|
310 |
+ */ |
|
311 |
+ public function setExclusions(array $exclusions); |
|
312 |
+ |
|
313 |
+ /* |
|
314 |
+ * Actually generate our archive for downloading. |
|
315 |
+ * |
|
316 |
+ * @return self return our own class |
|
317 |
+ */ |
|
318 |
+ public function generateArchive(); |
|
319 |
+ |
|
320 |
+ /* |
|
321 |
+ * Actually downloads a file. At this point we output the binary data and exit. |
|
322 |
+ * Parts of this is borrowed from SMF's showAttachment function. |
|
323 |
+ * |
|
324 |
+ * @return void Output is generated. |
|
325 |
+ */ |
|
326 |
+ public function downloadArchive(); |
|
327 |
+ |
|
328 |
+ /* |
|
329 |
+ * Searches our working directory for any additional temp files and delete them. |
|
330 |
+ * |
|
331 |
+ * @return void Nothing is returned |
|
332 |
+ */ |
|
333 |
+ public function cleanupArchives(): void; |
|
334 |
+} |
|
0 | 335 |
\ No newline at end of file |
... | ... |
@@ -0,0 +1,242 @@ |
1 |
+<?php |
|
2 |
+//namespace SMF\DevTools; |
|
3 |
+ |
|
4 |
+/** |
|
5 |
+ * The class for DevTools File Phar Base. |
|
6 |
+ * @package DevTools |
|
7 |
+ * @author SleePy <sleepy @ simplemachines (dot) org> |
|
8 |
+ * @copyright 2023 |
|
9 |
+ * @license 3-Clause BSD https://opensource.org/licenses/BSD-3-Clause |
|
10 |
+ * @version 1.1 |
|
11 |
+*/ |
|
12 |
+class DevToolsFilePharBase Extends DevToolsFileBase |
|
13 |
+{ |
|
14 |
+ /* |
|
15 |
+ * PharData handler. |
|
16 |
+ */ |
|
17 |
+ protected $pd; |
|
18 |
+ |
|
19 |
+ /* |
|
20 |
+ * Extension from PharData we will be creating. |
|
21 |
+ */ |
|
22 |
+ protected $extension = Phar::ZIP; |
|
23 |
+ |
|
24 |
+ /* |
|
25 |
+ * Compression method we are using. |
|
26 |
+ */ |
|
27 |
+ protected $compression = Phar::NONE; |
|
28 |
+ |
|
29 |
+ /* |
|
30 |
+ * Our file we are working with. |
|
31 |
+ */ |
|
32 |
+ protected $workingFile = null; |
|
33 |
+ |
|
34 |
+ /* |
|
35 |
+ * Temp directory we are working with. |
|
36 |
+ */ |
|
37 |
+ protected $workingDir = null; |
|
38 |
+ |
|
39 |
+ /* |
|
40 |
+ * The extension we are using for temp files. |
|
41 |
+ */ |
|
42 |
+ protected $tmpExtension = 'tmp'; |
|
43 |
+ |
|
44 |
+ /* |
|
45 |
+ * file Extensions for earch phar extesnion. |
|
46 |
+ */ |
|
47 |
+ protected $extensionMap = [ |
|
48 |
+ Phar::PHAR => 'tmp', |
|
49 |
+ Phar::TAR => 'tar', |
|
50 |
+ Phar::ZIP => 'zip' |
|
51 |
+ ]; |
|
52 |
+ |
|
53 |
+ /* |
|
54 |
+ * Is this file export method supported? |
|
55 |
+ * This is currently not used, but exists for future expansion. |
|
56 |
+ * |
|
57 |
+ * @return bool True if this file export method appears to have all the support needed. |
|
58 |
+ */ |
|
59 |
+ public static function IsSupported(): bool |
|
60 |
+ { |
|
61 |
+ return class_exists('PharData'); |
|
62 |
+ } |
|
63 |
+ |
|
64 |
+ /* |
|
65 |
+ * Set what extension we are exporting. |
|
66 |
+ * This should come in one of 3 options. Phar::PHAR, Phar::TAR, Phar::ZIP |
|
67 |
+ * |
|
68 |
+ * @param int $extension The extension we are exporting. |
|
69 |
+ * @return self return our own class |
|
70 |
+ */ |
|
71 |
+ public function setExtension(int $extension): self |
|
72 |
+ { |
|
73 |
+ if (in_array($extension, [Phar::PHAR, Phar::TAR, Phar::ZIP])) |
|
74 |
+ $this->extension = $extension; |
|
75 |
+ |
|
76 |
+ return $this; |
|
77 |
+ } |
|
78 |
+ |
|
79 |
+ /* |
|
80 |
+ * Actually generate our archive for downloading. |
|
81 |
+ * This logic handles setting up PharData, adding the files, compressing (if needed) and sets the physical download file. |
|
82 |
+ * |
|
83 |
+ * @return self return our own class |
|
84 |
+ */ |
|
85 |
+ public function generateArchive(): self |
|
86 |
+ { |
|
87 |
+ // Change to the working directory. |
|
88 |
+ $this->workingDir = dirname($this->GetWorkingFile()); |
|
89 |
+ chdir($this->workingDir); |
|
90 |
+ |
|
91 |
+ // Set our working info |
|
92 |
+ $this->workingFile = basename($this->GetWorkingFile()) . '.' . $this->tmpExtension; |
|
93 |
+ |
|
94 |
+ // Start our phar file. |
|
95 |
+ $this->pd = $this->PharData( |
|
96 |
+ $this->workingFile, |
|
97 |
+ FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS, |
|
98 |
+ null, |
|
99 |
+ $this->extension |
|
100 |
+ ); |
|
101 |
+ |
|
102 |
+ // Run a iterator of another iterator with a filter that has a iterator. |
|
103 |
+ $this->buildFromIterator($this->directory); |
|
104 |
+ |
|
105 |
+ // No files made it into the archive. |
|
106 |
+ if ($this->pd->count() === 0) |
|
107 |
+ throw new \ErrorException($this->dt->txt('attachment_transfer_no_find'), 0, E_ERROR, __FILE__, __LINE__); |
|
108 |
+ |
|
109 |
+ $this->convertToData(); |
|
110 |
+ $this->physicalDownloadFile = $this->GetWorkingFile() . '.' . $this->getRealExtension(); |
|
111 |
+ |
|
112 |
+ return $this; |
|
113 |
+ } |
|
114 |
+ |
|
115 |
+ /* |
|
116 |
+ * Wrapper for PharData to handle errors with open_basedir. |
|
117 |
+ * |
|
118 |
+ * @param mixed ...$args All the standard arguments you can pass to phardata |
|
119 |
+ * @return ?PharData A valid PharData object is returned if valid, null is returned otherwise. |
|
120 |
+ */ |
|
121 |
+ protected function PharData(...$args): ?PharData |
|
122 |
+ { |
|
123 |
+ // Safely build our phar, but handle a safe error with open_basedir restrictions. |
|
124 |
+ try |
|
125 |
+ { |
|
126 |
+ set_error_handler(static function ($severity, $message, $file, $line) { |
|
127 |
+ throw new \ErrorException($message, 0, $severity, $file, $line); |
|
128 |
+ }); |
|
129 |
+ |
|
130 |
+ // Start our phar file. |
|
131 |
+ $this->pd = new PharData(...$args); |
|
132 |
+ return $this->pd; |
|
133 |
+ } |
|
134 |
+ catch (Exception $e) |
|
135 |
+ { |
|
136 |
+ if (strpos($e->getMessage(), 'open_basedir') == false) |
|
137 |
+ throw new \ErrorException($e->getMessage(), 0, $e->getSeverity(), $e->getFile(), $e->getLine()); |
|
138 |
+ } |
|
139 |
+ finally |
|
140 |
+ { |
|
141 |
+ restore_error_handler(); |
|
142 |
+ } |
|
143 |
+ |
|
144 |
+ return null; |
|
145 |
+ } |
|
146 |
+ |
|
147 |
+ /* |
|
148 |
+ * Usinga a iterator, we build a list of files we will compress, skipping directories. |
|
149 |
+ * This logic does skip empty directories. |
|
150 |
+ * This will attempt to exclude files matching a direct name match and wildcards. |
|
151 |
+ * Upon a successful match, matches are automatically added to the phardata file. |
|
152 |
+ * |
|
153 |
+ * @param string $directory Directory we will scan for all matching files. |
|
154 |
+ * @return void No data is returned, howerver our PharData object is updated. |
|
155 |
+ */ |
|
156 |
+ protected function buildFromIterator(string $directory): void |
|
157 |
+ { |
|
158 |
+ $filter = function ($file, $key, $iterator) use ($directory) { |
|
159 |
+ // Simple is directory or exact matches. |
|
160 |
+ if ($iterator->hasChildren() && !in_array($file->getFilename(), $this->exclusions)) |
|
161 |
+ return true; |
|
162 |
+ |
|
163 |
+ // More complex wildcard matches or sub directories. Get a base directory, then run through all excludes to see if any more complex patterns match. |
|
164 |
+ $workingDirectory = $file->getPath(); |
|
165 |
+ if (0 === strpos($workingDirectory, $directory . DIRECTORY_SEPARATOR)) |
|
166 |
+ $workingDirectory = substr($workingDirectory, strlen($directory . DIRECTORY_SEPARATOR)); |
|
167 |
+ foreach ($this->exclusions as $e) |
|
168 |
+ if (fnmatch($e, $workingDirectory . DIRECTORY_SEPARATOR . $file->getFilename())) |
|
169 |
+ return false; |
|
170 |
+ |
|
171 |
+ // Otherwise, only include this if its a file. |
|
172 |
+ return $file->isFile(); |
|
173 |
+ }; |
|
174 |
+ |
|
175 |
+ $this->pd->buildFromIterator( |
|
176 |
+ new RecursiveIteratorIterator( |
|
177 |
+ new RecursiveCallbackFilterIterator( |
|
178 |
+ new RecursiveDirectoryIterator( |
|
179 |
+ $directory, |
|
180 |
+ RecursiveDirectoryIterator::SKIP_DOTS |
|
181 |
+ ), |
|
182 |
+ $filter |
|
183 |
+ ) |
|
184 |
+ ), |
|
185 |
+ $directory |
|
186 |
+ ); |
|
187 |
+ } |
|
188 |
+ |
|
189 |
+ /* |
|
190 |
+ * Convert the phar archive to a valid archive file. |
|
191 |
+ * |
|
192 |
+ * @return ?PharData PharData object is returned if successful, null if a error occurs. |
|
193 |
+ */ |
|
194 |
+ protected function convertToData(): ?PharData |
|
195 |
+ { |
|
196 |
+ // One more sanity check, Phar doesn't do overwrite |
|
197 |
+ if (file_exists($this->GetWorkingFile() . '.' . $this->getRealExtension())) |
|
198 |
+ unlink($this->GetWorkingFile() . '.' . $this->getRealExtension()); |
|
199 |
+ |
|
200 |
+ // Safely build our file, but handle a safe error with open_basedir restrictions. |
|
201 |
+ try |
|
202 |
+ { |
|
203 |
+ set_error_handler(static function ($severity, $message, $file, $line) { |
|
204 |
+ throw new \ErrorException($message, 0, $severity, $file, $line); |
|
205 |
+ }); |
|
206 |
+ |
|
207 |
+ return $this->pd->convertToData( |
|
208 |
+ $this->extension, |
|
209 |
+ $this->compression, |
|
210 |
+ $this->getRealExtension() |
|
211 |
+ ); |
|
212 |
+ } |
|
213 |
+ catch (BadMethodCallException $e) |
|
214 |
+ { |
|
215 |
+ throw new \ErrorException($e->getMessage(), 0, E_ERROR, $e->getFile(), $e->getLine()); |
|
216 |
+ } |
|
217 |
+ catch (Exception $e) |
|
218 |
+ { |
|
219 |
+ if (strpos($e->getMessage(), 'open_basedir') == false) |
|
220 |
+ { |
|
221 |
+ $this->cleanupArchives(); |
|
222 |
+ throw new \ErrorException($e->getMessage(), 0, $e->getSeverity(), $e->getFile(), $e->getLine()); |
|
223 |
+ } |
|
224 |
+ } |
|
225 |
+ finally |
|
226 |
+ { |
|
227 |
+ restore_error_handler(); |
|
228 |
+ } |
|
229 |
+ |
|
230 |
+ return null; |
|
231 |
+ } |
|
232 |
+ |
|
233 |
+ /* |
|
234 |
+ * Get the real extension we are wanting. |
|
235 |
+ * |
|
236 |
+ * @return string Our extension we are using. |
|
237 |
+ */ |
|
238 |
+ protected function getRealExtension(): string |
|
239 |
+ { |
|
240 |
+ return $this->extensionMap[$this->extension] ?? 'tar'; |
|
241 |
+ } |
|
242 |
+} |
|
0 | 243 |
\ No newline at end of file |
... | ... |
@@ -0,0 +1,18 @@ |
1 |
+<?php |
|
2 |
+ |
|
3 |
+/** |
|
4 |
+ * The class for DevTools File Phar Tgz. |
|
5 |
+ * @package DevTools |
|
6 |
+ * @author SleePy <sleepy @ simplemachines (dot) org> |
|
7 |
+ * @copyright 2023 |
|
8 |
+ * @license 3-Clause BSD https://opensource.org/licenses/BSD-3-Clause |
|
9 |
+ * @version 1.1 |
|
10 |
+*/ |
|
11 |
+final class DevToolsFilePharTgz Extends DevToolsFilePharBase implements DevToolsFileInterface |
|
12 |
+{ |
|
13 |
+ // Set our extension to Tar |
|
14 |
+ protected $extension = Phar::TAR; |
|
15 |
+ |
|
16 |
+ // Compress our tar with GZ |
|
17 |
+ protected $compression = Phar::GZ; |
|
18 |
+} |
|
0 | 19 |
\ No newline at end of file |
... | ... |
@@ -0,0 +1,14 @@ |
1 |
+<?php |
|
2 |
+ |
|
3 |
+/** |
|
4 |
+ * The class for DevTools File Phar Tgz. |
|
5 |
+ * @package DevTools |
|
6 |
+ * @author SleePy <sleepy @ simplemachines (dot) org> |
|
7 |
+ * @copyright 2023 |
|
8 |
+ * @license 3-Clause BSD https://opensource.org/licenses/BSD-3-Clause |
|
9 |
+ * @version 1.1 |
|
10 |
+*/ |
|
11 |
+final class DevToolsFilePharZip Extends DevToolsFilePharBase implements DevToolsFileInterface |
|
12 |
+{ |
|
13 |
+ /* Note, no additional logic is currently needed here. This howerver does setup the final class.*/ |
|
14 |
+} |
|
0 | 15 |
\ No newline at end of file |
... | ... |
@@ -0,0 +1,531 @@ |
1 |
+<?php |
|
2 |
+ |
|
3 |
+/** |
|
4 |
+ * The class for DevTools Hooks. |
|
5 |
+ * @package DevTools |
|
6 |
+ * @author SleePy <sleepy @ simplemachines (dot) org> |
|
7 |
+ * @copyright 2023 |
|
8 |
+ * @license 3-Clause BSD https://opensource.org/licenses/BSD-3-Clause |
|
9 |
+ * @version 1.1 |
|
10 |
+*/ |
|
11 |
+class DevToolsFiles |
|
12 |
+{ |
|
13 |
+ /* |
|
14 |
+ * Handler for our Developer tools main object. |
|
15 |
+ */ |
|
16 |
+ private DevTools $dt; |
|
17 |
+ |
|
18 |
+ /* |
|
19 |
+ * SMF variables we will load into here for easy reference later. |
|
20 |
+ */ |
|
21 |
+ private string $scripturl; |
|
22 |
+ private string $packagesdir; |
|
23 |
+ private string $boarddir; |
|
24 |
+ private string $sourcedir; |
|
25 |
+ private array $context; |
|
26 |
+ private array $smcFunc; |
|
27 |
+ private array $modSettings; |
|
28 |
+ private array $settings; |
|
29 |
+ /* Sometimes in SMF, this is null, which is unusal for a boolean */ |
|
30 |
+ private ?bool $db_show_debug; |
|
31 |
+ |
|
32 |
+ /* |
|
33 |
+ * SMF has this both as an array and a bool, no type delcartion. |
|
34 |
+ */ |
|
35 |
+ private $package_cache; |
|
36 |
+ |
|
37 |
+ /* |
|
38 |
+ * The data file we are looking for inside packages. |
|
39 |
+ */ |
|
40 |
+ private string $packageInfoName = 'package-info.xml'; |
|
41 |
+ |
|
42 |
+ /* |
|
43 |
+ * The extensions we support. |
|
44 |
+ */ |
|
45 |
+ private array $extensions = ['tgz', 'zip']; |
|
46 |
+ |
|
47 |
+ /* |
|
48 |
+ * The providers we support. |
|
49 |
+ */ |
|
50 |
+ private array $providers = ['phar']; |
|
51 |
+ |
|
52 |
+ /* |
|
53 |
+ * Builds the DevTools Packages object. This also loads a few globals into easy to access properties, some by reference so we can update them |
|
54 |
+ */ |
|
55 |
+ public function __construct() |
|
56 |
+ { |
|
57 |
+ foreach (['scripturl', 'packagesdir', 'settings', 'boarddir', 'sourcedir', 'db_show_debug'] as $f) |
|
58 |
+ $this->{$f} = $GLOBALS[$f]; |
|
59 |
+ foreach (['context', 'smcFunc', 'package_cache', 'modSettings'] as $f) |
|
60 |
+ $this->{$f} = &$GLOBALS[$f]; |
|
61 |
+ |
|
62 |
+ $this->dt = &$this->context['instances']['DevTools']; |
|
63 |
+ $this->dt->loadSources([ |
|
64 |
+ 'DevToolsFile-Base', |
|
65 |
+ 'DevToolsFile-PharBase', |
|
66 |
+ 'DevToolsFile-PharTgz', |
|
67 |
+ 'DevToolsFile-PharZip', |
|
68 |
+ 'Packages', |
|
69 |
+ 'Subs-Package', |
|
70 |
+ 'Subs-List', |
|
71 |
+ 'Class-Package' |
|
72 |
+ ]); |
|
73 |
+ $this->dt->loadLanguage(['Admin', 'Packages']); |
|
74 |
+ } |
|
75 |
+ |
|
76 |
+ /* |
|
77 |
+ * Loads the main package listing. |
|
78 |
+ * |
|
79 |
+ * @calls: $sourcedir/Subs-List.php:createList |
|
80 |
+ */ |
|
81 |
+ public function filesIndex(): void |
|
82 |
+ { |
|
83 |
+ $this->context['available_packages'] = 0; |
|
84 |
+ createList($this->context['packages'] = $this->buildPackagesList()); |
|
85 |
+ |
|
86 |
+ // An action was successful. |
|
87 |
+ if (isset($_REQUEST['success'])) |
|
88 |
+ $this->dt->showSuccessDialog($this->successMsg((string) $_REQUEST['success'])); |
|
89 |
+ } |
|
90 |
+ |
|
91 |
+ /* |
|
92 |
+ * Returns an array that will be passed into SMF's createList logic to build a packages listing. |
|
93 |
+ * |
|
94 |
+ * @calls: $sourcedir/Subs.php:timeformat |
|
95 |
+ */ |
|
96 |
+ private function buildPackagesList(): array |
|
97 |
+ { |
|
98 |
+ return [ |
|
99 |
+ 'id' => 'packages_lists_modification', |
|
100 |
+ 'no_items_label' => $this->dt->txt('no_packages'), |
|
101 |
+ 'get_items' => [ |
|
102 |
+ 'function' => [$this, 'listGetPackages'], |
|
103 |
+ 'params' => ['modification'], |
|
104 |
+ ], |
|
105 |
+ 'base_href' => $this->scripturl . '?action=devtools;area=files', |
|
106 |
+ 'default_sort_col' => 'idmodification', |
|
107 |
+ 'columns' => [ |
|
108 |
+ 'idmodification' => [ |
|
109 |
+ 'header' => [ |
|
110 |
+ 'value' => $this->dt->txt('package_id'), |
|
111 |
+ 'style' => 'width: 52px;', |
|
112 |
+ ], |
|
113 |
+ 'data' => [ |
|
114 |
+ 'db' => 'sort_id', |
|
115 |
+ ], |
|
116 |
+ 'sort' => [ |
|
117 |
+ 'default' => 'sort_id', |
|
118 |
+ 'reverse' => 'sort_id' |
|
119 |
+ ], |
|
120 |
+ ], |
|
121 |
+ 'mod_namemodification' => [ |
|
122 |
+ 'header' => [ |
|
123 |
+ 'value' => $this->dt->txt('mod_name'), |
|
124 |
+ 'style' => 'width: 25%;', |
|
125 |
+ ], |
|
126 |
+ 'data' => [ |
|
127 |
+ 'db' => 'name', |
|
128 |
+ ], |
|
129 |
+ 'sort' => [ |
|
130 |
+ 'default' => 'name', |
|
131 |
+ 'reverse' => 'name', |
|
132 |
+ ], |
|
133 |
+ ], |
|
134 |
+ 'versionmodification' => [ |
|
135 |
+ 'header' => [ |
|
136 |
+ 'value' => $this->dt->txt('mod_version'), |
|
137 |
+ ], |
|
138 |
+ 'data' => [ |
|
139 |
+ 'db' => 'version', |
|
140 |
+ ], |
|
141 |
+ 'sort' => [ |
|
142 |
+ 'default' => 'version', |
|
143 |
+ 'reverse' => 'version', |
|
144 |
+ ], |
|
145 |
+ ], |
|
146 |
+ 'time_installedmodification' => [ |
|
147 |
+ 'header' => [ |
|
148 |
+ 'value' => $this->dt->txt('mod_installed_time'), |
|
149 |
+ ], |
|
150 |
+ 'data' => [ |
|
151 |
+ 'function' => function($package) |
|
152 |
+ { |
|
153 |
+ return !empty($package['time_installed']) |
|
154 |
+ ? timeformat($package['time_installed']) |
|
155 |
+ : $this->dt->txt('not_applicable'); |
|
156 |
+ }, |
|
157 |
+ 'class' => 'smalltext', |
|
158 |
+ ], |
|
159 |
+ 'sort' => [ |
|
160 |
+ 'default' => 'time_installed', |
|
161 |
+ 'reverse' => 'time_installed', |
|
162 |
+ ], |
|
163 |
+ ], |
|
164 |
+ 'operationsmodification' => [ |
|
165 |
+ 'header' => [ |
|
166 |
+ 'value' => '', |
|
167 |
+ ], |
|
168 |
+ 'data' => [ |
|
169 |
+ 'function' => [$this, 'listColOperations'], |
|
170 |
+ 'class' => 'righttext', |
|
171 |
+ ], |
|
172 |
+ ], |
|
173 |
+ ], |
|
174 |
+ ]; |
|
175 |
+ } |
|
176 |
+ |
|
177 |
+ /* |
|
178 |
+ * Get a listing of packages from SMF, then run through a filter to remove any compressed files. |
|
179 |
+ * This also will exclude our own package. |
|
180 |
+ * |
|
181 |
+ * @param ...$args all params that will just be passed directly into SMF's native list_getPackages |
|
182 |
+ * @See: $sourcedir/Packages.php:list_getPackages |
|
183 |
+ * @return array List of filtered packages we can work with. |
|
184 |
+ */ |
|
185 |
+ public function listGetPackages(...$args): array |
|
186 |
+ { |
|
187 |
+ // Filter out anything with an extension, we don't support working with compressed files. |
|
188 |
+ // list_getPackages is from SMF in Packages.php |
|
189 |
+ return array_filter(list_getPackages(...$args), function($p) { |
|
190 |
+ return $this->isValidPackage($p['filename']) && (!empty($this->modSettings['dt_showAllPackages']) || strpos($p['id'], $this->devToolsPackageID) === false); |
|
191 |
+ }); |
|
192 |
+ } |
|
193 |
+ |
|
194 |
+ /* |
|
195 |
+ * All possible operations we can perform on a package. |
|
196 |
+ * If a package can not be uninstalled, we remove the uninstall/reinstall actions. |
|
197 |
+ * |
|
198 |
+ * @param array $packagethe package data |
|
199 |
+ * @return string The actions we can perform. |
|
200 |
+ */ |
|
201 |
+ public function listColOperations(array $package): string |
|
202 |
+ { |
|
203 |
+ $actions = []; |
|
204 |
+ |
|
205 |
+ foreach ($this->providers as $provider) |
|
206 |
+ foreach ($this->extensions as $ext) |
|
207 |
+ $actions[$ext . $provider] = '<a href="' . $this->scripturl . '?action=devtools;area=files;sa=archive;package=' . $package['filename'] . ';extension=' . $ext . ';provider=' . $provider . '" class="button floatnone">' . $this->dt->txt('devtools_extension_' . $ext) . '</a>'; |
|
208 |
+ |
|
209 |
+ return implode('', $actions); |
|
210 |
+ } |
|
211 |
+ |
|
212 |
+ /* |
|
213 |
+ * Download Archive. Will issue a failure if we can't do any step in this process. |
|
214 |
+ * Upon success, this will redirect back to package listing. |
|
215 |
+ * |
|
216 |
+ * @calls: $sourcedir/Errors.php:fatal_lang_error |
|
217 |
+ * @calls: $sourcedir/Errors.php:fatal_error |
|
218 |
+ * @calls: $sourcedir/Subs.php:redirectexit |
|
219 |
+ */ |
|
220 |
+ public function downloadArchive(): void |
|
221 |
+ { |
|
222 |
+ // Ensure the file is valid. |
|
223 |
+ if (($package = $this->getRequestedPackage()) == '' || !$this->isValidPackage($package)) |
|
224 |
+ fatal_lang_error('package_no_file', false); |
|
225 |
+ else if (($basedir = $this->getPackageBasedir($package)) == '') |
|
226 |
+ fatal_lang_error('package_get_error_not_found', false); |
|
227 |
+ |
|
228 |
+ $infoFile = $this->getPackageInfo($basedir . DIRECTORY_SEPARATOR . $this->packageInfoName); |
|
229 |
+ if (!is_a($infoFile, 'xmlArray')) |
|
230 |
+ fatal_lang_error('package_get_error_missing_xml', false); |
|
231 |
+ |
|
232 |
+ $devtools = $this->findDevTools($infoFile); |
|
233 |
+ |
|
234 |
+ // If we can't find some data in our package info, just do defaults. |
|
235 |
+ $packageName = null; |
|
236 |
+ $exclusions = []; |
|
237 |
+ if (is_a($devtools, 'xmlArray')) |
|
238 |
+ { |
|
239 |
+ $packageName = $this->findPackageName($devtools) ?? null; |
|
240 |
+ $exclusions = $this->findExclusions($devtools) ?? []; |
|
241 |
+ } |
|
242 |
+ |
|
243 |
+ if (empty($packageName)) |
|
244 |
+ $packageName = $this->defaultPackageName($package); |
|
245 |
+ |
|
246 |
+ // Handle some substitutions. |
|
247 |
+ $infoVersion = $this->findPackageInfoVersion($infoFile); |
|
248 |
+ $infoName = $this->findPackageInfoName($infoFile); |
|
249 |
+ $packageName = strtr($packageName, [ |
|
250 |
+ '{VERSION}' => $infoVersion, |
|
251 |
+ '{VERSION-}' => str_replace('.', '-', $infoVersion), |
|
252 |
+ '{VERSION_}' => str_replace('.', '_', $infoVersion), |
|
253 |
+ '{CUSTOMIZATION-NAME}' => preg_replace('~\s~i', '-', $packageName), |
|
254 |
+ '{CUSTOMIZATION_NAME}' => preg_replace('~\s~i', '_', $packageName), |
|
255 |
+ '{CUSTOMIZATION NAME}' => $packageName, |
|
256 |
+ ]); |
|
257 |
+ |
|
258 |
+ $className = 'DevToolsFile' . mb_convert_case($this->getRequestProvider(), MB_CASE_TITLE, 'UTF-8') . mb_convert_case($this->getRequestedExtension(), MB_CASE_TITLE, 'UTF-8'); |
|
259 |
+ $handler = new $className; |
|
260 |
+ |
|
261 |
+ //Set our file directory and exclusions. Also cleanup before we do anything else. |
|
262 |
+ $handler |
|
263 |
+ ->setFileName($packageName . '.' . $this->getRequestedExtension()) |
|
264 |
+ ->setDirectory($this->getPackageBasedir($package)) |
|
265 |
+ ->setExclusions($exclusions) |
|
266 |
+ ->cleanupArchives() |
|
267 |
+ ; |
|
268 |
+ |
|
269 |
+ // Catch any error during generation and just show a standard error. |
|
270 |
+ try |
|
271 |
+ { |
|
272 |
+ $handler->generateArchive(); |
|
273 |
+ } |
|
274 |
+ catch (Exception $e) |
|
275 |
+ { |
|
276 |
+ $handler->cleanupArchives(); |
|
277 |
+ |
|
278 |
+ if (empty($this->db_show_debug)) |
|
279 |
+ fatal_lang_error('devtools_error_archive_generation', false); |
|
280 |
+ else |
|
281 |
+ fatal_error($this->dt->txt('devtools_error_archive_generation') . "<br>" . $e->getMessage() . '<br>' . $e->getFile() . ':' . $e->getLine(), false); |
|
282 |
+ } |
|
283 |
+ |
|
284 |
+ $handler->downloadArchive(); |
|
285 |
+ } |
|
286 |
+ |
|
287 |
+ /* |
|
288 |
+ * Get the requested extension, filtering the data in the reuqest for santity checks. |
|
289 |
+ * |
|
290 |
+ * @return string The provider. |
|
291 |
+ */ |
|
292 |
+ private function getRequestProvider(): string |
|
293 |
+ { |
|
294 |
+ return isset($_REQUEST['provider']) && in_array($_REQUEST['provider'], $this->providers) ? $_REQUEST['provider'] : $this->providers[0]; |
|
295 |
+ } |
|
296 |
+ |
|
297 |
+ /* |
|
298 |
+ * Get the requested extension, filtering the data in the reuqest for santity checks. |
|
299 |
+ * |
|
300 |
+ * @return string The extension. |
|
301 |
+ */ |
|
302 |
+ private function getRequestedExtension(): string |
|
303 |
+ { |
|
304 |
+ return isset($_REQUEST['extension']) && in_array($_REQUEST['extension'], $this->extensions) ? $_REQUEST['extension'] : $this->extensions[0]; |
|
305 |
+ } |
|
306 |
+ |
|
307 |
+ /* |
|
308 |
+ * Get the requested package, filtering the data in the reuqest for santity checks. |
|
309 |
+ * |
|
310 |
+ * @return string The cleaned package. |
|
311 |
+ */ |
|
312 |
+ private function getRequestedPackage(): string |
|
313 |
+ { |
|
314 |
+ return (string) preg_replace('~[^a-z0-9\-_\.]+~i', '-', $_REQUEST['package'] ?? ''); |
|
315 |
+ } |
|
316 |
+ |
|
317 |
+ /* |
|
318 |
+ * Tests whether this package is valid. Looks for the directory to exist in the packages folder. |
|
319 |
+ * |
|
320 |
+ * @param string $package A package name. |
|
321 |
+ * @return bool True if the directory exists, false otherwise. |
|
322 |
+ */ |
|
323 |
+ private function isValidPackage(string $package): bool |
|
324 |
+ { |
|
325 |
+ return is_dir($this->packagesdir . DIRECTORY_SEPARATOR . $package); |
|
326 |
+ } |
|
327 |
+ |
|
328 |
+ /* |
|
329 |
+ * This looks in a package and attempts to get the info file. SMF only normally supports it in the root directory. |
|
330 |
+ * This attempts to do a bit more work to find it. As such, SMF may not actually install and the sync logic may not work. |
|
331 |
+ * |
|
332 |
+ * @param string $package The package we are looking at. |
|
333 |
+ * @return string The path to the directory inside the package that contains the info file. |
|
334 |
+ */ |
|
335 |
+ private function getPackageBasedir(string $package): string |
|
336 |
+ { |
|
337 |
+ // Simple, its at the file root |
|
338 |
+ if (file_exists($this->packagesdir . DIRECTORY_SEPARATOR . $package . DIRECTORY_SEPARATOR . $this->packageInfoName)) |
|
339 |
+ return $this->packagesdir . DIRECTORY_SEPARATOR . $package; |
|
340 |
+ |
|
341 |
+ $files = new RecursiveIteratorIterator( |
|
342 |
+ new RecursiveDirectoryIterator( |
|
343 |
+ $this->packagesdir . DIRECTORY_SEPARATOR . $package, |
|
344 |
+ RecursiveDirectoryIterator::SKIP_DOTS |
|
345 |
+ ) |
|
346 |
+ ); |
|
347 |
+ |
|
348 |
+ // Someday we could simplify this? |
|
349 |
+ foreach ($files as $f) |
|
350 |
+ { |
|
351 |
+ if ($f->getFilename() == $this->packageInfoName) |
|
352 |
+ { |
|
353 |
+ return dirname($f->getPathName()); |
|
354 |
+ break; |
|
355 |
+ } |
|
356 |
+ } |
|
357 |
+ |
|
358 |
+ return ''; |
|
359 |
+ } |
|
360 |
+ |
|
361 |
+ /* |
|
362 |
+ * This will pass the info file through SMF's xmlArray object and returns a valid xmlArray we will use to parse it. |
|
363 |
+ * This uses SMF's xmlArray rather than the built in xml tools in PHP as it is what package manager is using. |
|
364 |
+ * |
|
365 |
+ * @calls: $sourcedir/Class-Package.php:xmlArray |
|
366 |
+ * @param string $packageInfoFile The info we are looking at. |
|
367 |
+ * @return xmlArray A valid object of xml data from the info file. |
|
368 |
+ */ |
|
369 |
+ private function getPackageInfo(string $packageInfoFile): xmlArray |
|
370 |
+ { |
|
371 |
+ return new xmlArray(file_get_contents($packageInfoFile)); |
|
372 |
+ } |
|
373 |
+ |
|
374 |
+ /* |
|
375 |
+ * Finds the valid devtools action for a customization. |
|
376 |
+ * Note: This will match <devtools>. |
|
377 |
+ * |
|
378 |
+ * @calls: $sourcedir/Class-Package.php:xmlArray |
|
379 |
+ * @calls: $sourcedir/Sub-Package.php:matchPackageVersion |
|
380 |
+ * @param xmlArray $packageXML A valid xmlArray object. |
|
381 |
+ * @return xmlArray A valid object of xml data from the info file, limited to the matched install actions. |
|
382 |
+ */ |
|
383 |
+ private function findDevTools(xmlArray $packageXML): ?xmlArray |
|
384 |
+ { |
|
385 |
+ return $packageXML->path('package-info[0]')->exists('devtools') ? $packageXML->path('package-info[0]')->set('devtools')[0] ?? null : null; |
|
386 |
+ } |
|
387 |
+ |
|
388 |
+ /* |
|
389 |
+ * Finds the valid package name for download. |
|
390 |
+ * Note: This will match <devtools>. |
|
391 |
+ * |
|
392 |
+ * @calls: $sourcedir/Class-Package.php:xmlArray |
|
393 |
+ * @calls: $sourcedir/Sub-Package.php:matchPackageVersion |
|
394 |
+ * @param xmlArray $devtoolsXML A valid xmlArray object. |
|
395 |
+ * @return xmlArray A valid object of xml data from the info file, limited to the matched install actions. |
|
396 |
+ */ |
|
397 |
+ private function findPackageName(xmlArray $devtoolsXML): ?string |
|
398 |
+ { |
|
399 |
+ $packageName = $devtoolsXML->fetch('packagename'); |
|
400 |
+ |
|
401 |
+ if (!empty($packageName)) |
|
402 |
+ return $packageName; |
|
403 |
+ |
|
404 |
+ return null; |
|
405 |
+ } |
|
406 |
+ |
|
407 |
+ private function defaultPackageName(string $package): string |
|
408 |
+ { |
|
409 |
+ return filter_var($package, FILTER_SANITIZE_URL); |
|
410 |
+ } |
|
411 |
+ |
|
412 |
+ /* |
|
413 |
+ * Finds the version. |
|
414 |
+ * Note: This will match <version>. |
|
415 |
+ * |
|
416 |
+ * @calls: $sourcedir/Class-Package.php:xmlArray |
|
417 |
+ * @calls: $sourcedir/Sub-Package.php:matchPackageVersion |
|
418 |
+ * @param xmlArray $packageXML A valid xmlArray object. |
|
419 |
+ * @return string The version we found. |
|
420 |
+ */ |
|
421 |
+ private function findPackageInfoVersion(xmlArray $packageXML): string |
|
422 |
+ { |
|
423 |
+ return $packageXML->path('package-info[0]')->exists('version') ? $packageXML->path('package-info[0]')->fetch('version') ?? '' : ''; |
|
424 |
+ } |
|
425 |
+ |
|
426 |
+ /* |
|
427 |
+ * Finds the name. |
|
428 |
+ * Note: This will match <name>. |
|
429 |
+ * |
|
430 |
+ * @calls: $sourcedir/Class-Package.php:xmlArray |
|
431 |
+ * @calls: $sourcedir/Sub-Package.php:matchPackageVersion |
|
432 |
+ * @param xmlArray $packageXML A valid xmlArray object. |
|
433 |
+ * @return string The Package Name |
|
434 |
+ */ |
|
435 |
+ private function findPackageInfoName(xmlArray $packageXML): string |
|
436 |
+ { |
|
437 |
+ return $packageXML->path('package-info[0]')->exists('name') ? $packageXML->path('package-info[0]')->fetch('name') ?? '' : ''; |
|
438 |
+ } |
|
439 |
+ |
|
440 |
+ /* |
|
441 |
+ * Finds the valid exclusions for packaging. |
|
442 |
+ * Note: This will match <devtools>. |
|
443 |
+ * |
|
444 |
+ * @calls: $sourcedir/Class-Package.php:xmlArray |
|
445 |
+ * @calls: $sourcedir/Sub-Package.php:matchPackageVersion |
|
446 |
+ * @param xmlArray $devtoolsXML A valid xmlArray object. |
|
447 |
+ * @return xmlArray A valid object of xml data from the info file, limited to the matched install actions. |
|
448 |
+ */ |
|
449 |
+ private function findExclusions(xmlArray $devtoolsXML): array |
|
450 |
+ { |
|
451 |
+ $excludes = []; |
|
452 |
+ |
|
453 |
+ if ($devtoolsXML->exists('exclusion')) |
|
454 |
+ { |
|
455 |
+ $exs = $devtoolsXML->set('exclusion'); |
|
456 |
+ foreach ($exs as $ex) |
|
457 |
+ $excludes[] = $ex->fetch(''); |
|
458 |
+ } |
|
459 |
+ |
|
460 |
+ return $excludes; |
|
461 |
+ } |
|
462 |
+ |
|
463 |
+ /* |
|
464 |
+ * This checks if our success message is valid, if so we can use that text string, otherwise we use a generic message. |
|
465 |
+ * |
|
466 |
+ * @param string $action The success action we took |
|
467 |
+ * @return string The language string we will use on our succcess message. |
|
468 |
+ */ |
|
469 |
+ private function successMsg(string $action): string |
|
470 |
+ { |
|
471 |
+ return in_array($action, ['package']) ? 'devtools_success_' . $action : 'settings_saved'; |
|
472 |
+ } |
|
473 |
+ |
|
474 |
+ /* |
|
475 |
+ * ParsePath from SMF, but wrap it incase we need to do cleanup. |
|
476 |
+ * |
|
477 |
+ * @calls: $sourcedir/Subs-Package.php:parse_path |
|
478 |
+ * @param string $p The current path. |
|
479 |
+ * @return string A parsed parse with a valid directory. |
|
480 |
+ */ |
|
481 |
+ private function parsePath(string $p): string |
|
482 |
+ { |
|
483 |
+ return parse_path($p); |
|
484 |
+ } |
|
485 |
+ |
|
486 |
+ /* |
|
487 |
+ * SMF will cache package directory information. This disables it so we can work with the data without delays. |
|
488 |
+ */ |
|
489 |
+ private function disablePackageCache(): void |
|
490 |
+ { |
|
491 |
+ $this->package_cache = false; |
|
492 |
+ $this->modSettings['package_disable_cache'] = true; |
|
493 |
+ } |
|
494 |
+ |
|
495 |
+ /* |
|
496 |
+ * This is currently unused and a place holder for possible expansion to using the operating systems |
|
497 |
+ * built in zip/tar utilties to comrpess files. |
|
498 |
+ */ |
|
499 |
+ private function haveSystemSupport(): bool |
|
500 |
+ { |
|
501 |
+ if (isset($_SESSION['devToolsFile-haveSystemSupport'])) |
|
502 |
+ return (bool) $_SESSION['devToolsFile-haveSystemSupport']; |
|
503 |
+ |
|
504 |
+ $hasSystemSupport = true; |
|
505 |
+ |
|
506 |
+ // We need shell exec. |
|
507 |
+ if (!function_exists('exec')) |
|
508 |
+ $hasSystemSupport = false; |
|
509 |
+ |
|
510 |
+ $output = null; |
|
511 |
+ $result_code = null; |
|
512 |
+ if ($hasSystemSupport) |
|
513 |
+ { |
|
514 |
+ exec('command -v zip', $output, $result_code); |
|
515 |
+ if ($result_code != 0 || empty($output)) |
|
516 |
+ $hasSystemSupport = false; |
|
517 |
+ } |
|
518 |
+ |
|
519 |
+ $output = null; |
|
520 |
+ $result_code = null; |
|
521 |
+ if ($hasSystemSupport) |
|
522 |
+ { |
|
523 |
+ exec('command -v tar', $output, $result_code); |
|
524 |
+ if ($result_code != 0 || empty($output)) |
|
525 |
+ $hasSystemSupport = false; |
|
526 |
+ } |
|
527 |
+ |
|
528 |
+ $_SESSION['devToolsFile-haveSystemSupport'] = $hasSystemSupport; |
|
529 |
+ return $hasSystemSupport; |
|
530 |
+ } |
|
531 |
+} |
|
0 | 532 |
\ No newline at end of file |
... | ... |
@@ -4,9 +4,9 @@ |
4 | 4 |
* The class for DevTools Packages. |
5 | 5 |
* @package DevTools |
6 | 6 |
* @author SleePy <sleepy @ simplemachines (dot) org> |
7 |
- * @copyright 2022 |
|
7 |
+ * @copyright 2023 |
|
8 | 8 |
* @license 3-Clause BSD https://opensource.org/licenses/BSD-3-Clause |
9 |
- * @version 1.0 |
|
9 |
+ * @version 1.1 |
|
10 | 10 |
*/ |
11 | 11 |
class DevToolsPackages |
12 | 12 |
{ |
... | ... |
@@ -88,7 +88,7 @@ class DevToolsPackages |
88 | 88 |
else if (($basedir = $this->getPackageBasedir($package)) == '') |
89 | 89 |
fatal_lang_error('package_get_error_not_found', false); |
90 | 90 |
|
91 |
- $infoFile = $this->getPackageInfo($basedir . '/' . $this->packageInfoName); |
|
91 |
+ $infoFile = $this->getPackageInfo($basedir . DIRECTORY_SEPARATOR . $this->packageInfoName); |
|
92 | 92 |
if (!is_a($infoFile, 'xmlArray')) |
93 | 93 |
fatal_lang_error('package_get_error_missing_xml', false); |
94 | 94 |
|
... | ... |
@@ -120,7 +120,7 @@ class DevToolsPackages |
120 | 120 |
else if (($basedir = $this->getPackageBasedir($package)) == '') |
121 | 121 |
fatal_lang_error('package_get_error_not_found', false); |
122 | 122 |
|
123 |
- $infoFile = $this->getPackageInfo($basedir . '/' . $this->packageInfoName); |
|
123 |
+ $infoFile = $this->getPackageInfo($basedir . DIRECTORY_SEPARATOR . $this->packageInfoName); |
|
124 | 124 |
if (!is_a($infoFile, 'xmlArray')) |
125 | 125 |
fatal_lang_error('package_get_error_missing_xml', false); |
126 | 126 |
|
... | ... |
@@ -153,7 +153,7 @@ class DevToolsPackages |
153 | 153 |
else if (($basedir = $this->getPackageBasedir($package)) == '') |
154 | 154 |
fatal_lang_error('package_get_error_not_found', false); |
155 | 155 |
|
156 |
- $infoFile = $this->getPackageInfo($basedir . '/' . $this->packageInfoName); |
|
156 |
+ $infoFile = $this->getPackageInfo($basedir . DIRECTORY_SEPARATOR . $this->packageInfoName); |
|
157 | 157 |
if (!is_a($infoFile, 'xmlArray')) |
158 | 158 |
fatal_lang_error('package_get_error_missing_xml', false); |
159 | 159 |
|
... | ... |
@@ -195,7 +195,7 @@ class DevToolsPackages |
195 | 195 |
else if (($basedir = $this->getPackageBasedir($package)) == '') |
196 | 196 |
fatal_lang_error('package_get_error_not_found', false); |
197 | 197 |
|
198 |
- $infoFile = $this->getPackageInfo($basedir . '/' . $this->packageInfoName); |
|
198 |
+ $infoFile = $this->getPackageInfo($basedir . DIRECTORY_SEPARATOR . $this->packageInfoName); |
|
199 | 199 |
if (!is_a($infoFile, 'xmlArray')) |
200 | 200 |
fatal_lang_error('package_get_error_missing_xml', false); |
201 | 201 |
|
... | ... |
@@ -223,6 +223,8 @@ class DevToolsPackages |
223 | 223 |
|
224 | 224 |
/* |
225 | 225 |
* Returns an array that will be passed into SMF's createList logic to build a packages listing. |
226 |
+ * |
|
227 |
+ * @calls: $sourcedir/Subs.php:timeformat |
|
226 | 228 |
*/ |
227 | 229 |
private function buildPackagesList(): array |
228 | 230 |
{ |
... | ... |
@@ -432,7 +434,7 @@ class DevToolsPackages |
432 | 434 |
*/ |
433 | 435 |
private function getRequestedPackage(): string |
434 | 436 |
{ |
435 |
- return (string) preg_replace('~[^a-z0-9\-_\.]+~i', '-', $_REQUEST['package']); |
|
437 |
+ return (string) preg_replace('~[^a-z0-9\-_\.]+~i', '-', $_REQUEST['package'] ?? ''); |
|
436 | 438 |
} |
437 | 439 |
|
438 | 440 |
/* |
... | ... |
@@ -443,7 +445,7 @@ class DevToolsPackages |
443 | 445 |
*/ |
444 | 446 |
private function isValidPackage(string $package): bool |
445 | 447 |
{ |
446 |
- return is_dir($this->packagesdir . '/' . $package); |
|
448 |
+ return is_dir($this->packagesdir . DIRECTORY_SEPARATOR . $package); |
|
447 | 449 |
} |
448 | 450 |
|
449 | 451 |
/* |
... | ... |
@@ -456,12 +458,12 @@ class DevToolsPackages |
456 | 458 |
private function getPackageBasedir(string $package): string |
457 | 459 |
{ |
458 | 460 |
// Simple, its at the file root |
459 |
- if (file_exists($this->packagesdir . '/' . $package . '/' . $this->packageInfoName)) |
|
460 |
- return $this->packagesdir . '/' . $package; |
|
461 |
+ if (file_exists($this->packagesdir . DIRECTORY_SEPARATOR . $package . DIRECTORY_SEPARATOR . $this->packageInfoName)) |
|
462 |
+ return $this->packagesdir . DIRECTORY_SEPARATOR . $package; |
|
461 | 463 |
|
462 | 464 |
$files = new RecursiveIteratorIterator( |
463 | 465 |
new RecursiveDirectoryIterator( |
464 |
- $this->packagesdir . '/' . $package, |
|
466 |
+ $this->packagesdir . DIRECTORY_SEPARATOR . $package, |
|
465 | 467 |
RecursiveDirectoryIterator::SKIP_DOTS |
466 | 468 |
) |
467 | 469 |
); |
... | ... |
@@ -567,8 +569,8 @@ class DevToolsPackages |
567 | 569 |
continue; |
568 | 570 |
|
569 | 571 |
$hooks[] = [ |
570 |
- 'pkg' => $action->exists('@from') ? $this->parsePath($action->fetch('@from')) : $basedir . '/' . $action->fetch('@name'), |
|
571 |
- 'smf' => $this->parsePath($action->fetch('@destination')) . '/' . basename($action->fetch('@name')) |
|
572 |
+ 'pkg' => $action->exists('@from') ? $this->parsePath($action->fetch('@from')) : $basedir . DIRECTORY_SEPARATOR . $action->fetch('@name'), |
|
573 |
+ 'smf' => $this->parsePath($action->fetch('@destination')) . DIRECTORY_SEPARATOR . basename($action->fetch('@name')) |
|
572 | 574 |
]; |
573 | 575 |
} |
574 | 576 |
|
... | ... |
@@ -607,6 +609,8 @@ class DevToolsPackages |
607 | 609 |
// Do a empty file check. |
608 | 610 |
if (!$op['res'] && is_file($op[$dst]) && package_get_contents($op[$src]) == package_get_contents($op[$dst])) |
609 | 611 |
$op['res'] = true; |
612 |
+ elseif (is_dir($op[$src]) && is_dir($op[$dst])) |
|
613 |
+ $op['res'] = $this->validateDirectoriesAreEqual($op[$src], $op[$dst]); |
|
610 | 614 |
|
611 | 615 |
return $op; |
612 | 616 |
}, $ops); |
... | ... |
@@ -692,7 +696,7 @@ class DevToolsPackages |
692 | 696 |
private function cleanPath(string $path): string |
693 | 697 |
{ |
694 | 698 |
return strtr($path, [ |
695 |
- $this->settings['default_theme_dir'] . '/' . basename($GLOBALS['settings']['default_images_url']) => '$imagesdir', |
|
699 |
+ $this->settings['default_theme_dir'] . DIRECTORY_SEPARATOR . basename($GLOBALS['settings']['default_images_url']) => '$imagesdir', |
|
696 | 700 |
$this->settings['default_theme_dir'] . '/languages' => '$languagedir', |
697 | 701 |
$this->settings['default_theme_dir'] => '$themedir', |
698 | 702 |
$this->modSettings['avatar_directory'] => '$avatardir', |
... | ... |
@@ -703,4 +707,39 @@ class DevToolsPackages |
703 | 707 |
$this->boarddir => '$boarddir', |
704 | 708 |
]); |
705 | 709 |
} |
710 |
+ |
|
711 |
+ /* |
|
712 |
+ * Compare two directories to see if they appear consistent. |
|
713 |
+ * We do this by reading them, finding their sha1_file, json_encode the array and then sha1 that string. |
|
714 |
+ * By comparing two directories this way, we should end up with the same sha1 hash. |
|
715 |
+ * |
|
716 |
+ * @param string $src Source directory to compare. |
|
717 |
+ * @param string $dst Destination directory to compare. |
|
718 |
+ * @return bool True if they match, false otherwise. |
|
719 |
+ */ |
|
720 |
+ private function validateDirectoriesAreEqual(string $src, string $dst): bool |
|
721 |
+ { |
|
722 |
+ $srcFiles = $dstFiles = []; |
|
723 |
+ |
|
724 |
+ // Get our files. |
|
725 |
+ foreach (['src', 'dst'] as $op) |
|
726 |
+ { |
|
727 |
+ $s = new RecursiveIteratorIterator( |
|
728 |
+ new RecursiveDirectoryIterator( |
|
729 |
+ $$op, |
|
730 |
+ RecursiveDirectoryIterator::SKIP_DOTS |
|
731 |
+ ), |
|
732 |
+ ); |
|
733 |
+ |
|
734 |
+ foreach ($s as $file) |
|
735 |
+ { |
|
736 |
+ if ($file->isDir()) |
|
737 |
+ return true; |
|
738 |
+ $basePath = substr($file->getPathname(), strlen($$op . DIRECTORY_SEPARATOR), null); |
|
739 |
+ ${$op . 'Files'}[$basePath] = sha1_file($file->getPathname()); |
|
740 |
+ } |
|
741 |
+ } |
|
742 |
+ |
|
743 |
+ return sha1(json_encode($srcFiles)) == sha1(json_encode($dstFiles)); |
|
744 |
+ } |
|
706 | 745 |
} |
707 | 746 |
\ No newline at end of file |
... | ... |
@@ -5,13 +5,10 @@ This gives a popup window for you to work with a package to do actions such as: |
5 | 5 |
- Remove hooks (removes hooks as defined in the packages uninstall action |
6 | 6 |
- Pushes files out as per the packages install action |
7 | 7 |
- Pulls files in as per the packages install action |
8 |
+ - Compress customization into tgz (tar with gzip) and zip |
|
8 | 9 |
|
9 | 10 |
This is intended for development purposes, not production uses. |
10 | 11 |
This customization is intended to only be used with customizations that do not modify SMF sources (boardmod or xml) and are hook only. |
11 | 12 |
To use this, your customization must be in the folder format, not in a compressed archive (.tar.gz or .zip) inside the Packages folder. |
12 | 13 |
Extended information on how to use this tool can be found here: https://github.com/jdarwood007/smfmod_devtools/wiki |
13 | 14 |
|
14 |
-Want some additional developer tools? |
|
15 |
- - Error Log Popup: https://custom.simplemachines.org/index.php?mod=4323 |
|
16 |
- - Dev Center: https://custom.simplemachines.org/index.php?mod=3481 |
|
17 |
- - Dev tools category: https://custom.simplemachines.org/index.php?action=mods;id_type=25 |
|
18 | 15 |
\ No newline at end of file |
... | ... |
@@ -1,5 +1,6 @@ |
1 | 1 |
<?php |
2 | 2 |
$txt['devtools_menu'] = 'Developer Tools'; |
3 |
+$txt['files_title_list'] = 'Archives'; |
|
3 | 4 |
|
4 | 5 |
/* Packages stuff */ |
5 | 6 |
$txt['devtools_packages_uninstall'] = 'Uninstall Hooks'; |
... | ... |
@@ -21,3 +20,7 @@ $txt['devtools_success_toggle'] = 'Succesfully toggled hook'; |
21 | 20 |
$txt['devtools_success_addhook'] = 'Succesfully added hook'; |
22 | 21 |
$txt['devtools_success_modifyhook'] = 'Succesfully modified hook'; |
23 | 22 |
$txt['devtools_success_deletehook'] = 'Succesfully deleted hook'; |
23 |
+ |
|
24 |
+$txt['devtools_error_archive_generation'] = 'Unable to generate archive'; |
|
25 |
+$txt['devtools_extension_zip'] = 'ZIP'; |
|
26 |
+$txt['devtools_extension_tgz'] = 'TGZ'; |
|
24 | 27 |
\ No newline at end of file |
... | ... |
@@ -2,11 +2,19 @@ |
2 | 2 |
<package-info xmlns="http://www.simplemachines.org/xml/package-info" xmlns:smf="http://www.simplemachines.org/"> |
3 | 3 |
<id>sleepy:devtools</id> |
4 | 4 |
<name>Developer Tools</name> |
5 |
- <version>1.0.3</version> |
|
5 |
+ <version>1.1</version> |
|
6 | 6 |
<type>modification</type> |
7 | 7 |
|
8 |
- <upgrade from="1.0.2" for="2.1.*"> |
|
9 |
- <require-file name="DevTools-Packages.php" destination="$sourcedir" /> |
|
8 |
+ <upgrade from="1.0.*" for="2.1.*"> |
|
9 |
+ <require-file name="DevTools.php" destination="$sourcedir" /> |
|
10 |
+ <require-dir name="DevTools" destination="$sourcedir" /> |
|
11 |
+ <remove-file name="$sourcedir/DevTools-Packages.php" error="skip" /> |
|
12 |
+ <remove-file name="$sourcedir/DevTools-Hooks.php" error="skip" /> |
|
13 |
+ |
|
14 |
+ <require-file name="DevTools.template.php" destination="$themedir" /> |
|
15 |
+ <require-file name="DevTools.js" destination="$themes_dir/default/scripts" /> |
|
16 |
+ |
|
17 |
+ <require-file name="languages/DevTools.english.php" destination="$themes_dir/default/languages" /> |
|
10 | 18 |
<require-file name="languages/DevTools.spanish_es.php" destination="$themes_dir/default/languages" /> |
11 | 19 |
<require-file name="languages/DevTools.spanish_latin.php" destination="$themes_dir/default/languages" /> |
12 | 20 |
<require-file name="languages/DevTools.russian.php" destination="$themes_dir/default/languages" /> |
... | ... |
@@ -15,11 +23,9 @@ |
15 | 23 |
<install for="SMF 2.1.*"> |
16 | 24 |
<readme>README.txt</readme> |
17 | 25 |
<require-file name="DevTools.php" destination="$sourcedir" /> |
18 |
- <require-file name="DevTools-Packages.php" destination="$sourcedir" /> |
|
19 |
- <require-file name="DevTools-Hooks.php" destination="$sourcedir" /> |
|
26 |
+ <require-dir name="DevTools" destination="$sourcedir" /> |
|
20 | 27 |
|
21 | 28 |
<require-file name="DevTools.template.php" destination="$themedir" /> |
22 |
- |
|
23 | 29 |
<require-file name="DevTools.js" destination="$themes_dir/default/scripts" /> |
24 | 30 |
|
25 | 31 |
<require-file name="languages/DevTools.english.php" destination="$themes_dir/default/languages" /> |
... | ... |
@@ -46,13 +52,10 @@ |
46 | 52 |
<remove-file name="$themes_dir/default/languages/DevTools.spanish_latin.php" /> |
47 | 53 |
<remove-file name="$themes_dir/default/languages/DevTools.russian.php" /> |
48 | 54 |
|
49 |
- <remove-file name="$themes_dir/default/scripts/DevTools.js" /> |
|
50 |
- |
|
51 | 55 |
<remove-file name="$themedir/DevTools.template.php" /> |
56 |
+ <remove-file name="$themes_dir/default/scripts/DevTools.js" /> |
|
52 | 57 |
|
53 | 58 |
<remove-file name="$sourcedir/DevTools.php" /> |
54 |
- <remove-file name="$sourcedir/DevTools-Packages.php" /> |
|
55 |
- <remove-file name="$sourcedir/DevTools-Hooks.php" /> |
|
59 |
+ <remove-dir destination="$sourcedir/DevTools" /> |
|
56 | 60 |
</uninstall> |
57 |
- |
|
58 | 61 |
</package-info> |
59 | 62 |
\ No newline at end of file |
60 | 63 |