Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 102
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ChangesListQuery
0.00% covered (danger)
0.00%
0 / 102
0.00% covered (danger)
0.00%
0 / 7
2652
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setExtendWatchlist
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 loadMetadataBatch
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
210
 excludeFromChangesList
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getResult
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
72
 isRecordHidden
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
600
 changeSeparator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Flow\Formatter;
4
5use Flow\Data\Listener\RecentChangesListener;
6use Flow\Data\ManagerGroup;
7use Flow\Exception\FlowException;
8use Flow\FlowActions;
9use Flow\Model\UUID;
10use Flow\Repository\TreeRepository;
11use MediaWiki\Logger\LoggerFactory;
12use RecentChange;
13
14class ChangesListQuery extends AbstractQuery {
15
16    /**
17     * Check if the most recent action for an entity has been displayed already
18     *
19     * @var array
20     */
21    protected $displayStatus = [];
22
23    /**
24     * @var FlowActions
25     */
26    protected $actions;
27
28    /**
29     * @var bool
30     */
31    protected $extendWatchlist = false;
32
33    public function __construct( ManagerGroup $storage, TreeRepository $treeRepo, FlowActions $actions ) {
34        parent::__construct( $storage, $treeRepo );
35        $this->actions = $actions;
36    }
37
38    /**
39     * @param bool $extend
40     */
41    public function setExtendWatchlist( $extend ) {
42        $this->extendWatchlist = (bool)$extend;
43    }
44
45    /**
46     * @param \stdClass[] $rows List of recentchange database rows
47     * @param bool $isWatchlist
48     * @suppress PhanParamSignatureMismatch The signature doesn't match, though
49     */
50    public function loadMetadataBatch( $rows, $isWatchlist = false ) {
51        $needed = [];
52        foreach ( $rows as $row ) {
53            if ( !isset( $row->rc_source ) || $row->rc_source !== RecentChangesListener::SRC_FLOW ) {
54                continue;
55            }
56            if ( !isset( $row->rc_params ) ) {
57                wfDebugLog( 'Flow', __METHOD__ . ': Bad row without rc_params passed in $rows' );
58                continue;
59            }
60            $params = unserialize( $row->rc_params );
61            if ( !$params ) {
62                wfDebugLog( 'Flow', __METHOD__ . ": rc_params does not contain serialized content: {$row->rc_params}" );
63                continue;
64            }
65            $changeData = $params['flow-workflow-change'];
66            /**
67             * Check to make sure revision_type exists, this is to make sure corrupted
68             * flow recent change data doesn't throw error on the page.
69             * See bug 59106 for more detail
70             */
71            if ( !isset( $changeData['revision_type'] ) ) {
72                continue;
73            }
74            if ( $this->excludeFromChangesList( $isWatchlist, $changeData['action'] ) ) {
75                continue;
76            }
77            if ( $isWatchlist && $this->isRecordHidden( $changeData ) ) {
78                continue;
79            }
80            $revisionType = $changeData['revision_type'];
81            $needed[$revisionType][] = UUID::create( $changeData['revision'] );
82        }
83
84        $found = [];
85        foreach ( $needed as $type => $uids ) {
86            $found[] = $this->storage->getMulti( $type, $uids );
87        }
88
89        $found = array_filter( $found );
90        $count = count( $found );
91        if ( $count === 0 ) {
92            $results = [];
93        } elseif ( $count === 1 ) {
94            $results = reset( $found );
95        } else {
96            $results = array_merge( ...array_values( $found ) );
97        }
98
99        if ( $results ) {
100            parent::loadMetadataBatch( $results );
101        }
102    }
103
104    /**
105     * @param bool $isWatchlist Whether this is Special:Watchlist
106     * @param string $action The Flow action this line represents
107     * @return bool
108     */
109    private function excludeFromChangesList( $isWatchlist, $action ) {
110        // If we want to exclude things from watchlist, we can add exclude_from_watchlist
111        if ( $isWatchlist ) {
112            return false;
113        } else {
114            return (bool)$this->actions->getValue( $action, 'exclude_from_recentchanges' );
115        }
116    }
117
118    /**
119     * @param null $cl No longer used
120     * @param RecentChange $rc
121     * @param bool $isWatchlist
122     * @return RecentChangesRow|bool False on failure
123     * @throws FlowException
124     */
125    public function getResult( $cl, RecentChange $rc, $isWatchlist = false ) {
126        $rcParams = $rc->getAttribute( 'rc_params' );
127        $params = unserialize( $rcParams );
128        if ( !$params ) {
129            throw new FlowException( 'rc_params does not contain serialized content: ' . $rcParams );
130        }
131        $changeData = $params['flow-workflow-change'];
132
133        if ( !is_array( $changeData ) ) {
134            throw new FlowException( 'Flow data missing in recent changes.' );
135        }
136
137        /**
138         * Check to make sure revision_type exists, this is to make sure corrupted
139         * flow recent change data doesn't throw error on the page.
140         * See bug 59106 for more detail
141         */
142        if ( !isset( $changeData['revision_type'] ) ) {
143            throw new FlowException( 'Corrupted rc without changeData: ' . $rc->getAttribute( 'rc_id' ) );
144        }
145
146        if ( $this->excludeFromChangesList( $isWatchlist, $changeData['action'] ) ) {
147            return false;
148        }
149
150        // Only show most recent items for watchlist
151        if ( $isWatchlist && $this->isRecordHidden( $changeData ) ) {
152            return false;
153        }
154
155        $alpha = UUID::create( $changeData['revision'] )->getAlphadecimal();
156        if ( !isset( $this->revisionCache[$alpha] ) ) {
157            LoggerFactory::getInstance( 'Flow' )->error(
158                'Revision not found in revisionCache: {alpha}',
159                [
160                    'alpha' => $alpha,
161                    'rcParams' => $rcParams,
162                ]
163            );
164            return false;
165        }
166        $revision = $this->revisionCache[$alpha];
167
168        $res = new RecentChangesRow;
169        $this->buildResult( $revision, 'timestamp', $res );
170        $res->recentChange = $rc;
171
172        return $res;
173    }
174
175    /**
176     * Determines if a flow record should be displayed in Special:Watchlist
177     *
178     * @param array $changeData
179     * @return bool
180     */
181    protected function isRecordHidden( array $changeData ) {
182        if ( $this->extendWatchlist ) {
183            return false;
184        }
185        // Check for legacy action names and convert it
186        $alias = $this->actions->getValue( $changeData['action'] );
187        if ( is_string( $alias ) ) {
188            $action = $alias;
189        } else {
190            $action = $changeData['action'];
191        }
192        // * Display the most recent new post, edit post, edit title for a topic
193        // * Display the most recent header edit
194        // * Display all new topic and moderation actions
195        switch ( $action ) {
196            case 'create-header':
197            case 'edit-header':
198                if (
199                    isset( $this->displayStatus['header-' . $changeData['workflow']] ) &&
200                    $this->displayStatus['header-' . $changeData['workflow']] !== $changeData['revision']
201                ) {
202                    return true;
203                }
204                $this->displayStatus['header-' . $changeData['workflow']] = $changeData['revision'];
205                break;
206
207            case 'hide-post':
208            case 'hide-topic':
209            case 'delete-post':
210            case 'delete-topic':
211            case 'suppress-post':
212            case 'suppress-topic':
213            case 'restore-post':
214            case 'restore-topic':
215            case 'lock-topic':
216                // moderation actions are always shown when visible to the user
217                return false;
218
219            case 'new-topic':
220            case 'reply':
221            case 'edit-post':
222            case 'edit-title':
223            case 'create-topic-summary':
224            case 'edit-topic-summary':
225                if (
226                    isset( $this->displayStatus['topic-' . $changeData['workflow']] ) &&
227                    $this->displayStatus['topic-' . $changeData['workflow']] !== $changeData['revision']
228                ) {
229                    return true;
230                }
231                $this->displayStatus['topic-' . $changeData['workflow']] = $changeData['revision'];
232                break;
233        }
234
235        return false;
236    }
237
238    protected function changeSeparator() {
239        return ' <span class="mw-changeslist-separator">. .</span> ';
240    }
241}