Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
3.03% covered (danger)
3.03%
3 / 99
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
3.03% covered (danger)
3.03%
3 / 99
50.00% covered (danger)
50.00%
1 / 2
143.30
0.00% covered (danger)
0.00%
0 / 1
 onParserFirstCallInit
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 parserFunctionCallback
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
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
12namespace MediaWiki\Extension\WikifunctionsClient;
13
14// TODO (T371027): We shouldn't have cross-extension dependencies.
15use MediaWiki\Extension\WikiLambda\ActionAPI\ApiFunctionCall;
16use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry;
17use MediaWiki\Extension\WikiLambda\WikiLambdaServices;
18use MediaWiki\Extension\WikiLambda\ZErrorException;
19use MediaWiki\Extension\WikiLambda\ZObjects\ZFunctionCall;
20use MediaWiki\Html\Html;
21use MediaWiki\MediaWikiServices;
22use MediaWiki\Parser\Parser;
23use MediaWiki\Parser\Sanitizer;
24use MediaWiki\Title\Title;
25use PPFrame;
26
27class 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}