1<?php 2/** 3 * Value object representing the set of slots belonging to a revision. 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 */ 22 23namespace MediaWiki\Revision; 24 25use Content; 26use Wikimedia\Assert\Assert; 27use Wikimedia\NonSerializable\NonSerializableTrait; 28 29/** 30 * Value object representing the set of slots belonging to a revision. 31 * 32 * @note RevisionSlots provides "raw" access to the slots and does not apply audience checks. 33 * If audience checks are desired, use RevisionRecord::getSlot() or RevisionRecord::getContent() 34 * instead. 35 * 36 * @newable 37 * 38 * @since 1.31 39 * @since 1.32 Renamed from MediaWiki\Storage\RevisionSlots 40 */ 41class RevisionSlots { 42 use NonSerializableTrait; 43 44 /** @var SlotRecord[]|callable */ 45 protected $slots; 46 47 /** 48 * @stable to call. 49 * 50 * @param SlotRecord[]|callable $slots SlotRecords, 51 * or a callback that returns such a structure. 52 */ 53 public function __construct( $slots ) { 54 Assert::parameterType( 'array|callable', $slots, '$slots' ); 55 56 if ( is_callable( $slots ) ) { 57 $this->slots = $slots; 58 } else { 59 $this->setSlotsInternal( $slots ); 60 } 61 } 62 63 /** 64 * @param SlotRecord[] $slots 65 */ 66 private function setSlotsInternal( array $slots ) { 67 Assert::parameterElementType( SlotRecord::class, $slots, '$slots' ); 68 69 $this->slots = []; 70 71 // re-key the slot array 72 foreach ( $slots as $slot ) { 73 $role = $slot->getRole(); 74 $this->slots[$role] = $slot; 75 } 76 } 77 78 /** 79 * Returns the Content of the given slot. 80 * Call getSlotNames() to get a list of available slots. 81 * 82 * Note that for mutable Content objects, each call to this method will return a 83 * fresh clone. 84 * 85 * @see SlotRecord::getContent() 86 * 87 * @param string $role The role name of the desired slot 88 * 89 * @throws RevisionAccessException if the slot does not exist or slot data 90 * could not be lazy-loaded. See SlotRecord::getContent() for details. 91 * @return Content 92 */ 93 public function getContent( $role ) { 94 // Return a copy to be safe. Immutable content objects return $this from copy(). 95 return $this->getSlot( $role )->getContent()->copy(); 96 } 97 98 /** 99 * Returns the SlotRecord of the given slot. 100 * Call getSlotNames() to get a list of available slots. 101 * 102 * @param string $role The role name of the desired slot 103 * 104 * @throws RevisionAccessException if the slot does not exist or slot data 105 * could not be lazy-loaded. 106 * @return SlotRecord 107 */ 108 public function getSlot( $role ) { 109 $slots = $this->getSlots(); 110 111 if ( isset( $slots[$role] ) ) { 112 return $slots[$role]; 113 } else { 114 throw new RevisionAccessException( 'No such slot: ' . $role ); 115 } 116 } 117 118 /** 119 * Returns whether the given slot is set. 120 * 121 * @param string $role The role name of the desired slot 122 * 123 * @return bool 124 */ 125 public function hasSlot( $role ) { 126 $slots = $this->getSlots(); 127 128 return isset( $slots[$role] ); 129 } 130 131 /** 132 * Returns the slot names (roles) of all slots present in this revision. 133 * getContent() will succeed only for the names returned by this method. 134 * 135 * @return string[] 136 */ 137 public function getSlotRoles() { 138 $slots = $this->getSlots(); 139 return array_keys( $slots ); 140 } 141 142 /** 143 * Computes the total nominal size of the revision's slots, in bogo-bytes. 144 * 145 * @warning This is potentially expensive! It may cause some slots' content to be loaded 146 * and deserialized. 147 * 148 * @return int 149 */ 150 public function computeSize() { 151 return array_reduce( $this->getPrimarySlots(), static function ( $accu, SlotRecord $slot ) { 152 return $accu + $slot->getSize(); 153 }, 0 ); 154 } 155 156 /** 157 * Returns an associative array that maps role names to SlotRecords. Each SlotRecord 158 * represents the content meta-data of a slot, together they define the content of 159 * a revision. 160 * 161 * @note This may cause the content meta-data for the revision to be lazy-loaded. 162 * 163 * @return SlotRecord[] revision slot/content rows, keyed by slot role name. 164 */ 165 public function getSlots() { 166 if ( is_callable( $this->slots ) ) { 167 $slots = call_user_func( $this->slots ); 168 169 Assert::postcondition( 170 is_array( $slots ), 171 'Slots info callback should return an array of objects' 172 ); 173 174 $this->setSlotsInternal( $slots ); 175 } 176 177 return $this->slots; 178 } 179 180 /** 181 * Computes the combined hash of the revisions's slots. 182 * 183 * @note For backwards compatibility, the combined hash of a single slot 184 * is that slot's hash. For consistency, the combined hash of an empty set of slots 185 * is the hash of the empty string. 186 * 187 * @warning This is potentially expensive! It may cause some slots' content to be loaded 188 * and deserialized, then re-serialized and hashed. 189 * 190 * @return string 191 */ 192 public function computeSha1() { 193 $slots = $this->getPrimarySlots(); 194 ksort( $slots ); 195 196 if ( empty( $slots ) ) { 197 return SlotRecord::base36Sha1( '' ); 198 } 199 200 return array_reduce( $slots, static function ( $accu, SlotRecord $slot ) { 201 return $accu === null 202 ? $slot->getSha1() 203 : SlotRecord::base36Sha1( $accu . $slot->getSha1() ); 204 }, null ); 205 } 206 207 /** 208 * Return all slots that belong to the revision they originate from (that is, 209 * they are not inherited from some other revision). 210 * 211 * @note This may cause the slot meta-data for the revision to be lazy-loaded. 212 * 213 * @return SlotRecord[] 214 */ 215 public function getOriginalSlots() { 216 return array_filter( 217 $this->getSlots(), 218 static function ( SlotRecord $slot ) { 219 return !$slot->isInherited(); 220 } 221 ); 222 } 223 224 /** 225 * Return all slots that are not originate in the revision they belong to (that is, 226 * they are inherited from some other revision). 227 * 228 * @note This may cause the slot meta-data for the revision to be lazy-loaded. 229 * 230 * @return SlotRecord[] 231 */ 232 public function getInheritedSlots() { 233 return array_filter( 234 $this->getSlots(), 235 static function ( SlotRecord $slot ) { 236 return $slot->isInherited(); 237 } 238 ); 239 } 240 241 /** 242 * Return all primary slots (those that are not derived). 243 * 244 * @return SlotRecord[] 245 * @since 1.36 246 */ 247 public function getPrimarySlots() : array { 248 return array_filter( 249 $this->getSlots(), 250 static function ( SlotRecord $slot ) { 251 return !$slot->isDerived(); 252 } 253 ); 254 } 255 256 /** 257 * Checks whether the other RevisionSlots instance has the same content 258 * as this instance. Note that this does not mean that the slots have to be the same: 259 * they could for instance belong to different revisions. 260 * 261 * @param RevisionSlots $other 262 * 263 * @return bool 264 */ 265 public function hasSameContent( RevisionSlots $other ) { 266 if ( $other === $this ) { 267 return true; 268 } 269 270 $aSlots = $this->getSlots(); 271 $bSlots = $other->getSlots(); 272 273 ksort( $aSlots ); 274 ksort( $bSlots ); 275 276 if ( array_keys( $aSlots ) !== array_keys( $bSlots ) ) { 277 return false; 278 } 279 280 foreach ( $aSlots as $role => $s ) { 281 $t = $bSlots[$role]; 282 283 if ( !$s->hasSameContent( $t ) ) { 284 return false; 285 } 286 } 287 288 return true; 289 } 290 291 /** 292 * Find roles for which the $other RevisionSlots object has different content 293 * as this RevisionSlots object, including any roles that are present in one 294 * but not the other. 295 * 296 * @param RevisionSlots $other 297 * 298 * @return string[] a list of slot roles that are different. 299 */ 300 public function getRolesWithDifferentContent( RevisionSlots $other ) { 301 if ( $other === $this ) { 302 return []; 303 } 304 305 $aSlots = $this->getSlots(); 306 $bSlots = $other->getSlots(); 307 308 ksort( $aSlots ); 309 ksort( $bSlots ); 310 311 $different = array_keys( array_merge( 312 array_diff_key( $aSlots, $bSlots ), 313 array_diff_key( $bSlots, $aSlots ) 314 ) ); 315 316 /** @var SlotRecord[] $common */ 317 $common = array_intersect_key( $aSlots, $bSlots ); 318 319 foreach ( $common as $role => $s ) { 320 $t = $bSlots[$role]; 321 322 if ( !$s->hasSameContent( $t ) ) { 323 $different[] = $role; 324 } 325 } 326 327 return $different; 328 } 329 330} 331 332/** 333 * Retain the old class name for backwards compatibility. 334 * @deprecated since 1.32 335 */ 336class_alias( RevisionSlots::class, 'MediaWiki\Storage\RevisionSlots' ); 337