Completed
Push — master ( 3abc67...80e892 )
by mw
207:38 queued 172:37
created

StoreUpdater::doPerformUpdate()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 8
nop 0
dl 0
loc 26
ccs 13
cts 13
cp 1
crap 4
rs 8.5806
c 0
b 0
f 0
1
<?php
2
3
namespace SMW;
4
5
use Title;
6
use User;
7
use WikiPage;
8
9
/**
10
 * Initiates an update of the Store
11
 *
12
 * @license GNU GPL v2+
13
 * @since 1.9
14
 *
15
 * @author mwjames
16
 */
17
class StoreUpdater {
18
19
	/**
20
	 * @var Store
21
	 */
22
	private $store;
23
24
	/**
25
	 * @var SemanticData
26
	 */
27
	private $semanticData;
28
29
	/**
30
	 * @var boolean|null
31
	 */
32
	private $isEnabledWithUpdateJob = null;
33
34
	/**
35
	 * @var boolean|null
36
	 */
37
	private $processSemantics = null;
38
39
	/**
40
	 * @var ApplicationFactory
41
	 */
42
	private $applicationFactory = null;
43
44
	/**
45
	 * @since  1.9
46
	 *
47
	 * @param Store $store
48
	 * @param SemanticData $semanticData
49
	 */
50 228
	public function __construct( Store $store, SemanticData $semanticData ) {
51 228
		$this->store = $store;
52 228
		$this->semanticData = $semanticData;
53 228
	}
54
55
	/**
56
	 * @since 1.9
57
	 *
58
	 * @return DIWikiPage
59
	 */
60 227
	public function getSubject() {
61 227
		return $this->semanticData->getSubject();
62
	}
63
64
	/**
65
	 * @since 1.9
66
	 *
67
	 * @param boolean $isEnabledWithUpdateJob
68
	 */
69 225
	public function isEnabledWithUpdateJob( $isEnabledWithUpdateJob ) {
70 225
		$this->isEnabledWithUpdateJob = (bool)$isEnabledWithUpdateJob;
71 225
	}
72
73
	/**
74
	 * This function takes care of storing the collected semantic data and
75
	 * clearing out any outdated entries for the processed page. It assumes
76
	 * that parsing has happened and that all relevant information are
77
	 * contained and provided for.
78
	 *
79
	 * Optionally, this function also takes care of triggering indirect updates
80
	 * that might be needed for an overall database consistency. If the saved page
81
	 * describes a property or data type, the method checks whether the property
82
	 * type, the data type, the allowed values, or the conversion factors have
83
	 * changed. If so, it triggers UpdateDispatcherJob for the relevant articles,
84
	 * which then asynchronously undergoes an update.
85
	 *
86
	 * @since 1.9
87
	 *
88
	 * @return boolean
89
	 */
90
	public function doUpdate() {
91 227
92 227
		if ( !$this->canPerformUpdate() ) {
93
			return false;
94
		}
95 227
96
		$this->doPerformUpdate();
97 227
98
		return true;
99
	}
100 227
101 2
	private function canPerformUpdate() {
102
103
		$title = $this->getSubject()->getTitle();
104 225
105
		// Protect against null and namespace -1 see Bug 50153
106
		if ( $title === null || $title->isSpecialPage() ) {
107
			return false;
108
		}
109
110
		return true;
111
	}
112 225
113
	/**
114 225
	 * @note Make sure to have a valid revision (null means delete etc.) and
115
	 * check if semantic data should be processed and displayed for a page in
116 225
	 * the given namespace
117 1
	 */
118
	private function doPerformUpdate() {
119
120 225
		$this->applicationFactory = ApplicationFactory::getInstance();
121 225
122 225
		if ( $this->isEnabledWithUpdateJob === null ) {
123
			$this->isEnabledWithUpdateJob( $this->applicationFactory->getSettings()->get( 'smwgEnableUpdateJobs' ) );
124 225
		}
125 225
126 225
		$title = $this->getSubject()->getTitle();
127
		$wikiPage = $this->applicationFactory->newPageCreator()->createPage( $title );
0 ignored issues
show
Bug introduced by
It seems like $title defined by $this->getSubject()->getTitle() on line 126 can be null; however, SMW\MediaWiki\PageCreator::createPage() 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...
128 225
129
		$revision = $wikiPage->getRevision();
130
		$user = $revision !== null ? User::newFromId( $revision->getUser() ) : null;
131 225
132
		$this->addFinalAnnotations( $title, $wikiPage, $revision, $user );
0 ignored issues
show
Bug introduced by
It seems like $title defined by $this->getSubject()->getTitle() on line 126 can be null; however, SMW\StoreUpdater::addFinalAnnotations() 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...
133 225
134
		// In case of a restricted update, only the protection update is required
135 225
		// hence the process bails-out early to avoid unnecessary DB connections
136 42
		// or updates
137
		if ( $this->doUpdateEditProtection( $wikiPage, $user ) === true ) {
138
			return true;
139 220
		}
140
141
		$this->inspectPropertySpecification();
142 220
		$this->doRealUpdate();
143
	}
144
145 220
	private function addFinalAnnotations( Title $title, WikiPage $wikiPage, $revision, $user ) {
146
147 220
		$this->processSemantics = $revision !== null && $this->isSemanticEnabledNamespace( $title );
148 220
149
		if ( !$this->processSemantics ) {
150
			return $this->semanticData = new SemanticData( $this->getSubject() );
151 220
		}
152
153
		$pageInfoProvider = $this->applicationFactory->newMwCollaboratorFactory()->newPageInfoProvider(
154
			$wikiPage,
155
			$revision,
156 220
			$user
157 220
		);
158
159
		$propertyAnnotatorFactory = $this->applicationFactory->singleton( 'PropertyAnnotatorFactory' );
160
161
		$propertyAnnotator = $propertyAnnotatorFactory->newNullPropertyAnnotator(
162
			$this->semanticData
163 225
		);
164
165 225
		$propertyAnnotator = $propertyAnnotatorFactory->newPredefinedPropertyAnnotator(
166 24
			$propertyAnnotator,
167
			$pageInfoProvider
168
		);
169 220
170 220
		$propertyAnnotator->addAnnotation();
171 220
	}
172
173
	private function doUpdateEditProtection( $wikiPage, $user ) {
174 220
175 220
		$editProtectionUpdater = $this->applicationFactory->create( 'EditProtectionUpdater',
176
			$wikiPage,
177 220
			$user
178
		);
179 225
180
		$editProtectionUpdater->doUpdateFrom( $this->semanticData );
181 225
182
		return $editProtectionUpdater->isRestrictedUpdate();
183 225
	}
184 225
185
	/**
186
	 * @note Comparison must happen *before* the storage update;
187 225
	 * even finding uses of a property fails after its type changed.
188
	 */
189 225
	private function inspectPropertySpecification() {
190 220
191 42
		if ( !$this->isEnabledWithUpdateJob ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->isEnabledWithUpdateJob of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
192
			return;
193
		}
194
195
		$propertySpecificationChangeNotifier = new PropertySpecificationChangeNotifier(
196 13
			$this->store
197
		);
198
199 225
		$propertySpecificationChangeNotifier->setPropertyList(
200
			$this->applicationFactory->getSettings()->get( 'smwgDeclarationProperties' )
201
		);
202 220
203 220
		$propertySpecificationChangeNotifier->detectChangesOn( $this->semanticData );
204
		$propertySpecificationChangeNotifier->notify();
205
	}
206 225
207
	private function doRealUpdate() {
208
209
		$this->store->setUpdateJobsEnabledState( $this->isEnabledWithUpdateJob );
0 ignored issues
show
Bug introduced by
It seems like $this->isEnabledWithUpdateJob can also be of type null; however, SMW\Store::setUpdateJobsEnabledState() does only seem to accept boolean, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
210 225
211 24
		$semanticData = $this->checkOnRequiredRedirectUpdate(
212
			$this->semanticData
213
		);
214 220
215 220
		$subject = $semanticData->getSubject();
216
217
		if ( $this->processSemantics ) {
218 220
			$this->store->updateData( $semanticData );
219 26
		} elseif ( $this->store->getObjectIds()->hasIDFor( $subject ) ) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class SMW\Store as the method getObjectIds() does only exist in the following sub-classes of SMW\Store: SMWSQLStore3, SMWSparqlStore, SMW\SPARQLStore\SPARQLStore. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
220
			// Only clear the data where it is know that "hasIDFor" is true otherwise
221
			// an empty entity is created and later being removed by the
222 219
			// "PropertyTableOutdatedReferenceDisposer" since it is an entity that is
223
			// empty == has no reference
224
			$this->store->clearData( $subject );
225 26
		}
226
227
		return true;
228
	}
229
230
	private function isSemanticEnabledNamespace( Title $title ) {
231 26
		return $this->applicationFactory->getNamespaceExaminer()->isSemanticEnabled( $title->getNamespace() );
232 26
	}
233
234 26
	private function checkOnRequiredRedirectUpdate( SemanticData $semanticData ) {
235 26
236
		// Check only during online-mode so that when a user operates Special:MovePage
237
		// or #redirect the same process is applied
238
		if ( !$this->isEnabledWithUpdateJob ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->isEnabledWithUpdateJob of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
239
			return $semanticData;
240
		}
241 26
242 26
		$redirects = $semanticData->getPropertyValues(
243 26
			new DIProperty( '_REDI' )
244 26
		);
245 26
246
		if ( $redirects !== array() && !$semanticData->getSubject()->equals( end( $redirects ) ) ) {
247
			return $this->doUpdateUnknownRedirectTarget( $semanticData, end( $redirects ) );
248 26
		}
249 26
250
		return $semanticData;
251 26
	}
252 26
253
	private function doUpdateUnknownRedirectTarget( SemanticData $semanticData, DIWikiPage $target ) {
254
255
		// Only keep the reference to safeguard that even in case of a text keeping
256 26
		// its annotations there are removed from the Store. A redirect is not
257
		// expected to contain any other annotation other than that of the redirect
258
		// target
259
		$subject = $semanticData->getSubject();
260
		$semanticData = new SemanticData( $subject );
261
262
		$semanticData->addPropertyObjectValue(
263
			new DIProperty( '_REDI' ),
264
			$target
265
		);
266
267
		// Force a manual changeTitle before the general update otherwise
268
		// #redirect can cause an inconsistent data container as observed in #895
269
		$source = $subject->getTitle();
270
		$target = $target->getTitle();
271
272
		$this->store->changeTitle(
273
			$source,
0 ignored issues
show
Bug introduced by
It seems like $source defined by $subject->getTitle() on line 269 can be null; however, SMW\Store::changeTitle() 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...
274
			$target,
0 ignored issues
show
Bug introduced by
It seems like $target defined by $target->getTitle() on line 270 can be null; however, SMW\Store::changeTitle() 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...
275
			$source->getArticleID(),
276
			$target->getArticleID()
277
		);
278
279
		$dispatchContext = EventHandler::getInstance()->newDispatchContext();
280
		$dispatchContext->set( 'title', $subject->getTitle() );
281
282
		EventHandler::getInstance()->getEventDispatcher()->dispatch(
283
			'factbox.cache.delete',
284
			$dispatchContext
285
		);
286
287
		return $semanticData;
288
	}
289
290
}
291