Completed
Branch master (f93894)
by
unknown
27:35
created

ResourceLoaderWikiModule::expandVariables()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 5
c 1
b 0
f 1
nc 2
nop 2
dl 0
loc 8
rs 9.4285
1
<?php
2
/**
3
 * Abstraction for ResourceLoader modules that pull from wiki pages.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @author Trevor Parscal
22
 * @author Roan Kattouw
23
 */
24
25
/**
26
 * Abstraction for ResourceLoader modules which pull from wiki pages
27
 *
28
 * This can only be used for wiki pages in the MediaWiki and User namespaces,
29
 * because of its dependence on the functionality of Title::isCssJsSubpage.
30
 *
31
 * This module supports being used as a placeholder for a module on a remote wiki.
32
 * To do so, getDB() must be overloaded to return a foreign database object that
33
 * allows local wikis to query page metadata.
34
 *
35
 * Safe for calls on local wikis are:
36
 * - Option getters:
37
 *   - getGroup()
38
 *   - getPosition()
39
 *   - getPages()
40
 * - Basic methods that strictly involve the foreign database
41
 *   - getDB()
42
 *   - isKnownEmpty()
43
 *   - getTitleInfo()
44
 */
45
class ResourceLoaderWikiModule extends ResourceLoaderModule {
46
	/** @var string Position on the page to load this module at */
47
	protected $position = 'bottom';
48
49
	// Origin defaults to users with sitewide authority
50
	protected $origin = self::ORIGIN_USER_SITEWIDE;
51
52
	// In-process cache for title info
53
	protected $titleInfo = [];
54
55
	// List of page names that contain CSS
56
	protected $styles = [];
57
58
	// List of page names that contain JavaScript
59
	protected $scripts = [];
60
61
	// Group of module
62
	protected $group;
63
64
	/**
65
	 * @param array $options For back-compat, this can be omitted in favour of overwriting getPages.
66
	 */
67
	public function __construct( array $options = null ) {
68
		if ( is_null( $options ) ) {
69
			return;
70
		}
71
72
		foreach ( $options as $member => $option ) {
73
			switch ( $member ) {
74
				case 'position':
75
				case 'styles':
76
				case 'scripts':
77
				case 'group':
78
				case 'targets':
79
					$this->{$member} = $option;
80
					break;
81
			}
82
		}
83
	}
84
85
	/**
86
	 * Subclasses should return an associative array of resources in the module.
87
	 * Keys should be the title of a page in the MediaWiki or User namespace.
88
	 *
89
	 * Values should be a nested array of options.  The supported keys are 'type' and
90
	 * (CSS only) 'media'.
91
	 *
92
	 * For scripts, 'type' should be 'script'.
93
	 *
94
	 * For stylesheets, 'type' should be 'style'.
95
	 * There is an optional media key, the value of which can be the
96
	 * medium ('screen', 'print', etc.) of the stylesheet.
97
	 *
98
	 * @param ResourceLoaderContext $context
99
	 * @return array
100
	 */
101
	protected function getPages( ResourceLoaderContext $context ) {
102
		$config = $this->getConfig();
103
		$pages = [];
104
105
		// Filter out pages from origins not allowed by the current wiki configuration.
106
		if ( $config->get( 'UseSiteJs' ) ) {
107
			foreach ( $this->scripts as $script ) {
108
				$pages[$script] = [ 'type' => 'script' ];
109
			}
110
		}
111
112
		if ( $config->get( 'UseSiteCss' ) ) {
113
			foreach ( $this->styles as $style ) {
114
				$pages[$style] = [ 'type' => 'style' ];
115
			}
116
		}
117
118
		return $pages;
119
	}
120
121
	/**
122
	 * Get group name
123
	 *
124
	 * @return string
125
	 */
126
	public function getGroup() {
127
		return $this->group;
128
	}
129
130
	/**
131
	 * Get the Database object used in getTitleInfo().
132
	 *
133
	 * Defaults to the local slave DB. Subclasses may want to override this to return a foreign
134
	 * database object, or null if getTitleInfo() shouldn't access the database.
135
	 *
136
	 * NOTE: This ONLY works for getTitleInfo() and isKnownEmpty(), NOT FOR ANYTHING ELSE.
137
	 * In particular, it doesn't work for getContent() or getScript() etc.
138
	 *
139
	 * @return IDatabase|null
140
	 */
141
	protected function getDB() {
142
		return wfGetDB( DB_SLAVE );
143
	}
144
145
	/**
146
	 * @param string $title
0 ignored issues
show
Bug introduced by
There is no parameter named $title. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
147
	 * @return null|string
148
	 */
149
	protected function getContent( $titleText ) {
150
		$title = Title::newFromText( $titleText );
151
		if ( !$title ) {
152
			return null;
153
		}
154
155
		$handler = ContentHandler::getForTitle( $title );
156
		if ( $handler->isSupportedFormat( CONTENT_FORMAT_CSS ) ) {
157
			$format = CONTENT_FORMAT_CSS;
158
		} elseif ( $handler->isSupportedFormat( CONTENT_FORMAT_JAVASCRIPT ) ) {
159
			$format = CONTENT_FORMAT_JAVASCRIPT;
160
		} else {
161
			return null;
162
		}
163
164
		$revision = Revision::newFromTitle( $title, false, Revision::READ_NORMAL );
165
		if ( !$revision ) {
166
			return null;
167
		}
168
169
		$content = $revision->getContent( Revision::RAW );
170
171
		if ( !$content ) {
172
			wfDebugLog( 'resourceloader', __METHOD__ . ': failed to load content of JS/CSS page!' );
173
			return null;
174
		}
175
176
		return $content->serialize( $format );
177
	}
178
179
	/**
180
	 * @param ResourceLoaderContext $context
181
	 * @return string
182
	 */
183
	public function getScript( ResourceLoaderContext $context ) {
184
		$scripts = '';
185
		foreach ( $this->getPages( $context ) as $titleText => $options ) {
186
			if ( $options['type'] !== 'script' ) {
187
				continue;
188
			}
189
			$script = $this->getContent( $titleText );
190
			if ( strval( $script ) !== '' ) {
191
				$script = $this->validateScriptFile( $titleText, $script );
192
				$scripts .= ResourceLoader::makeComment( $titleText ) . $script . "\n";
193
			}
194
		}
195
		return $scripts;
196
	}
197
198
	/**
199
	 * @param ResourceLoaderContext $context
200
	 * @return array
201
	 */
202
	public function getStyles( ResourceLoaderContext $context ) {
203
		$styles = [];
204
		foreach ( $this->getPages( $context ) as $titleText => $options ) {
205
			if ( $options['type'] !== 'style' ) {
206
				continue;
207
			}
208
			$media = isset( $options['media'] ) ? $options['media'] : 'all';
209
			$style = $this->getContent( $titleText );
210
			if ( strval( $style ) === '' ) {
211
				continue;
212
			}
213
			if ( $this->getFlip( $context ) ) {
214
				$style = CSSJanus::transform( $style, true, false );
215
			}
216
			$style = MemoizedCallable::call( 'CSSMin::remap',
217
				[ $style, false, $this->getConfig()->get( 'ScriptPath' ), true ] );
218
			if ( !isset( $styles[$media] ) ) {
219
				$styles[$media] = [];
220
			}
221
			$style = ResourceLoader::makeComment( $titleText ) . $style;
222
			$styles[$media][] = $style;
223
		}
224
		return $styles;
225
	}
226
227
	/**
228
	 * Disable module content versioning.
229
	 *
230
	 * This class does not support generating content outside of a module
231
	 * request due to foreign database support.
232
	 *
233
	 * See getDefinitionSummary() for meta-data versioning.
234
	 *
235
	 * @return bool
236
	 */
237
	public function enableModuleContentVersion() {
238
		return false;
239
	}
240
241
	/**
242
	 * @param ResourceLoaderContext $context
243
	 * @return array
244
	 */
245
	public function getDefinitionSummary( ResourceLoaderContext $context ) {
246
		$summary = parent::getDefinitionSummary( $context );
247
		$summary[] = [
248
			'pages' => $this->getPages( $context ),
249
			// Includes SHA1 of content
250
			'titleInfo' => $this->getTitleInfo( $context ),
251
		];
252
		return $summary;
253
	}
254
255
	/**
256
	 * @param ResourceLoaderContext $context
257
	 * @return bool
258
	 */
259
	public function isKnownEmpty( ResourceLoaderContext $context ) {
260
		$revisions = $this->getTitleInfo( $context );
261
262
		// For user modules, don't needlessly load if there are no non-empty pages
263
		if ( $this->getGroup() === 'user' ) {
264
			foreach ( $revisions as $revision ) {
265
				if ( $revision['rev_len'] > 0 ) {
266
					// At least one non-empty page, module should be loaded
267
					return false;
268
				}
269
			}
270
			return true;
271
		}
272
273
		// Bug 68488: For other modules (i.e. ones that are called in cached html output) only check
274
		// page existance. This ensures that, if some pages in a module are temporarily blanked,
275
		// we don't end omit the module's script or link tag on some pages.
276
		return count( $revisions ) === 0;
277
	}
278
279
	/**
280
	 * Get the information about the wiki pages for a given context.
281
	 * @param ResourceLoaderContext $context
282
	 * @return array Keyed by page name. Contains arrays with 'rev_len' and 'rev_sha1' keys
283
	 */
284
	protected function getTitleInfo( ResourceLoaderContext $context ) {
285
		$dbr = $this->getDB();
286
		if ( !$dbr ) {
287
			// We're dealing with a subclass that doesn't have a DB
288
			return [];
289
		}
290
291
		$pages = $this->getPages( $context );
292
		$key = implode( '|', array_keys( $pages ) );
293
		if ( !isset( $this->titleInfo[$key] ) ) {
294
			$this->titleInfo[$key] = [];
295
			$batch = new LinkBatch;
296
			foreach ( $pages as $titleText => $options ) {
297
				$batch->addObj( Title::newFromText( $titleText ) );
0 ignored issues
show
Bug introduced by
It seems like \Title::newFromText($titleText) can be null; however, addObj() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
298
			}
299
300
			if ( !$batch->isEmpty() ) {
301
				$res = $dbr->select( [ 'page', 'revision' ],
302
					// Include page_touched to allow purging if cache is poisoned (T117587, T113916)
303
					[ 'page_namespace', 'page_title', 'page_touched', 'rev_len', 'rev_sha1' ],
304
					$batch->constructSet( 'page', $dbr ),
0 ignored issues
show
Bug introduced by
It seems like $batch->constructSet('page', $dbr) targeting LinkBatch::constructSet() can also be of type boolean; however, DatabaseBase::select() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
305
					__METHOD__,
306
					[],
307
					[ 'revision' => [ 'INNER JOIN', [ 'page_latest=rev_id' ] ] ]
308
				);
309
				foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
310
					// Avoid including ids or timestamps of revision/page tables so
311
					// that versions are not wasted
312
					$title = Title::makeTitle( $row->page_namespace, $row->page_title );
313
					$this->titleInfo[$key][$title->getPrefixedText()] = [
314
						'rev_len' => $row->rev_len,
315
						'rev_sha1' => $row->rev_sha1,
316
						'page_touched' => $row->page_touched,
317
					];
318
				}
319
			}
320
		}
321
		return $this->titleInfo[$key];
322
	}
323
324
	public function getPosition() {
325
		return $this->position;
326
	}
327
}
328