Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.79% covered (success)
95.79%
91 / 95
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageProps
96.81% covered (success)
96.81%
91 / 94
75.00% covered (warning)
75.00%
6 / 8
37
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 ensureCacheSize
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getProperties
96.88% covered (success)
96.88%
31 / 32
0.00% covered (danger)
0.00%
0 / 1
9
 getAllProperties
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
8
 getGoodIDs
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
10
 getCachedProperty
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getCachedProperties
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 cacheProperties
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Page;
22
23use MapCacheLRU;
24use MediaWiki\Cache\LinkBatchFactory;
25use MediaWiki\Title\Title;
26use MediaWiki\Title\TitleArrayFromResult;
27use Wikimedia\Rdbms\IConnectionProvider;
28
29/**
30 * Gives access to properties of a page.
31 *
32 * @since 1.27
33 * @ingroup Page
34 */
35class PageProps {
36    /* TTL in seconds */
37    private const CACHE_TTL = 10;
38    /* max cached pages */
39    private const CACHE_SIZE = 100;
40
41    private LinkBatchFactory $linkBatchFactory;
42    private IConnectionProvider $dbProvider;
43    private MapCacheLRU $cache;
44
45    /**
46     * @param LinkBatchFactory $linkBatchFactory
47     * @param IConnectionProvider $dbProvider
48     */
49    public function __construct(
50        LinkBatchFactory $linkBatchFactory,
51        IConnectionProvider $dbProvider
52    ) {
53        $this->linkBatchFactory = $linkBatchFactory;
54        $this->dbProvider = $dbProvider;
55        $this->cache = new MapCacheLRU( self::CACHE_SIZE );
56    }
57
58    /**
59     * Ensure that cache has at least this size
60     * @param int $size
61     */
62    public function ensureCacheSize( $size ) {
63        if ( $this->cache->getMaxSize() < $size ) {
64            $this->cache->setMaxSize( $size );
65        }
66    }
67
68    /**
69     * Fetch one or more properties for one or more Titles.
70     *
71     * Returns an associative array mapping page ID to property value.
72     *
73     * If a single Title is provided without an array, the output will still
74     * be returned as an array by page ID.
75     *
76     * Pages in the provided set of Titles that do not have a value for
77     * any of the properties will not appear in the returned array.
78     *
79     * If a single property name is requested, it does not need to be passed
80     * in as an array. In that case, the return array will map directly from
81     * page ID to property value. Otherwise, a multi-dimensional array is
82     * returned keyed by page ID, then property name, to property value.
83     *
84     * An empty array will be returned if no matching properties were found.
85     *
86     * @param iterable<PageIdentity>|PageIdentity $titles
87     * @param string[]|string $propertyNames
88     * @return array<int,string|array<string,string>> Keyed by page ID and property name
89     *  to property value
90     */
91    public function getProperties( $titles, $propertyNames ) {
92        if ( is_array( $propertyNames ) ) {
93            $gotArray = true;
94        } else {
95            $propertyNames = [ $propertyNames ];
96            $gotArray = false;
97        }
98
99        $values = [];
100        $goodIDs = $this->getGoodIDs( $titles );
101        $queryIDs = [];
102        foreach ( $goodIDs as $pageID ) {
103            foreach ( $propertyNames as $propertyName ) {
104                $propertyValue = $this->getCachedProperty( $pageID, $propertyName );
105                if ( $propertyValue === false ) {
106                    $queryIDs[] = $pageID;
107                    break;
108                } elseif ( $gotArray ) {
109                    $values[$pageID][$propertyName] = $propertyValue;
110                } else {
111                    $values[$pageID] = $propertyValue;
112                }
113            }
114        }
115
116        if ( $queryIDs ) {
117            $queryBuilder = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder();
118            $queryBuilder->select( [ 'pp_page', 'pp_propname', 'pp_value' ] )
119                ->from( 'page_props' )
120                ->where( [ 'pp_page' => $queryIDs, 'pp_propname' => $propertyNames ] )
121                ->caller( __METHOD__ );
122            $result = $queryBuilder->fetchResultSet();
123
124            foreach ( $result as $row ) {
125                $pageID = $row->pp_page;
126                $propertyName = $row->pp_propname;
127                $propertyValue = $row->pp_value;
128                $this->cache->setField( $pageID, $propertyName, $propertyValue );
129                if ( $gotArray ) {
130                    $values[$pageID][$propertyName] = $propertyValue;
131                } else {
132                    $values[$pageID] = $propertyValue;
133                }
134            }
135        }
136
137        return $values;
138    }
139
140    /**
141     * Get all page properties of one or more page titles.
142     *
143     * Given one or more Titles, returns an array keyed by page ID to another
144     * array from property names to property values.
145     *
146     * If a single Title is provided without an array, the output will still
147     * be returned as an array by page ID.
148     *
149     * Pages in the provided set of Titles that do have no page properties,
150     * will not get a page ID key in the returned array.
151     *
152     * An empty array will be returned if none of the titles have any page properties.
153     *
154     * @param iterable<PageIdentity>|PageIdentity $titles
155     * @return array<int,array<string,string>> Keyed by page ID and property name to property value
156     */
157    public function getAllProperties( $titles ) {
158        $values = [];
159        $goodIDs = $this->getGoodIDs( $titles );
160        $queryIDs = [];
161        foreach ( $goodIDs as $pageID ) {
162            $pageProperties = $this->getCachedProperties( $pageID );
163            if ( $pageProperties === false ) {
164                $queryIDs[] = $pageID;
165            } else {
166                $values[$pageID] = $pageProperties;
167            }
168        }
169
170        if ( $queryIDs != [] ) {
171            $queryBuilder = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder();
172            $queryBuilder->select( [ 'pp_page', 'pp_propname', 'pp_value' ] )
173                ->from( 'page_props' )
174                ->where( [ 'pp_page' => $queryIDs ] )
175                ->caller( __METHOD__ );
176            $result = $queryBuilder->fetchResultSet();
177
178            $currentPageID = 0;
179            $pageProperties = [];
180            foreach ( $result as $row ) {
181                $pageID = $row->pp_page;
182                if ( $currentPageID != $pageID ) {
183                    if ( $pageProperties ) {
184                        // @phan-suppress-next-line PhanTypeMismatchArgument False positive
185                        $this->cacheProperties( $currentPageID, $pageProperties );
186                        $values[$currentPageID] = $pageProperties;
187                    }
188                    $currentPageID = $pageID;
189                    $pageProperties = [];
190                }
191                $pageProperties[$row->pp_propname] = $row->pp_value;
192            }
193            if ( $pageProperties != [] ) {
194                // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable pageID set when used
195                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable pageID set when used
196                $this->cacheProperties( $pageID, $pageProperties );
197                // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable pageID set when used
198                // @phan-suppress-next-line PhanTypeMismatchDimAssignment pageID set when used
199                $values[$pageID] = $pageProperties;
200            }
201        }
202
203        return $values;
204    }
205
206    /**
207     * @param iterable<PageIdentity>|PageIdentity $titles
208     * @return int[] List of good page IDs
209     */
210    private function getGoodIDs( $titles ) {
211        $result = [];
212        if ( is_iterable( $titles ) ) {
213            if ( $titles instanceof TitleArrayFromResult ||
214                ( is_array( $titles ) && reset( $titles ) instanceof Title
215            ) ) {
216                // If the first element is a Title, assume all elements are Titles,
217                // and pre-fetch their IDs using a batch query. For PageIdentityValues
218                // or PageStoreRecords, this is not necessary, since they already
219                // know their ID.
220                $this->linkBatchFactory->newLinkBatch( $titles )->execute();
221            }
222
223            foreach ( $titles as $title ) {
224                // Until we only allow ProperPageIdentity, Title objects
225                // can deceive us with an unexpected Special page
226                if ( $title->canExist() ) {
227                    $pageID = $title->getId();
228                    if ( $pageID > 0 ) {
229                        $result[] = $pageID;
230                    }
231                }
232            }
233        } else {
234            // Until we only allow ProperPageIdentity, Title objects
235            // can deceive us with an unexpected Special page
236            if ( $titles->canExist() ) {
237                $pageID = $titles->getId();
238                if ( $pageID > 0 ) {
239                    $result[] = $pageID;
240                }
241            }
242        }
243        return $result;
244    }
245
246    /**
247     * Get a property from the cache.
248     *
249     * @param int $pageID page ID of page being queried
250     * @param string $propertyName name of property being queried
251     * @return string|bool property value array or false if not found
252     */
253    private function getCachedProperty( $pageID, $propertyName ) {
254        if ( $this->cache->hasField( $pageID, $propertyName, self::CACHE_TTL ) ) {
255            return $this->cache->getField( $pageID, $propertyName );
256        }
257        if ( $this->cache->hasField( 0, $pageID, self::CACHE_TTL ) ) {
258            $pageProperties = $this->cache->getField( 0, $pageID );
259            if ( isset( $pageProperties[$propertyName] ) ) {
260                return $pageProperties[$propertyName];
261            }
262        }
263        return false;
264    }
265
266    /**
267     * Get properties from the cache.
268     *
269     * @param int $pageID page ID of page being queried
270     * @return string|bool property value array or false if not found
271     */
272    private function getCachedProperties( $pageID ) {
273        if ( $this->cache->hasField( 0, $pageID, self::CACHE_TTL ) ) {
274            return $this->cache->getField( 0, $pageID );
275        }
276        return false;
277    }
278
279    /**
280     * Save properties to the cache.
281     *
282     * @param int $pageID page ID of page being cached
283     * @param string[] $pageProperties associative array of page properties to be cached
284     */
285    private function cacheProperties( $pageID, $pageProperties ) {
286        $this->cache->clear( $pageID );
287        $this->cache->setField( 0, $pageID, $pageProperties );
288    }
289}
290
291/** @deprecated class alias since 1.40 */
292class_alias( PageProps::class, 'PageProps' );