* (bug 24563) Entries on Special:WhatLinksHere now have a link to their history
[lhc/web/wiklou.git] / includes / BacklinkCache.php
1 <?php
2 /**
3 * Class for fetching backlink lists, approximate backlink counts and partitions.
4 * Instances of this class should typically be fetched with $title->getBacklinkCache().
5 *
6 * Ideally you should only get your backlinks from here when you think there is some
7 * advantage in caching them. Otherwise it's just a waste of memory.
8 */
9 class BacklinkCache {
10 var $partitionCache = array();
11 var $fullResultCache = array();
12 var $title;
13 var $db;
14
15 const CACHE_EXPIRY = 3600;
16
17 /**
18 * Create a new BacklinkCache
19 */
20 function __construct( $title ) {
21 $this->title = $title;
22 }
23
24 /**
25 * Clear locally stored data
26 */
27 function clear() {
28 $this->partitionCache = array();
29 $this->fullResultCache = array();
30 unset( $this->db );
31 }
32
33 /**
34 * Set the Database object to use
35 */
36 public function setDB( $db ) {
37 $this->db = $db;
38 }
39
40 protected function getDB() {
41 if ( !isset( $this->db ) ) {
42 $this->db = wfGetDB( DB_SLAVE );
43 }
44
45 return $this->db;
46 }
47
48 /**
49 * Get the backlinks for a given table. Cached in process memory only.
50 * @param $table String
51 * @param $startId Integer or false
52 * @param $endId Integer or false
53 * @return TitleArray
54 */
55 public function getLinks( $table, $startId = false, $endId = false ) {
56 wfProfileIn( __METHOD__ );
57
58 $fromField = $this->getPrefix( $table ) . '_from';
59
60 if ( $startId || $endId ) {
61 // Partial range, not cached
62 wfDebug( __METHOD__ . ": from DB (uncacheable range)\n" );
63 $conds = $this->getConditions( $table );
64
65 // Use the from field in the condition rather than the joined page_id,
66 // because databases are stupid and don't necessarily propagate indexes.
67 if ( $startId ) {
68 $conds[] = "$fromField >= " . intval( $startId );
69 }
70
71 if ( $endId ) {
72 $conds[] = "$fromField <= " . intval( $endId );
73 }
74
75 $res = $this->getDB()->select(
76 array( $table, 'page' ),
77 array( 'page_namespace', 'page_title', 'page_id' ),
78 $conds,
79 __METHOD__,
80 array(
81 'STRAIGHT_JOIN',
82 'ORDER BY' => $fromField
83 ) );
84 $ta = TitleArray::newFromResult( $res );
85
86 wfProfileOut( __METHOD__ );
87 return $ta;
88 }
89
90 if ( !isset( $this->fullResultCache[$table] ) ) {
91 wfDebug( __METHOD__ . ": from DB\n" );
92 $res = $this->getDB()->select(
93 array( $table, 'page' ),
94 array( 'page_namespace', 'page_title', 'page_id' ),
95 $this->getConditions( $table ),
96 __METHOD__,
97 array(
98 'STRAIGHT_JOIN',
99 'ORDER BY' => $fromField,
100 ) );
101 $this->fullResultCache[$table] = $res;
102 }
103
104 $ta = TitleArray::newFromResult( $this->fullResultCache[$table] );
105
106 wfProfileOut( __METHOD__ );
107 return $ta;
108 }
109
110 /**
111 * Get the field name prefix for a given table
112 */
113 protected function getPrefix( $table ) {
114 static $prefixes = array(
115 'pagelinks' => 'pl',
116 'imagelinks' => 'il',
117 'categorylinks' => 'cl',
118 'templatelinks' => 'tl',
119 'redirect' => 'rd',
120 );
121
122 if ( isset( $prefixes[$table] ) ) {
123 return $prefixes[$table];
124 } else {
125 throw new MWException( "Invalid table \"$table\" in " . __CLASS__ );
126 }
127 }
128
129 /**
130 * Get the SQL condition array for selecting backlinks, with a join on the page table
131 */
132 protected function getConditions( $table ) {
133 $prefix = $this->getPrefix( $table );
134
135 switch ( $table ) {
136 case 'pagelinks':
137 case 'templatelinks':
138 case 'redirect':
139 $conds = array(
140 "{$prefix}_namespace" => $this->title->getNamespace(),
141 "{$prefix}_title" => $this->title->getDBkey(),
142 "page_id={$prefix}_from"
143 );
144 break;
145 case 'imagelinks':
146 $conds = array(
147 'il_to' => $this->title->getDBkey(),
148 'page_id=il_from'
149 );
150 break;
151 case 'categorylinks':
152 $conds = array(
153 'cl_to' => $this->title->getDBkey(),
154 'page_id=cl_from',
155 );
156 break;
157 default:
158 throw new MWException( "Invalid table \"$table\" in " . __CLASS__ );
159 }
160
161 return $conds;
162 }
163
164 /**
165 * Get the approximate number of backlinks
166 */
167 public function getNumLinks( $table ) {
168 if ( isset( $this->fullResultCache[$table] ) ) {
169 return $this->fullResultCache[$table]->numRows();
170 }
171
172 if ( isset( $this->partitionCache[$table] ) ) {
173 $entry = reset( $this->partitionCache[$table] );
174 return $entry['numRows'];
175 }
176
177 $titleArray = $this->getLinks( $table );
178
179 return $titleArray->count();
180 }
181
182 /**
183 * Partition the backlinks into batches.
184 * Returns an array giving the start and end of each range. The first batch has
185 * a start of false, and the last batch has an end of false.
186 *
187 * @param $table String: the links table name
188 * @param $batchSize Integer
189 * @return Array
190 */
191 public function partition( $table, $batchSize ) {
192 // Try cache
193 if ( isset( $this->partitionCache[$table][$batchSize] ) ) {
194 wfDebug( __METHOD__ . ": got from partition cache\n" );
195 return $this->partitionCache[$table][$batchSize]['batches'];
196 }
197
198 $this->partitionCache[$table][$batchSize] = false;
199 $cacheEntry =& $this->partitionCache[$table][$batchSize];
200
201 // Try full result cache
202 if ( isset( $this->fullResultCache[$table] ) ) {
203 $cacheEntry = $this->partitionResult( $this->fullResultCache[$table], $batchSize );
204 wfDebug( __METHOD__ . ": got from full result cache\n" );
205
206 return $cacheEntry['batches'];
207 }
208
209 // Try memcached
210 global $wgMemc;
211
212 $memcKey = wfMemcKey(
213 'backlinks',
214 md5( $this->title->getPrefixedDBkey() ),
215 $table,
216 $batchSize
217 );
218
219 $memcValue = $wgMemc->get( $memcKey );
220
221 if ( is_array( $memcValue ) ) {
222 $cacheEntry = $memcValue;
223 wfDebug( __METHOD__ . ": got from memcached $memcKey\n" );
224
225 return $cacheEntry['batches'];
226 }
227
228 // Fetch from database
229 $this->getLinks( $table );
230 $cacheEntry = $this->partitionResult( $this->fullResultCache[$table], $batchSize );
231 // Save to memcached
232 $wgMemc->set( $memcKey, $cacheEntry, self::CACHE_EXPIRY );
233
234 wfDebug( __METHOD__ . ": got from database\n" );
235 return $cacheEntry['batches'];
236 }
237
238 /**
239 * Partition a DB result with backlinks in it into batches
240 */
241 protected function partitionResult( $res, $batchSize ) {
242 $batches = array();
243 $numRows = $res->numRows();
244 $numBatches = ceil( $numRows / $batchSize );
245
246 for ( $i = 0; $i < $numBatches; $i++ ) {
247 if ( $i == 0 ) {
248 $start = false;
249 } else {
250 $rowNum = intval( $numRows * $i / $numBatches );
251 $res->seek( $rowNum );
252 $row = $res->fetchObject();
253 $start = $row->page_id;
254 }
255
256 if ( $i == $numBatches - 1 ) {
257 $end = false;
258 } else {
259 $rowNum = intval( $numRows * ( $i + 1 ) / $numBatches );
260 $res->seek( $rowNum );
261 $row = $res->fetchObject();
262 $end = $row->page_id - 1;
263 }
264
265 # Sanity check order
266 if ( $start && $end && $start > $end ) {
267 throw new MWException( __METHOD__ . ': Internal error: query result out of order' );
268 }
269
270 $batches[] = array( $start, $end );
271 }
272
273 return array( 'numRows' => $numRows, 'batches' => $batches );
274 }
275 }