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    public function __construct(
46        LinkBatchFactory $linkBatchFactory,
47        IConnectionProvider $dbProvider
48    ) {
49        $this->linkBatchFactory = $linkBatchFactory;
50        $this->dbProvider = $dbProvider;
51        $this->cache = new MapCacheLRU( self::CACHE_SIZE );
52    }
53
54    /**
55     * Ensure that cache has at least this size
56     * @param int $size
57     */
58    public function ensureCacheSize( $size ) {
59        if ( $this->cache->getMaxSize() < $size ) {
60            $this->cache->setMaxSize( $size );
61        }
62    }
63
64    /**
65     * Fetch one or more properties for one or more Titles.
66     *
67     * Returns an associative array mapping page ID to property value.
68     *
69     * If a single Title is provided without an array, the output will still
70     * be returned as an array by page ID.
71     *
72     * Pages in the provided set of Titles that do not have a value for
73     * any of the properties will not appear in the returned array.
74     *
75     * If a single property name is requested, it does not need to be passed
76     * in as an array. In that case, the return array will map directly from
77     * page ID to property value. Otherwise, a multi-dimensional array is
78     * returned keyed by page ID, then property name, to property value.
79     *
80     * An empty array will be returned if no matching properties were found.
81     *
82     * @param iterable<PageIdentity>|PageIdentity $titles
83     * @param string[]|string $propertyNames
84     * @return array<int,string|array<string,string>> Keyed by page ID and property name
85     *  to property value
86     */
87    public function getProperties( $titles, $propertyNames ) {
88        if ( is_array( $propertyNames ) ) {
89            $gotArray = true;
90        } else {
91            $propertyNames = [ $propertyNames ];
92            $gotArray = false;
93        }
94
95        $values = [];
96        $goodIDs = $this->getGoodIDs( $titles );
97        $queryIDs = [];
98        foreach ( $goodIDs as $pageID ) {
99            foreach ( $propertyNames as $propertyName ) {
100                $propertyValue = $this->getCachedProperty( $pageID, $propertyName );
101                if ( $propertyValue === false ) {
102                    $queryIDs[] = $pageID;
103                    break;
104                } elseif ( $gotArray ) {
105                    $values[$pageID][$propertyName] = $propertyValue;
106                } else {
107                    $values[$pageID] = $propertyValue;
108                }
109            }
110        }
111
112        if ( $queryIDs ) {
113            $queryBuilder = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder();
114            $queryBuilder->select( [ 'pp_page', 'pp_propname', 'pp_value' ] )
115                ->from( 'page_props' )
116                ->where( [ 'pp_page' => $queryIDs, 'pp_propname' => $propertyNames ] )
117                ->caller( __METHOD__ );
118            $result = $queryBuilder->fetchResultSet();
119
120            foreach ( $result as $row ) {
121                $pageID = $row->pp_page;
122                $propertyName = $row->pp_propname;
123                $propertyValue = $row->pp_value;
124                $this->cache->setField( $pageID, $propertyName, $propertyValue );
125                if ( $gotArray ) {
126                    $values[$pageID][$propertyName] = $propertyValue;
127                } else {
128                    $values[$pageID] = $propertyValue;
129                }
130            }
131        }
132
133        return $values;
134    }
135
136    /**
137     * Get all page properties of one or more page titles.
138     *
139     * Given one or more Titles, returns an array keyed by page ID to another
140     * array from property names to property values.
141     *
142     * If a single Title is provided without an array, the output will still
143     * be returned as an array by page ID.
144     *
145     * Pages in the provided set of Titles that do have no page properties,
146     * will not get a page ID key in the returned array.
147     *
148     * An empty array will be returned if none of the titles have any page properties.
149     *
150     * @param iterable<PageIdentity>|PageIdentity $titles
151     * @return array<int,array<string,string>> Keyed by page ID and property name to property value
152     */
153    public function getAllProperties( $titles ) {
154        $values = [];
155        $goodIDs = $this->getGoodIDs( $titles );
156        $queryIDs = [];
157        foreach ( $goodIDs as $pageID ) {
158            $pageProperties = $this->getCachedProperties( $pageID );
159            if ( $pageProperties === false ) {
160                $queryIDs[] = $pageID;
161            } else {
162                $values[$pageID] = $pageProperties;
163            }
164        }
165
166        if ( $queryIDs != [] ) {
167            $queryBuilder = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder();
168            $queryBuilder->select( [ 'pp_page', 'pp_propname', 'pp_value' ] )
169                ->from( 'page_props' )
170                ->where( [ 'pp_page' => $queryIDs ] )
171                ->caller( __METHOD__ );
172            $result = $queryBuilder->fetchResultSet();
173
174            $currentPageID = 0;
175            $pageProperties = [];
176            foreach ( $result as $row ) {
177                $pageID = $row->pp_page;
178                if ( $currentPageID != $pageID ) {
179                    if ( $pageProperties ) {
180                        // @phan-suppress-next-line PhanTypeMismatchArgument False positive
181                        $this->cacheProperties( $currentPageID, $pageProperties );
182                        $values[$currentPageID] = $pageProperties;
183                    }
184                    $currentPageID = $pageID;
185                    $pageProperties = [];
186                }
187                $pageProperties[$row->pp_propname] = $row->pp_value;
188            }
189            if ( $pageProperties != [] ) {
190                // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable pageID set when used
191                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable pageID set when used
192                $this->cacheProperties( $pageID, $pageProperties );
193                // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable pageID set when used
194                // @phan-suppress-next-line PhanTypeMismatchDimAssignment pageID set when used
195                $values[$pageID] = $pageProperties;
196            }
197        }
198
199        return $values;
200    }
201
202    /**
203     * @param iterable<PageIdentity>|PageIdentity $titles
204     * @return int[] List of good page IDs
205     */
206    private function getGoodIDs( $titles ) {
207        $result = [];
208        if ( is_iterable( $titles ) ) {
209            if ( $titles instanceof TitleArrayFromResult ||
210                ( is_array( $titles ) && reset( $titles ) instanceof Title
211            ) ) {
212                // If the first element is a Title, assume all elements are Titles,
213                // and pre-fetch their IDs using a batch query. For PageIdentityValues
214                // or PageStoreRecords, this is not necessary, since they already
215                // know their ID.
216                $this->linkBatchFactory->newLinkBatch( $titles )->execute();
217            }
218
219            foreach ( $titles as $title ) {
220                // Until we only allow ProperPageIdentity, Title objects
221                // can deceive us with an unexpected Special page
222                if ( $title->canExist() ) {
223                    $pageID = $title->getId();
224                    if ( $pageID > 0 ) {
225                        $result[] = $pageID;
226                    }
227                }
228            }
229        } else {
230            // Until we only allow ProperPageIdentity, Title objects
231            // can deceive us with an unexpected Special page
232            if ( $titles->canExist() ) {
233                $pageID = $titles->getId();
234                if ( $pageID > 0 ) {
235                    $result[] = $pageID;
236                }
237            }
238        }
239        return $result;
240    }
241
242    /**
243     * Get a property from the cache.
244     *
245     * @param int $pageID page ID of page being queried
246     * @param string $propertyName name of property being queried
247     * @return string|bool property value array or false if not found
248     */
249    private function getCachedProperty( $pageID, $propertyName ) {
250        if ( $this->cache->hasField( $pageID, $propertyName, self::CACHE_TTL ) ) {
251            return $this->cache->getField( $pageID, $propertyName );
252        }
253        if ( $this->cache->hasField( 0, $pageID, self::CACHE_TTL ) ) {
254            $pageProperties = $this->cache->getField( 0, $pageID );
255            if ( isset( $pageProperties[$propertyName] ) ) {
256                return $pageProperties[$propertyName];
257            }
258        }
259        return false;
260    }
261
262    /**
263     * Get properties from the cache.
264     *
265     * @param int $pageID page ID of page being queried
266     * @return string|bool property value array or false if not found
267     */
268    private function getCachedProperties( $pageID ) {
269        if ( $this->cache->hasField( 0, $pageID, self::CACHE_TTL ) ) {
270            return $this->cache->getField( 0, $pageID );
271        }
272        return false;
273    }
274
275    /**
276     * Save properties to the cache.
277     *
278     * @param int $pageID page ID of page being cached
279     * @param string[] $pageProperties associative array of page properties to be cached
280     */
281    private function cacheProperties( $pageID, $pageProperties ) {
282        $this->cache->clear( $pageID );
283        $this->cache->setField( 0, $pageID, $pageProperties );
284    }
285}
286
287/** @deprecated class alias since 1.40 */
288class_alias( PageProps::class, 'PageProps' );