Initial commit of devtools package
Jeremy D

Jeremy D commited on 2022-08-19 19:11:22
Showing 19 changed files, with 2639 additions and 0 deletions.

... ...
@@ -0,0 +1,34 @@
1
+module.exports = {
2
+	'env': {
3
+		'browser': true,
4
+		'es2021': true,
5
+		'jquery': true
6
+	},
7
+	'extends': 'eslint:recommended',
8
+	'parserOptions': {
9
+		'ecmaVersion': 12,
10
+		'sourceType': 'module'
11
+	},
12
+	'rules': {
13
+		'indent': [
14
+			'error',
15
+			'tab',
16
+			{"SwitchCase": 1}
17
+		],
18
+		'linebreak-style': [
19
+			'error',
20
+			'unix'
21
+		],
22
+		'quotes': [
23
+			'error',
24
+			'single'
25
+		],
26
+		'no-unused-vars': [
27
+			'error',
28
+			{
29
+				'vars': 'local',
30
+				'args' : 'none'
31
+			}
32
+		]
33
+	}
34
+};
... ...
@@ -0,0 +1,32 @@
1
+<?php
2
+// Stuff we will ignore.
3
+$ignoreFiles = array(
4
+	'\.github/',
5
+);
6
+
7
+$curDir = '.';
8
+if (isset($_SERVER['argv'], $_SERVER['argv'][1]))
9
+	$curDir = $_SERVER['argv'][1];
10
+
11
+$foundBad = false;
12
+foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($curDir, FilesystemIterator::UNIX_PATHS)) as $currentFile => $fileInfo)
13
+{
14
+	// Only check PHP
15
+	if ($fileInfo->getExtension() !== 'php')
16
+		continue;
17
+
18
+	foreach ($ignoreFiles as $if)
19
+		if (preg_match('~' . $if . '~i', $currentFile))
20
+			continue 2;
21
+
22
+	$result = trim(shell_exec('php .github/scripts/check-eof.php ' . $currentFile . ' 2>&1'));
23
+
24
+	if (!preg_match('~Error:([^$]+)~', $result))
25
+		continue;
26
+
27
+	$foundBad = true;
28
+	fwrite(STDERR, $result . "\n");
29
+}
30
+
31
+if (!empty($foundBad))
32
+	exit(1);
0 33
\ No newline at end of file
... ...
@@ -0,0 +1,50 @@
1
+<?php
2
+// Stuff we will ignore.
3
+$ignoreFiles = array(
4
+	'\.github/',
5
+);
6
+
7
+// No file? Thats bad.
8
+if (!isset($_SERVER['argv'], $_SERVER['argv'][1]))
9
+	fatalError('Error: No File specified' . "\n");
10
+
11
+// The file has to exist.
12
+$currentFile = $_SERVER['argv'][1];
13
+if (!file_exists($currentFile))
14
+	fatalError('Error: File does not exist' . "\n");
15
+
16
+// Is this ignored?
17
+foreach ($ignoreFiles as $if)
18
+	if (preg_match('~' . $if . '~i', $currentFile))
19
+		die;
20
+
21
+// Less efficent than opening a file with fopen, but we want to be sure to get the right end of the file. file_get_contents
22
+$file = fopen($currentFile, 'r');
23
+
24
+// Error?
25
+if ($file === false)
26
+	fatalError('Error: Unable to open file ' . $currentFile . "\n");
27
+
28
+// Seek the end minus some bytes.
29
+fseek($file, -100, SEEK_END);
30
+$contents = fread($file, 100);
31
+
32
+// There is some white space here.
33
+if (preg_match('~}\s+$~', $contents, $matches))
34
+	fatalError('Error: End of File contains extra spaces in ' . $currentFile . "\n");
35
+// It exists! Leave.
36
+elseif (preg_match('~}$~', $contents, $matches))
37
+	die();
38
+
39
+// There is some white space here.
40
+if (preg_match('~\';\s+$~', $contents, $matches))
41
+	fatalError('Error: End of File Strings contains extra spaces in ' . $currentFile . "\n");
42
+// It exists! Leave.
43
+elseif (preg_match('~\';$~', $contents, $matches))
44
+	die();
45
+
46
+function fatalError($msg)
47
+{
48
+	fwrite(STDERR, $msg);
49
+	die;
50
+}
0 51
\ No newline at end of file
... ...
@@ -0,0 +1,54 @@
1
+<?php
2
+// Stuff we will ignore.
3
+$ignoreFiles = [];
4
+
5
+/* This is mostly meant for local usage.
6
+   To add additional PHP Binaries, create a check-php-syntax-binaries.txt
7
+   Add in this in each line the binary file, i.e: /usr/bin/php
8
+*/
9
+$addditionalPHPBinaries = [];
10
+if (file_exists(dirname(__FILE__) . '/check-php-syntax-binaries.txt'))
11
+	$addditionalPHPBinaries = file(dirname(__FILE__) . '/check-php-syntax-binaries.txt');
12
+
13
+$curDir = '.';
14
+if (isset($_SERVER['argv'], $_SERVER['argv'][1]))
15
+	$curDir = $_SERVER['argv'][1];
16
+
17
+$foundBad = false;
18
+foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($curDir, FilesystemIterator::UNIX_PATHS)) as $currentFile => $fileInfo)
19
+{
20
+	// Only check PHP
21
+	if ($fileInfo->getExtension() !== 'php')
22
+		continue;
23
+
24
+	foreach ($ignoreFiles as $if)
25
+		if (preg_match('~' . $if . '~i', $currentFile))
26
+			continue 2;
27
+
28
+	# Always check against the base.
29
+	$result = trim(shell_exec('php -l ' . $currentFile));
30
+
31
+	if (!preg_match('~No syntax errors detected in ' . $currentFile . '~', $result))
32
+	{
33
+		$foundBad = true;
34
+		fwrite(STDERR, 'PHP via $PATH: ' . $result . "\n");
35
+		continue;
36
+	}
37
+
38
+	// We have additional binaries we want to test against?
39
+	foreach ($addditionalPHPBinaries as $binary)
40
+	{
41
+		$binary = trim($binary);
42
+		$result = trim(shell_exec($binary . ' -l ' . $currentFile));
43
+
44
+		if (!preg_match('~No syntax errors detected in ' . $currentFile . '~', $result))
45
+		{
46
+			$foundBad = true;
47
+			fwrite(STDERR, 'PHP via ' . $binary . ': ' . $result . "\n");
48
+			continue 2;
49
+		}
50
+	}
51
+}
52
+
53
+if (!empty($foundBad))
54
+	exit(1);
0 55
\ No newline at end of file
... ...
@@ -0,0 +1,187 @@
1
+<?php
2
+
3
+// Debug stuff.
4
+global $debugMsgs, $debugMode;
5
+
6
+// Debug?
7
+if (isset($_SERVER['argv'], $_SERVER['argv'][2]) && $_SERVER['argv'][2] == 'debug')
8
+	$debugMode = true;
9
+
10
+// First, lets do a basic test.  This is non GPG signed commits.
11
+$signedoff = find_signed_off();
12
+
13
+// Now Try to test for the GPG if we don't have a message.
14
+if (empty($signedoff))
15
+	$signedoff = find_gpg();
16
+
17
+// Nothing yet?  Lets ask your parents.
18
+if (empty($signedoff) && isset($_SERVER['argv'], $_SERVER['argv'][1]) && ($_SERVER['argv'][1] == 'travis' || $_SERVER['argv'][1] == 'github'))
19
+	$signedoff = find_signed_off_parents();
20
+
21
+// Nothing?  Well darn.
22
+if (empty($signedoff))
23
+{
24
+	// Debugging, eh?
25
+	if ($debugMode)
26
+	{
27
+		echo "\n---DEBUG MSGS START ---\n";
28
+		var_dump($debugMsgs);
29
+		echo "\n---DEBUG MSGS END ---\n";
30
+	}
31
+
32
+	fatalError('Error: Signed-off-by not found in commit message' . "\n");
33
+}
34
+elseif ($debugMode)
35
+	debugPrint('Valid signed off found' . "\n");
36
+
37
+// Find a commit by Signed Off
38
+function find_signed_off($commit = 'HEAD', $childs = array(), $level = 0)
39
+{
40
+	global $debugMsgs;
41
+
42
+	$commit = trim($commit);
43
+
44
+	// Where we are at.
45
+	debugPrint('Attempting to Find signed off on commit [' . $commit . ']');
46
+
47
+	// To many recrusions here.
48
+	if ($level > 10)
49
+	{
50
+		$debugMsgs[$commit . ':' . time()] = array('error' => 'Recurision limit');
51
+		debugPrint('Recusion limit exceeded on find_signed_off');
52
+		return false;
53
+	}
54
+
55
+	// What string tests should we look for?
56
+	$stringTests = array('Signed-off-by:', 'Signed by');
57
+
58
+	// Get message data and clean it up, should only need the last line.
59
+	$message = trim(shell_exec('git show -s --format=%B ' . $commit));
60
+	$lines = explode("\n", trim(str_replace("\r", "\n", $message)));
61
+	$lastLine = $lines[count($lines) - 1];
62
+
63
+	// Debug info.
64
+	debugPrint('Testing Line [' . $lastLine . ']');
65
+
66
+	// loop through each test and find one.
67
+	$testedString = $result = false;
68
+	foreach ($stringTests as $testedString)
69
+	{
70
+		debugPrint('Testing [' . $testedString . ']');
71
+
72
+		$result = stripos($lastLine, $testedString);
73
+
74
+		// We got a result.
75
+		if ($result !== false)
76
+		{
77
+			debugPrint('Found Result [' . $testedString . ']');
78
+			break;
79
+		}
80
+	}
81
+
82
+	// Debugger.
83
+	$debugMsgs[$commit . ':' . time()] = array(
84
+		// Raw body.
85
+		'B' => shell_exec('git show -s --format=%B ' . $commit),
86
+		// Body.
87
+		'b2' => shell_exec('git show -s --format=%b ' . $commit),
88
+		// Commit notes.
89
+		'N' => shell_exec('git show -s --format=%N ' . $commit),
90
+		// Ref names.
91
+		'd' => shell_exec('git show -s --format=%d ' . $commit),
92
+		// Commit hash.
93
+		'H' => shell_exec('git show -s --format=%H ' . $commit),
94
+		// Tree hash.
95
+		'T' => shell_exec('git show -s --format=%T ' . $commit),
96
+		// Parent hash.
97
+		'P' => shell_exec('git show -s --format=%P ' . $commit),
98
+		// Result.
99
+		'result' => $result,
100
+		// Last tested string, or the correct string.
101
+		'testedString' => $testedString,
102
+	);
103
+
104
+	// No result and found a merge? Lets go deeper.
105
+	if ($result === false && preg_match('~Merge ([A-Za-z0-9]{40}) into ([A-Za-z0-9]{40})~i', $lastLine, $merges))
106
+	{
107
+		debugPrint('Found Merge, attempting to get more parent commit: ' . $merges[1]);
108
+
109
+		return find_signed_off($merges[1], array_merge(array($merges[1]), $childs), ++$level);
110
+	}
111
+
112
+	return $result !== false;
113
+}
114
+
115
+// Find a commit by GPG
116
+function find_gpg($commit = 'HEAD')
117
+{
118
+	global $debugMsgs;
119
+
120
+	$commit = trim($commit);
121
+
122
+	debugPrint('Attempting to Find GPG on commit [' . $commit . ']');
123
+
124
+	// Get verify commit data.
125
+	$message = trim(shell_exec('git verify-commit ' . $commit . ' -v --raw'));
126
+
127
+	// Should we actually test for gpg results?  Perhaps, but it seems doing that with travis may fail since it has no way to verify a GPG signature from GitHub.  GitHub should have prevented a bad GPG from making a commit to a authors repository and could be trusted in most cases it seems.
128
+	$result = strlen($message) > 0;
129
+
130
+	// Debugger.
131
+	$debugMsgs[$commit . ':' . time()] = array(
132
+		// Raw body.
133
+		'verify-commit' => shell_exec('git verify-commit ' . $commit . ' -v --raw'),
134
+		// Result.
135
+		'result' => $result,
136
+		// Last tested string, or the correct string.
137
+		'message' => $message,
138
+	);
139
+
140
+	return $result;
141
+}
142
+
143
+// Looks at all the parents, and tries to find a signed off by somewhere.
144
+function find_signed_off_parents($commit = 'HEAD')
145
+{
146
+	$commit = trim($commit);
147
+
148
+	debugPrint('Attempting to find parents on commit [' . $commit . ']');
149
+
150
+	$parentsRaw = shell_exec('git show -s --format=%P ' . $commit);
151
+	$parents = explode(' ', $parentsRaw);
152
+
153
+	// Test each one.
154
+	foreach ($parents as $p)
155
+	{
156
+		$p = trim($p);
157
+		debugPrint('Testing Parent for signed off [' . $commit . ']');
158
+
159
+		// Basic tests.
160
+		$test = find_signed_off($p);
161
+
162
+		// No, maybe it has a GPG parent.
163
+		if (empty($test))
164
+			$test = find_gpg($p);
165
+
166
+		if (!empty($test))
167
+			return $test;
168
+	}
169
+
170
+	// Lucked out.
171
+	return false;
172
+}
173
+
174
+// Print a debug line
175
+function debugPrint($msg)
176
+{
177
+	global $debugMode;
178
+
179
+	if ($debugMode)
180
+		echo "\nDEBUG: ", $msg;
181
+}
182
+
183
+function fatalError($msg)
184
+{
185
+	fwrite(STDERR, $msg . "\n");
186
+	die;
187
+}
0 188
\ No newline at end of file
... ...
@@ -0,0 +1,21 @@
1
+name: Javascript Checks
2
+
3
+on:
4
+  push:
5
+    branches: [ master ]
6
+  pull_request:
7
+    branches: [ master ]
8
+
9
+  workflow_dispatch:
10
+jobs:          
11
+  lint:
12
+    runs-on: ubuntu-latest
13
+    name: LINT Checks
14
+    steps:
15
+      - uses: actions/checkout@master
16
+        with:
17
+          submodules: true
18
+      - name: Javascript LINT
19
+        uses: tj-actions/eslint-changed-files@v4
20
+        with:
21
+          config-path: .github/eslintrc.js
0 22
\ No newline at end of file
... ...
@@ -0,0 +1,30 @@
1
+name: Software Checks
2
+
3
+on:
4
+  push:
5
+    branches: [ master ]
6
+  pull_request:
7
+    branches: [ master ]
8
+
9
+  workflow_dispatch:
10
+jobs:          
11
+  check-signedoff:
12
+    runs-on: ubuntu-latest
13
+    name: Check Signed Off
14
+    steps:
15
+      - uses: actions/checkout@master
16
+        with:
17
+          submodules: true
18
+      - name: Checking Sign off
19
+        id: check-signoff
20
+        run: php ./.github/scripts/check-signed-off.php github
21
+  check-eof:
22
+    runs-on: ubuntu-latest
23
+    name: Check End of File
24
+    steps:
25
+      - uses: actions/checkout@master
26
+        with:
27
+          submodules: true
28
+      - name: Checking End of File
29
+        id: check-eof
30
+        run: php ./.github/scripts/check-eof-master.php ./
0 31
\ No newline at end of file
... ...
@@ -0,0 +1,27 @@
1
+name: PHP Syntax Check
2
+
3
+on:
4
+  push:
5
+    branches: [ master ]
6
+  pull_request:
7
+    branches: [ master ]
8
+
9
+  workflow_dispatch:
10
+jobs:
11
+  syntax-checker:
12
+    runs-on: ${{ matrix.operating-system }}
13
+    strategy:
14
+      matrix:
15
+        operating-system: [ ubuntu-latest ]
16
+        php: [ '7.4', '8.0', '8.1' ]
17
+    name: PHP ${{ matrix.php }} Syntax Check
18
+    steps:
19
+      - uses: actions/checkout@master
20
+        with:
21
+          submodules: true
22
+      - name: Setup PHP
23
+        id: SetupPHP
24
+        uses: nanasess/setup-php@master
25
+        with:
26
+          php-version: ${{ matrix.php }}
27
+      - run: php ./.github/scripts/check-php-syntax.php ./
0 28
\ No newline at end of file
... ...
@@ -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 -b release-2.1 https://github.com/SimpleMachines/SMF smf
185
+
186
+filter:
187
+    dependency_paths:
188
+        - smf/
189
+    excluded_paths:
190
+        - '*.min.js'
... ...
@@ -0,0 +1,25 @@
1
+Developer's Certificate of Origin 1.1
2
+
3
+      By making a contribution to this project, I certify that:
4
+
5
+      (a) The contribution was created in whole or in part by me and I
6
+          have the right to submit it under the open source license
7
+          indicated in the file; or
8
+
9
+      (b) The contribution is based upon previous work that, to the best
10
+          of my knowledge, is covered under an appropriate open source
11
+          license and I have the right under that license to submit that
12
+          work with modifications, whether created in whole or in part
13
+          by me, under the same open source license (unless I am
14
+          permitted to submit under a different license), as indicated
15
+          in the file; or
16
+
17
+      (c) The contribution was provided directly to me by some other
18
+          person who certified (a), (b) or (c) and I have not modified
19
+          it.
20
+
21
+      (d) I understand and agree that this project and the contribution
22
+          are public and that a record of the contribution (including all
23
+          personal information I submit with it, including my sign-off) is
24
+          maintained indefinitely and may be redistributed consistent with
25
+          this project or the open source license(s) involved.
0 26
\ No newline at end of file
... ...
@@ -0,0 +1,559 @@
1
+<?php
2
+
3
+/**
4
+ * The class for DevTools Hooks.
5
+ * @package DevTools
6
+ * @author SleePy <sleepy @ simplemachines (dot) org>
7
+ * @copyright 2022
8
+ * @license 3-Clause BSD https://opensource.org/licenses/BSD-3-Clause
9
+ * @version 1.0
10
+*/
11
+class DevToolsHooks
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
+
30
+	/*
31
+	 * The data file we are looking for inside packages.
32
+	*/
33
+	private string $packageInfoName = 'package-info.xml';
34
+
35
+	/*
36
+	 * Valid search terms we can search for.
37
+	*/
38
+	private array $searchTerms = ['hook_name', 'included_file', 'real_function'];
39
+
40
+	/*
41
+	 * Our hooks can be sorted, this is how.
42
+	*/
43
+	private array $sortTypes = [
44
+		'hook_name' => ['hook_name', SORT_ASC],
45
+		'hook_name DESC' => ['hook_name', SORT_DESC],
46
+		'real_function' => ['real_function', SORT_ASC],
47
+		'real_function DESC' => ['real_function', SORT_DESC],
48
+		'included_file' => ['included_file', SORT_ASC],
49
+		'included_file DESC' => ['included_file', SORT_DESC],
50
+		'status' => ['status', SORT_ASC],
51
+		'status DESC' => ['status', SORT_DESC],
52
+	];
53
+
54
+	/*
55
+	 * How many hooks to show per page.
56
+	*/
57
+	private int $hooksPerPage = 20;
58
+
59
+	/*
60
+	 * Builds the DevTools Hooks object.  This also loads a few globals into easy to access properties, some by reference so we can update them
61
+	*/
62
+	public function __construct()
63
+	{
64
+		foreach (['scripturl', 'packagesdir', 'settings', 'boarddir', 'sourcedir', 'packagesdir'] as $f)
65
+			$this->{$f} = $GLOBALS[$f];
66
+		foreach (['context', 'smcFunc', 'modSettings'] as $f)
67
+			$this->{$f} = &$GLOBALS[$f];
68
+
69
+		$this->dt = &$this->context['instances']['DevTools'];
70
+		$this->dt->loadSources(['Subs-List', 'ManageMaintenance']);
71
+		$this->dt->loadLanguage(['Admin', 'Packages']);
72
+	}
73
+
74
+	/*
75
+	 * Loads the main hooks listing.
76
+	 * This will also look for various actions we are taking on thooks such as toggle, add or modify.
77
+	 *
78
+	 * @calls: $sourcedir/Subs-List.php:createList
79
+	 * @calls: $sourcedir/Security.php:validateToken
80
+	*/
81
+	public function hooksIndex(): void
82
+	{
83
+		// We are doing a action.
84
+		if (isset($_POST['toggle']) || isset($_POST['add']) || isset($_POST['modify']) || isset($_POST['delete']))
85
+			validateToken('devtools_hooks');
86
+
87
+		// We are asking to save data.
88
+		if (isset($_POST['toggle']))
89
+			$this->toggleHook($_POST['toggle']);
90
+		elseif (isset($_POST['add']))
91
+			$this->addHook();
92
+		elseif (isset($_POST['modify']))
93
+			$this->modifyHook($_POST['modify']);
94
+		elseif (isset($_POST['delete']))
95
+			$this->deleteHook($_POST['delete']);
96
+
97
+		// Build a list.
98
+		$this->context['available_packages'] = 0;
99
+		createList($this->context['hooks'] = $this->buildHooksList());
100
+	}
101
+
102
+	/*
103
+	 * Builds a list to pass to SMF's creatList for all valid hooks.  This is mocked up similar to the built in SMF logic to list hooks.
104
+	 *
105
+	 * @calls: $sourcedir/Security.php:createToken
106
+	*/
107
+	private function buildHooksList(): array
108
+	{
109
+		createToken('devtools_hooks');
110
+		$hookData = $this->getHookData($_POST['edit'] ?? '');
111
+
112
+		return [
113
+			'id' => 'hooks_list',
114
+			'no_items_label' => $this->dt->txt('hooks_no_hooks'),
115
+			'items_per_page' => $this->hooksPerPage,
116
+			'base_href' => $this->scripturl . '?action=devtools;area=hooks',
117
+			'default_sort_col' => 'hook_name',
118
+			'get_items' => [
119
+				'function' => [$this, 'listGetHooks'],
120
+			],
121
+			'get_count' => [
122
+				'function' => [$this, 'listGetHooksCount'],
123
+			],
124
+			'form' => [
125
+				'include_start' => true,
126
+				'include_sort' => true,
127
+				'token' => 'devtools_hooks',
128
+				'href' => $this->scripturl . '?action=devtools;area=hooks',
129
+				'name' => 'HooksList',
130
+			],
131
+			'columns' => [
132
+				'hook_name' => [
133
+					'header' => [
134
+						'value' => $this->dt->txt('hooks_field_hook_name'),
135
+					],
136
+					'data' => [
137
+						'db' => 'hook_name',
138
+					],
139
+					'sort' => [
140
+						'default' => 'hook_name',
141
+						'reverse' => 'hook_name DESC',
142
+					],
143
+				],
144
+				'instance' => [
145
+					'header' => [
146
+						'value' => $this->dt->txt('devtools_instance'),
147
+					],
148
+					'data' => [
149
+						'function' => function($data)
150
+						{
151
+							return is_null($data['instance']) ? '' : ('<span class="main_icons ' . (!empty($data['instance']) ? 'post_moderation_deny' : 'post_moderation_allow') . '" title="' . $this->dt->txt('hooks_field_function_method') . '"></span>');
152
+						},
153
+					],
154
+				],
155
+				'function_name' => [
156
+					'header' => [
157
+						'value' => $this->dt->txt('hooks_field_function_name'),
158
+					],
159
+					'data' => [
160
+						'db' => 'real_function',
161
+					],
162
+					'sort' => [
163
+						'default' => 'real_function',
164
+						'reverse' => 'real_function DESC',
165
+					],
166
+				],
167
+				'included_file' => [
168
+					'header' => [
169
+						'value' => $this->dt->txt('hooks_field_file_name'),
170
+					],
171
+					'data' => [
172
+						'db' => 'included_file',
173
+					],
174
+					'sort' => [
175
+						'default' => 'included_file',
176
+						'reverse' => 'included_file DESC',
177
+					],
178
+				],
179
+				'status' => [
180
+					'header' => [
181
+						'value' => $this->dt->txt('hooks_field_hook_exists'),
182
+						'style' => 'width:3%;',
183
+					],
184
+					'data' => [
185
+						'function' => function($data)
186
+						{
187
+							if (is_null($data['status']))
188
+								return '';
189
+
190
+							$change_status = array('before' => '', 'after' => '');
191
+
192
+							if ($data['can_disable'])
193
+							{
194
+								$actionData = base64_encode($this->smcFunc['json_encode']([
195
+									'do' => $data['enabled'] ? 'disable' : 'enable',
196
+									'hook' => $data['hook_name'],
197
+									'function' => $data['real_function']
198
+								]));
199
+								$change_status['before'] = '<button name="toggle" value="' . $data['key'] . '" data-confirm="' . $this->dt->txt('quickmod_confirm') . '" class="you_sure">';
200
+								$change_status['after'] = '</button>';
201
+							}
202
+
203
+							return $change_status['before'] . '<span class="main_icons ' . $data['status'] . '" title="' . $this->dt->txt('hook_' . ($data['enabled'] ? 'active' : 'disabled')). '"></span>' . $change_status['after'];
204
+						},
205
+						'class' => 'centertext',
206
+					],
207
+					'sort' => [
208
+						'default' => 'status',
209
+						'reverse' => 'status DESC',
210
+					],
211
+				],
212
+				'actions' => [
213
+					'header' => [
214
+						'value' => $this->dt->txt('package_install_action'),
215
+					],
216
+					'data' => [
217
+						'function' => function($data) {
218
+							if (is_null($data['instance']))
219
+								return '<input type="submit" value="' . $this->dt->txt('search') . '" />';
220
+							return '<button name="edit" value="' . $data['key'] . '">' . $this->dt->txt('edit') . '</button>';
221
+						},
222
+					],
223
+				],
224
+			],
225
+			'additional_rows' => [
226
+				[
227
+					'position' => 'bottom_of_list',
228
+					'value' => $this->template_hooks_modify($hookData),
229
+				],
230
+			],
231
+		];
232
+	}
233
+
234
+	/*
235
+	 * Get all valid hook data, sort it and return it with our filtered search data.
236
+	 *
237
+	 * @param int $start The start of the list, defaults to 0.
238
+	 * @param int $per_page The amount of hooks to show per page.
239
+	 * @param string $sort The current sorting method.
240
+	 * @return array Filtered, sorted and paginated hook data.
241
+	*/
242
+	public function listGetHooks(int $start, int $per_page, string $sort): array
243
+	{
244
+		$hooks = $this->getRawHooks();
245
+
246
+		// Sort the data.
247
+		uasort($hooks, function($a, $b) use ($sort) {
248
+			return (strcasecmp($a[$this->sortTypes[$sort][0]], $b[$this->sortTypes[$sort][0]]) ?? 0) * ($this->sortTypes[$sort][1] === SORT_DESC ? -1 : 1);
249
+		});
250
+
251
+		// Add in our "search" row, slice the data and return.
252
+		return array_merge(
253
+			$this->insertSearchRow(),
254
+			array_slice($hooks, $start, $per_page, true)
255
+		);
256
+	}
257
+
258
+	/*
259
+	 * Gets a proper count of how many hooks we have, so we can paginate properly.
260
+	 *
261
+	 * @return int The number of hooks in the system.
262
+	*/
263
+	public function listGetHooksCount(): int 
264
+	{
265
+		return array_reduce(
266
+			$this->getRawHooks(),
267
+			function($accumulator, $functions)
268
+			{
269
+				return ++$accumulator;
270
+			},
271
+			0
272
+		);
273
+	}
274
+
275
+	/*
276
+	 * Get all the hook data.  We parse the strings from the settings table into the valid data.
277
+	 * If the hidden setting, dt_showAllHooks is set, we will show dev tool hooks.
278
+	 *
279
+	 * @calls $sourcedir/ManageMaintenance.php:parse_integration_hook
280
+	 * @param bool $rebuildHooks When true, we will ignore the cached data.
281
+	 * @return array All valid hook data.
282
+	*/
283
+	private function getRawHooks(bool $rebuildHooks = false): array
284
+	{
285
+		static $hooks = [];
286
+
287
+		if (!empty($hooks) && empty($rebuildHooks))
288
+			return $hooks;
289
+		elseif (!empty($rebuildHooks))
290
+			$hooks = [];
291
+
292
+		$temp = array_map(
293
+			// Expand by the comma delimiter.
294
+			function ($value) {
295
+				return explode(',', $value);
296
+			},
297
+			// Filter out modSettings that are not hooks.
298
+			array_filter(
299
+				$this->modSettings,
300
+				function ($value, $key) {
301
+					return substr($key, 0, 10) === 'integrate_' && !empty($value) && (!empty($this->modSettings['dt_showAllHooks']) || strpos($value, 'DevTools') === false);
302
+				},
303
+				ARRAY_FILTER_USE_BOTH
304
+			)
305
+		);
306
+
307
+		// Flatten, PHP doesn't have a better way to do this than to loop foreaches.
308
+		foreach ($temp as $hookName => $rawFuncs)
309
+			foreach ($rawFuncs as $func)
310
+			{
311
+				$hookParsedData = parse_integration_hook($hookName, $func);
312
+
313
+				$hooks[] = [
314
+					'key' => md5($func),
315
+					'hook_name' => $hookName,
316
+					'function_name' => $hookParsedData['rawData'],
317
+					'real_function' => $hookParsedData['call'],
318
+					'included_file' => $hookParsedData['hookFile'],
319
+					'instance' => $hookParsedData['object'],
320
+					'status' => $hookParsedData['enabled'] ? 'valid' : 'error',
321
+					'enabled' => $hookParsedData['enabled'],
322
+					'can_disable' => $hookParsedData['call'] != '',
323
+				];
324
+			}
325
+
326
+		// Filter the results by our search terms.
327
+		foreach ($this->searchTerms as $term)
328
+			$hooks = array_filter(
329
+				$hooks,
330
+				function($value, $key) use ($term) {
331
+					return stripos($value[$term], $this->getSearchTerm($term)) > -1;
332
+				},
333
+				ARRAY_FILTER_USE_BOTH
334
+			);
335
+	
336
+		return $hooks;
337
+	}
338
+
339
+	/*
340
+	 * This adds a "fake" row to the hooks data that will act as our handler to hold search terms.
341
+	 *
342
+	 * @return array A "row" that contains search input fields.
343
+	*/
344
+	private function insertSearchRow(): array
345
+	{
346
+		return [
347
+			[
348
+				'hook_name' => '<input type="text" name="search[hook_name]" value="' . $this->getSearchTerm('hook_name') . '" size="30">',
349
+				'instance' => null,
350
+				'function_name' => null,
351
+				'included_file' => '<input type="text" name="search[included_file]" value="' . $this->getSearchTerm('included_file') . '" size="60">',
352
+				'status' => null,
353
+				'enabled' => null,
354
+				'can_disable' => false,
355
+				'real_function' => '<input type="text" name="search[real_function]" value="' . $this->getSearchTerm('real_function') . '" size="30">',
356
+			]
357
+		];
358
+	}
359
+
360
+	/*
361
+	 * Looks for the requested search term and sanitizes the input.
362
+	 * If we can't find the requested input, use a empty string.
363
+	 *
364
+	 * @param string $key, the search term we are looking for.
365
+	 * @return string The sanitized search term.
366
+	*/
367
+	private function getSearchTerm(string $key): string
368
+	{
369
+		return filter_var($_REQUEST['search'][$key] ?? '', FILTER_SANITIZE_SPECIAL_CHARS);
370
+	}
371
+
372
+	/*
373
+	 * This checks if our success message is valid, if so we can use that text string, otherwise we use a generic message.
374
+	 *
375
+	 * @param string $action The success action we took
376
+	 * @return string The language string we will use on our succcess message.
377
+	*/
378
+	private function successMsg(string $action): string
379
+	{
380
+		return in_array($action, ['toggle', 'add', 'modify']) ? 'devtools_success_' . $action : 'settings_saved';
381
+	}
382
+
383
+	/*
384
+	 * Toggle a hook on/off.  We dtermine which way to toggle by checked the enabled status of the hook.
385
+	 *
386
+	 * @calls: $sourcedir/Subs.php:remove_integration_function
387
+	 * @calls: $sourcedir/Subs.php:add_integration_function
388
+	 * @param string $hookID The ID of the hook we are looking for.
389
+	*/
390
+	private function toggleHook(string $hookID): void
391
+	{
392
+		$hooks = $this->getRawHooks();
393
+		$hook = array_filter(
394
+			$hooks,
395
+			function($value) use ($hookID) {
396
+				return stripos($value['key'], $hookID) > -1;
397
+			}
398
+		);
399
+
400
+		// Can't toggle this, its not  unique.
401
+		if (count($hook) !== 1)
402
+			return;
403
+
404
+		$hook = $hook[array_key_first($hook)];
405
+
406
+		$new_func = $old_func = $hook['real_function'];
407
+		if ($hook['enabled'])
408
+			$new_func = '!' . $new_func;
409
+		else
410
+			$old_func = '!' . $old_func;
411
+
412
+		remove_integration_function($hook['hook_name'], $old_func, true, $hook['included_file'], $hook['instance']);
413
+		add_integration_function($hook['hook_name'], $new_func, true, $hook['included_file'], $hook['instance']);
414
+
415
+		// Force the hooks to rebuild.
416
+		$this->getRawHooks(true);
417
+
418
+		$this->dt->showSuccessDialog($this->successMsg('toggle'));
419
+	}
420
+
421
+	/*
422
+	 * Add a hook.  Adds a hook to the system
423
+	 *
424
+	 * @calls: $sourcedir/Subs.php:add_integration_function
425
+	 * @param bool $rebuildHooks When true, we will issue the rebuild hooks.  This is used as we may use other logic elsewhere and we wish to wait on the rebuild logic.
426
+	*/
427
+	private function addHook(bool $rebuildHooks = true): void
428
+	{
429
+		$replacements = [
430
+			' ' => '_',
431
+			"\0" => '',
432
+		];
433
+
434
+		$hook = [
435
+			'hook_name' => strtr(strip_tags($_POST['hook_name']), $replacements),
436
+			'real_function' => strtr(strip_tags($_POST['real_function']), $replacements),
437
+			'included_file' => strtr(strip_tags($_POST['included_file']), $replacements),
438
+			'instance' => isset($_POST['instance']),
439
+		];
440
+
441
+		// Ensure the hook has the integrate prefix.
442
+		if (substr($hook['hook_name'], 0, 10) !== 'integrate_')
443
+			$hook['hook_name'] = 'integrate_' . $hook['hook_name'];
444
+
445
+		add_integration_function($hook['hook_name'], $hook['real_function'], true, $hook['included_file'], $hook['instance']);
446
+		$this->dt->showSuccessDialog($this->successMsg('addhook'));
447
+
448
+		// Rebuild the hooks?
449
+		$this->getRawHooks(true);
450
+	}
451
+
452
+	/*
453
+	 * Delete a hook.  Removes a hook to the system
454
+	 *
455
+	 * @calls: $sourcedir/Subs.php:remove_integration_function
456
+	 * @param string $hookID The ID of the hook we are looking for.
457
+	 * @param bool $rebuildHooks When true, we will issue the rebuild hooks.  This is used as we may use other logic elsewhere and we wish to wait on the rebuild logic.
458
+	*/
459
+	private function deleteHook(string $hookID, bool $rebuildHooks = true): void
460
+	{
461
+		// Find the hook we are looking for.
462
+		$hooks = $this->getRawHooks();
463
+		$hook = array_filter(
464
+			$hooks,
465
+			function($value) use ($hookID) {
466
+				return stripos($value['key'], $hookID) > -1;
467
+			}
468
+		);
469
+
470
+		// Can't toggle this, its not  unique.
471
+		if (count($hook) !== 1)
472
+			return;
473
+		$hook = $hook[array_key_first($hook)];
474
+
475
+		// Remove the hook.
476
+		remove_integration_function($hook['hook_name'], $hook['real_function'], true, $hook['included_file'], $hook['instance']);
477
+		$this->dt->showSuccessDialog($this->successMsg('deletehook'));
478
+
479
+		// Rebuild the hooks?
480
+		if ($rebuildHooks)
481
+			$this->getRawHooks(true);
482
+	}
483
+
484
+	/*
485
+	 * Modify a hook.  This simply calls the deleteHook logic to remove the old and then the addHook logic to add the hook.
486
+	 * This will only rebuild the hooks data after we complete the addHook logic.
487
+	 *
488
+	 * @param string $hookID The ID of the hook we are looking for.
489
+	*/
490
+	private function modifyHook(string $hookID): void
491
+	{
492
+		// Call the remove hook to remove it.
493
+		$this->deleteHook($hookID, false);
494
+
495
+		// Call the add hook, to add it.
496
+		$this->addHook();
497
+
498
+		// Thus we lie and say the hook was "modified".
499
+		$this->dt->showSuccessDialog($this->successMsg('modifyhook'));
500
+	}
501
+
502
+	/*
503
+	 * Takes a hook ID and returns the requested hook data, otherwise if it can't be found, returns empty hook data.
504
+	 *
505
+	 * @param string $hookID The ID of the hook we are looking for.
506
+	*/	 
507
+	private function getHookData(string $hookID): array
508
+	{
509
+		$hooks = $this->getRawHooks();
510
+		$hook = array_filter(
511
+			$hooks,
512
+			function($value) use ($hookID) {
513
+				return stripos($value['key'], $hookID) > -1;
514
+			}
515
+		);
516
+
517
+		// Can't toggle this, its not  unique.
518
+		if (count($hook) !== 1)
519
+			return [
520
+				'key' => '',
521
+				'hook_name' => '',
522
+				'real_function' => '',
523
+				'included_file' => '',
524
+				'instance' => false,
525
+				'new' => true
526
+			];
527
+
528
+		return $hook[array_key_first($hook)];
529
+	}
530
+
531
+	/*
532
+	 * This the add/modify template for hooks that is appended to the end of the createList function.
533
+	 *
534
+	 * @param array $hook All the hook data, or empty hook data.
535
+	 * @return string the Strinified HTML data to append to createList.
536
+	*/
537
+	private function template_hooks_modify(array $hook): string
538
+	{
539
+		$rt = '<fieldset><dl class="settings">'
540
+
541
+				. '<dt><label for="hook_name">' . $this->dt->txt('hooks_field_hook_name') . '</label></dt>'
542
+				. '<dd><input type="text" name="hook_name" value="' . $hook['hook_name'] . '"></dt>'
543
+
544
+				. '<dt><label for="real_function">' . $this->dt->txt('hooks_field_function_name') . '</label></dt>'
545
+				. '<dd><input type="text" name="real_function" value="' . $hook['real_function'] . '"></dt>'
546
+
547
+				. '<dt><label for="included_file">' . $this->dt->txt('hooks_field_included_file') . '</label></dt>'
548
+				. '<dd><input type="text" name="included_file" value="' . $hook['included_file'] . '"></dt>'
549
+
550
+				. '<dt><label for="instance">' . $this->dt->txt('devtools_instance') . '</label></dt>'
551
+				. '<dd><input type="checkbox" name="instance" value="1"' . (!empty($hook['instance']) ? ' checked': '') . '></dt>'
552
+
553
+				. '<dt></dt><dd><button name="' . (!empty($hook['new']) ? 'add' : 'modify') . '" value="' . $hook['key'] . '" class="button">' . $this->dt->txt(!empty($hook['new']) ? 'new' : 'edit') . '</button>' . (empty($hook['new']) ? (' <button name="delete" value="' . $hook['key'] . '" class="button you_sure">' . $this->dt->txt('delete') . '</button>') : '') . '</dd>'
554
+			. '</dl></fieldset>'
555
+		;
556
+
557
+		return $rt;
558
+	}
559
+}
0 560
\ No newline at end of file
... ...
@@ -0,0 +1,706 @@
1
+<?php
2
+
3
+/**
4
+ * The class for DevTools Packages.
5
+ * @package DevTools
6
+ * @author SleePy <sleepy @ simplemachines (dot) org>
7
+ * @copyright 2022
8
+ * @license 3-Clause BSD https://opensource.org/licenses/BSD-3-Clause
9
+ * @version 1.0
10
+*/
11
+class DevToolsPackages
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 $txt;
29
+	private array $settings;
30
+	private bool $db_show_debug;
31
+	/* 
32
+	 * SMF has this both as an array and a bool, no type delcartion.
33
+	*/
34
+	private $package_cache;
35
+
36
+	/*
37
+	 * The data file we are looking for inside packages.
38
+	*/
39
+	private string $packageInfoName = 'package-info.xml';
40
+
41
+	/*
42
+	 * This is the package id of dev tools, used to hide itself from being modified with under normal circumstances
43
+	*/
44
+	private string $devToolsPackageID = 'sleepy:devtools';
45
+
46
+	/*
47
+	 * Builds the DevTools Packages object.  This also loads a few globals into easy to access properties, some by reference so we can update them
48
+	*/
49
+	public function __construct()
50
+	{
51
+		foreach (['scripturl', 'packagesdir', 'settings', 'boarddir', 'sourcedir'] as $f)
52
+			$this->{$f} = $GLOBALS[$f];
53
+		foreach (['context', 'smcFunc', 'package_cache', 'modSettings'] as $f)
54
+			$this->{$f} = &$GLOBALS[$f];
55
+
56
+		$this->dt = &$this->context['instances']['DevTools'];
57
+		$this->dt->loadSources(['Packages', 'Subs-Package', 'Subs-List', 'Class-Package']);
58
+		$this->dt->loadLanguage(['Admin', 'Packages']);
59
+	}
60
+
61
+	/*
62
+	 * Loads the main package listing.
63
+	 *
64
+	 * @calls: $sourcedir/Subs-List.php:createList
65
+	*/
66
+	public function packagesIndex(): void
67
+	{
68
+		$this->context['available_packages'] = 0;
69
+		createList($this->context['packages'] = $this->buildPackagesList());
70
+
71
+		// An action was successful.
72
+		if (isset($_REQUEST['success']))
73
+			$this->dt->showSuccessDialog($this->successMsg((string) $_REQUEST['success']));
74
+	}
75
+
76
+	/*
77
+	 * Reinstall hooks logic.  Will issue a failure if we can't do any step in this process.
78
+	 * Upon success, this will redirect back to package listing.
79
+	 *
80
+	 * @calls: $sourcedir/Errors.php:fatal_lang_error
81
+	 * @calls: $sourcedir/Subs.php:redirectexit
82
+	*/
83
+	public function HooksReinstall(): void
84
+	{
85
+		// Ensure the file is valid.
86
+		if (($package = $this->getRequestedPackage()) == '' || !$this->isValidPackage($package))
87
+			fatal_lang_error('package_no_file', false);
88
+		else if (($basedir = $this->getPackageBasedir($package)) == '')
89
+			fatal_lang_error('package_get_error_not_found', false);
90
+
91
+		$infoFile = $this->getPackageInfo($basedir . '/' . $this->packageInfoName);
92
+		if (!is_a($infoFile, 'xmlArray'))
93
+			fatal_lang_error('package_get_error_missing_xml', false);
94
+
95
+		$install = $this->findInstall($infoFile, SMF_VERSION);
96
+
97
+		if (!is_a($infoFile, 'xmlArray'))
98
+			fatal_lang_error('package_get_error_packageinfo_corrupt', false);
99
+
100
+		$hooks = $this->findHooks($install);
101
+		
102
+		if (!$this->uninstallHooks($hooks) || !$this->installHooks($hooks))
103
+			fatal_lang_error('devtools_hook_reinstall_fail', false);
104
+
105
+		redirectexit('action=devtools;sa=packages;success=reinstall');		
106
+	}
107
+
108
+	/*
109
+	 * Uninstall hooks logic.  Will issue a failure if we can't do any step in this process.
110
+	 * Upon success, this will redirect back to package listing.
111
+	 *
112
+	 * @calls: $sourcedir/Errors.php:fatal_lang_error
113
+	 * @calls: $sourcedir/Subs.php:redirectexit
114
+	*/
115
+	public function HooksUninstall(): void
116
+	{
117
+		// Ensure the file is valid.
118
+		if (($package = $this->getRequestedPackage()) == '' || !$this->isValidPackage($package))
119
+			fatal_lang_error('package_no_file', false);
120
+		else if (($basedir = $this->getPackageBasedir($package)) == '')
121
+			fatal_lang_error('package_get_error_not_found', false);
122
+
123
+		$infoFile = $this->getPackageInfo($basedir . '/' . $this->packageInfoName);
124
+		if (!is_a($infoFile, 'xmlArray'))
125
+			fatal_lang_error('package_get_error_missing_xml', false);
126
+
127
+		$install = $this->findInstall($infoFile, SMF_VERSION);
128
+
129
+		if (!is_a($infoFile, 'xmlArray'))
130
+			fatal_lang_error('package_get_error_packageinfo_corrupt', false);
131
+
132
+		$hooks = $this->findHooks($install);
133
+		
134
+		if (!$this->uninstallHooks($hooks))
135
+			fatal_lang_error('devtools_hook_reinstall_fail', false);
136
+
137
+		redirectexit('action=devtools;sa=packages;success=uninstall');		
138
+	}
139
+
140
+	/*
141
+	 * Sync Files into packages.  Will issue a failure if we can't do any step in this process.
142
+	 * Upon success, this will redirect back to package listing.
143
+	 *
144
+	 * @calls: $sourcedir/Subs-List.php:createList
145
+	 * @calls: $sourcedir/Errors.php:fatal_lang_error
146
+	 * @calls: $sourcedir/Subs.php:redirectexit
147
+	*/
148
+	public function FilesSyncIn(): void
149
+	{
150
+		// Ensure the file is valid.
151
+		if (($package = $this->getRequestedPackage()) == '' || !$this->isValidPackage($package))
152
+			fatal_lang_error('package_no_file', false);
153
+		else if (($basedir = $this->getPackageBasedir($package)) == '')
154
+			fatal_lang_error('package_get_error_not_found', false);
155
+
156
+		$infoFile = $this->getPackageInfo($basedir . '/' . $this->packageInfoName);
157
+		if (!is_a($infoFile, 'xmlArray'))
158
+			fatal_lang_error('package_get_error_missing_xml', false);
159
+
160
+		$install = $this->findInstall($infoFile, SMF_VERSION);
161
+
162
+		if (!is_a($infoFile, 'xmlArray'))
163
+			fatal_lang_error('package_get_error_packageinfo_corrupt', false);
164
+
165
+		// File Operations we will do.
166
+		$ops = $this->findFileOperations($install, $basedir);
167
+
168
+		// Sync the files.
169
+		$acts = $this->doSyncFiles($ops);
170
+
171
+		// Find out if we have an error.
172
+		$has_error = array_search(false, array_column($acts, 'res'));
173
+
174
+		// No errors, just return.
175
+		if (!$has_error)
176
+			redirectexit('action=devtools;sa=packages;success=syncin');
177
+
178
+		// Create a list showing what failed to sync.
179
+		createList($this->context['syncfiles'] = $this->buildSyncStatusList($acts, $package));
180
+	}
181
+
182
+	/*
183
+	 * Sync Files out to SMF.  Will issue a failure if we can't do any step in this process.
184
+	 * Upon success, this will redirect back to package listing.
185
+	 *
186
+	 * @calls: $sourcedir/Subs-List.php:createList
187
+	 * @calls: $sourcedir/Errors.php:fatal_lang_error
188
+	 * @calls: $sourcedir/Subs.php:redirectexit
189
+	*/
190
+	public function FilesSyncOut(): void
191
+	{
192
+		// Ensure the file is valid.
193
+		if (($package = $this->getRequestedPackage()) == '' || !$this->isValidPackage($package))
194
+			fatal_lang_error('package_no_file', false);
195
+		else if (($basedir = $this->getPackageBasedir($package)) == '')
196
+			fatal_lang_error('package_get_error_not_found', false);
197
+
198
+		$infoFile = $this->getPackageInfo($basedir . '/' . $this->packageInfoName);
199
+		if (!is_a($infoFile, 'xmlArray'))
200
+			fatal_lang_error('package_get_error_missing_xml', false);
201
+
202
+		$install = $this->findInstall($infoFile, SMF_VERSION);
203
+
204
+		if (!is_a($infoFile, 'xmlArray'))
205
+			fatal_lang_error('package_get_error_packageinfo_corrupt', false);
206
+
207
+		// File Operations we will do.
208
+		$ops = $this->findFileOperations($install, $basedir);
209
+
210
+		// Sync the files.
211
+		$acts = $this->doSyncFiles($ops, true);
212
+
213
+		// Find out if we have an error.
214
+		$has_error = array_search(false, array_column($acts, 'res'));
215
+
216
+		// No errors, just return.
217
+		if (!$has_error)
218
+			redirectexit('action=devtools;sa=packages;success=syncout');
219
+
220
+		// Create a list showing what failed to sync.
221
+		createList($this->context['syncfiles'] = $this->buildSyncStatusList($acts, $package, true));
222
+	}
223
+
224
+	/*
225
+	 * Returns an array that will be passed into SMF's createList logic to build a packages listing.
226
+	*/
227
+	private function buildPackagesList(): array
228
+	{
229
+		return [
230
+			'id' => 'packages_lists_modification',
231
+			'no_items_label' => $this->dt->txt('no_packages'),
232
+			'get_items' => [
233
+				'function' => [$this, 'listGetPackages'],
234
+				'params' => ['modification'],
235
+			],
236
+			'base_href' => $this->scripturl . '?action=devtools;area=packages',
237
+			'default_sort_col' => 'idmodification',
238
+			'columns' => [
239
+				'idmodification' => [
240
+					'header' => [
241
+						'value' => $this->dt->txt('package_id'),
242
+						'style' => 'width: 52px;',
243
+					],
244
+					'data' => [
245
+						'db' => 'sort_id',
246
+					],
247
+					'sort' => [
248
+						'default' => 'sort_id',
249
+						'reverse' => 'sort_id'
250
+					],
251
+				],
252
+				'mod_namemodification' => [
253
+					'header' => [
254
+						'value' => $this->dt->txt('mod_name'),
255
+						'style' => 'width: 25%;',
256
+					],
257
+					'data' => [
258
+						'db' => 'name',
259
+					],
260
+					'sort' => [
261
+						'default' => 'name',
262
+						'reverse' => 'name',
263
+					],
264
+				],
265
+				'versionmodification' => [
266
+					'header' => [
267
+						'value' => $this->dt->txt('mod_version'),
268
+					],
269
+					'data' => [
270
+						'db' => 'version',
271
+					],
272
+					'sort' => [
273
+						'default' => 'version',
274
+						'reverse' => 'version',
275
+					],
276
+				],
277
+				'time_installedmodification' => [
278
+					'header' => [
279
+						'value' => $this->dt->txt('mod_installed_time'),
280
+					],
281
+					'data' => [
282
+						'function' => function($package)
283
+						{
284
+							return !empty($package['time_installed'])
285
+								? timeformat($package['time_installed'])
286
+								: $this->dt->txt('not_applicable');
287
+						},
288
+						'class' => 'smalltext',
289
+					],
290
+					'sort' => [
291
+						'default' => 'time_installed',
292
+						'reverse' => 'time_installed',
293
+					],
294
+				],
295
+				'operationsmodification' => [
296
+					'header' => [
297
+						'value' => '',
298
+					],
299
+					'data' => [
300
+						'function' => [$this, 'listColOperations'],
301
+						'class' => 'righttext',
302
+					],
303
+				],
304
+			],
305
+		];
306
+	}
307
+
308
+	/*
309
+	 * Get a listing of packages from SMF, then run through a filter to remove any compressed files.
310
+	 * This also will exclude our own package.
311
+	 *
312
+	 * @param ...$args all params that will just be passed directly into SMF's native list_getPackages
313
+	 * @See: $sourcedir/Packages.php:list_getPackages
314
+	 * @return array List of filtered packages we can work with.
315
+	*/
316
+	public function listGetPackages(...$args): array
317
+	{
318
+		// Filter out anything with an extension, we don't support working with compressed files.
319
+		// list_getPackages is from SMF in Packages.php
320
+		return array_filter(list_getPackages(...$args), function($p) {
321
+			return empty(pathinfo($p['filename'], PATHINFO_EXTENSION)) && (!empty($this->modSettings['dt_showAllPackages']) || strpos($p['id'], $this->devToolsPackageID) === false);
322
+		});
323
+	}
324
+
325
+	/*
326
+	 * All possible operations we can perform on a package.
327
+	 * If a package can not be uninstalled, we remove the uninstall/reinstall actions.
328
+	 *
329
+	 * @param array $packagethe package data
330
+	 * @return string The actions we can perform.
331
+	*/
332
+	public function listColOperations(array $package): string
333
+	{
334
+		$actions = [
335
+			'uninstall' => '<a href="' . $this->scripturl . '?action=devtools;sa=uninstall;package=' . $package['filename'] . '" class="button floatnone">' . $this->dt->txt('devtools_packages_uninstall') . '</a>',
336
+			'reinstall' => '<a href="' . $this->scripturl . '?action=devtools;sa=reinstall;package=' . $package['filename'] . '" class="button floatnone">' . $this->dt->txt('devtools_packages_reinstall') . '</a>',
337
+			'syncin' => '<a href="' . $this->scripturl . '?action=devtools;sa=syncin;package=' . $package['filename'] . '" class="button floatnone">' . $this->dt->txt('devtools_packages_syncin') . '</a>',
338
+			'syncout' => '<a href="' . $this->scripturl . '?action=devtools;sa=syncout;package=' . $package['filename'] . '" class="button floatnone">' . $this->dt->txt('devtools_packages_syncout') . '</a>',
339
+		];
340
+
341
+		if (!$package['can_uninstall'])
342
+			unset($actions['uninstall'], $actions['reinstall']);
343
+
344
+		return implode('', $actions);
345
+	}
346
+
347
+	/*
348
+	 * Builds a list for our sync status to show what errored out.
349
+	 *
350
+	 * @param array $actsThe actions we took.
351
+	 * @param string $packageThe package we are performing the action on.
352
+	 * @param bool $reverseThe direction we are going.  When reversing we are syncing from SMF to the package.
353
+	 * @return array The data that we will pass to createList.
354
+	*/
355
+	private function buildSyncStatusList(array $acts, string $package, bool $reverse = false): array
356
+	{
357
+		$src = $reverse ? 'smf' : 'pkg';
358
+		$dst = $reverse ? 'pkg' : 'smf';
359
+
360
+		return [
361
+			'id' => 'syncfiles_list',
362
+			'no_items_label' => $this->dt->txt('no_packages'),
363
+			'get_items' => [
364
+				'value' => $acts,
365
+			],
366
+			'columns' => [
367
+				'file' => [
368
+					'header' => [
369
+						'value' => $this->dt->txt('package_file'),
370
+					],
371
+					'data' => [
372
+						'function' => function ($data) use ($src) {
373
+							return basename($data[$src]);
374
+						},
375
+					],
376
+				],
377
+				'src' => [
378
+					'header' => [
379
+						'value' => $this->dt->txt('file_location'),
380
+					],
381
+					'data' => [
382
+						'function' => function ($data) use ($src) {
383
+							return $this->cleanPath(dirname($data[$src]));
384
+						},
385
+					],
386
+				],
387
+				'dst' => [
388
+					'header' => [
389
+						'value' => $this->dt->txt('package_extract'),
390
+					],
391
+					'data' => [
392
+						'function' => function ($data) use ($dst) {
393
+							return $this->cleanPath($data[$dst]);
394
+						},
395
+					],
396
+				],
397
+				'writeable' => [
398
+					'header' => [
399
+						'value' => $this->dt->txt('package_file_perms_status'),
400
+					],
401
+					'data' => [
402
+						'function' => function ($data) {
403
+							return $this->dt->txt(empty($data['isw']) ? 'package_file_perms_not_writable' : 'package_file_perms_writable');
404
+						},
405
+					],
406
+				],
407
+				'status' => [
408
+					'header' => [
409
+						'value' => $this->dt->txt('package_file_perms_status'),
410
+					],
411
+					'data' => [
412
+						'function' => function ($data) {
413
+							return $this->dt->txt(empty($data['res']) ? 'package_restore_permissions_action_failure' : 'package_restore_permissions_action_success');
414
+						},
415
+					],
416
+				],
417
+			],
418
+			'additional_rows' => [
419
+				[
420
+					'position' => 'bottom_of_list',
421
+					'value' => '<a href="' . $this->scripturl . '?action=devtools;sa=' . ($reverse ? 'syncout' : 'syncin') . ';package=' . $package . '" class="button floatnone">' . $this->dt->txt($reverse ? 'devtools_packages_syncout' : 'devtools_packages_syncin') . '</a>',
422
+					'class' => 'floatright',
423
+				],
424
+			],
425
+		];
426
+	}
427
+
428
+	/*
429
+	 * Get the requested package, filtering the data in the reuqest for santity checks.
430
+	 *
431
+	 * @return string The cleaned package.
432
+	*/
433
+	private function getRequestedPackage(): string
434
+	{
435
+		return (string) preg_replace('~[^a-z0-9\-_\.]+~i', '-', $_REQUEST['package']);
436
+	}
437
+	
438
+	/*
439
+	 * Tests whether this package is valid.  Looks for the directory to exist in the packages folder.
440
+	 *
441
+	 * @param string $package A package name.
442
+	 * @return bool True if the directory exists, false otherwise.
443
+	*/
444
+	private function isValidPackage(string $package): bool
445
+	{
446
+		return is_dir($this->packagesdir . '/' . $package);
447
+	}
448
+
449
+	/*
450
+	 * This looks in a package and attempts to get the info file.  SMF only normally supports it in the root directory.
451
+	 * 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.
452
+	 *
453
+	 * @param string $package The package we are looking at.
454
+	 * @return string The path to the directory inside the package that contains the info file.
455
+	*/
456
+	private function getPackageBasedir(string $package): string
457
+	{
458
+		// Simple, its at the file root
459
+		if (file_exists($this->packagesdir . '/' . $package . '/' . $this->packageInfoName))
460
+			return $this->packagesdir . '/' . $package;
461
+
462
+		$files = new RecursiveIteratorIterator(
463
+			new RecursiveDirectoryIterator(
464
+				$this->packagesdir . '/' . $package,
465
+				RecursiveDirectoryIterator::SKIP_DOTS
466
+			)
467
+		);
468
+
469
+		// Someday we could simplify this?
470
+		foreach ($files as $f)
471
+		{
472
+			if ($f->getFilename() == $this->packageInfoName)
473
+			{
474
+				return dirname($f->getPathName());
475
+				break;
476
+			}
477
+		}
478
+
479
+		return '';
480
+	}
481
+	
482
+	/*
483
+	 * This will pass the info file through SMF's xmlArray object and returns a valid xmlArray we will use to parse it.
484
+	 * This uses SMF's xmlArray rather than the built in xml tools in PHP as it is what package manager is using.
485
+	 *
486
+	 * @calls: $sourcedir/Class-Package.php:xmlArray
487
+	 * @param string $packageInfoFile The info we are looking at.
488
+	 * @return xmlArray A valid object of xml data from the info file.
489
+	*/
490
+	private function getPackageInfo(string $packageInfoFile): xmlArray
491
+	{
492
+		return new xmlArray(file_get_contents($packageInfoFile));
493
+	}
494
+
495
+	/*
496
+	 * Finds the valid install action for a customization.
497
+	 * Note: This will match <install> and <install for="SMF X.Y"> with a matching SMF version.  Ideally we should limit this to a matching version.
498
+	 *
499
+	 * @calls: $sourcedir/Class-Package.php:xmlArray
500
+	 * @calls: $sourcedir/Sub-Package.php:matchPackageVersion
501
+	 * @param xmlArray $packageXML A valid xmlArray object.
502
+	 * @param string $smfVersion The current SMF version we are looking for.
503
+	 * @return xmlArray A valid object of xml data from the info file, limited to the matched install actions.
504
+	*/
505
+	private function findInstall(xmlArray $packageXML, string $smfVersion): xmlArray
506
+	{
507
+		$methods = $packageXML->path('package-info[0]')->set('install');
508
+
509
+		// matchPackageVersion is in Subs-Package.php
510
+		foreach ($methods as $i)
511
+		{
512
+			// Found a for in the install, skip if it doesn't match our version.
513
+			if ($i->exists('@for') && !matchPackageVersion($smfVersion, $i->fetch('@for')))
514
+				continue;
515
+			return $i;
516
+		}
517
+	}
518
+
519
+	/*
520
+	 * Processes a xmlArray install action for any hook related call.
521
+	 *
522
+	 * @calls: $sourcedir/Class-Package.php:xmlArray
523
+	 * @param xmlArray $installXML A valid xmlArray install object.
524
+	 * @return array All valid hooks in the package.
525
+	*/
526
+	private function findHooks(xmlArray $installXML): array
527
+	{
528
+		$hooks = [];
529
+		$actions = $installXML->set('*');
530
+		foreach ($actions as $action)
531
+		{
532
+			$actionType = $action->name();
533
+
534
+			if (!in_array($actionType, ['hook']))
535
+				continue;
536
+
537
+			$hooks[] = [
538
+				'function' => $action->exists('@function') ? $action->fetch('@function') : '',
539
+				'hook' => $action->exists('@hook') ? $action->fetch('@hook') : $action->fetch('.'),
540
+				'include_file' => $action->exists('@file') ? $action->fetch('@file') : '',
541
+				'reverse' => $action->exists('@reverse') && $action->fetch('@reverse') == 'true' ? true : false,
542
+				'object' => $action->exists('@object') && $action->fetch('@object') == 'true' ? true : false,
543
+			];
544
+		}
545
+
546
+		return $hooks;
547
+	}
548
+
549
+	/*
550
+	 * Processes a xmlArray install action for any file operations related call.
551
+	 *
552
+	 * @calls: $sourcedir/Class-Package.php:xmlArray
553
+	 * @param xmlArray $installXML A valid xmlArray install object.
554
+	 * @param string $basedir The base directory we are working with.  This should be the directory we found the info file in.
555
+	 * @return array All valid hooks in the package.
556
+	*/
557
+	private function findFileOperations(xmlArray $installXML, string $basedir): array
558
+	{
559
+		$hooks = [];
560
+		$actions = $installXML->set('*');
561
+		foreach ($actions as $action)
562
+		{
563
+			$actionType = $action->name();
564
+
565
+			// Only supporting right now require file/dir as it is used to move files from the package into SMF.
566
+			if (!in_array($actionType, ['require-file', 'require-dir']))
567
+				continue;
568
+
569
+			$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
+			];
573
+		}
574
+
575
+		return $hooks;
576
+	}
577
+
578
+	/*
579
+	 * Syncs files from one location to another.  The direction of the search is handled by the bool $reverse logic.
580
+	 *
581
+	 * @calls: $sourcedir/Subs-Package.php:package_chmod
582
+	 * @calls: $sourcedir/Subs-Package.php:copytree
583
+	 * @calls: $sourcedir/Subs-Package.php:package_put_contents
584
+	 * @calls: $sourcedir/Subs-Package.php:package_get_contents
585
+	 * @param array $ops All the file operations we need to take.
586
+	 * @param bool $reverse When reversed we sync from the packages to SMF.
587
+	 * @return array All operations and the result status.
588
+	*/
589
+	private function doSyncFiles(array $ops, bool $reverse = false): array
590
+	{
591
+		$this->disablePackageCache();
592
+		$src = $reverse ? 'pkg' : 'smf';
593
+		$dst = $reverse ? 'smf' : 'pkg';
594
+
595
+		// package_put/get_contents in Subs-Package.php
596
+		return array_map(function($op) use ($src, $dst) {
597
+			// Let us know the writable status.
598
+			$op['isw'] = package_chmod($op[$dst]);
599
+
600
+			if (is_dir($op[$src]))
601
+				$op['res'] = copytree($op[$src], $op[$dst]);
602
+			elseif (is_file($op[$src]))
603
+				$op['res'] =  package_put_contents($op[$dst], package_get_contents($op[$src]));
604
+			else
605
+				$op['res'] =  'unknown';
606
+
607
+			// Do a empty file check.
608
+			if (!$op['res'] && is_file($op[$dst]) && package_get_contents($op[$src]) == package_get_contents($op[$dst]))
609
+				$op['res'] = true;
610
+				
611
+			return $op;
612
+		}, $ops);
613
+	}
614
+
615
+	/*
616
+	 * Uninstall all hooks specified in this action.
617
+	 * This may be confusing, but SMF may be telling us to "reverse" the action", so we would actually install it.
618
+	 *
619
+	 * @calls: $sourcedir/Subs.php:remove_integration_function
620
+	 * @calls: $sourcedir/Subs.php:add_integration_function
621
+	 * @param array $hooks All the hooks we will process.
622
+	 * @return bool Successful indication of hook removal or not.  We currently don't track this as SMF doesn't indicate success/failure.
623
+	*/
624
+	private function uninstallHooks(array $hooks): bool
625
+	{
626
+		return array_walk($hooks, function($action) {
627
+			// During uninstall we will typically "remove", but try to handle "adds" that are "removes", confusing.
628
+			if (!$action['reverse'])
629
+				remove_integration_function($action['hook'], $action['function'], true, $action['include_file'], $action['object']);
630
+			else
631
+				add_integration_function($action['hook'], $action['function'], true, $action['include_file'], $action['object']);
632
+		});
633
+	}
634
+
635
+	/*
636
+	 * Install all hooks specified in this action.
637
+	 * This may be confusing, but SMF may be telling us to "reverse" the action", so we would actually uninstall it.
638
+	 *
639
+	 * @calls: $sourcedir/Subs.php:remove_integration_function
640
+	 * @calls: $sourcedir/Subs.php:add_integration_function
641
+	 * @param array $hooks All the hooks we will process.
642
+	 * @return bool Successful indication of hook removal or not.  We currently don't track this as SMF doesn't indicate success/failure.
643
+	*/
644
+	private function installHooks(array $hooks): bool
645
+	{
646
+		return array_walk($hooks, function($action) {
647
+			if ($action['reverse'])
648
+				remove_integration_function($action['hook'], $action['function'], true, $action['include_file'], $action['object']);
649
+			else
650
+				add_integration_function($action['hook'], $action['function'], true, $action['include_file'], $action['object']);
651
+		});
652
+	}
653
+
654
+	/*
655
+	 * This checks if our success message is valid, if so we can use that text string, otherwise we use a generic message.
656
+	 *
657
+	 * @param string $action The success action we took
658
+	 * @return string The language string we will use on our succcess message.
659
+	*/
660
+	private function successMsg(string $action): string
661
+	{
662
+		return in_array($action, ['reinstall', 'uninstall', 'syncin', 'syncout']) ? 'devtools_success_' . $action : 'settings_saved';
663
+	}
664
+
665
+	/*
666
+	 * ParsePath from SMF, but wrap it incase we need to do cleanup.
667
+	 *
668
+	 * @calls: $sourcedir/Subs-Package.php:parse_path
669
+	 * @param string $p The current path.
670
+	 * @return string A parsed parse with a valid directory.
671
+	*/
672
+	private function parsePath(string $p): string
673
+	{
674
+		return parse_path($p);
675
+	}
676
+
677
+	/*
678
+	 * SMF will cache package directory information.  This disables it so we can work with the data without delays.
679
+	*/
680
+	private function disablePackageCache(): void
681
+	{
682
+		$this->package_cache = false;
683
+		$this->modSettings['package_disable_cache'] = true;
684
+	}
685
+
686
+	/*
687
+	 * Cleanup any paths we find to what they would be parsed out as with placeholders.
688
+	 *
689
+	 * @param string $path The path to be cleaned.
690
+	 * @return string THe path with placeholders.
691
+	*/
692
+	private function cleanPath(string $path): string
693
+	{
694
+		return strtr($path, [
695
+			$this->settings['default_theme_dir'] . '/' . basename($GLOBALS['settings']['default_images_url']) => '$imagesdir',
696
+			$this->settings['default_theme_dir'] . '/languages' => '$languagedir',
697
+			$this->settings['default_theme_dir'] => '$themedir',
698
+			$this->modSettings['avatar_directory'] => '$avatardir',
699
+			$this->modSettings['smileys_dir'] => '$smileysdir',
700
+			$this->boarddir . '/Themes' => '$themes_dir',
701
+			$this->sourcedir => '$sourcedir',
702
+			$this->packagesdir => '$packagesdir',
703
+			$this->boarddir => '$boarddir',
704
+		]);
705
+	}
706
+}
0 707
\ No newline at end of file
... ...
@@ -0,0 +1,23 @@
1
+<?php
2
+$txt['devtools_menu'] = 'Developer Tools';
3
+
4
+/* Packages stuff */
5
+$txt['devtools_packages_uninstall'] = 'Uninstall Hooks';
6
+$txt['devtools_packages_reinstall'] = '(Re-)install Hooks';
7
+$txt['devtools_packages_syncin'] = 'Sync files to Package';
8
+$txt['devtools_packages_syncout'] = 'Sync files to SMF';
9
+$txt['devtools_hook_reinstall_fail'] = 'Failed to reinstall hooks';
10
+
11
+$txt['devtools_success_reinstall'] = 'Succesfully reinstalled hooks';
12
+$txt['devtools_success_uninstall'] = 'Succesfully uninstalled hooks';
13
+$txt['devtools_success_syncin'] = 'Succesfully Synced Files to Packages';
14
+$txt['devtools_success_syncout'] = 'Succesfully Synced Files to SMF';
15
+
16
+
17
+$txt['devtools_instance'] = 'Instance';
18
+
19
+
20
+$txt['devtools_success_toggle'] = 'Succesfully toggled hook';
21
+$txt['devtools_success_addhook'] = 'Succesfully added hook';
22
+$txt['devtools_success_modifyhook'] = 'Succesfully modified hook';
23
+$txt['devtools_success_deletehook'] = 'Succesfully deleted hook';
... ...
@@ -0,0 +1,143 @@
1
+/**
2
+ * Javascript for DevTools.
3
+ * @package DevTools
4
+ * @author SleePy <sleepy @ simplemachines (dot) org>
5
+ * @copyright 2022
6
+ * @license 3-Clause BSD https://opensource.org/licenses/BSD-3-Clause
7
+ * @version 1.0
8
+ */
9
+
10
+/* Load up some logic for devtools once we are ready */
11
+$(document).ready(function() {
12
+	/* Inject the dev tools as a button in the user menu */
13
+	let devtools_menu = '<li>' +
14
+			'<a href="' + smf_scripturl + '?action=devtools" id="devtools_menu_top"><span class="textmenu">' + txt_devtools_menu + '</span></a>' +
15
+			'<div id="devtools_menu" class="top_menu scrollable" style="width: 90vw; max-width: 1200px; position: absolute; left: 0;"></div>' +
16
+		'</li>';
17
+	$('ul#top_info').append(devtools_menu);
18
+	let dev_menu = new smc_PopupMenu();
19
+	dev_menu.add('devtools', smf_scripturl + '?action=devtools');
20
+
21
+	/* Ensures admin login works */
22
+	$("div#devtools_menu").on("submit", "#frmLogin", {form: "div#devtools_menu #frmLogin"}, devtools_formhandler);
23
+
24
+	/* Ensures the hooks form works */
25
+	$("div#devtools_menu").on("submit", "#HooksList", {form: "div#devtools_menu #HooksList", frame: "div#devtools_container"}, devtools_formhandler);
26
+
27
+	/* Fixes links on the popup to use ajax */
28
+	$("div#devtools_menu").on("click", "a",  devtools_links);
29
+});
30
+
31
+/* Ensures admin login works */
32
+function devtools_formhandler(e) {
33
+	e.preventDefault();
34
+	e.stopPropagation();
35
+	
36
+	let form = $(e.data.form ?? "div#devtools_menu #frmLogin");
37
+
38
+	$.ajax({
39
+		url: form.prop("action") + ";ajax",
40
+		method: "POST",
41
+		headers: {
42
+			"X-SMF-AJAX": 1
43
+		},
44
+		xhrFields: {
45
+			withCredentials: typeof allow_xhjr_credentials !== "undefined" ? allow_xhjr_credentials : false
46
+		},
47
+		data: form.serialize(),
48
+		success: function(data, status, xhr) {
49
+			if (e.data.frame.length > 0) {
50
+				$(document).find(e.data.frame).html($(data).html());
51
+			}
52
+			else if (data.indexOf("<bo" + "dy") > -1) {
53
+				document.open();
54
+				document.write(data);
55
+				document.close();
56
+			}
57
+			else if (data.indexOf("<form") > -1) {
58
+				form.html($(data).html());
59
+			}
60
+			else if ($(data).find(".roundframe").length > 0) {
61
+				form.parent().html($(data).find(".roundframe").html());
62
+			}
63
+			else {
64
+				form.parent().html($(data).html());
65
+			}
66
+
67
+			$("div#devtools_menu").customScrollbar().resize();
68
+			checkSuccessFailPrompt(data);
69
+		},
70
+		error: function(xhr) {
71
+			var data = xhr.responseText;
72
+			if (data.indexOf("<bo" + "dy") > -1) {
73
+				document.open();
74
+				document.write(data);
75
+				document.close();
76
+			}
77
+			else
78
+				form.parent().html($(data).filter("#fatal_error").html());
79
+
80
+			$("div#devtools_menu").customScrollbar().resize();
81
+			checkSuccessFailPrompt(data);
82
+		}
83
+	});
84
+
85
+	return false;
86
+}
87
+
88
+/* Fixes links on the popup to use ajax */
89
+function devtools_links(e) {
90
+	e.preventDefault();
91
+	e.stopPropagation();
92
+
93
+	let currentLink = e.currentTarget.href;
94
+	let contentBox = $("div#devtools_menu .overview");
95
+
96
+	$.ajax({
97
+		url: currentLink + ";ajax",
98
+		method: "GET",
99
+		headers: {
100
+			"X-SMF-AJAX": 1
101
+		},
102
+		xhrFields: {
103
+			withCredentials: typeof allow_xhjr_credentials !== "undefined" ? allow_xhjr_credentials : false
104
+		},
105
+		success: function(data, status, xhr) {
106
+			if (data.indexOf("<bo" + "dy") > -1) {
107
+				document.open();
108
+				document.write(data);
109
+				document.close();
110
+			}
111
+			else
112
+				contentBox.html(data);
113
+
114
+			$("div#devtools_menu").customScrollbar().resize();
115
+			checkSuccessFailPrompt(data);
116
+		},
117
+		error: function(xhr) {
118
+			var data = xhr.responseText;
119
+			if (data.indexOf("<bo" + "dy") > -1) {
120
+				document.open();
121
+				document.write(data);
122
+				document.close();
123
+			}
124
+			else
125
+				contentBox.html($(data).filter("#fatal_error").html());
126
+
127
+			$("div#devtools_menu").customScrollbar().resize();
128
+			checkSuccessFailPrompt(data);
129
+		}
130
+	});
131
+}
132
+
133
+/* If a success prompt shows up, fade it away */
134
+function checkSuccessFailPrompt(data)
135
+{
136
+	if ($(data).find('#devtool_success').length > 0)
137
+	{
138
+		$("#devtool_success").fadeOut(2000, function() {
139
+			$(this).remove();
140
+			$("div#devtools_menu").customScrollbar().resize();
141
+		});
142
+	}
143
+}
0 144
\ No newline at end of file
... ...
@@ -0,0 +1,416 @@
1
+<?php
2
+
3
+/**
4
+ * The class for DevTools Main class.
5
+ * @package DevTools
6
+ * @author SleePy <sleepy @ simplemachines (dot) org>
7
+ * @copyright 2022
8
+ * @license 3-Clause BSD https://opensource.org/licenses/BSD-3-Clause
9
+ * @version 1.0
10
+*/
11
+class DevTools
12
+{
13
+	/*
14
+	 * The javascript files hashed and this logic is cached for this length of time.
15
+	*/
16
+	private int $cacheTime = 900;
17
+
18
+	/*
19
+	 * This logic ensures if SMF hooks gets in a loop or happens to be called more than once, we prevent that.
20
+	*/
21
+	private array $calledOnce = [];
22
+
23
+	/*
24
+	 * SMF variables we will load into here for easy reference later.
25
+	*/
26
+	private string $scripturl;
27
+	private array $context;
28
+	private array $smcFunc;
29
+	private array $modSettings;
30
+	private array $txt;
31
+	private bool $db_show_debug;
32
+
33
+	/*
34
+	 * Builds the main DevTools object.  This also loads a few globals into easy to access properties, some by reference so we can update them
35
+	*/
36
+	public function __construct()
37
+	{
38
+		$this->scripturl = $GLOBALS['scripturl'];
39
+		foreach (['context', 'smcFunc', 'txt', 'db_show_debug', 'modSettings'] as $f)
40
+			$this->{$f} = &$GLOBALS[$f];
41
+
42
+		$this->loadLanguage(['DevTools', 'Admin']);
43
+		$this->loadSources(['DevToolsPackages', 'DevToolsHooks', 'Subs-Menu']);
44
+	}
45
+
46
+	/*
47
+	 * Inject into the menu system current action.
48
+	 * Nothing is returned, but we do inject some javascript and css.
49
+	 *
50
+	 * @CalledBy $sourcedir/Subs.php:setupMenuContext - integrate_current_action
51
+	*/
52
+	public function hook_current_action(): void
53
+	{
54
+		if (!empty($this->calledOnce[__FUNCTION__])) return;
55
+		$this->calledOnce[__FUNCTION__] = true;
56
+
57
+		// Don't bother with non admins.
58
+		if (!$this->isAdmin())
59
+			return;
60
+
61
+		// Fixes a minor bug where the content isn't sized right.
62
+		addInlineCss('
63
+			div#devtools_menu .half_content { width: 49%;}
64
+		');
65
+	}
66
+
67
+
68
+	/*
69
+	 * Inject into the menu system valid action.
70
+	 * Nothing is returned, but we do add to the actionArray.
71
+	 *
72
+	 * @CalledBy $boarddir/index.php:smf_main - integrate_actions
73
+	*/
74
+	public function hook_actions(array &$actionArray): void
75
+	{
76
+		if (!empty($this->calledOnce[__FUNCTION__])) return;
77
+		$this->calledOnce[__FUNCTION__] = true;
78
+
79
+		$actionArray['devtools'] = ['DevTools.php', [$this->context['instances'][__CLASS__], 'main_action']];
80
+	}
81
+
82
+	/*
83
+	 * When we are on the logs sub action, we allow a ajax action to strip html.
84
+	 *
85
+	 * @CalledBy $sourcedir/Admin.php:AdminLogs - integrate_manage_logs
86
+	*/
87
+	public function hook_validateSession(&$types): void
88
+	{
89
+		if (!empty($this->calledOnce[__FUNCTION__])) return;
90
+		$this->calledOnce[__FUNCTION__] = true;
91
+
92
+		// Not a AJAX request.
93
+		if (
94
+			!isset($_REQUEST['ajax'], $_REQUEST['action'])
95
+			|| $_REQUEST['action'] != 'devtools'
96
+		)
97
+			return;
98
+
99
+		// Strip away layers and remove debugger.
100
+		$this->setTemplateLayer('', true);
101
+		$this->db_show_debug = false;
102
+	}
103
+
104
+	/*
105
+	 * When we are on the logs sub action, we allow a ajax action to strip html.
106
+	 *
107
+	 * @CalledBy $sourcedir/Subs.php:redirectexit - integrate_redirect
108
+	*/
109
+	public function hook_redirect(&$setLocation, &$refresh, &$permanent): void
110
+	{
111
+		if (!empty($this->calledOnce[__FUNCTION__])) return;
112
+		$this->calledOnce[__FUNCTION__] = true;
113
+
114
+		// We are on a error log action such as delete.
115
+		if (
116
+			isset($_REQUEST['ajax'], $_REQUEST['action'])
117
+			&& $_REQUEST['action'] == 'devtools'
118
+		)
119
+			$setLocation .= ';ajax';
120
+	}
121
+
122
+	/*
123
+	 * When we load the theme we will add some extra javascript we need..
124
+	 *
125
+	 * @CalledBy $sourcedir/Load.php:loadTheme - integrate_load_theme
126
+	 * @calls: $sourcedir/Load.php:cache_put_data
127
+	 * @calls: $sourcedir/Load.php:loadJavaScriptFile
128
+	 * @calls: $sourcedir/Load.php:addJavaScriptVar
129
+	*/
130
+	public function hook_load_theme(): void
131
+	{
132
+		if (!empty($this->calledOnce[__FUNCTION__])) return;
133
+		$this->calledOnce[__FUNCTION__] = true;
134
+
135
+		if (empty($this->modSettings['dt_debug']) && ($hash = cache_get_data('devtools-js-hash', $this->cacheTime)) === null)
136
+		{
137
+			$hash = base64_encode(hash_file('sha384', $GLOBALS['settings']['default_theme_dir'] . '/scripts/DevTools.js', true));
138
+			cache_put_data('devtools-js-hash', $hash, $this->cacheTime);
139
+		}
140
+
141
+		// Load up our javascript files.
142
+		loadJavaScriptFile(
143
+			'DevTools.js',
144
+			[
145
+				'defer' => true,
146
+				'minimize' => false,
147
+				'seed' => microtime(),
148
+				'attributes' => [
149
+					'integrity' => !empty($hash) ? 'sha384-' . $hash : false,
150
+				],
151
+			],
152
+			'devtools'
153
+		);
154
+
155
+		addJavaScriptVar('txt_devtools_menu', $this->txt('devtools_menu'), true);
156
+	}
157
+ 
158
+	/*
159
+	 * This is called when we first enter the devtools action.  We check for admin access here.
160
+	 * This will determine what we do next, prepare all output handles.
161
+	 *
162
+	 * @calls: $sourcedir/Subs.php:redirectexit
163
+	 * @calls: $sourcedir/Security.php:validateSession
164
+	*/
165
+	public function main_action(): void
166
+	{
167
+		if (!$this->isAdmin())
168
+			redirectexit();
169
+		validateSession();
170
+
171
+		// If this is from ajax, prepare the system to do the popup container.
172
+		if ($this->isAjaxRequest())
173
+			$this->preareAjaxRequest();
174
+
175
+		// Valid actions we can take.
176
+		$areas = [
177
+			'index' => 'action_index',
178
+			'packages' => 'action_packages',
179
+			'hooks' => 'action_hooks',
180
+		];
181
+
182
+		$this->{$this->getAreaAction($areas, 'packages')}();
183
+		$this->setupDevtoolLayers();
184
+	}
185
+
186
+	/*
187
+	 * When the area=packages, this chooses the sub action we want to work with.
188
+	*/
189
+	private function action_packages(): void
190
+	{
191
+		$subActions = [
192
+			'list' => 'packagesIndex',
193
+			'reinstall' => 'HooksReinstall',
194
+			'uninstall' => 'HooksUninstall',
195
+			'syncin' => 'FilesSyncIn',
196
+			'syncout' => 'FilesSyncOut'
197
+		];
198
+
199
+		if (!isset($this->context['instances']['DevToolsPackages']))
200
+			$this->context['instances']['DevToolsPackages'] = new DevToolsPackages;
201
+
202
+		$this->context['instances']['DevToolsPackages']->{$this->getSubAction($subActions, 'list')}();
203
+		$this->setSubTemplate($this->getSubAction($subActions, 'list'));
204
+	}
205
+
206
+	/*
207
+	 * When the area=hooks, this chooses the sub action we want to work with.
208
+	*/
209
+	private function action_hooks(): void
210
+	{
211
+		$subActions = [
212
+			'list' => 'hooksIndex',
213
+		];
214
+
215
+		if (!isset($this->context['instances']['DevToolsHooks']))
216
+			$this->context['instances']['DevToolsHooks'] = new DevToolsHooks;
217
+
218
+		$this->context['instances']['DevToolsHooks']->{$this->getSubAction($subActions, 'list')}();
219
+		$this->setSubTemplate($this->getSubAction($subActions, 'list'));
220
+	}
221
+
222
+	/*
223
+	 * Loads a sub template.  If we specify the second parameter, we will also load the template file.
224
+	 *
225
+	 * @param string $subTemplate(default: index) The sub template we wish to use in SMF.
226
+	 * @param string $template(optional) If specified, we will call the loadTemplate function.
227
+	*/
228
+	public function setSubTemplate(string $subTemplate = 'index', string $template = ''): void
229
+	{
230
+		if (!empty($template))
231
+			$this->loadTemplate($template);
232
+
233
+		$this->context['sub_template'] = $subTemplate;
234
+	}
235
+
236
+	/*
237
+	 * Set the template layers, we can optionally clear all the layers out if needed.
238
+	 *
239
+	 * @param string $layerThe layer we wish to add.  If we are clearing, this can be any string.
240
+	 * @param bool $clear(optional) If specified, this clears all layers.
241
+	*/
242
+	public function setTemplateLayer(string $layer, bool $clear = false): void
243
+	{
244
+		if ($clear)
245
+			$this->context['template_layers'] = [];
246
+		else
247
+			$this->context['template_layers'][] = $layer;
248
+	}
249
+ 
250
+	/*
251
+	 * Handles loading languages and calling our strings, as well as passing to sprintf if we are using args.
252
+	 *
253
+	 * @param string $keyThe language string key we will call.
254
+	 * @param mixed ...$args If we specify any additional args after this, we will pass them into a sprintf process.
255
+	*/
256
+	public function txt(string $key, string ...$args): string
257
+	{
258
+		// If we have args passed, we want to pass this to sprintf.  We will keep args in a array and unpack it into sprintf.
259
+		if (!empty($args))
260
+			return isset($this->txt[$key]) ? sprintf($this->txt[$key], ...$args) : $key;
261
+	
262
+		return $this->txt[$key] ?? $key;
263
+	}
264
+
265
+	/*
266
+	 * This passes data along to our txt handler, but returns it to SMF's handler for showing a success dialog box.
267
+	 *
268
+	 * @param mixed ...$args: All args are passed through to the txt function.
269
+	*/
270
+	public function showSuccessDialog(...$args): void
271
+	{
272
+		$this->context['saved_successful'] = $this->txt(...$args);
273
+	}
274
+
275
+	/*
276
+	 * Determines if the current requests is a valid request from a javascript based request.
277
+	 *
278
+	 * @return bool True if this was a ajax based request, false otherwise.
279
+	*/
280
+	private function isAjaxRequest(): bool
281
+	{
282
+		return isset($_REQUEST['ajax']);
283
+	}
284
+
285
+	/*
286
+	 * Determines if the current user has admin access.
287
+	 *
288
+	 * @return bool True if this user is an administrator, false otherwise.
289
+	*/
290
+	private function isAdmin(): bool
291
+	{
292
+		return !empty($this->context['user']['is_admin']);
293
+	}
294
+
295
+	/*
296
+	 * Prepares a the output for a Ajax based response.
297
+	*/
298
+	private function preareAjaxRequest(): void
299
+	{
300
+		// Strip away layers and remove debugger.
301
+		$this->setTemplateLayer('', true);
302
+		$this->db_show_debug = false;
303
+	}
304
+
305
+	/*
306
+	 * Gets the current area or the default.
307
+	 * We do a null check on both the rqeuest input and the area.  It fixes a issue where the input is invalid and we force the default again.
308
+	 *
309
+	 * @param array $areasAll valid areas allowed.
310
+	 * @param string $defaultAreaThe default area to take.
311
+	 * @return bool True if this user is an administrator, false otherwise.
312
+	*/
313
+	private function getAreaAction(array $areas, string $defaultArea): string
314
+	{
315
+		return $areas[$_REQUEST['area'] ?? $defaultArea] ?? $areas[$defaultArea]; 
316
+	}
317
+
318
+	/*
319
+	 * Gets the current sub action or the default.
320
+	 * We do a null check on both the rqeuest input and the area.  It fixes a issue where the input is invalid and we force the default again.
321
+	 *
322
+	 * @param array $subActionsAll valid sub actions allowed.
323
+	 * @param string $defaultSubActionThe default sub action to take.
324
+	 * @return bool True if this user is an administrator, false otherwise.
325
+	*/
326
+	private function getSubAction(array $subActions, string $defaultSubAction): string
327
+	{
328
+		return $subActions[$_REQUEST['sa'] ?? $defaultSubAction] ?? $subActions[$defaultSubAction]; 
329
+	}
330
+	
331
+	/*
332
+	 * @calls the correct logic to setup the developer tools layers and add menu button injections.
333
+	 *
334
+	 * @param bool $removeWill remove the dev tools logic.
335
+	*/
336
+	private function setupDevtoolLayers(bool $remove = false): void
337
+	{
338
+		if ($remove)
339
+			$this->context['template_layers'] = array_diff($context['template_layers'], ['devtools']);
340
+		else
341
+		{
342
+			$this->loadTemplate(['DevTools', 'GenericMenu']);
343
+			$this->loadMenuButtons();
344
+			$this->setTemplateLayer('devtools');
345
+		}
346
+	}
347
+
348
+	/*
349
+	 * Loads up all valid buttons on our dev tools section.  This is passed into SMF's logic to build a button menu.
350
+	 *
351
+	 * @param string $activeWhich action is the 'default' action we will load.
352
+	*/
353
+	private function loadMenuButtons(string $active = 'packages'): void
354
+	{
355
+		$this->context['devtools_buttons'] = [
356
+			'packages' => [
357
+				'text' => 'installed_packages',
358
+				'url' => $this->scripturl . '?action=devtools;area=packages',
359
+			],
360
+			'hooks' => [
361
+				'text' => 'hooks_title_list',
362
+				'url' => $this->scripturl . '?action=devtools;area=hooks',
363
+			],
364
+		];
365
+
366
+		$this->context['devtools_buttons'][$active]['active'] ?? null;
367
+	}
368
+
369
+	/*
370
+	 * Load additional language files.
371
+	 * There are 3 way to pass multiple languages in.  A single string, SMF's tradditional + separated list or an array.
372
+	 *
373
+	 * @param $languages array|string The list of languages to load.
374
+	*/
375
+	public function loadLanguage(array|string $languages): string
376
+	{
377
+		return loadLanguage(implode('+', (array) $languages));
378
+	}
379
+
380
+	/*
381
+	 * Load additional sources files.
382
+	 *
383
+	 * @param array $sourcesThe list of additional sources to load.
384
+	*/
385
+	public function loadSources(array $sources): void
386
+	{
387
+		array_map(function($rs) {
388
+			require_once($GLOBALS['sourcedir'] . '/' . strtr($rs, ['DevTools' => 'DevTools-']) . '.php');
389
+		}, $sources);
390
+	}
391
+
392
+	/*
393
+	 * Load additional template files.
394
+	 * There are 2 way to pass multiple languages in.  A single string or an array.
395
+	 *
396
+	 * @calls: $sourcedir/Load.php:loadTemplate
397
+	 * @param $languages array|string The list of languages to load.
398
+	*/
399
+	public function loadTemplate(array|string $templates): void
400
+	{
401
+		array_map(function($t) {
402
+			loadTemplate($t);
403
+		}, (array) $templates);
404
+	}
405
+
406
+	/*
407
+	 * Loads up this class into a instance.  We use the same storage location SMF uses so SMF could also load this instance.
408
+	*/
409
+	public static function load(): self
410
+	{
411
+		if (!isset($GLOBALS['context']['instances'][__CLASS__]))
412
+			$GLOBALS['context']['instances'][__CLASS__] = new self();
413
+
414
+		return $GLOBALS['context']['instances'][__CLASS__];
415
+	}
416
+}
0 417
\ No newline at end of file
... ...
@@ -0,0 +1,58 @@
1
+<?php
2
+/**
3
+ * Template for DevTools.
4
+ * @package DevTools
5
+ * @author SleePy <sleepy @ simplemachines (dot) org>
6
+ * @copyright 2022
7
+ * @license 3-Clause BSD https://opensource.org/licenses/BSD-3-Clause
8
+ * @version 1.0
9
+ */
10
+
11
+/* The wrapper upper template */
12
+function template_devtools_above()
13
+{
14
+	global $context, $txt;
15
+
16
+	echo '
17
+	<div id="devtools_container" class="scrollable">';
18
+
19
+	if (!empty($context['saved_successful']))
20
+		echo '
21
+		<div id="devtool_success" class="infobox">', $context['saved_successful'], '</div>';
22
+
23
+	template_button_strip($context['devtools_buttons']);
24
+
25
+	echo '
26
+	<hr>';
27
+}
28
+
29
+/* The wrapper lower template */
30
+function template_devtools_below()
31
+{
32
+	echo '
33
+	</div><!-- devtools_container -->';
34
+}
35
+
36
+/* This just calls the template for showing a list on our packages */
37
+function template_packagesIndex()
38
+{
39
+	template_show_list('packages_lists_modification');
40
+}
41
+
42
+/* This just calls the template for showing data for syncing files */
43
+function template_FilesSyncIn()
44
+{
45
+	template_show_list('syncfiles_list');
46
+}
47
+
48
+/* This just calls the template for showing data for syncing files */
49
+function template_FilesSyncOut()
50
+{
51
+	template_show_list('syncfiles_list');
52
+}
53
+
54
+/* This just calls the template for showing a list on our hooks */
55
+function template_hooksIndex()
56
+{
57
+	template_show_list('hooks_list');
58
+}
0 59
\ No newline at end of file
... ...
@@ -0,0 +1,29 @@
1
+BSD 3-Clause License
2
+
3
+Copyright (c) 2022, 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,10 @@
1
+This tool is to help developers working with hooks only customizations.
2
+
3
+This gives a popup window for you to work with a package to do actions such as:
4
+	- Reinstall hooks (adds/replaces matching hooks) as defined in the packages install action
5
+	- Remove hooks (removes hooks as defined in the packages uninstall action
6
+	- Pushes files out as per the packages install action
7
+	- Pulls files in as per the packages install action
8
+
9
+This is intended for development purposes, not production uses.
10
+This customization is intended to only be used with customizations that do not modify SMF sources (boardmod or xml) and are hook only.
0 11
\ No newline at end of file
... ...
@@ -0,0 +1,45 @@
1
+<!DOCTYPE package-info SYSTEM "http://www.simplemachines.org/xml/package-info">
2
+<package-info xmlns="http://www.simplemachines.org/xml/package-info" xmlns:smf="http://www.simplemachines.org/">
3
+	<id>sleepy:devtools</id>
4
+	<name>Developer Tools</name>
5
+	<version>1.0</version>
6
+	<type>modification</type>
7
+
8
+	<install for="SMF 2.1.*">
9
+		<readme>README.txt</readme>
10
+		<require-file name="DevTools.php" destination="$sourcedir" />
11
+		<require-file name="DevTools-Packages.php" destination="$sourcedir" />
12
+		<require-file name="DevTools-Hooks.php" destination="$sourcedir" />
13
+
14
+		<require-file name="DevTools.template.php" destination="$themedir" />
15
+
16
+		<require-file name="DevTools.js" destination="$themes_dir/default/scripts" />
17
+
18
+		<require-file name="DevTools.english.php" destination="$themes_dir/default/languages" />
19
+
20
+		<hook hook="integrate_actions" function="DevTools::hook_actions" file="$sourcedir/DevTools.php" object="true" />
21
+		<hook hook="integrate_current_action" function="DevTools::hook_current_action" file="$sourcedir/DevTools.php" object="true" />
22
+		<hook hook="integrate_validateSession" function="DevTools::hook_validateSession" file="$sourcedir/DevTools.php" object="true" />
23
+		<hook hook="integrate_redirect" function="DevTools::hook_redirect" file="$sourcedir/DevTools.php" object="true" />
24
+		<hook hook="integrate_load_theme" function="DevTools::hook_load_theme" file="$sourcedir/DevTools.php" object="true" />
25
+	</install>
26
+	
27
+	<uninstall for="SMF 2.1.*">
28
+		<hook reverse="true" hook="integrate_current_action" function="DevTools::hook_current_action" file="$sourcedir/DevTools.php" object="true" />
29
+		<hook reverse="true" hook="integrate_actions" function="DevTools::main_action" file="$sourcedir/DevTools.php" object="true" />
30
+		<hook reverse="true" hook="integrate_validateSession" function="DevTools::hook_validateSession" file="$sourcedir/DevTools.php" object="true" />
31
+		<hook reverse="true" hook="integrate_redirect" function="ErrorPoDevToolspup::hook_redirect" file="$sourcedir/DevTools.php" object="true" />
32
+		<hook reverse="true" hook="integrate_load_theme" function="DevTools::hook_load_theme" file="$sourcedir/DevTools.php" object="true" />
33
+
34
+		<remove-file name="$themes_dir/default/languages/DevTools.english.php" />
35
+
36
+		<remove-file name="$themes_dir/default/scripts/DevTools.js" />
37
+
38
+		<remove-file name="$themedir/DevTools.template.php" />
39
+
40
+		<remove-file name="$sourcedir/DevTools.php" />
41
+		<remove-file name="$sourcedir/DevTools-Packages.php" />
42
+		<remove-file name="$sourcedir/DevTools-Hooks.php" />
43
+	</uninstall>
44
+
45
+</package-info>
0 46
\ No newline at end of file
1 47