Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 108
0.00% covered (danger)
0.00%
0 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
Workflow
0.00% covered (danger)
0.00%
0 / 108
0.00% covered (danger)
0.00%
0 / 20
1806
0.00% covered (danger)
0.00%
0 / 1
 fromStorageRow
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 toStorageRow
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 create
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 updateFromPageId
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getArticleTitle
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getOwnerTitle
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getFromTitleCache
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 getId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getWiki
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isDeleted
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 isNew
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLastUpdated
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLastUpdatedObj
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 updateLastUpdated
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNamespaceName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getTitleFullText
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 matchesTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 userCan
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getPermissionErrors
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace Flow\Model;
4
5use Flow\Exception\CrossWikiException;
6use Flow\Exception\DataModelException;
7use Flow\Exception\FailCommitException;
8use Flow\Exception\InvalidInputException;
9use MapCacheLRU;
10use MediaWiki\Context\RequestContext;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Title\Title;
13use MediaWiki\User\User;
14use MediaWiki\Utils\MWTimestamp;
15use MediaWiki\WikiMap\WikiMap;
16use Wikimedia\Rdbms\IDBAccessObject;
17
18class Workflow {
19
20    /**
21     * @var MapCacheLRU
22     */
23    private static $titleCache;
24
25    /**
26     * @var string[]
27     */
28    private static $allowedTypes = [ 'discussion', 'topic' ];
29
30    /**
31     * @var UUID
32     */
33    protected $id;
34
35    /**
36     * @var string e.g. topic, discussion, etc.
37     */
38    protected $type;
39
40    /**
41     * @var string
42     */
43    protected $wiki;
44
45    /**
46     * @var int
47     */
48    protected $pageId = 0;
49
50    /**
51     * @var int
52     */
53    protected $namespace;
54
55    /**
56     * @var string
57     */
58    protected $titleText;
59
60    /**
61     * @var string
62     */
63    protected $lastUpdated;
64
65    /**
66     * @var Title
67     */
68    protected $title;
69
70    /**
71     * @var Title
72     */
73    protected $ownerTitle;
74
75    /**
76     * @var bool|null Indicates if associated page_id exists (null if not yet looked up)
77     */
78    protected $exists;
79
80    /**
81     * @param array $row
82     * @param Workflow|null $obj
83     * @return Workflow
84     * @throws DataModelException
85     */
86    public static function fromStorageRow( array $row, $obj = null ) {
87        if ( $obj === null ) {
88            $obj = new self;
89        } elseif ( !$obj instanceof self ) {
90            throw new DataModelException( 'Wrong obj type: ' . get_class( $obj ), 'process-data' );
91        }
92        $obj->id = UUID::create( $row['workflow_id'] );
93        $obj->type = $row['workflow_type'];
94        $obj->wiki = $row['workflow_wiki'];
95        $obj->pageId = (int)$row['workflow_page_id'];
96        $obj->namespace = (int)$row['workflow_namespace'];
97        $obj->titleText = $row['workflow_title_text'];
98        $obj->lastUpdated = wfTimestamp( TS_MW, $row['workflow_last_update_timestamp'] );
99
100        return $obj;
101    }
102
103    /**
104     * @param Workflow $obj
105     * @return array
106     * @throws FailCommitException
107     */
108    public static function toStorageRow( Workflow $obj ) {
109        if ( $obj->pageId === 0 ) {
110            /*
111             * We try to defer creating a new page as long as possible, which
112             * means that a new board page won't have been created by the time
113             * Workflow object was created: new workflows will have a 0 pageId.
114             * This method is called when the workflow is about to be inserted.
115             * By now, the page has been inserted & we should store the real
116             * page_id this workflow is associated with.
117             */
118
119            // store ID of newly created page & reset exists status
120            $title = $obj->getOwnerTitle();
121            $obj->pageId = $title->getArticleID( IDBAccessObject::READ_LATEST );
122            $obj->exists = null;
123
124            if ( $obj->pageId === 0 ) {
125                throw new FailCommitException( 'No page for workflow: ' . serialize( $obj ) );
126            }
127        }
128        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
129
130        return [
131            'workflow_id' => $obj->id->getAlphadecimal(),
132            'workflow_type' => $obj->type,
133            'workflow_wiki' => $obj->wiki,
134            'workflow_page_id' => $obj->pageId,
135            'workflow_namespace' => $obj->namespace,
136            'workflow_title_text' => $obj->titleText,
137            'workflow_lock_state' => 0, // unused
138            'workflow_last_update_timestamp' => $dbr->timestamp( $obj->lastUpdated ),
139            // not used, but set it to empty string so it doesn't fail in strict mode
140            'workflow_name' => '',
141        ];
142    }
143
144    /**
145     * @param string $type
146     * @param Title $title
147     * @return Workflow
148     * @throws DataModelException
149     */
150    public static function create( $type, Title $title ) {
151        // temporary limitation until we implement something more concrete
152        if ( !in_array( $type, self::$allowedTypes ) ) {
153            throw new DataModelException( 'Invalid workflow type provided: ' . $type, 'process-data' );
154        }
155        if ( $title->isLocal() ) {
156            $wiki = WikiMap::getCurrentWikiId();
157        } else {
158            $wiki = $title->getTransWikiID();
159        }
160
161        $obj = new self;
162        $obj->id = UUID::create();
163        $obj->type = $type;
164        $obj->wiki = $wiki;
165
166        // for new pages, article id will be 0; it'll be fetched again in toStorageRow
167        $obj->pageId = $title->getArticleID();
168        $obj->namespace = $title->getNamespace();
169        $obj->titleText = $title->getDBkey();
170        $obj->updateLastUpdated( $obj->id );
171
172        // we just created a new workflow; wipe out any cached data for the
173        // associated title
174        if ( self::$titleCache !== null ) {
175            $key = implode( '|', [ $obj->wiki, $obj->namespace, $obj->titleText ] );
176            self::$titleCache->clear( [ $key ] );
177        }
178
179        return $obj;
180    }
181
182    /**
183     * Update the workflow after a change to title or ID (such as page move or
184     * restoration).
185     *
186     * @param int $oldPageId The page_id the workflow is currently located at
187     * @param Title $newPage The page the workflow is moving to
188     * @throws DataModelException
189     */
190    public function updateFromPageId( $oldPageId, Title $newPage ) {
191        if ( $oldPageId !== $this->pageId ) {
192            throw new DataModelException( 'Must update from same page id. ' . $this->pageId . ' !== ' . $oldPageId );
193        }
194
195        $this->pageId = $newPage->getArticleID();
196        $this->namespace = $newPage->getNamespace();
197        $this->titleText = $newPage->getDBkey();
198    }
199
200    /**
201     * Return the title this workflow responds at
202     *
203     * @return Title
204     * @throws CrossWikiException
205     */
206    public function getArticleTitle() {
207        if ( $this->title ) {
208            return $this->title;
209        }
210        // evil hax
211        if ( $this->type === 'topic' ) {
212            $namespace = NS_TOPIC;
213            $titleText = $this->id->getAlphadecimal();
214        } else {
215            $namespace = $this->namespace;
216            $titleText = $this->titleText;
217        }
218        $this->title = self::getFromTitleCache( $this->wiki, $namespace, $titleText );
219        return $this->title;
220    }
221
222    /**
223     * Return the title this workflow was created at
224     *
225     * @return Title
226     * @throws CrossWikiException
227     */
228    public function getOwnerTitle() {
229        if ( $this->ownerTitle ) {
230            return $this->ownerTitle;
231        }
232        $this->ownerTitle = self::getFromTitleCache( $this->wiki, $this->namespace, $this->titleText );
233        return $this->ownerTitle;
234    }
235
236    /**
237     * Can't use the title cache in Title class, it only operates on default namespace
238     *
239     * @param string $wiki
240     * @param int $namespace
241     * @param string $titleText
242     * @return Title
243     * @throws CrossWikiException
244     * @throws InvalidInputException
245     */
246    public static function getFromTitleCache( $wiki, $namespace, $titleText ) {
247        if ( self::$titleCache === null ) {
248            self::$titleCache = new MapCacheLRU( 50 );
249        }
250
251        $key = implode( '|', [ $wiki, $namespace, $titleText ] );
252        $title = self::$titleCache->get( $key );
253        if ( $title === null ) {
254            $title = Title::makeTitleSafe( $namespace, $titleText );
255            if ( $title ) {
256                self::$titleCache->set( $key, $title );
257            } else {
258                throw new InvalidInputException(
259                    "Fail to create title from namespace $namespace and title text '$titleText'",
260                    'invalid-input'
261                );
262            }
263        }
264
265        return $title;
266    }
267
268    /**
269     * @return UUID
270     */
271    public function getId() {
272        return $this->id;
273    }
274
275    /**
276     * @return string
277     */
278    public function getType() {
279        return $this->type;
280    }
281
282    /**
283     * Get the wiki ID, e.g. eswiki
284     *
285     * @return string
286     */
287    public function getWiki() {
288        return $this->wiki;
289    }
290
291    /**
292     * @return bool
293     */
294    public function isDeleted() {
295        if ( $this->exists === null ) {
296            // If in the context of a POST request, check against the primary DB.
297            // This is important for recentchanges actions; if a user posts a topic on an
298            // empty flow board then querying the replica results in $this->exists getting set to
299            // false. Querying the primary DB correctly returns that the title exists, and the
300            // recent changes event can propagate.
301            $this->exists = Title::newFromID(
302                $this->pageId,
303                RequestContext::getMain()->getRequest()->wasPosted() ? IDBAccessObject::READ_LATEST : 0
304            ) !== null;
305        }
306
307        // a board that does not yet exist (because workflow has not yet
308        // been stored) is not deleted, it just doesn't exist yet
309        return !$this->isNew() && !$this->exists;
310    }
311
312    /**
313     * Returns true if the workflow is new as of this request.
314     *
315     * @return bool
316     */
317    public function isNew() {
318        return $this->pageId === 0;
319    }
320
321    /**
322     * @return string
323     */
324    public function getLastUpdated() {
325        return $this->lastUpdated;
326    }
327
328    /**
329     * @return MWTimestamp
330     */
331    public function getLastUpdatedObj() {
332        return new MWTimestamp( $this->lastUpdated );
333    }
334
335    public function updateLastUpdated( UUID $latestRevisionId ) {
336        $this->lastUpdated = $latestRevisionId->getTimestamp();
337    }
338
339    /**
340     * @return string
341     */
342    public function getNamespaceName() {
343        $contentLang = MediaWikiServices::getInstance()->getContentLanguage();
344        return $contentLang->getNsText( $this->namespace );
345    }
346
347    /**
348     * @return string
349     */
350    public function getTitleFullText() {
351        $ns = $this->getNamespaceName();
352        if ( $ns ) {
353            return $ns . ':' . $this->titleText;
354        } else {
355            return $this->titleText;
356        }
357    }
358
359    /**
360     * these are exceptions currently to make debugging easier
361     * it should return false later on to allow wider use.
362     *
363     * @param Title $title
364     * @return bool
365     * @throws InvalidInputException
366     * @throws InvalidInputException
367     */
368    public function matchesTitle( Title $title ) {
369        return $this->getArticleTitle()->equals( $title );
370    }
371
372    /**
373     * Convenience wrapper for checking user permissions as boolean.
374     * getPermissionErrors 'quick' + blocked check only for logged in users
375     *
376     * @param string $permission Permission to check; for 'edit', 'create' will also be
377     *  checked if the title does not exist
378     * @param User $user
379     * @return bool Whether the user can take the action, based on a quick check
380     */
381    public function userCan( $permission, $user ) {
382        return !count( $this->getPermissionErrors( $permission, $user, 'quick' ) ) &&
383
384        // We only check the blocked status of actual users and not anons, because
385        // the anonymous version can be cached and served to many different IP
386        // addresses which will not all be blocked.
387        // See T61928
388
389        !( $user->isRegistered() &&
390            MediaWikiServices::getInstance()->getPermissionManager()
391                ->isBlockedFrom( $user, $this->getOwnerTitle(), true ) );
392    }
393
394    /**
395     * Pass-through to Title::getUserPermissionsErrors
396     * with title, and owning title if needed.
397     *
398     * @param string $permission Permission to check; for 'edit', 'create' will also be
399     *  checked if the title does not exist
400     * @param User $user User to check permissions for
401     * @param string $rigor Rigor of check; see Title->getUserPermissionsErrors
402     * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
403     */
404    public function getPermissionErrors( $permission, $user, $rigor ) {
405        $title = $this->type === 'topic' ? $this->getOwnerTitle() : $this->getArticleTitle();
406        $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
407        $errors = $permissionManager->getPermissionErrors( $permission, $user, $title, $rigor );
408
409        if ( count( $errors ) ) {
410            return $errors;
411        }
412
413        return [];
414    }
415}