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 |