Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 110
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 / 110
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 / 20
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 IDBAccessObject;
10use MapCacheLRU;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Title\Title;
13use MediaWiki\User\User;
14use MediaWiki\Utils\MWTimestamp;
15use MediaWiki\WikiMap\WikiMap;
16use RequestContext;
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()
129            ->getDBLoadBalancer()
130            ->getConnection( DB_REPLICA );
131
132        return [
133            'workflow_id' => $obj->id->getAlphadecimal(),
134            'workflow_type' => $obj->type,
135            'workflow_wiki' => $obj->wiki,
136            'workflow_page_id' => $obj->pageId,
137            'workflow_namespace' => $obj->namespace,
138            'workflow_title_text' => $obj->titleText,
139            'workflow_lock_state' => 0, // unused
140            'workflow_last_update_timestamp' => $dbr->timestamp( $obj->lastUpdated ),
141            // not used, but set it to empty string so it doesn't fail in strict mode
142            'workflow_name' => '',
143        ];
144    }
145
146    /**
147     * @param string $type
148     * @param Title $title
149     * @return Workflow
150     * @throws DataModelException
151     */
152    public static function create( $type, Title $title ) {
153        // temporary limitation until we implement something more concrete
154        if ( !in_array( $type, self::$allowedTypes ) ) {
155            throw new DataModelException( 'Invalid workflow type provided: ' . $type, 'process-data' );
156        }
157        if ( $title->isLocal() ) {
158            $wiki = WikiMap::getCurrentWikiId();
159        } else {
160            $wiki = $title->getTransWikiID();
161        }
162
163        $obj = new self;
164        $obj->id = UUID::create();
165        $obj->type = $type;
166        $obj->wiki = $wiki;
167
168        // for new pages, article id will be 0; it'll be fetched again in toStorageRow
169        $obj->pageId = $title->getArticleID();
170        $obj->namespace = $title->getNamespace();
171        $obj->titleText = $title->getDBkey();
172        $obj->updateLastUpdated( $obj->id );
173
174        // we just created a new workflow; wipe out any cached data for the
175        // associated title
176        if ( self::$titleCache !== null ) {
177            $key = implode( '|', [ $obj->wiki, $obj->namespace, $obj->titleText ] );
178            self::$titleCache->clear( [ $key ] );
179        }
180
181        return $obj;
182    }
183
184    /**
185     * Update the workflow after a change to title or ID (such as page move or
186     * restoration).
187     *
188     * @param int $oldPageId The page_id the workflow is currently located at
189     * @param Title $newPage The page the workflow is moving to
190     * @throws DataModelException
191     */
192    public function updateFromPageId( $oldPageId, Title $newPage ) {
193        if ( $oldPageId !== $this->pageId ) {
194            throw new DataModelException( 'Must update from same page id. ' . $this->pageId . ' !== ' . $oldPageId );
195        }
196
197        $this->pageId = $newPage->getArticleID();
198        $this->namespace = $newPage->getNamespace();
199        $this->titleText = $newPage->getDBkey();
200    }
201
202    /**
203     * Return the title this workflow responds at
204     *
205     * @return Title
206     * @throws CrossWikiException
207     */
208    public function getArticleTitle() {
209        if ( $this->title ) {
210            return $this->title;
211        }
212        // evil hax
213        if ( $this->type === 'topic' ) {
214            $namespace = NS_TOPIC;
215            $titleText = $this->id->getAlphadecimal();
216        } else {
217            $namespace = $this->namespace;
218            $titleText = $this->titleText;
219        }
220        $this->title = self::getFromTitleCache( $this->wiki, $namespace, $titleText );
221        return $this->title;
222    }
223
224    /**
225     * Return the title this workflow was created at
226     *
227     * @return Title
228     * @throws CrossWikiException
229     */
230    public function getOwnerTitle() {
231        if ( $this->ownerTitle ) {
232            return $this->ownerTitle;
233        }
234        $this->ownerTitle = self::getFromTitleCache( $this->wiki, $this->namespace, $this->titleText );
235        return $this->ownerTitle;
236    }
237
238    /**
239     * Can't use the title cache in Title class, it only operates on default namespace
240     *
241     * @param string $wiki
242     * @param int $namespace
243     * @param string $titleText
244     * @return Title
245     * @throws CrossWikiException
246     * @throws InvalidInputException
247     */
248    public static function getFromTitleCache( $wiki, $namespace, $titleText ) {
249        if ( self::$titleCache === null ) {
250            self::$titleCache = new MapCacheLRU( 50 );
251        }
252
253        $key = implode( '|', [ $wiki, $namespace, $titleText ] );
254        $title = self::$titleCache->get( $key );
255        if ( $title === null ) {
256            $title = Title::makeTitleSafe( $namespace, $titleText );
257            if ( $title ) {
258                self::$titleCache->set( $key, $title );
259            } else {
260                throw new InvalidInputException(
261                    "Fail to create title from namespace $namespace and title text '$titleText'",
262                    'invalid-input'
263                );
264            }
265        }
266
267        return $title;
268    }
269
270    /**
271     * @return UUID
272     */
273    public function getId() {
274        return $this->id;
275    }
276
277    /**
278     * @return string
279     */
280    public function getType() {
281        return $this->type;
282    }
283
284    /**
285     * Get the wiki ID, e.g. eswiki
286     *
287     * @return string
288     */
289    public function getWiki() {
290        return $this->wiki;
291    }
292
293    /**
294     * @return bool
295     */
296    public function isDeleted() {
297        if ( $this->exists === null ) {
298            // If in the context of a POST request, check against the primary DB.
299            // This is important for recentchanges actions; if a user posts a topic on an
300            // empty flow board then querying the replica results in $this->exists getting set to
301            // false. Querying the primary DB correctly returns that the title exists, and the
302            // recent changes event can propagate.
303            $this->exists = Title::newFromID(
304                $this->pageId,
305                RequestContext::getMain()->getRequest()->wasPosted() ? IDBAccessObject::READ_LATEST : 0
306            ) !== null;
307        }
308
309        // a board that does not yet exist (because workflow has not yet
310        // been stored) is not deleted, it just doesn't exist yet
311        return !$this->isNew() && !$this->exists;
312    }
313
314    /**
315     * Returns true if the workflow is new as of this request.
316     *
317     * @return bool
318     */
319    public function isNew() {
320        return $this->pageId === 0;
321    }
322
323    /**
324     * @return string
325     */
326    public function getLastUpdated() {
327        return $this->lastUpdated;
328    }
329
330    /**
331     * @return MWTimestamp
332     */
333    public function getLastUpdatedObj() {
334        return new MWTimestamp( $this->lastUpdated );
335    }
336
337    public function updateLastUpdated( UUID $latestRevisionId ) {
338        $this->lastUpdated = $latestRevisionId->getTimestamp();
339    }
340
341    /**
342     * @return string
343     */
344    public function getNamespaceName() {
345        $contentLang = MediaWikiServices::getInstance()->getContentLanguage();
346        return $contentLang->getNsText( $this->namespace );
347    }
348
349    /**
350     * @return string
351     */
352    public function getTitleFullText() {
353        $ns = $this->getNamespaceName();
354        if ( $ns ) {
355            return $ns . ':' . $this->titleText;
356        } else {
357            return $this->titleText;
358        }
359    }
360
361    /**
362     * these are exceptions currently to make debugging easier
363     * it should return false later on to allow wider use.
364     *
365     * @param Title $title
366     * @return bool
367     * @throws InvalidInputException
368     * @throws InvalidInputException
369     */
370    public function matchesTitle( Title $title ) {
371        return $this->getArticleTitle()->equals( $title );
372    }
373
374    /**
375     * Convenience wrapper for checking user permissions as boolean.
376     * getPermissionErrors 'quick' + blocked check only for logged in users
377     *
378     * @param string $permission Permission to check; for 'edit', 'create' will also be
379     *  checked if the title does not exist
380     * @param User $user
381     * @return bool Whether the user can take the action, based on a quick check
382     */
383    public function userCan( $permission, $user ) {
384        return !count( $this->getPermissionErrors( $permission, $user, 'quick' ) ) &&
385
386        // We only check the blocked status of actual users and not anons, because
387        // the anonymous version can be cached and served to many different IP
388        // addresses which will not all be blocked.
389        // See T61928
390
391        !( $user->isRegistered() &&
392            MediaWikiServices::getInstance()->getPermissionManager()
393                ->isBlockedFrom( $user, $this->getOwnerTitle(), true ) );
394    }
395
396    /**
397     * Pass-through to Title::getUserPermissionsErrors
398     * with title, and owning title if needed.
399     *
400     * @param string $permission Permission to check; for 'edit', 'create' will also be
401     *  checked if the title does not exist
402     * @param User $user User to check permissions for
403     * @param string $rigor Rigor of check; see Title->getUserPermissionsErrors
404     * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
405     */
406    public function getPermissionErrors( $permission, $user, $rigor ) {
407        $title = $this->type === 'topic' ? $this->getOwnerTitle() : $this->getArticleTitle();
408        $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
409        $errors = $permissionManager->getPermissionErrors( $permission, $user, $title, $rigor );
410
411        if ( count( $errors ) ) {
412            return $errors;
413        }
414
415        return [];
416    }
417}