Completed
Branch master (33c24b)
by
unknown
30:03
created

ParserCache::save()   C

Complexity

Conditions 7
Paths 8

Size

Total Lines 46
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 30
c 1
b 0
f 0
nc 8
nop 5
dl 0
loc 46
rs 6.7272
1
<?php
2
/**
3
 * Cache for outputs of the PHP parser
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
 * @ingroup Cache Parser
22
 */
23
24
/**
25
 * @ingroup Cache Parser
26
 * @todo document
27
 */
28
class ParserCache {
29
	/** @var BagOStuff */
30
	private $mMemc;
31
	/**
32
	 * Get an instance of this object
33
	 *
34
	 * @return ParserCache
35
	 */
36
	public static function singleton() {
37
		static $instance;
38
		if ( !isset( $instance ) ) {
39
			global $parserMemc;
40
			$instance = new ParserCache( $parserMemc );
41
		}
42
		return $instance;
43
	}
44
45
	/**
46
	 * Setup a cache pathway with a given back-end storage mechanism.
47
	 *
48
	 * This class use an invalidation strategy that is compatible with
49
	 * MultiWriteBagOStuff in async replication mode.
50
	 *
51
	 * @param BagOStuff $memCached
52
	 * @throws MWException
53
	 */
54
	protected function __construct( BagOStuff $memCached ) {
55
		$this->mMemc = $memCached;
56
	}
57
58
	/**
59
	 * @param WikiPage $article
60
	 * @param string $hash
61
	 * @return mixed|string
62
	 */
63
	protected function getParserOutputKey( $article, $hash ) {
64
		global $wgRequest;
65
66
		// idhash seem to mean 'page id' + 'rendering hash' (r3710)
67
		$pageid = $article->getId();
68
		$renderkey = (int)( $wgRequest->getVal( 'action' ) == 'render' );
69
70
		$key = wfMemcKey( 'pcache', 'idhash', "{$pageid}-{$renderkey}!{$hash}" );
71
		return $key;
72
	}
73
74
	/**
75
	 * @param WikiPage $article
76
	 * @return mixed|string
77
	 */
78
	protected function getOptionsKey( $article ) {
79
		$pageid = $article->getId();
80
		return wfMemcKey( 'pcache', 'idoptions', "{$pageid}" );
81
	}
82
83
	/**
84
	 * Provides an E-Tag suitable for the whole page. Note that $article
85
	 * is just the main wikitext. The E-Tag has to be unique to the whole
86
	 * page, even if the article itself is the same, so it uses the
87
	 * complete set of user options. We don't want to use the preference
88
	 * of a different user on a message just because it wasn't used in
89
	 * $article. For example give a Chinese interface to a user with
90
	 * English preferences. That's why we take into account *all* user
91
	 * options. (r70809 CR)
92
	 *
93
	 * @param WikiPage $article
94
	 * @param ParserOptions $popts
95
	 * @return string
96
	 */
97
	public function getETag( $article, $popts ) {
98
		return 'W/"' . $this->getParserOutputKey( $article,
99
			$popts->optionsHash( ParserOptions::legacyOptions(), $article->getTitle() ) ) .
100
				"--" . $article->getTouched() . '"';
101
	}
102
103
	/**
104
	 * Retrieve the ParserOutput from ParserCache, even if it's outdated.
105
	 * @param WikiPage $article
106
	 * @param ParserOptions $popts
107
	 * @return ParserOutput|bool False on failure
108
	 */
109
	public function getDirty( $article, $popts ) {
110
		$value = $this->get( $article, $popts, true );
111
		return is_object( $value ) ? $value : false;
112
	}
113
114
	/**
115
	 * Generates a key for caching the given article considering
116
	 * the given parser options.
117
	 *
118
	 * @note Which parser options influence the cache key
119
	 * is controlled via ParserOutput::recordOption() or
120
	 * ParserOptions::addExtraKey().
121
	 *
122
	 * @note Used by Article to provide a unique id for the PoolCounter.
123
	 * It would be preferable to have this code in get()
124
	 * instead of having Article looking in our internals.
125
	 *
126
	 * @todo Document parameter $useOutdated
127
	 *
128
	 * @param WikiPage $article
129
	 * @param ParserOptions $popts
130
	 * @param bool $useOutdated (default true)
131
	 * @return bool|mixed|string
132
	 */
133
	public function getKey( $article, $popts, $useOutdated = true ) {
134
		global $wgCacheEpoch;
135
136
		if ( $popts instanceof User ) {
137
			wfWarn( "Use of outdated prototype ParserCache::getKey( &\$article, &\$user )\n" );
138
			$popts = ParserOptions::newFromUser( $popts );
139
		}
140
141
		// Determine the options which affect this article
142
		$casToken = null;
143
		$optionsKey = $this->mMemc->get(
144
			$this->getOptionsKey( $article ), $casToken, BagOStuff::READ_VERIFIED );
145
		if ( $optionsKey instanceof CacheTime ) {
146
			if ( !$useOutdated && $optionsKey->expired( $article->getTouched() ) ) {
147
				wfIncrStats( "pcache.miss.expired" );
148
				$cacheTime = $optionsKey->getCacheTime();
149
				wfDebugLog( "ParserCache",
150
					"Parser options key expired, touched " . $article->getTouched()
151
					. ", epoch $wgCacheEpoch, cached $cacheTime\n" );
152
				return false;
153 View Code Duplication
			} elseif ( !$useOutdated && $optionsKey->isDifferentRevision( $article->getLatest() ) ) {
154
				wfIncrStats( "pcache.miss.revid" );
155
				$revId = $article->getLatest();
156
				$cachedRevId = $optionsKey->getCacheRevisionId();
157
				wfDebugLog( "ParserCache",
158
					"ParserOutput key is for an old revision, latest $revId, cached $cachedRevId\n"
159
				);
160
				return false;
161
			}
162
163
			// $optionsKey->mUsedOptions is set by save() by calling ParserOutput::getUsedOptions()
164
			$usedOptions = $optionsKey->mUsedOptions;
165
			wfDebug( "Parser cache options found.\n" );
166
		} else {
167
			if ( !$useOutdated ) {
168
				return false;
169
			}
170
			$usedOptions = ParserOptions::legacyOptions();
171
		}
172
173
		return $this->getParserOutputKey(
174
			$article,
175
			$popts->optionsHash( $usedOptions, $article->getTitle() )
0 ignored issues
show
Bug introduced by
It seems like $usedOptions defined by $optionsKey->mUsedOptions on line 164 can also be of type boolean; however, ParserOptions::optionsHash() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
176
		);
177
	}
178
179
	/**
180
	 * Retrieve the ParserOutput from ParserCache.
181
	 * false if not found or outdated.
182
	 *
183
	 * @param WikiPage|Article $article
184
	 * @param ParserOptions $popts
185
	 * @param bool $useOutdated (default false)
186
	 *
187
	 * @return ParserOutput|bool False on failure
188
	 */
189
	public function get( $article, $popts, $useOutdated = false ) {
190
		global $wgCacheEpoch;
191
192
		$canCache = $article->checkTouched();
193
		if ( !$canCache ) {
194
			// It's a redirect now
195
			return false;
196
		}
197
198
		$touched = $article->getTouched();
199
200
		$parserOutputKey = $this->getKey( $article, $popts, $useOutdated );
0 ignored issues
show
Bug introduced by
It seems like $article defined by parameter $article on line 189 can also be of type object<Article>; however, ParserCache::getKey() does only seem to accept object<WikiPage>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and 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...
201
		if ( $parserOutputKey === false ) {
202
			wfIncrStats( 'pcache.miss.absent' );
203
			return false;
204
		}
205
206
		$casToken = null;
207
		/** @var ParserOutput $value */
208
		$value = $this->mMemc->get( $parserOutputKey, $casToken, BagOStuff::READ_VERIFIED );
209
		if ( !$value ) {
210
			wfDebug( "ParserOutput cache miss.\n" );
211
			wfIncrStats( "pcache.miss.absent" );
212
			return false;
213
		}
214
215
		wfDebug( "ParserOutput cache found.\n" );
216
217
		// The edit section preference may not be the appropiate one in
218
		// the ParserOutput, as we are not storing it in the parsercache
219
		// key. Force it here. See bug 31445.
220
		$value->setEditSectionTokens( $popts->getEditSection() );
221
222
		$wikiPage = method_exists( $article, 'getPage' )
223
			? $article->getPage()
0 ignored issues
show
Bug introduced by
The method getPage does only exist in Article, but not in WikiPage.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
224
			: $article;
225
226
		if ( !$useOutdated && $value->expired( $touched ) ) {
227
			wfIncrStats( "pcache.miss.expired" );
228
			$cacheTime = $value->getCacheTime();
229
			wfDebugLog( "ParserCache",
230
				"ParserOutput key expired, touched $touched, "
231
				. "epoch $wgCacheEpoch, cached $cacheTime\n" );
232
			$value = false;
233 View Code Duplication
		} elseif ( !$useOutdated && $value->isDifferentRevision( $article->getLatest() ) ) {
234
			wfIncrStats( "pcache.miss.revid" );
235
			$revId = $article->getLatest();
236
			$cachedRevId = $value->getCacheRevisionId();
237
			wfDebugLog( "ParserCache",
238
				"ParserOutput key is for an old revision, latest $revId, cached $cachedRevId\n"
239
			);
240
			$value = false;
241
		} elseif (
242
			Hooks::run( 'RejectParserCacheValue', [ $value, $wikiPage, $popts ] ) === false
243
		) {
244
			wfIncrStats( 'pcache.miss.rejected' );
245
			wfDebugLog( "ParserCache",
246
				"ParserOutput key valid, but rejected by RejectParserCacheValue hook handler.\n"
247
			);
248
			$value = false;
249
		} else {
250
			wfIncrStats( "pcache.hit" );
251
		}
252
253
		return $value;
254
	}
255
256
	/**
257
	 * @param ParserOutput $parserOutput
258
	 * @param WikiPage $page
259
	 * @param ParserOptions $popts
260
	 * @param string $cacheTime Time when the cache was generated
261
	 * @param int $revId Revision ID that was parsed
262
	 */
263
	public function save( $parserOutput, $page, $popts, $cacheTime = null, $revId = null ) {
264
		$expire = $parserOutput->getCacheExpiry();
265
		if ( $expire > 0 && !$this->mMemc instanceof EmptyBagOStuff ) {
266
			$cacheTime = $cacheTime ?: wfTimestampNow();
267
			if ( !$revId ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $revId of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
268
				$revision = $page->getRevision();
269
				$revId = $revision ? $revision->getId() : null;
270
			}
271
272
			$optionsKey = new CacheTime;
273
			$optionsKey->mUsedOptions = $parserOutput->getUsedOptions();
274
			$optionsKey->updateCacheExpiry( $expire );
275
276
			$optionsKey->setCacheTime( $cacheTime );
0 ignored issues
show
Security Bug introduced by
It seems like $cacheTime defined by $cacheTime ?: wfTimestampNow() on line 266 can also be of type false; however, CacheTime::setCacheTime() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
277
			$parserOutput->setCacheTime( $cacheTime );
0 ignored issues
show
Security Bug introduced by
It seems like $cacheTime defined by $cacheTime ?: wfTimestampNow() on line 266 can also be of type false; however, CacheTime::setCacheTime() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
278
			$optionsKey->setCacheRevisionId( $revId );
279
			$parserOutput->setCacheRevisionId( $revId );
280
281
			$parserOutputKey = $this->getParserOutputKey( $page,
282
				$popts->optionsHash( $optionsKey->mUsedOptions, $page->getTitle() ) );
283
284
			// Save the timestamp so that we don't have to load the revision row on view
285
			$parserOutput->setTimestamp( $page->getTimestamp() );
286
287
			$msg = "Saved in parser cache with key $parserOutputKey" .
288
				" and timestamp $cacheTime" .
289
				" and revision id $revId" .
290
				"\n";
291
292
			$parserOutput->mText .= "\n<!-- $msg -->\n";
293
			wfDebug( $msg );
294
295
			// Save the parser output
296
			$this->mMemc->set( $parserOutputKey, $parserOutput, $expire );
297
298
			// ...and its pointer
299
			$this->mMemc->set( $this->getOptionsKey( $page ), $optionsKey, $expire );
300
301
			Hooks::run(
302
				'ParserCacheSaveComplete',
303
				[ $this, $parserOutput, $page->getTitle(), $popts, $revId ]
304
			);
305
		} elseif ( $expire <= 0 ) {
306
			wfDebug( "Parser output was marked as uncacheable and has not been saved.\n" );
307
		}
308
	}
309
}
310