Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 388
0.00% covered (danger)
0.00%
0 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
CentralNoticeCampaignLogPager
0.00% covered (danger)
0.00%
0 / 388
0.00% covered (danger)
0.00%
0 / 17
3906
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getIndexField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
90
 formatRow
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 1
56
 showInitialSettings
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 1
90
 showChanges
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
42
 getBannerStats
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 testBooleanChange
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 testSetChange
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
56
 testPriorityChange
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 getPriorityMessage
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 testPercentageChange
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 testTextChange
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 testTypeChange
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 getTypeText
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 getStartBody
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
2
 getEndBody
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3use MediaWiki\Html\Html;
4use MediaWiki\MainConfigNames;
5use MediaWiki\Pager\ReverseChronologicalPager;
6use MediaWiki\Title\Title;
7use MediaWiki\User\User;
8use Wikimedia\Rdbms\IExpression;
9use Wikimedia\Rdbms\LikeValue;
10
11class CentralNoticeCampaignLogPager extends ReverseChronologicalPager {
12    /** @var Title */
13    public $viewPage;
14
15    public function __construct(
16        protected readonly SpecialCentralNoticeLogs $special,
17    ) {
18        parent::__construct();
19
20        // Override paging defaults
21        [ $this->mLimit, ] = $this->mRequest->getLimitOffsetForUser(
22            $this->getUser(),
23            20,
24            ''
25        );
26        $this->mLimitsShown = [ 20, 50, 100 ];
27
28        $this->viewPage = Campaign::getTitleForURL();
29    }
30
31    /**
32     * Sort the log list by timestamp
33     * @return string
34     */
35    public function getIndexField() {
36        return 'notlog_timestamp';
37    }
38
39    /**
40     * Pull log entries from the database
41     * @inheritDoc
42     */
43    public function getQueryInfo() {
44        $request = $this->getRequest();
45
46        $filterStartDate = 0;
47        $filterEndDate = 0;
48        $start = $this->special->getDateValue( 'start' );
49        $end = $this->special->getDateValue( 'end' );
50
51        if ( $start ) {
52            $filterStartDate = substr( $start, 0, 8 );
53        }
54        if ( $end ) {
55            $filterEndDate = substr( $end, 0, 8 );
56        }
57        $filterCampaign = $request->getVal( 'campaign' );
58        $filterUser = $request->getVal( 'user' );
59        $reset = $request->getVal( 'centralnoticelogreset' );
60
61        $info = [
62            'tables' => [ 'notice_log' => 'cn_notice_log' ],
63            'fields' => '*',
64            'conds' => []
65        ];
66
67        if ( !$reset ) {
68            $dbr = $this->getDatabase();
69            if ( $filterStartDate > 0 ) {
70                $filterStartDate = intval( $filterStartDate . '000000' );
71                $info['conds'][] = $dbr->expr( 'notlog_timestamp', '>=', $filterStartDate );
72            }
73            if ( $filterEndDate > 0 ) {
74                $filterEndDate = intval( $filterEndDate . '000000' );
75                $info['conds'][] = $dbr->expr( 'notlog_timestamp', '<', $filterEndDate );
76            }
77            if ( $filterCampaign ) {
78                $info['conds'][] = $dbr->expr( 'notlog_not_name', IExpression::LIKE, new LikeValue(
79                    $dbr->anyString(), $filterCampaign, $dbr->anyString() ) );
80            }
81            if ( $filterUser ) {
82                $user = User::newFromName( $filterUser );
83                if ( $user ) {
84                    $info['conds']['notlog_user_id'] = $user->getId();
85                }
86            }
87        }
88
89        return $info;
90    }
91
92    /**
93     * Generate the content of each table row (1 row = 1 log entry)
94     * @param stdClass $row
95     * @return string HTML
96     */
97    public function formatRow( $row ) {
98        $lang = $this->getLanguage();
99
100        // Create a user object so we can pull the name, user page, etc.
101        $loggedUser = User::newFromId( $row->notlog_user_id );
102        // Create the user page link
103        $userLink = $this->special->getLinkRenderer()->makeKnownLink(
104            $loggedUser->getUserPage(),
105            $loggedUser->getName()
106        );
107        $userTalkLink = $this->special->getLinkRenderer()->makeKnownLink(
108            $loggedUser->getTalkPage(),
109            $this->msg( 'centralnotice-talk-link' )->text()
110        );
111
112        // Create the campaign link
113        $campaignLink = $this->special->getLinkRenderer()->makeKnownLink(
114            $this->viewPage,
115            $row->notlog_not_name,
116            [],
117            Campaign::getQueryForURL( $row->notlog_not_name )
118        );
119
120        // Begin log entry primary row
121        $htmlOut = Html::openElement( 'tr' );
122
123        $htmlOut .= Html::openElement( 'td', [ 'valign' => 'top' ] );
124        $notlogId = (int)$row->notlog_id;
125        if ( $row->notlog_action !== 'removed' ) {
126            $collapsedImg = $this->getLanguage()->isRtl() ?
127                'collapsed-rtl.png' :
128                'collapsed-ltr.png';
129
130            $extensionAssetsPath = $this->getConfig()->get( MainConfigNames::ExtensionAssetsPath );
131            $htmlOut .= '<a href="javascript:toggleLogDisplay(\'' . $notlogId . '\')">' .
132                '<img src="' . $extensionAssetsPath . '/CentralNotice/resources/images/' . $collapsedImg . '" ' .
133                'id="cn-collapsed-' . $notlogId . '" style="display:block;"/>' .
134                '<img src="' . $extensionAssetsPath . '/CentralNotice/resources/images/uncollapsed.png" ' .
135                'id="cn-uncollapsed-' . $notlogId . '" style="display:none;"/>' .
136                '</a>';
137        }
138        $htmlOut .= Html::closeElement( 'td' );
139        $htmlOut .= Html::element( 'td', [ 'valign' => 'top', 'class' => 'primary' ],
140            $lang->date( $row->notlog_timestamp ) . $this->msg( 'word-separator' )->plain() .
141                $lang->time( $row->notlog_timestamp )
142        );
143        $htmlOut .= Html::rawElement( 'td', [ 'valign' => 'top', 'class' => 'primary' ],
144            $this->msg( 'centralnotice-user-links' )
145                ->rawParams( $userLink, $userTalkLink )
146                ->escaped()
147        );
148        // The following messages are generated here:
149        // * centralnotice-action-created
150        // * centralnotice-action-modified
151        // * centralnotice-action-removed
152        $htmlOut .= Html::element( 'td', [ 'valign' => 'top', 'class' => 'primary' ],
153            $this->msg( 'centralnotice-action-' . $row->notlog_action )->text()
154        );
155        $htmlOut .= Html::rawElement( 'td', [ 'valign' => 'top', 'class' => 'primary' ],
156            $campaignLink
157        );
158
159        $summary = $row->notlog_comment === null
160            ? '&nbsp;'
161            : htmlspecialchars( $row->notlog_comment );
162
163        $htmlOut .= Html::rawElement( 'td',
164            [ 'valign' => 'top', 'class' => 'primary-summary' ],
165            $summary
166        );
167
168        $htmlOut .= Html::rawElement( 'td', [],
169            '&nbsp;'
170        );
171
172        // End log entry primary row
173        $htmlOut .= Html::closeElement( 'tr' );
174
175        if ( $row->notlog_action !== 'removed' ) {
176            // Begin log entry secondary row
177            $htmlOut .= Html::openElement( 'tr',
178                [ 'id' => 'cn-log-details-' . $notlogId, 'style' => 'display:none;' ] );
179
180            $htmlOut .= Html::rawElement( 'td', [ 'valign' => 'top' ],
181                // force a table cell in older browsers
182                '&nbsp;'
183            );
184            $htmlOut .= Html::openElement( 'td', [ 'valign' => 'top', 'colspan' => '6' ] );
185            if ( $row->notlog_action === 'created' ) {
186                $htmlOut .= $this->showInitialSettings( $row );
187            } elseif ( $row->notlog_action === 'modified' ) {
188                $htmlOut .= $this->showChanges( $row );
189            }
190            $htmlOut .= Html::closeElement( 'td' );
191
192            // End log entry secondary row
193            $htmlOut .= Html::closeElement( 'tr' );
194        }
195
196        return $htmlOut;
197    }
198
199    /**
200     * @param stdClass $row
201     * @return string
202     */
203    public function showInitialSettings( $row ) {
204        $lang = $this->getLanguage();
205        $details = '';
206        $wordSeparator = $this->msg( 'word-separator' )->plain();
207        $details .= $this->msg(
208            'centralnotice-log-label',
209            $this->msg( 'centralnotice-start-timestamp' )->text(),
210            $lang->date( $row->notlog_end_start ) . $wordSeparator .
211                $lang->time( $row->notlog_end_start )
212        )->parse() . "<br />";
213        $details .= $this->msg(
214            'centralnotice-log-label',
215            $this->msg( 'centralnotice-end-timestamp' )->text(),
216            $lang->date( $row->notlog_end_end ) . $wordSeparator .
217                $lang->time( $row->notlog_end_end )
218        )->parse() . "<br />";
219        $details .= $this->msg(
220            'centralnotice-log-label',
221            $this->msg( 'centralnotice-projects' )->text(),
222            wfEscapeWikiText( $row->notlog_end_projects )
223        )->parse() . "<br />";
224        $language_count = count( explode( ', ', $row->notlog_end_languages ) );
225        $languageList = '';
226        if ( $language_count > 15 ) {
227            $languageList = $this->msg( 'centralnotice-multiple-languages' )
228                ->numParams( $language_count )->text();
229        } elseif ( $language_count > 0 ) {
230            $languageList = $row->notlog_end_languages;
231        }
232        $details .= $this->msg(
233            'centralnotice-log-label',
234            $this->msg( 'centralnotice-languages' )->text(),
235            wfEscapeWikiText( $languageList )
236        )->parse() . "<br />";
237        $details .= $this->msg(
238            'centralnotice-log-label',
239            $this->msg( 'centralnotice-geo' )->text(),
240            ( $row->notlog_end_geo ? 'on' : 'off' )
241        )->parse() . "<br />";
242        if ( $row->notlog_end_geo ) {
243            $country_count = count( explode( ', ', $row->notlog_end_countries ?? '' ) );
244            $countryList = '';
245            if ( $country_count > 20 ) {
246                $countryList = $this->msg( 'centralnotice-multiple-countries' )
247                    ->numParams( $country_count )->text();
248            } elseif ( $country_count > 0 ) {
249                $countryList = $row->notlog_end_countries;
250            }
251            $details .= $this->msg(
252                'centralnotice-log-label',
253                $this->msg( 'centralnotice-countries' )->text(),
254                wfEscapeWikiText( $countryList )
255            )->parse() . "<br />";
256
257            $regions_count = count( explode( ', ', $row->notlog_end_regions ?? '' ) );
258            $regionsList = '';
259            if ( $regions_count > 20 ) {
260                $regionsList = $this->msg( 'centralnotice-multiple-regions' )
261                    ->numParams( $regions_count )->text();
262            } elseif ( $regions_count > 0 ) {
263                $regionsList = $row->notlog_end_regions;
264            }
265            $details .= $this->msg(
266                'centralnotice-log-label',
267                $this->msg( 'centralnotice-regions' )->text(),
268                wfEscapeWikiText( $regionsList )
269            )->parse() . "<br />";
270
271        }
272        return $details;
273    }
274
275    /**
276     * @param stdClass $row
277     * @return string
278     */
279    public function showChanges( $row ) {
280        $lang = $this->getLanguage();
281        $details = '';
282        $wordSeparator = $this->msg( 'word-separator' )->plain();
283        if ( $row->notlog_begin_start !== $row->notlog_end_start ) {
284            $details .= $this->msg(
285                'centralnotice-log-label',
286                $this->msg( 'centralnotice-start-timestamp' )->text(),
287                $this->msg(
288                    'centralnotice-changed',
289                    $lang->date( $row->notlog_begin_start ) . $wordSeparator .
290                        $lang->time( $row->notlog_begin_start ),
291                    $lang->date( $row->notlog_end_start ) . $wordSeparator .
292                        $lang->time( $row->notlog_end_start )
293                )->text()
294            )->parse() . "<br />";
295        }
296        if ( $row->notlog_begin_end !== $row->notlog_end_end ) {
297            $details .= $this->msg(
298                'centralnotice-log-label',
299                $this->msg( 'centralnotice-end-timestamp' )->text(),
300                $this->msg(
301                    'centralnotice-changed',
302                    $lang->date( $row->notlog_begin_end ) . $wordSeparator .
303                        $lang->time( $row->notlog_begin_end ),
304                    $lang->date( $row->notlog_end_end ) . $wordSeparator .
305                        $lang->time( $row->notlog_end_end )
306                )->text()
307            )->parse() . "<br />";
308        }
309        // When adding new params, update the possibly generated
310        // i18n keys in the respective functions.
311        $details .= $this->testBooleanChange( 'enabled', $row );
312        $details .= $this->testPriorityChange( 'preferred', $row );
313        $details .= $this->testBooleanChange( 'locked', $row );
314        $details .= $this->testBooleanChange( 'geo', $row );
315        $details .= $this->testBooleanChange( 'buckets', $row );
316        $details .= $this->testPercentageChange( 'throttle', $row );
317        $details .= $this->testSetChange( 'projects', $row );
318        $details .= $this->testSetChange( 'languages', $row );
319        $details .= $this->testSetChange( 'countries', $row );
320        $details .= $this->testSetChange( 'regions', $row );
321        $details .= $this->testBooleanChange( 'archived', $row );
322        $details .= $this->testTypeChange( $row );
323
324        $details .= $this->testTextChange(
325            'campaign-mixins',
326            $row->notlog_end_mixins,
327            $row->notlog_begin_mixins
328        );
329
330        if ( $row->notlog_begin_banners !== $row->notlog_end_banners ) {
331            // Show changes to banner weights and assignment
332            $beginBanners = $this->getBannerStats( json_decode( $row->notlog_begin_banners, true ) );
333            $endBanners = $this->getBannerStats( json_decode( $row->notlog_end_banners, true ) );
334            if ( $beginBanners ) {
335                $before = $lang->commaList( $beginBanners );
336            } else {
337                $before = $this->msg( 'centralnotice-no-assignments' )->text();
338            }
339            if ( $endBanners ) {
340                $after = $lang->commaList( $endBanners );
341            } else {
342                $after = $this->msg( 'centralnotice-no-assignments' )->text();
343            }
344            $details .= $this->msg(
345                'centralnotice-log-label',
346                $this->msg( 'centralnotice-templates' )->text(),
347                $this->msg( 'centralnotice-changed', $before, $after )->text()
348            )->parse() . "<br />";
349        }
350        return $details;
351    }
352
353    private function getBannerStats( array $array ): array {
354        $ret = [];
355        foreach ( $array as $key => $params ) {
356            if ( is_array( $params ) ) {
357                $weight = $params['weight'];
358                $bucket = chr( 65 + $params['bucket'] );
359            } else {
360                // Legacy, we used to only store the weight
361                $weight = $params;
362                $bucket = 0;
363            }
364            $ret[$key] = "$key ($bucket$weight)";
365        }
366
367        return $ret;
368    }
369
370    /**
371     * @param string $param
372     * @param stdClass $row
373     * @return string
374     */
375    private function testBooleanChange( $param, $row ) {
376        $result = '';
377        $beginField = 'notlog_begin_' . $param;
378        $endField = 'notlog_end_' . $param;
379        if ( $row->$beginField !== $row->$endField ) {
380            // The following messages are generated here:
381            // * centralnotice-enabled
382            // * centralnotice-locked
383            // * centralnotice-geo
384            // * centralnotice-buckets
385            // * centralnotice-archived
386            $result .= $this->msg(
387                'centralnotice-log-label',
388                $this->msg( 'centralnotice-' . $param )->text(),
389                $this->msg(
390                    'centralnotice-changed',
391                    ( $row->$beginField
392                        ? $this->msg( 'centralnotice-on' )->text()
393                        : $this->msg( 'centralnotice-off' )->text() ),
394                    ( $row->$endField
395                        ? $this->msg( 'centralnotice-on' )->text()
396                        : $this->msg( 'centralnotice-off' )->text() )
397                )->text()
398            )->parse() . "<br />";
399        }
400        return $result;
401    }
402
403    /**
404     * @param string $param
405     * @param stdClass $row
406     * @return string
407     */
408    private function testSetChange( $param, $row ) {
409        $result = '';
410        $beginField = 'notlog_begin_' . $param;
411        $endField = 'notlog_end_' . $param;
412
413        if ( $row->$beginField !== $row->$endField ) {
414            $lang = $this->getLanguage();
415            $beginSet = [];
416            $endSet = [];
417            if ( $row->$beginField ) {
418                $beginSet = explode( ', ', $row->$beginField );
419            }
420            if ( $row->$endField ) {
421                $endSet = explode( ', ', $row->$endField );
422            }
423            $added = array_diff( $endSet, $beginSet );
424            $removed = array_diff( $beginSet, $endSet );
425            $differences = '';
426            if ( $added ) {
427                $differences .= $this->msg(
428                    'centralnotice-added', $lang->commaList( $added ) )->text();
429                if ( $removed ) {
430                    $differences .= '; ';
431                }
432            }
433            if ( $removed ) {
434                $differences .= $this->msg(
435                    'centralnotice-removed', $lang->commaList( $removed ) )->text();
436            }
437            // The following messages are generated here:
438            // * centralnotice-projects
439            // * centralnotice-languages
440            // * centralnotice-countries
441            // * centralnotice-regions
442            $result .= $this->msg(
443                'centralnotice-log-label',
444                $this->msg( 'centralnotice-' . $param )->text(),
445                $differences
446            )->parse() . "<br />";
447        }
448        return $result;
449    }
450
451    /**
452     * Test for changes to campaign priority
453     * @param string $param
454     * @param stdClass $row
455     * @return string
456     */
457    private function testPriorityChange( $param, $row ) {
458        $result = '';
459        $beginField = 'notlog_begin_' . $param;
460        $endField = 'notlog_end_' . $param;
461        if ( $row->$beginField !== $row->$endField ) {
462            $beginMessage = $this->getPriorityMessage( $row->$beginField );
463            $endMessage = $this->getPriorityMessage( $row->$endField );
464
465            // The following messages are generated here:
466            // * centralnotice-preferred
467            $result .= $this->msg(
468                'centralnotice-log-label',
469                $this->msg( 'centralnotice-' . $param )->text(),
470                $this->msg(
471                    'centralnotice-changed',
472                    $beginMessage,
473                    $endMessage
474                )->text()
475            )->parse() . "<br />";
476        }
477        return $result;
478    }
479
480    private function getPriorityMessage( int $value ): string {
481        return match ( $value ) {
482            CentralNotice::LOW_PRIORITY => $this->msg( 'centralnotice-priority-low' )->text(),
483            CentralNotice::NORMAL_PRIORITY => $this->msg( 'centralnotice-priority-normal' )->text(),
484            CentralNotice::HIGH_PRIORITY => $this->msg( 'centralnotice-priority-high' )->text(),
485            CentralNotice::EMERGENCY_PRIORITY => $this->msg( 'centralnotice-priority-emergency' )->text(),
486            default => '',
487        };
488    }
489
490    /**
491     * Test for changes to a property interpreted as a percentage
492     * @param string $param name
493     * @param stdClass $row settings
494     * @return string
495     */
496    private function testPercentageChange( $param, $row ) {
497        $beginField = 'notlog_begin_' . $param;
498        $endField = 'notlog_end_' . $param;
499        $result = '';
500        if ( $row->$beginField !== $row->$endField ) {
501            $beginMessage = strval( $row->$beginField ) . '%';
502            $endMessage = strval( $row->$endField ) . '%';
503            // The following messages are generated here:
504            // * centralnotice-throttle
505            $result .= $this->msg(
506                'centralnotice-log-label',
507                $this->msg( 'centralnotice-' . $param )->text(),
508                $this->msg(
509                    'centralnotice-changed',
510                    $beginMessage,
511                    $endMessage
512                )->text()
513            )->parse() . "<br />";
514        }
515        return $result;
516    }
517
518    /**
519     * @param string $param
520     * @param string $newval
521     * @param string $oldval
522     * @return string
523     */
524    protected function testTextChange( $param, $newval, $oldval ) {
525        $result = '';
526        if ( $oldval !== $newval ) {
527            // The following messages are generated here:
528            // * centralnotice-campaign-mixins
529            // * centralnotice-category
530            // * centralnotice-landingpages
531            // * centralnotice-controller_mixin
532            // * centralnotice-prioritylangs
533            // * centralnotice-devices
534            $result .= $this->msg(
535                'centralnotice-log-label',
536                $this->msg( 'centralnotice-' . $param )->text(),
537                $this->msg(
538                    'centralnotice-changed',
539                    wfEscapeWikiText( $oldval ),
540                    wfEscapeWikiText( $newval )
541                )->text()
542            )->parse() . "<br/>";
543        }
544        return $result;
545    }
546
547    /**
548     * @param stdClass $row
549     * @return string
550     */
551    private function testTypeChange( $row ) {
552        $result = '';
553
554        $oldval = $row->notlog_begin_type;
555        $newval = $row->notlog_end_type;
556
557        if ( $oldval !== $newval ) {
558            $result .= $this->msg(
559                'centralnotice-log-label',
560                $this->msg( 'centralnotice-campaign-type' )->text(),
561                $this->msg(
562                    'centralnotice-changed',
563                    $this->getTypeText( $oldval ),
564                    $this->getTypeText( $newval )
565                )->text()
566            )->parse() . "<br/>";
567        }
568
569        return $result;
570    }
571
572    private function getTypeText( ?string $typeId ): string {
573        // This is the case for no type set; $typeId should be null.
574        if ( !$typeId ) {
575            return $this->msg( 'centralnotice-empty-campaign-type-option' )->plain();
576        }
577
578        $type = CampaignType::getById( $typeId );
579
580        // This is the case for a type that exists in the logs but not in the config
581        if ( !$type ) {
582            return $typeId;
583        }
584
585        $message = $this->msg( $type->getMessageKey() );
586
587        // This is the case for a type that exists in the logs and the config but has no
588        // associated i18n message
589        if ( !$message->exists() ) {
590            return $typeId;
591        }
592
593        return $message->plain();
594    }
595
596    /**
597     * Specify table headers
598     * @return string HTML
599     */
600    public function getStartBody() {
601        $htmlOut = Html::openElement( 'table', [ 'id' => 'cn-campaign-logs', 'cellpadding' => 3 ] );
602        $htmlOut .= Html::openElement( 'tr' );
603        $htmlOut .= Html::element( 'th', [ 'style' => 'width: 20px;' ] );
604        $htmlOut .= Html::element( 'th', [ 'align' => 'left', 'style' => 'width: 130px;' ],
605            $this->msg( 'centralnotice-timestamp' )->text()
606        );
607        $htmlOut .= Html::element( 'th', [ 'align' => 'left', 'style' => 'width: 160px;' ],
608            $this->msg( 'centralnotice-user' )->text()
609        );
610        $htmlOut .= Html::element( 'th', [ 'align' => 'left', 'style' => 'width: 100px;' ],
611            $this->msg( 'centralnotice-action' )->text()
612        );
613        $htmlOut .= Html::element( 'th', [ 'align' => 'left', 'style' => 'width: 160px;' ],
614            $this->msg( 'centralnotice-notice' )->text()
615        );
616        $htmlOut .= Html::element( 'th', [ 'align' => 'left', 'style' => 'width: 250px;' ],
617            $this->msg( 'centralnotice-change-summary-heading' )->text()
618        );
619        $htmlOut .= Html::rawElement( 'td', [],
620            '&nbsp;'
621        );
622        $htmlOut .= Html::closeElement( 'tr' );
623        return $htmlOut;
624    }
625
626    /**
627     * Close table
628     * @return string HTML
629     */
630    public function getEndBody() {
631        return Html::closeElement( 'table' );
632    }
633}