Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
3.03% |
3 / 99 |
|
50.00% |
1 / 2 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
3.03% |
3 / 99 |
|
50.00% |
1 / 2 |
143.30 | |
0.00% |
0 / 1 |
onParserFirstCallInit | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
parserFunctionCallback | |
0.00% |
0 / 96 |
|
0.00% |
0 / 1 |
110 |
1 | <?php |
2 | |
3 | /** |
4 | * WikifunctionsClient extension Parser-related hooks |
5 | * |
6 | * @file |
7 | * @ingroup Extensions |
8 | * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt |
9 | * @license MIT |
10 | */ |
11 | |
12 | namespace MediaWiki\Extension\WikifunctionsClient; |
13 | |
14 | // TODO (T371027): We shouldn't have cross-extension dependencies. |
15 | use MediaWiki\Extension\WikiLambda\ActionAPI\ApiFunctionCall; |
16 | use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry; |
17 | use MediaWiki\Extension\WikiLambda\WikiLambdaServices; |
18 | use MediaWiki\Extension\WikiLambda\ZErrorException; |
19 | use MediaWiki\Extension\WikiLambda\ZObjects\ZFunctionCall; |
20 | use MediaWiki\Html\Html; |
21 | use MediaWiki\MediaWikiServices; |
22 | use MediaWiki\Parser\Parser; |
23 | use MediaWiki\Parser\Sanitizer; |
24 | use MediaWiki\Title\Title; |
25 | use PPFrame; |
26 | |
27 | class Hooks implements |
28 | \MediaWiki\Hook\ParserFirstCallInitHook |
29 | { |
30 | |
31 | /** |
32 | * Register {{#function:…}} as a wikitext parser function to trigger function evaluation. |
33 | * |
34 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/ParserFirstCallInit |
35 | * |
36 | * @param Parser $parser |
37 | */ |
38 | public function onParserFirstCallInit( $parser ) { |
39 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
40 | if ( $config->get( 'WikifunctionsClientEnableParserFunction' ) ) { |
41 | $parser->setFunctionHook( 'function', [ self::class, 'parserFunctionCallback' ], Parser::SFH_OBJECT_ARGS ); |
42 | } |
43 | } |
44 | |
45 | /** |
46 | * @param Parser $parser |
47 | * @param PPFrame $frame |
48 | * @param array $args |
49 | * @return array |
50 | */ |
51 | public static function parserFunctionCallback( Parser $parser, $frame, $args = [] ) { |
52 | // TODO (T362251): Turn $args into the request more properly. |
53 | |
54 | $cleanupInput = static function ( $input ) use ( $frame ) { |
55 | return trim( Sanitizer::decodeCharReferences( $frame->expand( $input ) ) ); |
56 | }; |
57 | |
58 | $cleanedArgs = array_map( $cleanupInput, $args ); |
59 | |
60 | $target = $cleanedArgs[0]; |
61 | |
62 | // TODO (T371027): We shouldn't have the logic for what functions exist here, but in WikiLambda. |
63 | $zObjectStore = WikiLambdaServices::getZObjectStore(); |
64 | $targetTitle = Title::newFromText( $target, NS_MAIN ); |
65 | if ( !( $targetTitle->exists() ) ) { |
66 | // User is trying to use a function that doesn't exist |
67 | $ret = Html::errorBox( |
68 | wfMessage( |
69 | 'wikifunctionsclient-functioncall-error-unknown', |
70 | $target |
71 | )->parseAsBlock() |
72 | ); |
73 | $parser->addTrackingCategory( 'wikifunctionsclient-functioncall-error-unknown-category' ); |
74 | return [ $ret ]; |
75 | } |
76 | |
77 | $targetObject = $zObjectStore->fetchZObjectByTitle( $targetTitle ); |
78 | |
79 | // ZObjectStore's fetchZObjectByTitle() will return a ZObjectContent, so just check it's a valid ZObject |
80 | if ( !$targetObject->isValid() ) { |
81 | // User is trying to use an invalid ZObject or somehow non-ZObject in our namespace |
82 | $ret = Html::errorBox( |
83 | wfMessage( |
84 | 'wikifunctionsclient-functioncall-error-invalid-zobject', |
85 | $target |
86 | )->parseAsBlock() |
87 | ); |
88 | // Triggers use of message wikifunctionsclient-functioncall-error-invalid-zobject-category-desc |
89 | $parser->addTrackingCategory( 'wikifunctionsclient-functioncall-error-invalid-zobject-category' ); |
90 | return [ $ret ]; |
91 | } |
92 | |
93 | if ( $targetObject->getZType() !== ZTypeRegistry::Z_FUNCTION ) { |
94 | // User is trying to use a ZObject that's not a ZFunction |
95 | $ret = Html::errorBox( |
96 | wfMessage( |
97 | 'wikifunctionsclient-functioncall-error-nonfunction', |
98 | $target |
99 | )->parseAsBlock() |
100 | ); |
101 | // Triggers use of message wikifunctionsclient-functioncall-error-nonfunction-category-desc |
102 | $parser->addTrackingCategory( 'wikifunctionsclient-functioncall-error-nonfunction-category' ); |
103 | return [ $ret ]; |
104 | } |
105 | |
106 | $targetFunction = $targetObject->getInnerZObject(); |
107 | |
108 | if ( |
109 | $targetFunction->getValueByKey( ZTypeRegistry::Z_FUNCTION_RETURN_TYPE )->getZValue() |
110 | !== ZTypeRegistry::Z_STRING |
111 | ) { |
112 | // User is trying to use a ZFunction that returns something other than a Z6/String |
113 | $ret = Html::errorBox( |
114 | wfMessage( |
115 | 'wikifunctionsclient-functioncall-error-nonstringoutput', |
116 | $target |
117 | )->parseAsBlock() |
118 | ); |
119 | // Triggers use of message wikifunctionsclient-functioncall-error-nonstringoutput-category-desc |
120 | $parser->addTrackingCategory( 'wikifunctionsclient-functioncall-error-nonstringoutput-category' ); |
121 | return [ $ret ]; |
122 | } |
123 | |
124 | // TODO (T362252): Check for and use renderers rather than check for Z6 output |
125 | $targetFunctionArguments = $targetFunction->getValueByKey( ZTypeRegistry::Z_FUNCTION_ARGUMENTS ); |
126 | '@phan-var \MediaWiki\Extension\WikiLambda\ZObjects\ZTypedList $targetFunctionArguments'; |
127 | $nonStringArgumentsDefinition = array_filter( |
128 | $targetFunctionArguments->getAsArray(), |
129 | static function ( $arg_value ) { |
130 | return !( |
131 | is_object( $arg_value ) |
132 | && $arg_value->getValueByKey( ZTypeRegistry::Z_OBJECT_TYPE )->getZValue() |
133 | === ZTypeRegistry::Z_ARGUMENTDECLARATION |
134 | && $arg_value->getValueByKey( ZTypeRegistry::Z_ARGUMENTDECLARATION_TYPE )->getZValue() |
135 | === ZTypeRegistry::Z_STRING |
136 | ); |
137 | }, |
138 | ARRAY_FILTER_USE_BOTH |
139 | ); |
140 | |
141 | if ( count( $nonStringArgumentsDefinition ) ) { |
142 | |
143 | // TODO (T362252): Would be nice to deal with multiple |
144 | $nonStringArgumentDefinition = $nonStringArgumentsDefinition[0]; |
145 | |
146 | $nonStringArgumentType = $nonStringArgumentDefinition->getValueByKey( |
147 | ZTypeRegistry::Z_ARGUMENTDECLARATION_TYPE |
148 | ); |
149 | $nonStringArgument = $nonStringArgumentDefinition->getValueByKey( |
150 | ZTypeRegistry::Z_ARGUMENTDECLARATION_ID |
151 | ); |
152 | |
153 | // User is trying to use a ZFunction that takes something other than a Z6/String |
154 | $ret = Html::errorBox( |
155 | wfMessage( |
156 | 'wikifunctionsclient-functioncall-error-nonstringinput', |
157 | $target, |
158 | $nonStringArgument, |
159 | $nonStringArgumentType |
160 | )->parseAsBlock() |
161 | ); |
162 | // Triggers use of message wikifunctionsclient-functioncall-error-nonstringinput-category-desc |
163 | $parser->addTrackingCategory( 'wikifunctionsclient-functioncall-error-nonstringinput-category' ); |
164 | return [ $ret ]; |
165 | } |
166 | |
167 | $arguments = array_slice( $cleanedArgs, 1 ); |
168 | |
169 | // TODO (T371027): We shouldn't have the logic for making the call here, but in WikiLambda. |
170 | $call = ( new ZFunctionCall( $target, $arguments ) )->getSerialized(); |
171 | |
172 | // TODO (T362254): We want a much finer control on execution time than this. |
173 | // TODO (T362254): Actually do this, or something similar? |
174 | // set_time_limit( 1 ); |
175 | // TODO (T362256): We should retain this object for re-use if there's more than one call per page. |
176 | try { |
177 | $ret = [ |
178 | ApiFunctionCall::makeRequest( $call ), |
179 | /* Force content to be escaped */ 'nowiki' |
180 | ]; |
181 | } catch ( \Throwable $th ) { |
182 | $parser->addTrackingCategory( 'wikifunctionsclient-functioncall-error-category' ); |
183 | if ( $th instanceof ZErrorException ) { |
184 | // TODO (T362236): Pass in the rendering language as a parameter, don't default to English |
185 | $errorMessage = $th->getZErrorMessage(); |
186 | } else { |
187 | // Something went wrong elsewhere; no nice translatable ZError to show, sadly. |
188 | $errorMessage = $th->getMessage(); |
189 | } |
190 | |
191 | $ret = Html::errorBox( |
192 | wfMessage( 'wikifunctionsclient-functioncall-error', $errorMessage )->parseAsBlock() |
193 | ); |
194 | } finally { |
195 | // Restore time limits to status quo. |
196 | // TODO (T362254): Actually do this, or something similar? |
197 | // set_time_limit( 0 ); |
198 | } |
199 | |
200 | return [ |
201 | trim( $ret ), |
202 | /* Force content to be escaped */ 'nowiki' |
203 | ]; |
204 | } |
205 | } |