Issues (1401)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

repo/includes/ChangeOp/ChangeOpsMerge.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

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
It seems like $this->fromItem->getId() can be null; however, applyConstraintChecks() does not accept null, maybe add an additional type check?

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:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
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