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