This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php |
||
| 2 | |||
| 3 | namespace Wikibase\Repo\ChangeOp; |
||
| 4 | |||
| 5 | use InvalidArgumentException; |
||
| 6 | use Site; |
||
| 7 | use SiteLookup; |
||
| 8 | use ValueValidators\Error; |
||
| 9 | use ValueValidators\Result; |
||
| 10 | use Wikibase\DataModel\Entity\EntityId; |
||
| 11 | use Wikibase\DataModel\Entity\Item; |
||
| 12 | use Wikibase\DataModel\Entity\ItemId; |
||
| 13 | use Wikibase\DataModel\SiteLink; |
||
| 14 | use Wikibase\Repo\Merge\StatementsMerger; |
||
| 15 | use Wikibase\Repo\Merge\Validator\NoCrossReferencingStatements; |
||
| 16 | use Wikibase\Repo\Validators\CompositeEntityValidator; |
||
| 17 | use Wikibase\Repo\Validators\EntityConstraintProvider; |
||
| 18 | use Wikibase\Repo\Validators\UniquenessViolation; |
||
| 19 | |||
| 20 | /** |
||
| 21 | * @license GPL-2.0-or-later |
||
| 22 | * @author Addshore |
||
| 23 | * @author Daniel Kinzler |
||
| 24 | */ |
||
| 25 | class ChangeOpsMerge { |
||
| 26 | |||
| 27 | /** |
||
| 28 | * @var Item |
||
| 29 | */ |
||
| 30 | private $fromItem; |
||
| 31 | |||
| 32 | /** |
||
| 33 | * @var Item |
||
| 34 | */ |
||
| 35 | private $toItem; |
||
| 36 | |||
| 37 | /** |
||
| 38 | * @var ChangeOps |
||
| 39 | */ |
||
| 40 | private $fromChangeOps; |
||
| 41 | |||
| 42 | /** |
||
| 43 | * @var ChangeOps |
||
| 44 | */ |
||
| 45 | private $toChangeOps; |
||
| 46 | |||
| 47 | /** |
||
| 48 | * @var string[] |
||
| 49 | */ |
||
| 50 | private $ignoreConflicts; |
||
| 51 | |||
| 52 | /** |
||
| 53 | * @var EntityConstraintProvider |
||
| 54 | */ |
||
| 55 | private $constraintProvider; |
||
| 56 | |||
| 57 | /** |
||
| 58 | * @var ChangeOpFactoryProvider |
||
| 59 | */ |
||
| 60 | private $changeOpFactoryProvider; |
||
| 61 | |||
| 62 | /** |
||
| 63 | * @var SiteLookup |
||
| 64 | */ |
||
| 65 | private $siteLookup; |
||
| 66 | |||
| 67 | public static $conflictTypes = [ 'description', 'sitelink', 'statement' ]; |
||
| 68 | |||
| 69 | /** |
||
| 70 | * @var StatementsMerger |
||
| 71 | */ |
||
| 72 | private $statementsMerger; |
||
| 73 | |||
| 74 | /** |
||
| 75 | * @param Item $fromItem |
||
| 76 | * @param Item $toItem |
||
| 77 | * @param string[] $ignoreConflicts list of elements to ignore conflicts for |
||
| 78 | * can only contain 'description' and or 'sitelink' and or 'statement' |
||
| 79 | * @param EntityConstraintProvider $constraintProvider |
||
| 80 | * @param ChangeOpFactoryProvider $changeOpFactoryProvider |
||
| 81 | * @param SiteLookup $siteLookup |
||
| 82 | * @param StatementsMerger $statementsMerger |
||
| 83 | * |
||
| 84 | * @todo Injecting ChangeOpFactoryProvider is an Abomination Unto Nuggan, we'll |
||
| 85 | * need a MergeChangeOpsSequenceBuilder or some such. This will allow us |
||
| 86 | * to merge different kinds of entities nicely, too. |
||
| 87 | */ |
||
| 88 | public function __construct( |
||
| 89 | Item $fromItem, |
||
| 90 | Item $toItem, |
||
| 91 | array $ignoreConflicts, |
||
| 92 | EntityConstraintProvider $constraintProvider, |
||
| 93 | ChangeOpFactoryProvider $changeOpFactoryProvider, |
||
| 94 | SiteLookup $siteLookup, |
||
| 95 | StatementsMerger $statementsMerger |
||
| 96 | ) { |
||
| 97 | $this->assertValidIgnoreConflictValues( $ignoreConflicts ); |
||
| 98 | |||
| 99 | $this->fromItem = $fromItem; |
||
| 100 | $this->toItem = $toItem; |
||
| 101 | $this->fromChangeOps = new ChangeOps(); |
||
| 102 | $this->toChangeOps = new ChangeOps(); |
||
| 103 | $this->ignoreConflicts = $ignoreConflicts; |
||
| 104 | $this->constraintProvider = $constraintProvider; |
||
| 105 | $this->siteLookup = $siteLookup; |
||
| 106 | |||
| 107 | $this->changeOpFactoryProvider = $changeOpFactoryProvider; |
||
| 108 | $this->statementsMerger = $statementsMerger; |
||
| 109 | } |
||
| 110 | |||
| 111 | /** |
||
| 112 | * @param string[] $ignoreConflicts can contain strings 'description' or 'sitelink' |
||
| 113 | * |
||
| 114 | * @throws InvalidArgumentException |
||
| 115 | */ |
||
| 116 | private function assertValidIgnoreConflictValues( array $ignoreConflicts ) { |
||
| 117 | if ( array_diff( $ignoreConflicts, self::$conflictTypes ) ) { |
||
| 118 | throw new InvalidArgumentException( |
||
| 119 | '$ignoreConflicts array can only contain "description" and or "sitelink" and or "statement" values' |
||
| 120 | ); |
||
| 121 | } |
||
| 122 | } |
||
| 123 | |||
| 124 | /** |
||
| 125 | * @return FingerprintChangeOpFactory |
||
| 126 | */ |
||
| 127 | private function getFingerprintChangeOpFactory() { |
||
| 128 | return $this->changeOpFactoryProvider->getFingerprintChangeOpFactory(); |
||
| 129 | } |
||
| 130 | |||
| 131 | /** |
||
| 132 | * @return SiteLinkChangeOpFactory |
||
| 133 | */ |
||
| 134 | private function getSiteLinkChangeOpFactory() { |
||
| 135 | return $this->changeOpFactoryProvider->getSiteLinkChangeOpFactory(); |
||
| 136 | } |
||
| 137 | |||
| 138 | /** |
||
| 139 | * @throws ChangeOpException |
||
| 140 | */ |
||
| 141 | public function apply() { |
||
| 142 | // NOTE: we don't want to validate the ChangeOps individually, since they represent |
||
| 143 | // data already present and saved on the system. Also, validating each would be |
||
| 144 | // potentially expensive. |
||
| 145 | |||
| 146 | $this->generateChangeOps(); |
||
| 147 | |||
| 148 | $this->fromChangeOps->apply( $this->fromItem ); |
||
| 149 | $this->toChangeOps->apply( $this->toItem ); |
||
| 150 | |||
| 151 | $this->checkStatementLinks(); |
||
| 152 | $this->statementsMerger->merge( $this->fromItem, $this->toItem ); |
||
| 153 | |||
| 154 | //NOTE: we apply constraint checks on the modified items, but no |
||
| 155 | // validation of individual change ops, since we are merging |
||
| 156 | // two valid items. |
||
| 157 | $this->applyConstraintChecks( $this->toItem, $this->fromItem->getId() ); |
||
|
0 ignored issues
–
show
|
|||
| 158 | |||
| 159 | return new DummyChangeOpResult(); |
||
| 160 | } |
||
| 161 | |||
| 162 | private function generateChangeOps() { |
||
| 163 | $this->generateLabelsChangeOps(); |
||
| 164 | $this->generateDescriptionsChangeOps(); |
||
| 165 | $this->generateAliasesChangeOps(); |
||
| 166 | $this->generateSitelinksChangeOps(); |
||
| 167 | } |
||
| 168 | |||
| 169 | private function generateLabelsChangeOps() { |
||
| 170 | foreach ( $this->fromItem->getLabels()->toTextArray() as $langCode => $label ) { |
||
| 171 | if ( !$this->toItem->getLabels()->hasTermForLanguage( $langCode ) |
||
| 172 | || $this->toItem->getLabels()->getByLanguage( $langCode )->getText() === $label |
||
| 173 | ) { |
||
| 174 | $this->fromChangeOps->add( $this->getFingerprintChangeOpFactory()->newRemoveLabelOp( $langCode ) ); |
||
| 175 | $this->toChangeOps->add( $this->getFingerprintChangeOpFactory()->newSetLabelOp( $langCode, $label ) ); |
||
| 176 | } else { |
||
| 177 | $this->fromChangeOps->add( $this->getFingerprintChangeOpFactory()->newRemoveLabelOp( $langCode ) ); |
||
| 178 | $this->toChangeOps->add( $this->getFingerprintChangeOpFactory()->newAddAliasesOp( $langCode, [ $label ] ) ); |
||
| 179 | } |
||
| 180 | } |
||
| 181 | } |
||
| 182 | |||
| 183 | private function generateDescriptionsChangeOps() { |
||
| 184 | foreach ( $this->fromItem->getDescriptions()->toTextArray() as $langCode => $desc ) { |
||
| 185 | if ( !$this->toItem->getDescriptions()->hasTermForLanguage( $langCode ) |
||
| 186 | || $this->toItem->getDescriptions()->getByLanguage( $langCode )->getText() === $desc |
||
| 187 | ) { |
||
| 188 | $this->fromChangeOps->add( $this->getFingerprintChangeOpFactory()->newRemoveDescriptionOp( $langCode ) ); |
||
| 189 | $this->toChangeOps->add( $this->getFingerprintChangeOpFactory()->newSetDescriptionOp( $langCode, $desc ) ); |
||
| 190 | } elseif ( !in_array( 'description', $this->ignoreConflicts ) ) { |
||
| 191 | throw new ChangeOpException( "Conflicting descriptions for language {$langCode}" ); |
||
| 192 | } |
||
| 193 | } |
||
| 194 | } |
||
| 195 | |||
| 196 | private function generateAliasesChangeOps() { |
||
| 197 | foreach ( $this->fromItem->getAliasGroups()->toTextArray() as $langCode => $aliases ) { |
||
| 198 | $this->fromChangeOps->add( $this->getFingerprintChangeOpFactory()->newRemoveAliasesOp( $langCode, $aliases ) ); |
||
| 199 | $this->toChangeOps->add( $this->getFingerprintChangeOpFactory()->newAddAliasesOp( $langCode, $aliases ) ); |
||
| 200 | } |
||
| 201 | } |
||
| 202 | |||
| 203 | private function generateSitelinksChangeOps() { |
||
| 204 | foreach ( $this->fromItem->getSiteLinkList()->toArray() as $fromSiteLink ) { |
||
| 205 | $siteId = $fromSiteLink->getSiteId(); |
||
| 206 | if ( !$this->toItem->getSiteLinkList()->hasLinkWithSiteId( $siteId ) ) { |
||
| 207 | $this->generateSitelinksChangeOpsWithNoConflict( $fromSiteLink ); |
||
| 208 | } else { |
||
| 209 | $this->generateSitelinksChangeOpsWithConflict( $fromSiteLink ); |
||
| 210 | } |
||
| 211 | } |
||
| 212 | } |
||
| 213 | |||
| 214 | private function generateSitelinksChangeOpsWithNoConflict( SiteLink $fromSiteLink ) { |
||
| 215 | $siteId = $fromSiteLink->getSiteId(); |
||
| 216 | $this->fromChangeOps->add( $this->getSiteLinkChangeOpFactory()->newRemoveSiteLinkOp( $siteId ) ); |
||
| 217 | $this->toChangeOps->add( |
||
| 218 | $this->getSiteLinkChangeOpFactory()->newSetSiteLinkOp( |
||
| 219 | $siteId, |
||
| 220 | $fromSiteLink->getPageName(), |
||
| 221 | $fromSiteLink->getBadges() |
||
| 222 | ) |
||
| 223 | ); |
||
| 224 | } |
||
| 225 | |||
| 226 | private function generateSitelinksChangeOpsWithConflict( SiteLink $fromSiteLink ) { |
||
| 227 | $siteId = $fromSiteLink->getSiteId(); |
||
| 228 | $toSiteLink = $this->toItem->getSiteLink( $siteId ); |
||
| 229 | $fromPageName = $fromSiteLink->getPageName(); |
||
| 230 | $toPageName = $toSiteLink->getPageName(); |
||
| 231 | |||
| 232 | if ( $fromPageName !== $toPageName ) { |
||
| 233 | $site = $this->getSite( $siteId ); |
||
| 234 | $fromPageName = $site->normalizePageName( $fromPageName ); |
||
| 235 | $toPageName = $site->normalizePageName( $toPageName ); |
||
| 236 | } |
||
| 237 | if ( $fromPageName === $toPageName ) { |
||
| 238 | $this->fromChangeOps->add( $this->getSiteLinkChangeOpFactory()->newRemoveSiteLinkOp( $siteId ) ); |
||
| 239 | $this->toChangeOps->add( |
||
| 240 | $this->getSiteLinkChangeOpFactory()->newSetSiteLinkOp( |
||
| 241 | $siteId, |
||
| 242 | $fromPageName, |
||
| 243 | array_unique( array_merge( $fromSiteLink->getBadges(), $toSiteLink->getBadges() ) ) |
||
| 244 | ) |
||
| 245 | ); |
||
| 246 | } elseif ( !in_array( 'sitelink', $this->ignoreConflicts ) ) { |
||
| 247 | throw new ChangeOpException( "Conflicting sitelinks for {$siteId}" ); |
||
| 248 | } |
||
| 249 | } |
||
| 250 | |||
| 251 | /** |
||
| 252 | * @param string $siteId |
||
| 253 | * |
||
| 254 | * @throws ChangeOpException |
||
| 255 | * @return Site |
||
| 256 | */ |
||
| 257 | private function getSite( $siteId ) { |
||
| 258 | $site = $this->siteLookup->getSite( $siteId ); |
||
| 259 | if ( $site === null ) { |
||
| 260 | throw new ChangeOpException( "Conflicting sitelinks for {$siteId}, Failed to normalize" ); |
||
| 261 | } |
||
| 262 | return $site; |
||
| 263 | } |
||
| 264 | |||
| 265 | /** |
||
| 266 | * @param Item $item |
||
| 267 | * @param ItemId $fromId |
||
| 268 | * |
||
| 269 | * @throws ChangeOpValidationException if it would not be possible to save the updated items. |
||
| 270 | */ |
||
| 271 | private function applyConstraintChecks( Item $item, ItemId $fromId ) { |
||
| 272 | $constraintValidator = new CompositeEntityValidator( |
||
| 273 | $this->constraintProvider->getUpdateValidators( $item->getType() ) |
||
| 274 | ); |
||
| 275 | |||
| 276 | $result = $constraintValidator->validateEntity( $item ); |
||
| 277 | $errors = $result->getErrors(); |
||
| 278 | |||
| 279 | $errors = $this->removeConflictsWithEntity( $errors, $fromId ); |
||
| 280 | |||
| 281 | if ( !empty( $errors ) ) { |
||
| 282 | $result = Result::newError( $errors ); |
||
| 283 | throw new ChangeOpValidationException( $result ); |
||
| 284 | } |
||
| 285 | } |
||
| 286 | |||
| 287 | /** |
||
| 288 | * Strip any conflicts with the given $fromId from the array of Error objects |
||
| 289 | * |
||
| 290 | * @param Error[] $errors |
||
| 291 | * @param EntityId $fromId |
||
| 292 | * |
||
| 293 | * @return Error[] |
||
| 294 | */ |
||
| 295 | private function removeConflictsWithEntity( array $errors, EntityId $fromId ) { |
||
| 296 | $filtered = []; |
||
| 297 | |||
| 298 | foreach ( $errors as $error ) { |
||
| 299 | if ( $error instanceof UniquenessViolation |
||
| 300 | && $fromId->equals( $error->getConflictingEntity() ) |
||
| 301 | ) { |
||
| 302 | continue; |
||
| 303 | } |
||
| 304 | |||
| 305 | $filtered[] = $error; |
||
| 306 | } |
||
| 307 | |||
| 308 | return $filtered; |
||
| 309 | } |
||
| 310 | |||
| 311 | private function checkStatementLinks() { |
||
| 312 | if ( in_array( 'statement', $this->ignoreConflicts ) ) { |
||
| 313 | return; |
||
| 314 | } |
||
| 315 | |||
| 316 | $validator = new NoCrossReferencingStatements(); |
||
| 317 | if ( $validator->validate( $this->fromItem, $this->toItem ) ) { |
||
| 318 | return; |
||
| 319 | } |
||
| 320 | |||
| 321 | throw new ChangeOpException( |
||
| 322 | 'The two items cannot be merged because one of them links to the other using the properties: ' . |
||
| 323 | implode( ', ', $validator->getViolations() ) |
||
| 324 | ); |
||
| 325 | } |
||
| 326 | |||
| 327 | } |
||
| 328 |
Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code: