Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.59% |
7 / 1192 |
|
6.38% |
3 / 47 |
CRAP | |
0.00% |
0 / 1 |
| CentralNotice | |
0.59% |
7 / 1192 |
|
6.38% |
3 / 47 |
36789.61 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| execute | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
90 | |||
| outputEnclosingDivStartTag | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| outputEnclosingDivEndTag | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| outputListOfNotices | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
30 | |||
| handleNoticePostFromList | |
0.00% |
0 / 46 |
|
0.00% |
0 / 1 |
156 | |||
| dateSelector | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
12 | |||
| timeSelectorTd | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
| timeSelector | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
| campaignTypeSelector | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
132 | |||
| prioritySelector | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
20 | |||
| createSelector | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
| addNoticeForm | |
0.00% |
0 / 81 |
|
0.00% |
0 / 1 |
12 | |||
| handleAddCampaignPost | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
72 | |||
| getDateTime | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
| outputNoticeDetail | |
0.00% |
0 / 78 |
|
0.00% |
0 / 1 |
272 | |||
| handleNoticeDetailPost | |
0.00% |
0 / 137 |
|
0.00% |
0 / 1 |
1122 | |||
| displayCampaignWarnings | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| noticeDetailForm | |
0.00% |
0 / 215 |
|
0.00% |
0 / 1 |
132 | |||
| makeNoticeMixinControlName | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| assignedTemplatesForm | |
0.00% |
0 / 108 |
|
0.00% |
0 / 1 |
90 | |||
| weightDropdown | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
| bucketDropdown | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
20 | |||
| numBucketsDropdown | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
| addTemplatesForm | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
6 | |||
| languageMultiSelector | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
12 | |||
| projectMultiSelector | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
| dropdownList | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| makeSummaryField | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
| getSummaryFromRequest | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| paddedRange | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
| showError | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| geoMultiSelectorTree | |
0.00% |
0 / 109 |
|
0.00% |
0 / 1 |
72 | |||
| sanitizeSearchTerms | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| truncateSummaryField | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| getAssociatedNavigationLinks | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| getShortDescription | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| getCNSessionVar | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| setCNSessionVar | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| listProjects | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| listCountriesRegions | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
30 | |||
| listLanguages | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| makeShortList | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
| listToArray | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| outputHeader | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| 1 | <?php |
| 2 | |
| 3 | use MediaWiki\Exception\ErrorPageError; |
| 4 | use MediaWiki\Exception\ReadOnlyError; |
| 5 | use MediaWiki\Html\Html; |
| 6 | use MediaWiki\Json\FormatJson; |
| 7 | use MediaWiki\MainConfigNames; |
| 8 | use MediaWiki\MediaWikiServices; |
| 9 | use MediaWiki\Parser\ParserOptions; |
| 10 | use MediaWiki\Parser\Sanitizer; |
| 11 | use MediaWiki\Request\WebRequest; |
| 12 | use MediaWiki\SpecialPage\SpecialPage; |
| 13 | use MediaWiki\SpecialPage\UnlistedSpecialPage; |
| 14 | use MediaWiki\Xml\XmlSelect; |
| 15 | |
| 16 | class CentralNotice extends UnlistedSpecialPage { |
| 17 | |
| 18 | // TODO review usage of Xml class and unnecessary openElement() and closeElement() |
| 19 | // methods. |
| 20 | |
| 21 | // Note: These values are not arbitrary. Higher priority is indicated by a |
| 22 | // higher value. |
| 23 | public const LOW_PRIORITY = 0; |
| 24 | public const NORMAL_PRIORITY = 1; |
| 25 | public const HIGH_PRIORITY = 2; |
| 26 | public const EMERGENCY_PRIORITY = 3; |
| 27 | |
| 28 | // String to use in drop-down to indicate no campaign type (repesented as null in DB) |
| 29 | private const EMPTY_CAMPAIGN_TYPE_OPTION = 'empty-campaign-type-option'; |
| 30 | |
| 31 | // When displaying a long list, display the complement "all except ~LIST" |
| 32 | // past a threshold, given as a proportion of the "all" list length. |
| 33 | private const LIST_COMPLEMENT_THRESHOLD = 0.75; |
| 34 | |
| 35 | /** @var bool|null */ |
| 36 | public $editable; |
| 37 | /** @var bool|null */ |
| 38 | public $centralNoticeError; |
| 39 | |
| 40 | /** |
| 41 | * @var Campaign |
| 42 | */ |
| 43 | private $campaign; |
| 44 | /** @var array */ |
| 45 | private $campaignWarnings = []; |
| 46 | |
| 47 | public function __construct( string $name = 'CentralNotice' ) { |
| 48 | // Register special page |
| 49 | parent::__construct( $name ); |
| 50 | } |
| 51 | |
| 52 | /** @inheritDoc */ |
| 53 | public function doesWrites() { |
| 54 | return true; |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * Handle different types of page requests |
| 59 | * @param string|null $sub |
| 60 | */ |
| 61 | public function execute( $sub ) { |
| 62 | // Begin output |
| 63 | $this->setHeaders(); |
| 64 | $this->outputHeader(); |
| 65 | |
| 66 | $out = $this->getOutput(); |
| 67 | $request = $this->getRequest(); |
| 68 | |
| 69 | $this->addHelpLink( |
| 70 | '//meta.wikimedia.org/wiki/Special:MyLanguage/Help:CentralNotice', |
| 71 | true |
| 72 | ); |
| 73 | |
| 74 | // Check permissions |
| 75 | $this->editable = $this->getUser()->isAllowed( 'centralnotice-admin' ); |
| 76 | $this->getOutput()->addJsConfigVars( [ 'CentralNoticeEditable' => $this->editable ] ); |
| 77 | |
| 78 | // Initialize error variable |
| 79 | $this->centralNoticeError = false; |
| 80 | |
| 81 | $subaction = $request->getVal( 'subaction' ); |
| 82 | |
| 83 | // Switch to campaign detail interface if requested. |
| 84 | // This will also handle post submissions from the detail interface. |
| 85 | if ( $subaction === 'noticeDetail' ) { |
| 86 | $notice = $request->getVal( 'notice' ); |
| 87 | $this->outputNoticeDetail( $notice ); |
| 88 | return; |
| 89 | } |
| 90 | |
| 91 | // Handle form submissions from "Manage campaigns" or "Add a campaign" interface |
| 92 | if ( $this->editable && $request->wasPosted() ) { |
| 93 | if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() |
| 94 | || CNDatabase::getPrimaryDb()->isReadOnly() |
| 95 | ) { |
| 96 | throw new ReadOnlyError(); |
| 97 | } |
| 98 | |
| 99 | // Check authentication token |
| 100 | if ( $this->getUser()->matchEditToken( $request->getVal( 'authtoken' ) ) ) { |
| 101 | // Handle adding a campaign or changing existing campaign settings |
| 102 | // via the list interface. In either case, we'll retirect to the |
| 103 | // list view. |
| 104 | if ( $subaction === 'addCampaign' ) { |
| 105 | $this->handleAddCampaignPost(); |
| 106 | } else { |
| 107 | $this->handleNoticePostFromList(); |
| 108 | } |
| 109 | |
| 110 | // If there were no errors, reload the page to prevent duplicate form submission |
| 111 | if ( !$this->centralNoticeError ) { |
| 112 | $out->redirect( $this->getPageTitle()->getLocalURL() ); |
| 113 | return; |
| 114 | } |
| 115 | } else { |
| 116 | $this->showError( 'sessionfailure' ); |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | $this->outputListOfNotices( $subaction === 'showArchived' ); |
| 121 | } |
| 122 | |
| 123 | /** |
| 124 | * Output the start tag for the enclosing div we use on all subactions |
| 125 | */ |
| 126 | private function outputEnclosingDivStartTag() { |
| 127 | $this->getOutput()->addHTML( Html::openElement( 'div', [ 'id' => 'preferences' ] ) ); |
| 128 | } |
| 129 | |
| 130 | /** |
| 131 | * Output the end tag for the enclosing div we use on all subactions |
| 132 | */ |
| 133 | private function outputEnclosingDivEndTag() { |
| 134 | $this->getOutput()->addHTML( Html::closeElement( 'div' ) ); |
| 135 | } |
| 136 | |
| 137 | /** |
| 138 | * Send the list of notices (campaigns) to output and, if appropriate, |
| 139 | * the "Add campaign" form. |
| 140 | * @param bool|null $showArchived Set true to only show archived campaigns, |
| 141 | * false to only show unarchived campaigns |
| 142 | */ |
| 143 | private function outputListOfNotices( $showArchived = null ) { |
| 144 | $this->outputEnclosingDivStartTag(); |
| 145 | |
| 146 | $out = $this->getOutput(); |
| 147 | $out->addModules( 'ext.centralNotice.adminUi' ); |
| 148 | |
| 149 | $out->addHTML( Html::element( 'h2', |
| 150 | [ 'class' => 'cn-special-section' ], |
| 151 | $this->msg( |
| 152 | $showArchived ? 'centralnotice-archived-campaigns' : 'centralnotice-manage' |
| 153 | )->text() |
| 154 | ) ); |
| 155 | |
| 156 | $out->addModules( 'ext.centralNotice.adminUi.campaignPager' ); |
| 157 | |
| 158 | if ( $showArchived === false ) { |
| 159 | $out->addHTML( |
| 160 | $this->getLinkRenderer()->makeLink( |
| 161 | SpecialPage::getTitleFor( 'CentralNotice' ), |
| 162 | $this->msg( 'centralnotice-archive-show' )->text(), |
| 163 | [], |
| 164 | [ 'subaction' => 'showArchived' ] |
| 165 | ) |
| 166 | ); |
| 167 | } |
| 168 | |
| 169 | $pager = new CNCampaignPager( $this, $this->editable, null, $showArchived ); |
| 170 | $popts = ParserOptions::newFromContext( $this->getContext() ); |
| 171 | $out->addParserOutputContent( $pager->getBodyOutput(), $popts ); |
| 172 | $out->addHTML( $pager->getNavigationBar() ); |
| 173 | |
| 174 | // If the user has edit rights, show a form for adding a campaign |
| 175 | if ( $this->editable && $showArchived !== true ) { |
| 176 | $this->addNoticeForm(); |
| 177 | } |
| 178 | |
| 179 | $this->outputEnclosingDivEndTag(); |
| 180 | } |
| 181 | |
| 182 | private function handleNoticePostFromList() { |
| 183 | $request = $this->getRequest(); |
| 184 | $changes = json_decode( $request->getText( 'changes' ), true ); |
| 185 | $summary = $this->getSummaryFromRequest( $request ); |
| 186 | |
| 187 | // Make the changes requested |
| 188 | foreach ( $changes as $campaignName => $campaignChanges ) { |
| 189 | $initialSettings = Campaign::getCampaignSettings( $campaignName, true ); |
| 190 | |
| 191 | // Next campaign if somehow this one doesn't exist |
| 192 | if ( !$initialSettings ) { |
| 193 | wfLogWarning( 'Change requested for non-existent campaign ' . |
| 194 | $campaignName ); |
| 195 | |
| 196 | continue; |
| 197 | } |
| 198 | |
| 199 | // Set values as per $changes |
| 200 | if ( isset( $campaignChanges['archived'] ) ) { |
| 201 | Campaign::setBooleanCampaignSetting( $campaignName, 'archived', |
| 202 | $campaignChanges['archived'] ); |
| 203 | } |
| 204 | |
| 205 | if ( isset( $campaignChanges['locked'] ) ) { |
| 206 | Campaign::setBooleanCampaignSetting( $campaignName, 'locked', |
| 207 | $campaignChanges['locked'] ); |
| 208 | } |
| 209 | |
| 210 | if ( isset( $campaignChanges['enabled'] ) ) { |
| 211 | Campaign::setBooleanCampaignSetting( $campaignName, 'enabled', |
| 212 | $campaignChanges['enabled'] ); |
| 213 | } |
| 214 | |
| 215 | if ( isset( $campaignChanges['priority'] ) ) { |
| 216 | Campaign::setNumericCampaignSetting( |
| 217 | $campaignName, |
| 218 | 'preferred', |
| 219 | intval( $campaignChanges['priority'] ), |
| 220 | self::EMERGENCY_PRIORITY, |
| 221 | self::LOW_PRIORITY |
| 222 | ); |
| 223 | } |
| 224 | |
| 225 | if ( isset( $campaignChanges['campaign_type'] ) ) { |
| 226 | $type = $campaignChanges['campaign_type']; |
| 227 | $type = $type === self::EMPTY_CAMPAIGN_TYPE_OPTION ? null : $type; |
| 228 | |
| 229 | // Sanity check: does the requested campaign type exist? |
| 230 | if ( $type && !CampaignType::getById( $type ) ) { |
| 231 | $this->showError( 'centralnotice-non-existent-campaign-type-error' ); |
| 232 | return; |
| 233 | } |
| 234 | |
| 235 | Campaign::setType( $campaignName, $type ); |
| 236 | } |
| 237 | |
| 238 | // Log any differences in settings |
| 239 | $newSettings = Campaign::getCampaignSettings( $campaignName, true ); |
| 240 | $diffs = array_diff_assoc( $initialSettings, $newSettings ); |
| 241 | |
| 242 | if ( $diffs ) { |
| 243 | $campaignId = Campaign::getNoticeId( $campaignName, CNDatabase::getPrimaryDb() ); |
| 244 | Campaign::processAfterCampaignChange( |
| 245 | 'modified', |
| 246 | $campaignId, |
| 247 | $campaignName, |
| 248 | $this->getUser(), |
| 249 | $initialSettings, |
| 250 | $newSettings, |
| 251 | $summary |
| 252 | ); |
| 253 | } |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | /** |
| 258 | * Render a field suitable for jquery.ui datepicker |
| 259 | * @param string $name |
| 260 | * @param bool $editable |
| 261 | * @param string|null $timestamp |
| 262 | * @return string |
| 263 | */ |
| 264 | protected function dateSelector( $name, $editable, $timestamp = null ) { |
| 265 | if ( $editable ) { |
| 266 | // Normalize timestamp format. If no timestamp is passed, default to now. If -1 is |
| 267 | // passed, set no defaults. |
| 268 | if ( $timestamp === -1 ) { |
| 269 | $ts = ''; |
| 270 | } else { |
| 271 | $ts = wfTimestamp( TS_MW, $timestamp ); |
| 272 | } |
| 273 | |
| 274 | $out = Html::element( 'input', |
| 275 | [ |
| 276 | 'id' => "{$name}Date", |
| 277 | 'name' => "{$name}Date", |
| 278 | 'type' => 'text', |
| 279 | 'class' => 'centralnotice-datepicker centralnotice-datepicker-limit_one_year', |
| 280 | ] |
| 281 | ); |
| 282 | $out .= Html::element( 'input', |
| 283 | [ |
| 284 | 'id' => "{$name}Date_timestamp", |
| 285 | 'name' => "{$name}Date_timestamp", |
| 286 | 'type' => 'hidden', |
| 287 | 'value' => $ts, |
| 288 | ] |
| 289 | ); |
| 290 | return $out; |
| 291 | } else { |
| 292 | return htmlspecialchars( $this->getLanguage()->date( $timestamp ) ); |
| 293 | } |
| 294 | } |
| 295 | |
| 296 | /** |
| 297 | * @param string $prefix |
| 298 | * @param bool $editable |
| 299 | * @param string|null $timestamp |
| 300 | * @return string |
| 301 | */ |
| 302 | private function timeSelectorTd( $prefix, $editable, $timestamp = null ) { |
| 303 | return Html::rawElement( |
| 304 | 'td', |
| 305 | [ |
| 306 | // Time is left-to-right in all languages |
| 307 | 'dir' => 'ltr', |
| 308 | 'class' => 'cn-timepicker', |
| 309 | ], |
| 310 | $this->timeSelector( $prefix, $editable, $timestamp ) |
| 311 | ); |
| 312 | } |
| 313 | |
| 314 | /** |
| 315 | * @param string $prefix |
| 316 | * @param bool $editable |
| 317 | * @param string|null $timestamp |
| 318 | * @return string |
| 319 | */ |
| 320 | private function timeSelector( $prefix, $editable, $timestamp = null ) { |
| 321 | if ( $editable ) { |
| 322 | $minutes = $this->paddedRange( 0, 59 ); |
| 323 | $hours = $this->paddedRange( 0, 23 ); |
| 324 | |
| 325 | // Normalize timestamp format... |
| 326 | $ts = wfTimestamp( TS_MW, $timestamp ); |
| 327 | |
| 328 | $fields = [ |
| 329 | [ "hour", "centralnotice-hours", $hours, substr( $ts, 8, 2 ) ], |
| 330 | [ "min", "centralnotice-min", $minutes, substr( $ts, 10, 2 ) ], |
| 331 | ]; |
| 332 | |
| 333 | return $this->createSelector( $prefix, $fields ); |
| 334 | } else { |
| 335 | return htmlspecialchars( $this->getLanguage()->time( $timestamp ) ); |
| 336 | } |
| 337 | } |
| 338 | |
| 339 | /** |
| 340 | * @param bool $editable |
| 341 | * @param string|null $selectedTypeId |
| 342 | * @param string|null $index The name of the campaign (used when selector is included |
| 343 | * in a list of campaigns by CNCampaignPager). |
| 344 | * @return string |
| 345 | */ |
| 346 | public function campaignTypeSelector( $editable, $selectedTypeId, $index = null ) { |
| 347 | $types = CampaignType::getTypes(); |
| 348 | if ( $editable ) { |
| 349 | $options = Html::element( |
| 350 | 'option', |
| 351 | [ 'value' => self::EMPTY_CAMPAIGN_TYPE_OPTION ], |
| 352 | $this->msg( 'centralnotice-empty-campaign-type-option' )->plain() |
| 353 | ); |
| 354 | |
| 355 | foreach ( $types as $type ) { |
| 356 | $message = $this->msg( $type->getMessageKey() ); |
| 357 | $text = $message->exists() ? $message->text() : $type->getId(); |
| 358 | $options .= Html::element( |
| 359 | 'option', |
| 360 | [ 'value' => $type->getId(), 'selected' => $selectedTypeId === $type->getId() ], |
| 361 | $text |
| 362 | ); |
| 363 | } |
| 364 | |
| 365 | // Handle the case of a type removed from config but still assigned to |
| 366 | // a campaign in the DB. |
| 367 | if ( $selectedTypeId && !CampaignType::getById( $selectedTypeId ) ) { |
| 368 | $options .= Html::element( |
| 369 | 'option', |
| 370 | [ 'value' => $selectedTypeId, 'selected' => true ], |
| 371 | $this->msg( |
| 372 | 'centralntoice-deleted-campaign-type', |
| 373 | $selectedTypeId |
| 374 | )->text() |
| 375 | ); |
| 376 | } |
| 377 | |
| 378 | // Data attributes set below (data-campaign-name and |
| 379 | // data-initial-value) must coordinate with CNCampaignPager and |
| 380 | // ext.centralNotice.adminUi.campaignPager.js |
| 381 | |
| 382 | $selectAttribs = [ |
| 383 | 'name' => 'campaign_type', |
| 384 | ]; |
| 385 | |
| 386 | if ( $selectedTypeId ) { |
| 387 | $selectAttribs['data-initial-value'] = $selectedTypeId; |
| 388 | } |
| 389 | if ( $index ) { |
| 390 | $selectAttribs['data-campaign-name'] = $index; |
| 391 | } |
| 392 | |
| 393 | return Html::rawElement( 'select', $selectAttribs, $options ); |
| 394 | |
| 395 | } else { |
| 396 | if ( $selectedTypeId ) { |
| 397 | $type = CampaignType::getById( $selectedTypeId ); |
| 398 | // We might get a null type if the DB has type identifiers that are |
| 399 | // not currently in the configuraiton. |
| 400 | if ( $type ) { |
| 401 | $message = $this->msg( $type->getMessageKey() ); |
| 402 | return $message->exists() |
| 403 | ? $message->escaped() |
| 404 | : htmlspecialchars( $type->getId() ); |
| 405 | } else { |
| 406 | return htmlspecialchars( $selectedTypeId ); |
| 407 | } |
| 408 | } |
| 409 | return $this->msg( 'centralnotice-empty-campaign-type-option' )->escaped(); |
| 410 | } |
| 411 | } |
| 412 | |
| 413 | /** |
| 414 | * Construct the priority select list for a campaign |
| 415 | * |
| 416 | * @param string|bool $index The name of the campaign (or false if it isn't needed) |
| 417 | * @param bool $editable Whether or not the form is editable by the user |
| 418 | * @param int $priorityValue The current priority value for this campaign |
| 419 | * |
| 420 | * @return string HTML for the select list |
| 421 | */ |
| 422 | public function prioritySelector( $index, $editable, $priorityValue ) { |
| 423 | $priorities = [ |
| 424 | self::LOW_PRIORITY => $this->msg( 'centralnotice-priority-low' ), |
| 425 | self::NORMAL_PRIORITY => |
| 426 | $this->msg( 'centralnotice-priority-normal' ), |
| 427 | self::HIGH_PRIORITY => $this->msg( 'centralnotice-priority-high' ), |
| 428 | self::EMERGENCY_PRIORITY => |
| 429 | $this->msg( 'centralnotice-priority-emergency' ), |
| 430 | ]; |
| 431 | |
| 432 | if ( $editable ) { |
| 433 | // The HTML for the select list options |
| 434 | $options = ''; |
| 435 | foreach ( $priorities as $key => $labelMsg ) { |
| 436 | $options .= Html::element( |
| 437 | 'option', |
| 438 | [ 'value' => (string)$key, 'selected' => $priorityValue == $key ], |
| 439 | $labelMsg->text() |
| 440 | ); |
| 441 | } |
| 442 | |
| 443 | // Data attributes set below (data-campaign-name and |
| 444 | // data-initial-value) must coordinate with CNCampaignPager and |
| 445 | // ext.centralNotice.adminUi.campaignPager.js |
| 446 | |
| 447 | $selectAttribs = [ |
| 448 | 'name' => 'priority', |
| 449 | 'data-initial-value' => $priorityValue |
| 450 | ]; |
| 451 | |
| 452 | if ( $index ) { |
| 453 | $selectAttribs['data-campaign-name'] = $index; |
| 454 | } |
| 455 | |
| 456 | return Html::rawElement( 'select', $selectAttribs, $options ); |
| 457 | } else { |
| 458 | return $priorities[$priorityValue]->escaped(); |
| 459 | } |
| 460 | } |
| 461 | |
| 462 | /** |
| 463 | * Build a set of select lists. Used by timeSelector. |
| 464 | * @param string $prefix string to identify selector set, for example, 'start' or 'end' |
| 465 | * @param array $fields array of select lists to build |
| 466 | * @return string |
| 467 | */ |
| 468 | private function createSelector( $prefix, $fields ) { |
| 469 | $out = ''; |
| 470 | foreach ( $fields as [ $field, $label, $set, $current ] ) { |
| 471 | $options = Html::listDropdownOptions( |
| 472 | self::dropdownList( $this->msg( $label )->text(), $set ), |
| 473 | [ 'other' => '' ] |
| 474 | ); |
| 475 | |
| 476 | $xmlSelect = new XmlSelect( "{$prefix}[{$field}]", "{$prefix}[{$field}]", $current ); |
| 477 | $xmlSelect->addOptions( $options ); |
| 478 | |
| 479 | $out .= $xmlSelect->getHTML(); |
| 480 | } |
| 481 | return $out; |
| 482 | } |
| 483 | |
| 484 | /** |
| 485 | * Output a form for adding a campaign. |
| 486 | */ |
| 487 | private function addNoticeForm() { |
| 488 | $request = $this->getRequest(); |
| 489 | $start = null; |
| 490 | $campaignType = null; |
| 491 | $noticeProjects = []; |
| 492 | $noticeLanguages = []; |
| 493 | // If there was an error, we'll need to restore the state of the form |
| 494 | if ( $request->wasPosted() && ( $request->getVal( 'subaction' ) === 'addCampaign' ) ) { |
| 495 | $start = $this->getDateTime( 'start' ); |
| 496 | $noticeLanguages = $request->getArray( 'project_languages', [] ); |
| 497 | $noticeProjects = $request->getArray( 'projects', [] ); |
| 498 | $campaignType = $request->getText( 'campaign_type' ); |
| 499 | } |
| 500 | '@phan-var array $noticeLanguages'; |
| 501 | '@phan-var array $noticeProjects'; |
| 502 | |
| 503 | $htmlOut = ''; |
| 504 | |
| 505 | // Section heading |
| 506 | $htmlOut .= Html::element( 'h2', |
| 507 | [ 'class' => 'cn-special-section' ], |
| 508 | $this->msg( 'centralnotice-add-notice' )->text() ); |
| 509 | |
| 510 | // Begin Add a campaign fieldset |
| 511 | $htmlOut .= Html::openElement( 'fieldset', [ 'class' => 'prefsection' ] ); |
| 512 | |
| 513 | // Form for adding a campaign |
| 514 | $htmlOut .= Html::openElement( 'form', [ 'method' => 'post' ] ); |
| 515 | $htmlOut .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ); |
| 516 | $htmlOut .= Html::hidden( 'subaction', 'addCampaign' ); |
| 517 | |
| 518 | $htmlOut .= Html::openElement( 'table', [ 'cellpadding' => 9 ] ); |
| 519 | |
| 520 | // Name |
| 521 | $htmlOut .= Html::rawElement( 'tr', [], |
| 522 | Html::element( 'td', [], |
| 523 | $this->msg( 'centralnotice-notice-name' )->text() |
| 524 | ) . |
| 525 | Html::rawElement( 'td', [], |
| 526 | Html::input( 'noticeName', $request->getVal( 'noticeName', '' ), 'text', [ 'size' => 25 ] ) |
| 527 | ) |
| 528 | ); |
| 529 | |
| 530 | // Campaign type selector |
| 531 | $htmlOut .= Html::rawElement( 'tr', [], |
| 532 | Html::rawElement( 'td', [], |
| 533 | Html::label( $this->msg( 'centralnotice-campaign-type' )->text(), 'campaign_type' ) |
| 534 | ) . |
| 535 | Html::rawElement( 'td', [], |
| 536 | $this->campaignTypeSelector( $this->editable, $campaignType ) |
| 537 | ) |
| 538 | ); |
| 539 | |
| 540 | // Start Date |
| 541 | $htmlOut .= Html::rawElement( 'tr', [], |
| 542 | Html::element( 'td', [], |
| 543 | $this->msg( 'centralnotice-start-date' )->text() |
| 544 | ) . |
| 545 | Html::rawElement( 'td', [], $this->dateSelector( 'start', $this->editable, $start ) ) |
| 546 | ); |
| 547 | // Start Time |
| 548 | $htmlOut .= Html::rawElement( 'tr', [], |
| 549 | Html::element( 'td', [], |
| 550 | $this->msg( 'centralnotice-start-time' )->text() |
| 551 | ) . |
| 552 | $this->timeSelectorTd( 'start', $this->editable, $start ) |
| 553 | ); |
| 554 | // Project |
| 555 | $htmlOut .= Html::rawElement( 'tr', [], |
| 556 | Html::element( 'td', [ 'valign' => 'top' ], |
| 557 | $this->msg( 'centralnotice-projects' )->text() |
| 558 | ) . |
| 559 | Html::rawElement( 'td', [], $this->projectMultiSelector( $noticeProjects ) ) |
| 560 | ); |
| 561 | // Languages |
| 562 | $htmlOut .= Html::rawElement( 'tr', [], |
| 563 | Html::element( 'td', [ 'valign' => 'top' ], |
| 564 | $this->msg( 'centralnotice-languages' )->text() |
| 565 | ) . |
| 566 | Html::rawElement( 'td', [], $this->languageMultiSelector( $noticeLanguages ) ) |
| 567 | ); |
| 568 | // Countries |
| 569 | $htmlOut .= Html::openElement( 'tr' ); |
| 570 | $htmlOut .= Html::rawElement( 'td', [], |
| 571 | Html::label( $this->msg( 'centralnotice-geo' )->text(), 'geotargeted' ) ); |
| 572 | $htmlOut .= Html::rawElement( 'td', [], |
| 573 | Html::check( 'geotargeted', false, [ 'value' => 1, 'id' => 'geotargeted' ] ) ); |
| 574 | $htmlOut .= Html::closeElement( 'tr' ); |
| 575 | |
| 576 | // Locations multi-selector |
| 577 | $htmlOut .= Html::openElement( 'tr', [ 'id' => 'centralnotice-geo-region-multiselector' ] ); |
| 578 | $htmlOut .= Html::element( 'td', [ 'valign' => 'top' ], |
| 579 | $this->msg( 'centralnotice-location' )->text() ); |
| 580 | $htmlOut .= Html::rawElement( 'td', [], $this->geoMultiSelectorTree() ); |
| 581 | $htmlOut .= Html::closeElement( 'tr' ); |
| 582 | |
| 583 | $htmlOut .= Html::closeElement( 'table' ); |
| 584 | $htmlOut .= Html::hidden( 'change', 'weight' ); |
| 585 | $htmlOut .= Html::hidden( 'authtoken', $this->getUser()->getEditToken() ); |
| 586 | |
| 587 | // Submit button |
| 588 | $htmlOut .= Html::rawElement( 'div', |
| 589 | [ 'class' => 'cn-buttons' ], |
| 590 | $this->makeSummaryField( true ) . |
| 591 | Html::submitButton( $this->msg( 'centralnotice-modify' )->text() ) |
| 592 | ); |
| 593 | |
| 594 | $htmlOut .= Html::closeElement( 'form' ); |
| 595 | |
| 596 | // End Add a campaign fieldset |
| 597 | $htmlOut .= Html::closeElement( 'fieldset' ); |
| 598 | |
| 599 | // Output HTML |
| 600 | $this->getOutput()->addHTML( $htmlOut ); |
| 601 | } |
| 602 | |
| 603 | private function handleAddCampaignPost() { |
| 604 | $request = $this->getRequest(); |
| 605 | $noticeName = $request->getVal( 'noticeName' ); |
| 606 | $start = $this->getDateTime( 'start' ); |
| 607 | $projects = $request->getArray( 'projects' ); |
| 608 | $project_languages = $request->getArray( 'project_languages' ); |
| 609 | $geotargeted = $request->getCheck( 'geotargeted' ); |
| 610 | |
| 611 | $geo_countries = $request->getVal( 'geo_countries' ); |
| 612 | if ( $geo_countries ) { |
| 613 | $geo_countries = explode( ',', $geo_countries ); |
| 614 | } else { |
| 615 | $geo_countries = []; |
| 616 | } |
| 617 | |
| 618 | $geo_regions = $request->getVal( 'geo_regions' ); |
| 619 | if ( $geo_regions ) { |
| 620 | $geo_regions = explode( ',', $geo_regions ); |
| 621 | } else { |
| 622 | $geo_regions = []; |
| 623 | } |
| 624 | |
| 625 | $campaignType = $request->getText( 'campaign_type' ); |
| 626 | $campaignType = |
| 627 | $campaignType === self::EMPTY_CAMPAIGN_TYPE_OPTION ? null : $campaignType; |
| 628 | |
| 629 | // Sanity check: does the requested campaign type exist? |
| 630 | if ( $campaignType && !CampaignType::getById( $campaignType ) ) { |
| 631 | $this->showError( 'centralnotice-non-existent-campaign-type-error' ); |
| 632 | return; |
| 633 | } |
| 634 | |
| 635 | if ( $noticeName == '' ) { |
| 636 | $this->showError( 'centralnotice-null-string' ); |
| 637 | } else { |
| 638 | $result = Campaign::addCampaign( |
| 639 | $noticeName, |
| 640 | false, |
| 641 | $start, |
| 642 | $projects, |
| 643 | $project_languages, |
| 644 | $geotargeted, |
| 645 | $geo_countries, |
| 646 | $geo_regions, |
| 647 | 100, |
| 648 | self::NORMAL_PRIORITY, |
| 649 | $this->getUser(), |
| 650 | $campaignType, |
| 651 | $this->getSummaryFromRequest( $request ) |
| 652 | ); |
| 653 | if ( is_string( $result ) ) { |
| 654 | // TODO Better error handling |
| 655 | $this->showError( $result ); |
| 656 | } |
| 657 | } |
| 658 | } |
| 659 | |
| 660 | /** |
| 661 | * Retrieve jquery.ui.datepicker date and homebrew time, |
| 662 | * and return as a MW timestamp string. |
| 663 | * @param string $prefix |
| 664 | * @return null|string |
| 665 | */ |
| 666 | private function getDateTime( $prefix ) { |
| 667 | $request = $this->getRequest(); |
| 668 | // Check whether the user left the date field blank. |
| 669 | // Interpret any form of "empty" as a blank value. |
| 670 | $manual_entry = $request->getVal( "{$prefix}Date" ); |
| 671 | if ( !$manual_entry ) { |
| 672 | return null; |
| 673 | } |
| 674 | |
| 675 | $datestamp = $request->getVal( "{$prefix}Date_timestamp" ); |
| 676 | $timeArray = $request->getArray( $prefix ); |
| 677 | $timestamp = substr( $datestamp, 0, 8 ) . |
| 678 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
| 679 | $timeArray[ 'hour' ] . |
| 680 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
| 681 | $timeArray[ 'min' ] . '00'; |
| 682 | return $timestamp; |
| 683 | } |
| 684 | |
| 685 | /** |
| 686 | * Show the interface for viewing/editing an individual campaign |
| 687 | * |
| 688 | * @param string $notice The name of the campaign to view |
| 689 | */ |
| 690 | private function outputNoticeDetail( $notice ) { |
| 691 | $out = $this->getOutput(); |
| 692 | |
| 693 | // Output specific ResourceLoader module |
| 694 | $out->addModules( 'ext.centralNotice.adminUi.campaignManager' ); |
| 695 | |
| 696 | // Output ResourceLoader modules for campaign mixins with custom controls |
| 697 | foreach ( $this->getConfig()->get( 'CentralNoticeCampaignMixins' ) as $mixinConfig ) { |
| 698 | if ( !empty( $mixinConfig['customAdminUIControlsModule'] ) ) { |
| 699 | $out->addModules( $mixinConfig['customAdminUIControlsModule'] ); |
| 700 | } |
| 701 | } |
| 702 | |
| 703 | $this->outputEnclosingDivStartTag(); |
| 704 | |
| 705 | // Todo: Convert the rest of this page to use this object |
| 706 | $this->campaign = new Campaign( $notice ); |
| 707 | try { |
| 708 | if ( $this->campaign->isArchived() || $this->campaign->isLocked() ) { |
| 709 | $out->setSubtitle( $this->msg( 'centralnotice-archive-edit-prevented' ) ); |
| 710 | // TODO: Fix this gross hack to prevent editing |
| 711 | $this->editable = false; |
| 712 | } |
| 713 | $out->addSubtitle( |
| 714 | $this->getLinkRenderer()->makeKnownLink( |
| 715 | SpecialPage::getTitleFor( 'CentralNoticeLogs' ), |
| 716 | $this->msg( 'centralnotice-campaign-view-logs' )->text(), |
| 717 | [], |
| 718 | [ |
| 719 | 'log_type' => 'campaignSettings', |
| 720 | 'campaign' => $notice |
| 721 | ] |
| 722 | ) |
| 723 | ); |
| 724 | } catch ( CampaignExistenceException ) { |
| 725 | throw new ErrorPageError( 'centralnotice', 'centralnotice-notice-doesnt-exist' ); |
| 726 | } |
| 727 | |
| 728 | if ( $this->editable && $this->getRequest()->wasPosted() ) { |
| 729 | $this->handleNoticeDetailPost( $notice ); |
| 730 | } |
| 731 | |
| 732 | $htmlOut = ''; |
| 733 | |
| 734 | // Begin Campaign detail fieldset |
| 735 | $htmlOut .= Html::openElement( 'fieldset', [ 'class' => 'prefsection' ] ); |
| 736 | |
| 737 | if ( $this->editable ) { |
| 738 | $htmlOut .= Html::openElement( 'form', |
| 739 | [ |
| 740 | 'method' => 'post', |
| 741 | 'id' => 'centralnotice-notice-detail', |
| 742 | 'autocomplete' => 'off', |
| 743 | 'action' => $this->getPageTitle()->getLocalURL( [ |
| 744 | 'subaction' => 'noticeDetail', |
| 745 | 'notice' => $notice |
| 746 | ] ) |
| 747 | ] |
| 748 | ); |
| 749 | } |
| 750 | |
| 751 | $output_detail = $this->noticeDetailForm( $notice ); |
| 752 | $output_assigned = $this->assignedTemplatesForm( $notice ); |
| 753 | $output_templates = $this->addTemplatesForm(); |
| 754 | |
| 755 | $htmlOut .= $output_detail; |
| 756 | |
| 757 | // Catch for no banners so that we don't double message |
| 758 | if ( $output_assigned == '' && $output_templates == '' ) { |
| 759 | $htmlOut .= $this->msg( 'centralnotice-no-templates' )->escaped(); |
| 760 | $htmlOut .= Html::element( 'p' ); |
| 761 | $newPage = SpecialPage::getTitleFor( 'NoticeTemplate', 'add' ); |
| 762 | $htmlOut .= $this->getLinkRenderer()->makeLink( |
| 763 | $newPage, |
| 764 | $this->msg( 'centralnotice-add-template' )->text() |
| 765 | ); |
| 766 | $htmlOut .= Html::element( 'p' ); |
| 767 | } elseif ( $output_assigned == '' ) { |
| 768 | $htmlOut .= Html::openElement( 'fieldset' ) . "\n" . |
| 769 | Html::element( 'legend', [], $this->msg( 'centralnotice-assigned-templates' )->text() ) . "\n"; |
| 770 | $htmlOut .= $this->msg( 'centralnotice-no-templates-assigned' )->escaped(); |
| 771 | $htmlOut .= Html::closeElement( 'fieldset' ); |
| 772 | if ( $this->editable ) { |
| 773 | $htmlOut .= $output_templates; |
| 774 | } |
| 775 | } else { |
| 776 | $htmlOut .= $output_assigned; |
| 777 | if ( $this->editable ) { |
| 778 | $htmlOut .= $output_templates; |
| 779 | } |
| 780 | } |
| 781 | if ( $this->editable ) { |
| 782 | $htmlOut .= Html::hidden( 'authtoken', $this->getUser()->getEditToken() ); |
| 783 | |
| 784 | $htmlOut .= $this->makeSummaryField(); |
| 785 | |
| 786 | // Submit button |
| 787 | $htmlOut .= Html::rawElement( 'div', |
| 788 | [ 'class' => 'cn-buttons' ], |
| 789 | Html::submitButton( |
| 790 | $this->msg( 'centralnotice-modify' )->text(), |
| 791 | [ 'id' => 'noticeDetailSubmit' ] |
| 792 | ) |
| 793 | ); |
| 794 | } |
| 795 | |
| 796 | if ( $this->editable ) { |
| 797 | $htmlOut .= Html::closeElement( 'form' ); |
| 798 | } |
| 799 | $htmlOut .= Html::closeElement( 'fieldset' ); |
| 800 | |
| 801 | $this->displayCampaignWarnings(); |
| 802 | |
| 803 | $out->addHTML( $htmlOut ); |
| 804 | $this->outputEnclosingDivEndTag(); |
| 805 | } |
| 806 | |
| 807 | /** |
| 808 | * Process a post request from the campaign (notice) detail subaction. Make |
| 809 | * changes to the campaign based on the post parameters. |
| 810 | * |
| 811 | * @param string $notice |
| 812 | */ |
| 813 | private function handleNoticeDetailPost( $notice ) { |
| 814 | $request = $this->getRequest(); |
| 815 | |
| 816 | // If what we're doing is actually serious (ie: not updating the banner |
| 817 | // filter); process the request. Recall that if the serious request |
| 818 | // succeeds, the page will be reloaded again. |
| 819 | if ( !$request->getCheck( 'template-search' ) ) { |
| 820 | // Check authentication token |
| 821 | if ( $this->getUser()->matchEditToken( $request->getVal( 'authtoken' ) ) ) { |
| 822 | // Handle removing campaign |
| 823 | if ( $request->getVal( 'archive' ) ) { |
| 824 | Campaign::setBooleanCampaignSetting( $notice, 'archived', true ); |
| 825 | } |
| 826 | |
| 827 | $initialCampaignSettings = Campaign::getCampaignSettings( $notice, true ); |
| 828 | |
| 829 | // Handle locking/unlocking campaign |
| 830 | Campaign::setBooleanCampaignSetting( |
| 831 | $notice, 'locked', $request->getCheck( 'locked' ) |
| 832 | ); |
| 833 | |
| 834 | // Handle enabling/disabling campaign |
| 835 | Campaign::setBooleanCampaignSetting( |
| 836 | $notice, 'enabled', $request->getCheck( 'enabled' ) |
| 837 | ); |
| 838 | |
| 839 | // Set campaign traffic throttle |
| 840 | if ( $request->getCheck( 'throttle-enabled' ) ) { |
| 841 | $throttle = $request->getInt( 'throttle-cur', 100 ); |
| 842 | } else { |
| 843 | $throttle = 100; |
| 844 | } |
| 845 | Campaign::setNumericCampaignSetting( $notice, 'throttle', $throttle, 100, 0 ); |
| 846 | |
| 847 | $config = $this->getConfig(); |
| 848 | $noticeNumberOfBuckets = $config->get( 'NoticeNumberOfBuckets' ); |
| 849 | // Handle user bucketing setting for campaign |
| 850 | $numCampaignBuckets = min( $request->getInt( 'buckets', 1 ), |
| 851 | $noticeNumberOfBuckets ); |
| 852 | $numCampaignBuckets = (int)pow( 2, floor( log( $numCampaignBuckets, 2 ) ) ); |
| 853 | |
| 854 | Campaign::setNumericCampaignSetting( |
| 855 | $notice, |
| 856 | 'buckets', |
| 857 | $numCampaignBuckets, |
| 858 | $noticeNumberOfBuckets, |
| 859 | 1 |
| 860 | ); |
| 861 | |
| 862 | // Handle setting campaign priority |
| 863 | Campaign::setNumericCampaignSetting( |
| 864 | $notice, |
| 865 | 'preferred', |
| 866 | $request->getInt( 'priority', self::NORMAL_PRIORITY ), |
| 867 | self::EMERGENCY_PRIORITY, |
| 868 | self::LOW_PRIORITY |
| 869 | ); |
| 870 | |
| 871 | // Handle setting campaign type |
| 872 | |
| 873 | $type = $request->getText( 'campaign_type' ); |
| 874 | $type = $type === self::EMPTY_CAMPAIGN_TYPE_OPTION ? null : $type; |
| 875 | |
| 876 | // Sanity check: does the requested campaign type exist? |
| 877 | if ( $type && !CampaignType::getById( $type ) ) { |
| 878 | $this->showError( 'centralnotice-non-existent-campaign-type-error' ); |
| 879 | return; |
| 880 | } |
| 881 | |
| 882 | Campaign::setType( $notice, $type ); |
| 883 | |
| 884 | // Handle updating geotargeting |
| 885 | if ( $request->getCheck( 'geotargeted' ) ) { |
| 886 | Campaign::setBooleanCampaignSetting( $notice, 'geo', true ); |
| 887 | |
| 888 | $countries = $this->listToArray( $request->getVal( 'geo_countries' ) ); |
| 889 | Campaign::updateCountries( $notice, $countries ); |
| 890 | |
| 891 | // Regions in format CountryCode_RegionCode |
| 892 | $regions = $this->listToArray( $request->getVal( 'geo_regions' ) ); |
| 893 | Campaign::updateRegions( $notice, $regions ); |
| 894 | |
| 895 | } else { |
| 896 | Campaign::setBooleanCampaignSetting( $notice, 'geo', false ); |
| 897 | } |
| 898 | |
| 899 | // Handle updating the start and end settings |
| 900 | $start = $this->getDateTime( 'start' ); |
| 901 | $end = $this->getDateTime( 'end' ); |
| 902 | if ( $start && $end ) { |
| 903 | Campaign::updateNoticeDate( $notice, $start, $end ); |
| 904 | } |
| 905 | |
| 906 | // Handle adding of banners to the campaign |
| 907 | $templatesToAdd = $request->getArray( 'addTemplates' ); |
| 908 | if ( $templatesToAdd ) { |
| 909 | $weight = $request->getArray( 'weight' ); |
| 910 | foreach ( $templatesToAdd as $templateName ) { |
| 911 | $templateId = Banner::fromName( $templateName )->getId(); |
| 912 | $bucket = $request->getInt( "bucket-{$templateName}" ); |
| 913 | $result = Campaign::addTemplateTo( |
| 914 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
| 915 | $notice, $templateName, $weight[$templateId], $bucket |
| 916 | ); |
| 917 | if ( $result !== true ) { |
| 918 | $this->showError( $result ); |
| 919 | } |
| 920 | } |
| 921 | } |
| 922 | |
| 923 | // Handle removing of banners from the campaign |
| 924 | $templateToRemove = $request->getArray( 'removeTemplates' ); |
| 925 | if ( $templateToRemove ) { |
| 926 | foreach ( $templateToRemove as $template ) { |
| 927 | Campaign::removeTemplateFor( $notice, $template ); |
| 928 | } |
| 929 | } |
| 930 | |
| 931 | // Handle weight changes |
| 932 | $updatedWeights = $request->getArray( 'weight' ); |
| 933 | $balanced = $request->getCheck( 'balanced' ); |
| 934 | if ( $updatedWeights ) { |
| 935 | foreach ( $updatedWeights as $templateId => $weight ) { |
| 936 | if ( $balanced ) { |
| 937 | $weight = 25; |
| 938 | } |
| 939 | Campaign::updateWeight( $notice, $templateId, $weight ); |
| 940 | } |
| 941 | } |
| 942 | |
| 943 | // Handle bucket changes - keep in mind that the number of campaign buckets |
| 944 | // might have changed simultaneously (and might have happened server side) |
| 945 | $updatedBuckets = $request->getArray( 'bucket' ); |
| 946 | if ( $updatedBuckets ) { |
| 947 | foreach ( $updatedBuckets as $templateId => $bucket ) { |
| 948 | Campaign::updateBucket( |
| 949 | $notice, |
| 950 | $templateId, |
| 951 | intval( $bucket ) % $numCampaignBuckets |
| 952 | ); |
| 953 | } |
| 954 | } |
| 955 | |
| 956 | // Handle new projects |
| 957 | $projects = $request->getArray( 'projects' ); |
| 958 | if ( $projects ) { |
| 959 | Campaign::updateProjects( $notice, $projects ); |
| 960 | } |
| 961 | |
| 962 | // Handle new project languages |
| 963 | $projectLangs = $request->getArray( 'project_languages' ); |
| 964 | if ( $projectLangs ) { |
| 965 | Campaign::updateProjectLanguages( $notice, $projectLangs ); |
| 966 | } |
| 967 | |
| 968 | // Handle campaign-associated mixins |
| 969 | foreach ( $config->get( 'CentralNoticeCampaignMixins' ) as $mixinName => $mixinDef ) { |
| 970 | $mixinControlName = self::makeNoticeMixinControlName( $mixinName ); |
| 971 | |
| 972 | if ( $request->getCheck( $mixinControlName ) ) { |
| 973 | $params = []; |
| 974 | |
| 975 | foreach ( $mixinDef['parameters'] as $paramName => $paramDef ) { |
| 976 | $requestParamName = |
| 977 | self::makeNoticeMixinControlName( $mixinName, $paramName ); |
| 978 | |
| 979 | switch ( $paramDef['type'] ) { |
| 980 | case 'string': |
| 981 | case 'json': |
| 982 | $paramVal = Sanitizer::removeSomeTags( |
| 983 | $request->getText( $requestParamName ) |
| 984 | ); |
| 985 | break; |
| 986 | |
| 987 | case 'integer': |
| 988 | $paramVal = $request->getInt( $requestParamName ); |
| 989 | break; |
| 990 | |
| 991 | case 'float': |
| 992 | $paramVal = $request->getFloat( $requestParamName ); |
| 993 | break; |
| 994 | |
| 995 | case 'boolean': |
| 996 | $paramVal = $request->getCheck( $requestParamName ); |
| 997 | break; |
| 998 | |
| 999 | default: |
| 1000 | throw new DomainException( |
| 1001 | "Unknown parameter type: '{$paramDef['type']}'" ); |
| 1002 | } |
| 1003 | |
| 1004 | $params[$paramName] = $paramVal; |
| 1005 | } |
| 1006 | |
| 1007 | Campaign::updateCampaignMixins( |
| 1008 | $notice, $mixinName, true, $params ); |
| 1009 | |
| 1010 | } else { |
| 1011 | Campaign::updateCampaignMixins( $notice, $mixinName, false ); |
| 1012 | } |
| 1013 | } |
| 1014 | |
| 1015 | $finalCampaignSettings = Campaign::getCampaignSettings( $notice, true ); |
| 1016 | $campaignId = Campaign::getNoticeId( $notice, CNDatabase::getPrimaryDb() ); |
| 1017 | |
| 1018 | $summary = $this->getSummaryFromRequest( $request ); |
| 1019 | |
| 1020 | Campaign::processAfterCampaignChange( |
| 1021 | 'modified', |
| 1022 | $campaignId, |
| 1023 | $notice, |
| 1024 | $this->getUser(), |
| 1025 | $initialCampaignSettings, |
| 1026 | $finalCampaignSettings, |
| 1027 | $summary |
| 1028 | ); |
| 1029 | |
| 1030 | // If there were no errors, reload the page to prevent duplicate form submission |
| 1031 | if ( !$this->centralNoticeError ) { |
| 1032 | $this->getOutput()->redirect( $this->getPageTitle()->getLocalURL( [ |
| 1033 | 'subaction' => 'noticeDetail', |
| 1034 | 'notice' => $notice |
| 1035 | ] ) ); |
| 1036 | return; |
| 1037 | } |
| 1038 | |
| 1039 | ChoiceDataProvider::invalidateCache(); |
| 1040 | } else { |
| 1041 | $this->showError( 'sessionfailure' ); |
| 1042 | } |
| 1043 | } |
| 1044 | } |
| 1045 | |
| 1046 | /** |
| 1047 | * Output stored campaign warnings |
| 1048 | */ |
| 1049 | private function displayCampaignWarnings() { |
| 1050 | foreach ( $this->campaignWarnings as $message ) { |
| 1051 | $this->getOutput()->wrapWikiMsg( "<div class='cn-error'>\n$1\n</div>", $message ); |
| 1052 | } |
| 1053 | } |
| 1054 | |
| 1055 | /** |
| 1056 | * Create the form for managing campaign settings (start date, end date, languages, etc.) |
| 1057 | * @param string $notice |
| 1058 | * @return string HTML |
| 1059 | */ |
| 1060 | private function noticeDetailForm( $notice ) { |
| 1061 | $readonly = [ 'disabled' => !$this->editable ]; |
| 1062 | |
| 1063 | $request = $this->getRequest(); |
| 1064 | $wasPosted = $request->wasPosted(); |
| 1065 | |
| 1066 | // If it's being posted, it's probably a write, so read from primary |
| 1067 | $campaign = Campaign::getCampaignSettings( $notice, $wasPosted ); |
| 1068 | |
| 1069 | if ( $campaign ) { |
| 1070 | // If there was an error, we'll need to restore the state of the form |
| 1071 | if ( $wasPosted ) { |
| 1072 | // TODO: Avoid duplicating handleNoticePostFromList which is where the logic |
| 1073 | // for reading and parsing post data resides. While that method is used when |
| 1074 | // saving changes, the one below is only used when clicking misc buttons |
| 1075 | // like "Apply filters", thus leading to subtle bugs (T182343). |
| 1076 | $start = $this->getDateTime( 'start' ); |
| 1077 | $end = $this->getDateTime( 'end' ); |
| 1078 | $isEnabled = $request->getCheck( 'enabled' ); |
| 1079 | $priority = $request->getInt( 'priority', self::NORMAL_PRIORITY ); |
| 1080 | if ( $request->getCheck( 'throttle-enabled' ) ) { |
| 1081 | $throttle = $request->getInt( 'throttle-cur', 100 ); |
| 1082 | } else { |
| 1083 | $throttle = 100; |
| 1084 | } |
| 1085 | $isLocked = $request->getCheck( 'locked' ); |
| 1086 | $isArchived = $request->getCheck( 'archived' ); |
| 1087 | $noticeProjects = $request->getArray( 'projects', [] ); |
| 1088 | $noticeLanguages = $request->getArray( 'project_languages', [] ); |
| 1089 | $isGeotargeted = $request->getCheck( 'geotargeted' ); |
| 1090 | $numBuckets = $request->getInt( 'buckets', 1 ); |
| 1091 | $countries = $this->listToArray( $request->getVal( 'geo_countries' ) ); |
| 1092 | $regions = $this->listToArray( $request->getVal( 'geo_regions' ) ); |
| 1093 | $type = $request->getText( 'campaign_type' ); |
| 1094 | } else { |
| 1095 | // Defaults |
| 1096 | $start = $campaign[ 'start' ]; |
| 1097 | $end = $campaign[ 'end' ]; |
| 1098 | $isEnabled = (bool)$campaign['enabled']; |
| 1099 | $priority = $campaign[ 'preferred' ]; |
| 1100 | $throttle = intval( $campaign[ 'throttle' ] ); |
| 1101 | $isLocked = (bool)$campaign['locked']; |
| 1102 | $isArchived = (bool)$campaign['archived']; |
| 1103 | $noticeProjects = Campaign::getNoticeProjects( $notice ); |
| 1104 | $noticeLanguages = Campaign::getNoticeLanguages( $notice ); |
| 1105 | $isGeotargeted = (bool)$campaign['geo']; |
| 1106 | $numBuckets = intval( $campaign[ 'buckets' ] ); |
| 1107 | $countries = Campaign::getNoticeCountries( $notice ); |
| 1108 | $regions = Campaign::getNoticeRegions( $notice ); |
| 1109 | $type = $campaign['type']; |
| 1110 | } |
| 1111 | '@phan-var array $noticeLanguages'; |
| 1112 | '@phan-var array $noticeProjects'; |
| 1113 | $isThrottled = ( $throttle < 100 ); |
| 1114 | $type = $type === self::EMPTY_CAMPAIGN_TYPE_OPTION ? null : $type; |
| 1115 | |
| 1116 | $htmlOut = Html::rawElement( 'h2', [], |
| 1117 | $this->msg( 'centralnotice-notice-heading', $notice )->parse() ); |
| 1118 | $htmlOut .= Html::openElement( 'table', [ 'cellpadding' => 9 ] ); |
| 1119 | |
| 1120 | // Rows |
| 1121 | // Campaign type selector |
| 1122 | $htmlOut .= Html::openElement( 'tr' ); |
| 1123 | $htmlOut .= Html::rawElement( 'td', [], |
| 1124 | Html::label( $this->msg( 'centralnotice-campaign-type' )->text(), 'campaign_type' ) ); |
| 1125 | $htmlOut .= Html::rawElement( 'td', [], |
| 1126 | $this->campaignTypeSelector( $this->editable, $type ) ); |
| 1127 | $htmlOut .= Html::closeElement( 'tr' ); |
| 1128 | |
| 1129 | // Start Date |
| 1130 | $htmlOut .= Html::rawElement( 'tr', [], |
| 1131 | Html::element( 'td', [], |
| 1132 | $this->msg( 'centralnotice-start-date' )->text() |
| 1133 | ) . |
| 1134 | Html::rawElement( 'td', [], |
| 1135 | $this->dateSelector( 'start', $this->editable, $start ) |
| 1136 | ) |
| 1137 | ); |
| 1138 | // Start Time |
| 1139 | $htmlOut .= Html::rawElement( 'tr', [], |
| 1140 | Html::element( 'td', [], |
| 1141 | $this->msg( 'centralnotice-start-time' )->text() |
| 1142 | ) . |
| 1143 | $this->timeSelectorTd( 'start', $this->editable, $start ) |
| 1144 | ); |
| 1145 | // End Date |
| 1146 | $htmlOut .= Html::rawElement( 'tr', [], |
| 1147 | Html::element( 'td', [], |
| 1148 | $this->msg( 'centralnotice-end-date' )->text() |
| 1149 | ) . |
| 1150 | Html::rawElement( 'td', [], |
| 1151 | $this->dateSelector( 'end', $this->editable, $end ) |
| 1152 | ) |
| 1153 | ); |
| 1154 | // End Time |
| 1155 | $htmlOut .= Html::rawElement( 'tr', [], |
| 1156 | Html::element( 'td', [], |
| 1157 | $this->msg( 'centralnotice-end-time' )->text() |
| 1158 | ) . |
| 1159 | $this->timeSelectorTd( 'end', $this->editable, $end ) |
| 1160 | ); |
| 1161 | // Project |
| 1162 | $htmlOut .= Html::rawElement( 'tr', [], |
| 1163 | Html::element( 'td', [ 'valign' => 'top' ], |
| 1164 | $this->msg( 'centralnotice-projects' )->text() |
| 1165 | ) . |
| 1166 | Html::rawElement( 'td', [], $this->projectMultiSelector( $noticeProjects ) ) |
| 1167 | ); |
| 1168 | // Languages |
| 1169 | $htmlOut .= Html::rawElement( 'tr', [], |
| 1170 | Html::element( 'td', [ 'valign' => 'top' ], |
| 1171 | $this->msg( 'centralnotice-languages' )->text() |
| 1172 | ) . |
| 1173 | Html::rawElement( 'td', [], $this->languageMultiSelector( $noticeLanguages ) ) |
| 1174 | ); |
| 1175 | // Countries |
| 1176 | $htmlOut .= Html::rawElement( 'tr', [], |
| 1177 | Html::rawElement( 'td', [], |
| 1178 | Html::label( $this->msg( 'centralnotice-geo' )->text(), 'geotargeted' ) |
| 1179 | ) . |
| 1180 | Html::rawElement( 'td', [], |
| 1181 | Html::check( 'geotargeted', $isGeotargeted, |
| 1182 | [ ...$readonly, 'value' => $notice, 'id' => 'geotargeted' ] |
| 1183 | ) |
| 1184 | ) |
| 1185 | ); |
| 1186 | |
| 1187 | // Locations multi-selector |
| 1188 | $htmlOut .= Html::rawElement( 'tr', |
| 1189 | [ 'id' => 'centralnotice-geo-region-multiselector' ], |
| 1190 | Html::element( 'td', [ 'valign' => 'top' ], |
| 1191 | $this->msg( 'centralnotice-location' )->text() |
| 1192 | ) . |
| 1193 | Html::rawElement( 'td', [], $this->geoMultiSelectorTree( $countries, $regions ) ) |
| 1194 | ); |
| 1195 | |
| 1196 | $config = $this->getConfig(); |
| 1197 | // User bucketing |
| 1198 | $htmlOut .= Html::rawElement( 'tr', [], |
| 1199 | Html::rawElement( 'td', [], |
| 1200 | Html::label( $this->msg( 'centralnotice-buckets' )->text(), 'buckets' ) |
| 1201 | ) . |
| 1202 | Html::rawElement( 'td', [], |
| 1203 | $this->numBucketsDropdown( $config->get( 'NoticeNumberOfBuckets' ), $numBuckets ) |
| 1204 | ) |
| 1205 | ); |
| 1206 | // Enabled |
| 1207 | $htmlOut .= Html::rawElement( 'tr', [], |
| 1208 | Html::rawElement( 'td', [], |
| 1209 | Html::label( $this->msg( 'centralnotice-enabled' )->text(), 'enabled' ) |
| 1210 | ) . |
| 1211 | Html::rawElement( 'td', [], |
| 1212 | Html::check( 'enabled', $isEnabled, |
| 1213 | [ ...$readonly, 'value' => $notice, 'id' => 'enabled' ] |
| 1214 | ) |
| 1215 | ) |
| 1216 | ); |
| 1217 | // Preferred / Priority |
| 1218 | $htmlOut .= Html::rawElement( 'tr', [], |
| 1219 | Html::rawElement( 'td', [], |
| 1220 | Html::label( $this->msg( 'centralnotice-preferred' )->text(), 'priority' ) |
| 1221 | ) . |
| 1222 | Html::rawElement( 'td', [], |
| 1223 | $this->prioritySelector( false, $this->editable, $priority ) |
| 1224 | ) |
| 1225 | ); |
| 1226 | // Throttle impressions |
| 1227 | $htmlOut .= Html::rawElement( 'tr', [], |
| 1228 | Html::rawElement( 'td', [], |
| 1229 | Html::label( $this->msg( 'centralnotice-throttle' )->text(), 'throttle-enabled' ) |
| 1230 | ) . |
| 1231 | Html::rawElement( 'td', [], |
| 1232 | Html::check( 'throttle-enabled', $isThrottled, |
| 1233 | [ ...$readonly, 'value' => $notice, 'id' => 'throttle-enabled' ] |
| 1234 | ) |
| 1235 | ) |
| 1236 | ); |
| 1237 | // Throttle value |
| 1238 | $htmlOut .= Html::openElement( 'tr', [ 'class' => 'cn-throttle-amount' ] ); |
| 1239 | $htmlOut .= Html::rawElement( 'td', [], |
| 1240 | Html::label( $this->msg( 'centralnotice-throttle-amount' )->text(), 'throttle' ) ); |
| 1241 | $throttleLabel = $this->msg( 'percent' )->numParams( $throttle )->text(); |
| 1242 | if ( $this->editable ) { |
| 1243 | $htmlOut .= Html::rawElement( 'td', [], |
| 1244 | Html::element( 'span', |
| 1245 | [ 'class' => 'cn-throttle', 'id' => 'centralnotice-throttle-echo' ], |
| 1246 | $throttleLabel ) . |
| 1247 | Html::hidden( 'throttle-cur', $throttle, |
| 1248 | [ 'id' => 'centralnotice-throttle-cur' ] ) . |
| 1249 | Html::rawElement( 'div', [ 'id' => 'centralnotice-throttle-amount' ], '' ) ); |
| 1250 | } else { |
| 1251 | $htmlOut .= Html::element( 'td', [], $throttleLabel ); |
| 1252 | } |
| 1253 | $htmlOut .= Html::closeElement( 'tr' ); |
| 1254 | // Locked |
| 1255 | $htmlOut .= Html::rawElement( 'tr', [], |
| 1256 | Html::rawElement( 'td', [], |
| 1257 | Html::label( $this->msg( 'centralnotice-locked' )->text(), 'locked' ) |
| 1258 | ) . |
| 1259 | Html::rawElement( 'td', [], |
| 1260 | Html::check( 'locked', $isLocked, |
| 1261 | [ ...$readonly, 'value' => $notice, 'id' => 'locked' ] |
| 1262 | ) |
| 1263 | ) |
| 1264 | ); |
| 1265 | if ( $this->editable ) { |
| 1266 | // Locked |
| 1267 | $htmlOut .= Html::rawElement( 'tr', [], |
| 1268 | Html::rawElement( 'td', [], |
| 1269 | Html::label( $this->msg( 'centralnotice-archive-campaign' )->text(), 'archive' ) |
| 1270 | ) . |
| 1271 | Html::rawElement( 'td', [], |
| 1272 | Html::check( 'archive', $isArchived, [ 'value' => $notice, 'id' => 'archive' ] ) |
| 1273 | ) |
| 1274 | ); |
| 1275 | } |
| 1276 | $htmlOut .= Html::closeElement( 'table' ); |
| 1277 | |
| 1278 | // Create controls for campaign-associated mixins (if there are any) |
| 1279 | $centralNoticeCampaignMixins = $config->get( 'CentralNoticeCampaignMixins' ); |
| 1280 | if ( $centralNoticeCampaignMixins ) { |
| 1281 | $mixinsThisNotice = Campaign::getCampaignMixins( $notice, false, $wasPosted ); |
| 1282 | |
| 1283 | $htmlOut .= Html::openElement( 'fieldset' ) . "\n" . |
| 1284 | Html::element( 'legend', [], $this->msg( 'centralnotice-notice-mixins-fieldset' )->text() ) . "\n"; |
| 1285 | |
| 1286 | foreach ( $centralNoticeCampaignMixins as $mixinName => $mixinDef ) { |
| 1287 | $mixinControlName = self::makeNoticeMixinControlName( $mixinName ); |
| 1288 | |
| 1289 | $attribs = [ |
| 1290 | 'value' => $notice, |
| 1291 | 'class' => 'noticeMixinCheck', |
| 1292 | 'id' => $mixinControlName, |
| 1293 | 'data-mixin-name' => $mixinName, |
| 1294 | ...$readonly, |
| 1295 | ]; |
| 1296 | |
| 1297 | if ( isset( $mixinsThisNotice[$mixinName] ) ) { |
| 1298 | // We have data on the mixin for this campaign, though |
| 1299 | // it may not have been enabled. |
| 1300 | |
| 1301 | $checked = $mixinsThisNotice[$mixinName]['enabled']; |
| 1302 | |
| 1303 | $attribs['data-mixin-param-values'] = |
| 1304 | FormatJson::encode( |
| 1305 | $mixinsThisNotice[$mixinName]['parameters'] ); |
| 1306 | |
| 1307 | } else { |
| 1308 | |
| 1309 | // No data; it's never been enabled for this campaign |
| 1310 | // before. Note: default settings values are set on the |
| 1311 | // client. |
| 1312 | $checked = false; |
| 1313 | } |
| 1314 | |
| 1315 | $htmlOut .= Html::openElement( 'div' ); |
| 1316 | |
| 1317 | $htmlOut .= Html::check( |
| 1318 | $mixinControlName, |
| 1319 | $checked, |
| 1320 | $attribs |
| 1321 | ); |
| 1322 | |
| 1323 | $htmlOut .= Html::label( |
| 1324 | $this->msg( $mixinDef['nameMsg'] )->text(), |
| 1325 | $mixinControlName, |
| 1326 | [ 'for' => $mixinControlName ] |
| 1327 | ); |
| 1328 | |
| 1329 | if ( !empty( $mixinDef['helpMsg'] ) ) { |
| 1330 | $htmlOut .= Html::element( 'div', |
| 1331 | [ 'class' => 'htmlform-help' ], |
| 1332 | $this->msg( $mixinDef['helpMsg'] )->text() |
| 1333 | ); |
| 1334 | } |
| 1335 | |
| 1336 | $htmlOut .= Html::closeElement( 'div' ); |
| 1337 | |
| 1338 | } |
| 1339 | |
| 1340 | $htmlOut .= Html::closeElement( 'fieldset' ); |
| 1341 | } |
| 1342 | |
| 1343 | return $htmlOut; |
| 1344 | } |
| 1345 | |
| 1346 | return ''; |
| 1347 | } |
| 1348 | |
| 1349 | private static function makeNoticeMixinControlName( |
| 1350 | string $mixinName, ?string $mixinParam = null |
| 1351 | ): string { |
| 1352 | return 'notice-mixin-' . $mixinName . |
| 1353 | ( $mixinParam ? '-' . $mixinParam : '' ); |
| 1354 | } |
| 1355 | |
| 1356 | /** |
| 1357 | * Create form for managing banners assigned to a campaign |
| 1358 | * |
| 1359 | * Common campaign misconfigurations will cause warnings to appear |
| 1360 | * at the top of this form. |
| 1361 | * @param string $notice |
| 1362 | * @return string HTML |
| 1363 | */ |
| 1364 | private function assignedTemplatesForm( $notice ) { |
| 1365 | $dbr = CNDatabase::getReplicaDb(); |
| 1366 | $res = $dbr->newSelectQueryBuilder() |
| 1367 | // Aliases are needed to avoid problems with table prefixes |
| 1368 | ->select( [ |
| 1369 | 'templates.tmp_id', |
| 1370 | 'templates.tmp_name', |
| 1371 | 'assignments.tmp_weight', |
| 1372 | 'assignments.asn_bucket', |
| 1373 | 'notices.not_buckets', |
| 1374 | ] ) |
| 1375 | ->from( 'cn_notices', 'notices' ) |
| 1376 | ->join( 'cn_assignments', 'assignments', 'notices.not_id = assignments.not_id' ) |
| 1377 | ->join( 'cn_templates', 'templates', 'assignments.tmp_id = templates.tmp_id' ) |
| 1378 | ->where( [ |
| 1379 | 'notices.not_name' => $notice, |
| 1380 | ] ) |
| 1381 | ->orderBy( [ 'assignments.asn_bucket', 'notices.not_id' ] ) |
| 1382 | ->caller( __METHOD__ ) |
| 1383 | ->fetchResultSet(); |
| 1384 | |
| 1385 | // No banners found |
| 1386 | if ( $res->numRows() < 1 ) { |
| 1387 | return ''; |
| 1388 | } |
| 1389 | |
| 1390 | $weights = []; |
| 1391 | |
| 1392 | $banners = []; |
| 1393 | foreach ( $res as $row ) { |
| 1394 | $banners[] = $row; |
| 1395 | |
| 1396 | $weights[] = $row->tmp_weight; |
| 1397 | } |
| 1398 | $isBalanced = ( count( array_unique( $weights ) ) === 1 ); |
| 1399 | |
| 1400 | // Build Assigned banners HTML |
| 1401 | |
| 1402 | $htmlOut = Html::hidden( 'change', 'weight' ); |
| 1403 | |
| 1404 | // Prepare data about assigned banners to provide to client-side code, and |
| 1405 | // make it available within the fieldsset element. |
| 1406 | |
| 1407 | $bannersForJS = array_map( |
| 1408 | static function ( $banner ) { |
| 1409 | return [ |
| 1410 | 'bannerName' => $banner->tmp_name, |
| 1411 | 'bucket' => $banner->asn_bucket |
| 1412 | ]; |
| 1413 | }, |
| 1414 | $banners |
| 1415 | ); |
| 1416 | |
| 1417 | $readonly = [ 'disabled' => !$this->editable ]; |
| 1418 | |
| 1419 | $htmlOut .= Html::openElement( 'fieldset', [ |
| 1420 | 'data-assigned-banners' => json_encode( $bannersForJS ), |
| 1421 | 'id' => 'centralnotice-assigned-banners' |
| 1422 | ] ) . "\n" . |
| 1423 | Html::element( 'legend', [], $this->msg( 'centralnotice-assigned-templates' )->text() ) . "\n"; |
| 1424 | |
| 1425 | // Equal weight banners |
| 1426 | $htmlOut .= Html::rawElement( 'tr', [], |
| 1427 | Html::rawElement( 'td', [], |
| 1428 | Html::label( $this->msg( 'centralnotice-balanced' )->text(), 'balanced' ) |
| 1429 | ) . |
| 1430 | Html::rawElement( 'td', [], |
| 1431 | Html::check( 'balanced', $isBalanced, |
| 1432 | [ ...$readonly, 'value' => $notice, 'id' => 'balanced' ] |
| 1433 | ) |
| 1434 | ) |
| 1435 | ); |
| 1436 | |
| 1437 | $htmlOut .= Html::openElement( 'table', |
| 1438 | [ |
| 1439 | 'cellpadding' => 9, |
| 1440 | 'width' => '100%' |
| 1441 | ] |
| 1442 | ); |
| 1443 | if ( $this->editable ) { |
| 1444 | $htmlOut .= Html::element( 'th', [ 'align' => 'left', 'width' => '5%' ], |
| 1445 | $this->msg( "centralnotice-remove" )->text() ); |
| 1446 | } |
| 1447 | $htmlOut .= Html::element( 'th', |
| 1448 | [ 'align' => 'left', 'width' => '5%', 'class' => 'cn-weight' ], |
| 1449 | $this->msg( 'centralnotice-weight' )->text() ); |
| 1450 | $htmlOut .= Html::element( 'th', [ 'align' => 'left', 'width' => '5%' ], |
| 1451 | $this->msg( 'centralnotice-bucket' )->text() ); |
| 1452 | $htmlOut .= Html::element( 'th', [ 'align' => 'left', 'width' => '70%' ], |
| 1453 | $this->msg( 'centralnotice-templates' )->text() ); |
| 1454 | |
| 1455 | // Table rows |
| 1456 | $noticeNumberOfBuckets = $this->getConfig()->get( 'NoticeNumberOfBuckets' ); |
| 1457 | foreach ( $banners as $row ) { |
| 1458 | $htmlOut .= Html::openElement( 'tr' ); |
| 1459 | |
| 1460 | if ( $this->editable ) { |
| 1461 | // Remove |
| 1462 | $htmlOut .= Html::rawElement( 'td', [ 'valign' => 'top' ], |
| 1463 | Html::check( 'removeTemplates[]', false, [ |
| 1464 | 'value' => $row->tmp_name, |
| 1465 | 'class' => 'bannerRemoveCheckbox' |
| 1466 | ] ) |
| 1467 | ); |
| 1468 | } |
| 1469 | |
| 1470 | // Weight |
| 1471 | $htmlOut .= Html::rawElement( 'td', [ 'valign' => 'top', 'class' => 'cn-weight' ], |
| 1472 | $this->weightDropdown( "weight[$row->tmp_id]", $row->tmp_weight ) |
| 1473 | ); |
| 1474 | |
| 1475 | // Bucket |
| 1476 | $numCampaignBuckets = min( intval( $row->not_buckets ), $noticeNumberOfBuckets ); |
| 1477 | $htmlOut .= Html::rawElement( 'td', [ 'valign' => 'top' ], |
| 1478 | $this->bucketDropdown( |
| 1479 | "bucket[$row->tmp_id]", |
| 1480 | ( $numCampaignBuckets == 1 ? null : intval( $row->asn_bucket ) ), |
| 1481 | $numCampaignBuckets, |
| 1482 | $row->tmp_name |
| 1483 | ) |
| 1484 | ); |
| 1485 | |
| 1486 | // Banner |
| 1487 | $htmlOut .= Html::rawElement( 'td', [ 'valign' => 'top' ], |
| 1488 | BannerRenderer::linkToBanner( $row->tmp_name ) |
| 1489 | ); |
| 1490 | |
| 1491 | $htmlOut .= Html::closeElement( 'tr' ); |
| 1492 | } |
| 1493 | $htmlOut .= Html::closeElement( 'table' ); |
| 1494 | $htmlOut .= Html::closeElement( 'fieldset' ); |
| 1495 | |
| 1496 | // Sneak in some extra processing, to detect errors in bucket assignment. |
| 1497 | // Test for campaign buckets without an assigned banner or with multiple banners. |
| 1498 | $assignedBuckets = []; |
| 1499 | $numBuckets = $this->campaign->getBuckets(); |
| 1500 | foreach ( $banners as $banner ) { |
| 1501 | $bannerBucket = $banner->asn_bucket; |
| 1502 | $bannerName = $banner->tmp_name; |
| 1503 | |
| 1504 | $assignedBuckets[$bannerBucket] = $bannerName; |
| 1505 | } |
| 1506 | // Do any buckets not have a banner assigned? |
| 1507 | if ( count( $assignedBuckets ) < $numBuckets ) { |
| 1508 | $this->campaignWarnings[] = [ |
| 1509 | 'centralnotice-banner-empty-bucket' |
| 1510 | ]; |
| 1511 | } |
| 1512 | |
| 1513 | return $htmlOut; |
| 1514 | } |
| 1515 | |
| 1516 | /** |
| 1517 | * @param string $name |
| 1518 | * @param int|null $selected |
| 1519 | * @return string |
| 1520 | */ |
| 1521 | private function weightDropdown( $name, $selected ) { |
| 1522 | $selected = intval( $selected ); |
| 1523 | |
| 1524 | if ( $this->editable ) { |
| 1525 | $html = Html::openElement( 'select', [ 'name' => $name ] ); |
| 1526 | foreach ( range( 5, 100, 5 ) as $value ) { |
| 1527 | $html .= Html::element( |
| 1528 | 'option', |
| 1529 | [ 'value' => (string)$value, 'selected' => $value === $selected ], |
| 1530 | (string)$value |
| 1531 | ); |
| 1532 | } |
| 1533 | $html .= Html::closeElement( 'select' ); |
| 1534 | return $html; |
| 1535 | } else { |
| 1536 | return htmlspecialchars( (string)$selected ); |
| 1537 | } |
| 1538 | } |
| 1539 | |
| 1540 | /** |
| 1541 | * @param string $name |
| 1542 | * @param int|null $selected |
| 1543 | * @param int $numberCampaignBuckets |
| 1544 | * @param string $bannerName |
| 1545 | * @return string |
| 1546 | */ |
| 1547 | private function bucketDropdown( $name, $selected, $numberCampaignBuckets, $bannerName ) { |
| 1548 | $bucketLabel = static fn ( int $val ) => chr( ord( 'A' ) + $val ); |
| 1549 | |
| 1550 | if ( $this->editable ) { |
| 1551 | // Default to bucket 'A' |
| 1552 | $selected ??= 0; |
| 1553 | $selected %= $numberCampaignBuckets; |
| 1554 | |
| 1555 | // bucketSelector class is for all bucket selectors (for assigned or |
| 1556 | // unassigned banners). Coordinate with CentralNoticePager::bucketDropdown(). |
| 1557 | $html = Html::openElement( 'select', [ |
| 1558 | 'name' => $name, |
| 1559 | 'class' => 'bucketSelector bucketSelectorForAssignedBanners', |
| 1560 | 'data-banner-name' => $bannerName |
| 1561 | ] ); |
| 1562 | |
| 1563 | foreach ( range( 0, $this->getConfig()->get( 'NoticeNumberOfBuckets' ) - 1 ) as $value ) { |
| 1564 | $html .= Html::element( |
| 1565 | 'option', |
| 1566 | [ |
| 1567 | 'value' => $value, |
| 1568 | 'selected' => $value === $selected, |
| 1569 | 'disabled' => $value >= $numberCampaignBuckets, |
| 1570 | ], |
| 1571 | $bucketLabel( $value ) |
| 1572 | ); |
| 1573 | } |
| 1574 | $html .= Html::closeElement( 'select' ); |
| 1575 | return $html; |
| 1576 | } else { |
| 1577 | if ( $selected === null ) { |
| 1578 | return '-'; |
| 1579 | } |
| 1580 | return htmlspecialchars( $bucketLabel( $selected ) ); |
| 1581 | } |
| 1582 | } |
| 1583 | |
| 1584 | /** |
| 1585 | * @param int $numBuckets |
| 1586 | * @param int|null $selected |
| 1587 | * @return string |
| 1588 | */ |
| 1589 | private function numBucketsDropdown( $numBuckets, $selected ) { |
| 1590 | $selected ??= 1; |
| 1591 | |
| 1592 | if ( $this->editable ) { |
| 1593 | $html = Html::openElement( 'select', [ 'name' => 'buckets', 'id' => 'buckets' ] ); |
| 1594 | foreach ( range( 0, intval( log( $numBuckets, 2 ) ) ) as $value ) { |
| 1595 | $value = pow( 2, $value ); |
| 1596 | $html .= Html::element( |
| 1597 | 'option', |
| 1598 | [ 'value' => (string)$value, 'selected' => $value === $selected ], |
| 1599 | (string)$value |
| 1600 | ); |
| 1601 | } |
| 1602 | $html .= Html::closeElement( 'select' ); |
| 1603 | return $html; |
| 1604 | } else { |
| 1605 | return htmlspecialchars( (string)$selected ); |
| 1606 | } |
| 1607 | } |
| 1608 | |
| 1609 | /** |
| 1610 | * Create form for adding banners to a campaign |
| 1611 | * @return string |
| 1612 | */ |
| 1613 | private function addTemplatesForm() { |
| 1614 | // Sanitize input on search key and split out terms |
| 1615 | $searchTerms = $this->sanitizeSearchTerms( $this->getRequest()->getText( 'tplsearchkey' ) ); |
| 1616 | |
| 1617 | $pager = new CentralNoticePager( $this, $searchTerms ); |
| 1618 | |
| 1619 | // Build HTML |
| 1620 | $htmlOut = Html::openElement( 'fieldset' ) . "\n" . |
| 1621 | Html::element( 'legend', [], $this->msg( 'centralnotice-available-templates' )->text() ) . "\n"; |
| 1622 | |
| 1623 | // Banner search box |
| 1624 | $htmlOut .= Html::openElement( 'fieldset', [ 'id' => 'cn-template-searchbox' ] ); |
| 1625 | $htmlOut .= Html::element( |
| 1626 | 'legend', [], $this->msg( 'centralnotice-filter-template-banner' )->text() |
| 1627 | ); |
| 1628 | |
| 1629 | $htmlOut .= Html::element( 'label', [ 'for' => 'tplsearchkey' ], |
| 1630 | $this->msg( 'centralnotice-filter-template-prompt' )->text() ); |
| 1631 | $htmlOut .= Html::input( 'tplsearchkey', $searchTerms ); |
| 1632 | $htmlOut .= Html::element( |
| 1633 | 'input', |
| 1634 | [ |
| 1635 | 'type' => 'submit', |
| 1636 | 'name' => 'template-search', |
| 1637 | 'value' => $this->msg( 'centralnotice-filter-template-submit' )->text() |
| 1638 | ] |
| 1639 | ); |
| 1640 | |
| 1641 | $htmlOut .= Html::closeElement( 'fieldset' ); |
| 1642 | |
| 1643 | // And now the banners, if any |
| 1644 | if ( $pager->getNumRows() > 0 ) { |
| 1645 | // Show paginated list of banners |
| 1646 | $htmlOut .= Html::rawElement( 'div', |
| 1647 | [ 'class' => 'cn-pager' ], |
| 1648 | $pager->getNavigationBar() ); |
| 1649 | $htmlOut .= $pager->getBody(); |
| 1650 | $htmlOut .= Html::rawElement( 'div', |
| 1651 | [ 'class' => 'cn-pager' ], |
| 1652 | $pager->getNavigationBar() ); |
| 1653 | |
| 1654 | } else { |
| 1655 | $htmlOut .= $this->msg( 'centralnotice-no-templates' )->escaped(); |
| 1656 | } |
| 1657 | $htmlOut .= Html::closeElement( 'fieldset' ); |
| 1658 | |
| 1659 | return $htmlOut; |
| 1660 | } |
| 1661 | |
| 1662 | /** |
| 1663 | * Generates a multiple select list of all languages. |
| 1664 | * |
| 1665 | * @param array $selected The language codes of the selected languages |
| 1666 | * |
| 1667 | * @return string multiple select list |
| 1668 | */ |
| 1669 | private function languageMultiSelector( $selected = [] ) { |
| 1670 | // Retrieve the list of languages in user's language |
| 1671 | $languages = MediaWikiServices::getInstance()->getLanguageNameUtils() |
| 1672 | ->getLanguageNames( $this->getLanguage()->getCode() ); |
| 1673 | |
| 1674 | // Make sure the site language is in the list; a custom language code |
| 1675 | // might not have a defined name... |
| 1676 | $languageCode = $this->getConfig()->get( MainConfigNames::LanguageCode ); |
| 1677 | if ( !array_key_exists( $languageCode, $languages ) ) { |
| 1678 | $languages[$languageCode] = $languageCode; |
| 1679 | } |
| 1680 | ksort( $languages ); |
| 1681 | |
| 1682 | $options = "\n"; |
| 1683 | foreach ( $languages as $code => $name ) { |
| 1684 | $options .= Html::element( |
| 1685 | 'option', |
| 1686 | [ 'value' => $code, 'selected' => in_array( $code, $selected ) ], |
| 1687 | $this->msg( 'centralnotice-language-listing', $code, $name )->text() |
| 1688 | ) . "\n"; |
| 1689 | } |
| 1690 | |
| 1691 | $attribs = [ |
| 1692 | 'multiple' => true, |
| 1693 | 'id' => 'project_languages', |
| 1694 | 'name' => 'project_languages[]', |
| 1695 | 'class' => 'cn-multiselect', |
| 1696 | 'autocomplete' => 'off', |
| 1697 | 'disabled' => !$this->editable, |
| 1698 | ]; |
| 1699 | |
| 1700 | return Html::rawElement( 'select', $attribs, $options ); |
| 1701 | } |
| 1702 | |
| 1703 | /** |
| 1704 | * Generates a multiple select list of all project types. |
| 1705 | * |
| 1706 | * @param array $selected The name of the selected project type |
| 1707 | * |
| 1708 | * @return string multiple select list |
| 1709 | */ |
| 1710 | private function projectMultiSelector( $selected = [] ) { |
| 1711 | $options = "\n"; |
| 1712 | foreach ( $this->getConfig()->get( 'NoticeProjects' ) as $project ) { |
| 1713 | $options .= Html::element( |
| 1714 | 'option', |
| 1715 | [ 'value' => $project, 'selected' => in_array( $project, $selected ) ], |
| 1716 | $project |
| 1717 | ) . "\n"; |
| 1718 | } |
| 1719 | |
| 1720 | $attribs = [ |
| 1721 | 'multiple' => true, |
| 1722 | 'id' => 'projects', |
| 1723 | 'name' => 'projects[]', |
| 1724 | 'class' => 'cn-multiselect', |
| 1725 | 'autocomplete' => 'off', |
| 1726 | 'disabled' => !$this->editable, |
| 1727 | ]; |
| 1728 | |
| 1729 | return Html::rawElement( 'select', $attribs, $options ); |
| 1730 | } |
| 1731 | |
| 1732 | /** |
| 1733 | * @param string $text |
| 1734 | * @param string[] $values |
| 1735 | * @return string |
| 1736 | */ |
| 1737 | public static function dropdownList( $text, $values ) { |
| 1738 | $dropdown = "*{$text}\n"; |
| 1739 | foreach ( $values as $value ) { |
| 1740 | $dropdown .= "**{$value}\n"; |
| 1741 | } |
| 1742 | return $dropdown; |
| 1743 | } |
| 1744 | |
| 1745 | /** |
| 1746 | * Create a string with summary label and text field. |
| 1747 | * |
| 1748 | * @param bool $action If true, use a placeholder message appropriate for |
| 1749 | * a single action (such as creating a campaign). |
| 1750 | * @return string |
| 1751 | */ |
| 1752 | public function makeSummaryField( $action = false ) { |
| 1753 | $placeholderMsg = $action ? 'centralnotice-change-summary-action-prompt' |
| 1754 | : 'centralnotice-change-summary-prompt'; |
| 1755 | |
| 1756 | return Html::element( 'label', |
| 1757 | [ 'class' => 'cn-change-summary-label' ], |
| 1758 | $this->msg( 'centralnotice-change-summary-label' )->text() |
| 1759 | ) . Html::element( 'input', |
| 1760 | [ |
| 1761 | 'class' => 'cn-change-summary-input', |
| 1762 | 'placeholder' => $this->msg( $placeholderMsg )->text(), |
| 1763 | 'size' => 45, |
| 1764 | 'name' => 'changeSummary' |
| 1765 | ] |
| 1766 | ); |
| 1767 | } |
| 1768 | |
| 1769 | private function getSummaryFromRequest( WebRequest $request ): string { |
| 1770 | return static::truncateSummaryField( $request->getVal( 'changeSummary' ) ); |
| 1771 | } |
| 1772 | |
| 1773 | /** |
| 1774 | * @param int $begin |
| 1775 | * @param int $end |
| 1776 | * @return string[] |
| 1777 | */ |
| 1778 | private function paddedRange( $begin, $end ) { |
| 1779 | $unpaddedRange = range( $begin, $end ); |
| 1780 | $paddedRange = []; |
| 1781 | foreach ( $unpaddedRange as $number ) { |
| 1782 | // pad number with 0 if needed |
| 1783 | $paddedRange[] = sprintf( "%02d", $number ); |
| 1784 | } |
| 1785 | return $paddedRange; |
| 1786 | } |
| 1787 | |
| 1788 | private function showError( string $message ) { |
| 1789 | $this->getOutput()->wrapWikiMsg( "<div class='cn-error'>\n$1\n</div>", $message ); |
| 1790 | $this->centralNoticeError = true; |
| 1791 | } |
| 1792 | |
| 1793 | /** |
| 1794 | * Generates a multiple select list of all countries. |
| 1795 | * |
| 1796 | * @param array $selectedCountries The country codes of the selected countries |
| 1797 | * @param array $selectedRegions The unique region codes of the selected regions |
| 1798 | * in format CountryCode_RegionCode |
| 1799 | * |
| 1800 | * @return string multiple select list |
| 1801 | */ |
| 1802 | private function geoMultiSelectorTree( $selectedCountries = [], $selectedRegions = [] ) { |
| 1803 | $userLanguageCode = $this->getLanguage()->getCode(); |
| 1804 | $countries = GeoTarget::getCountriesList( $userLanguageCode ); |
| 1805 | $locationElements = "\n"; |
| 1806 | foreach ( $countries as $countryCode => $country ) { |
| 1807 | |
| 1808 | $regions = ''; |
| 1809 | if ( $country->getRegions() ) { |
| 1810 | foreach ( $country->getRegions() as $regionCode => $name ) { |
| 1811 | $uniqueRegionCode = GeoTarget::makeUniqueRegionCode( |
| 1812 | $countryCode, $regionCode |
| 1813 | ); |
| 1814 | $isSelected = in_array( $uniqueRegionCode, $selectedRegions ); |
| 1815 | $data = [ |
| 1816 | 'type' => 'region', |
| 1817 | 'code' => $regionCode, |
| 1818 | 'opened' => $isSelected, |
| 1819 | 'selected' => $isSelected |
| 1820 | ]; |
| 1821 | if ( !$this->editable ) { |
| 1822 | $data['disabled'] = true; |
| 1823 | } |
| 1824 | $regions .= Html::element( |
| 1825 | 'li', |
| 1826 | [ |
| 1827 | 'id' => $uniqueRegionCode, |
| 1828 | 'data-jstree' => json_encode( $data ) |
| 1829 | ], |
| 1830 | $this->msg( |
| 1831 | 'centralnotice-location-name-and-code', |
| 1832 | $name, |
| 1833 | $regionCode |
| 1834 | )->text() |
| 1835 | ); |
| 1836 | } |
| 1837 | } |
| 1838 | |
| 1839 | $isSelected = in_array( $countryCode, $selectedCountries ); |
| 1840 | $data = [ |
| 1841 | 'type' => 'country', |
| 1842 | 'code' => $countryCode, |
| 1843 | 'opened' => $isSelected, |
| 1844 | 'selected' => $isSelected |
| 1845 | ]; |
| 1846 | if ( !$this->editable ) { |
| 1847 | $data['disabled'] = true; |
| 1848 | } |
| 1849 | |
| 1850 | $countryNameAndCode = $this->msg( |
| 1851 | 'centralnotice-location-name-and-code', |
| 1852 | $country->getName(), |
| 1853 | $countryCode |
| 1854 | )->escaped(); |
| 1855 | |
| 1856 | $locationElements .= Html::rawElement( |
| 1857 | 'li', |
| 1858 | [ |
| 1859 | 'data-jstree' => json_encode( $data ), |
| 1860 | 'id' => $countryCode |
| 1861 | ], |
| 1862 | $countryNameAndCode . ( $regions ? Html::rawElement( 'ul', [], $regions ) : '' ) |
| 1863 | ); |
| 1864 | } |
| 1865 | |
| 1866 | $properties = [ |
| 1867 | 'id' => 'geo_locations', |
| 1868 | 'class' => 'cn-tree' |
| 1869 | ]; |
| 1870 | |
| 1871 | if ( !$this->editable ) { |
| 1872 | $properties['disabled'] = 'disabled'; |
| 1873 | } |
| 1874 | |
| 1875 | $search = Html::rawElement( |
| 1876 | 'input', |
| 1877 | [ |
| 1878 | 'type' => 'text', |
| 1879 | 'class' => 'cn-tree-search' |
| 1880 | ], |
| 1881 | '' |
| 1882 | ); |
| 1883 | $searchClear = Html::element( |
| 1884 | 'button', |
| 1885 | [ |
| 1886 | 'class' => 'cn-tree-clear' |
| 1887 | ], |
| 1888 | $this->msg( 'centralnotice-location-filter-clear' )->text() |
| 1889 | ); |
| 1890 | $searchLabelText = $this->msg( 'centralnotice-location-filter' )->escaped(); |
| 1891 | $searchLabel = Html::rawElement( |
| 1892 | 'label', |
| 1893 | [ |
| 1894 | 'class' => 'cn-tree-search-label' |
| 1895 | ], |
| 1896 | $searchLabelText . $search . $searchClear |
| 1897 | ); |
| 1898 | |
| 1899 | $statusText = Html::rawElement( 'div', [ 'class' => 'cn-tree-status' ], '' ); |
| 1900 | |
| 1901 | $tree = Html::rawElement( |
| 1902 | 'div', |
| 1903 | $properties, |
| 1904 | Html::rawElement( |
| 1905 | 'ul', |
| 1906 | [], |
| 1907 | $locationElements |
| 1908 | ) |
| 1909 | ); |
| 1910 | |
| 1911 | $hiddenInputs = Html::input( |
| 1912 | 'geo_countries', |
| 1913 | implode( ',', $selectedCountries ), |
| 1914 | 'hidden', |
| 1915 | [ 'id' => 'geo_countries_value' ] |
| 1916 | ); |
| 1917 | |
| 1918 | $hiddenInputs .= Html::input( |
| 1919 | 'geo_regions', |
| 1920 | implode( ',', $selectedRegions ), |
| 1921 | 'hidden', |
| 1922 | [ 'id' => 'geo_regions_value' ] |
| 1923 | ); |
| 1924 | |
| 1925 | return Html::rawElement( |
| 1926 | 'div', |
| 1927 | [ 'class' => 'cn-tree-wrapper' ], |
| 1928 | $searchLabel . $tree . $statusText . $hiddenInputs |
| 1929 | ); |
| 1930 | } |
| 1931 | |
| 1932 | /** |
| 1933 | * Sanitizes template search terms by removing non alpha and ensuring space delimiting. |
| 1934 | * |
| 1935 | * @param string $terms Search terms to sanitize |
| 1936 | * |
| 1937 | * @return string Space delimited string |
| 1938 | */ |
| 1939 | public function sanitizeSearchTerms( $terms ) { |
| 1940 | preg_match_all( '/([\w-]+)\S*/s', $terms, $matches ); |
| 1941 | return implode( ' ', $matches[1] ); |
| 1942 | } |
| 1943 | |
| 1944 | /** |
| 1945 | * Truncate the summary field in a linguistically appropriate way. |
| 1946 | * @param string|null $summary |
| 1947 | * @return string |
| 1948 | */ |
| 1949 | public static function truncateSummaryField( $summary ) { |
| 1950 | return MediaWikiServices::getInstance()->getContentLanguage() |
| 1951 | ->truncateForDatabase( $summary ?? '', 255 ); |
| 1952 | } |
| 1953 | |
| 1954 | /** |
| 1955 | * Provides names of sub-pages of the CentralNotice admin interface. |
| 1956 | * |
| 1957 | * @inheritDoc |
| 1958 | */ |
| 1959 | public function getAssociatedNavigationLinks(): array { |
| 1960 | // Keys of (default value of $wgNoticeTabifyPages) are the names of the special |
| 1961 | // pages of the admin interface without the initial 'Special:'. |
| 1962 | // TODO Make $wgNoticeTabifyPages a constant rather than a config variable. |
| 1963 | return array_map( |
| 1964 | static fn ( $name ) => "Special:$name", |
| 1965 | array_keys( $this->getConfig()->get( 'NoticeTabifyPages' ) ) |
| 1966 | ); |
| 1967 | } |
| 1968 | |
| 1969 | /** |
| 1970 | * Return the localized text used for subpages of the CN admin interface (on skins |
| 1971 | * that support navigation links). |
| 1972 | * |
| 1973 | * @inheritDoc |
| 1974 | */ |
| 1975 | public function getShortDescription( string $path = '' ): string { |
| 1976 | $noticeTabifyPages = $this->getConfig()->get( 'NoticeTabifyPages' ); |
| 1977 | return $this->msg( $noticeTabifyPages[ $path ][ 'message' ] )->parse(); |
| 1978 | } |
| 1979 | |
| 1980 | /** |
| 1981 | * Loads a CentralNotice variable from session data. |
| 1982 | * |
| 1983 | * @param string $variable Name of the variable |
| 1984 | * @param mixed|null $default Default value of the variable |
| 1985 | * |
| 1986 | * @return mixed Stored variable or default |
| 1987 | */ |
| 1988 | public function getCNSessionVar( $variable, $default = null ) { |
| 1989 | return $this->getRequest()->getSessionData( "centralnotice-$variable" ) ?? $default; |
| 1990 | } |
| 1991 | |
| 1992 | /** |
| 1993 | * Sets a CentralNotice session variable. Note that this will fail silently if a |
| 1994 | * session does not exist for the user. |
| 1995 | * |
| 1996 | * @param string $variable Name of the variable |
| 1997 | * @param mixed $value Value for the variable |
| 1998 | */ |
| 1999 | public function setCNSessionVar( $variable, $value ) { |
| 2000 | $this->getRequest()->setSessionData( "centralnotice-{$variable}", $value ); |
| 2001 | } |
| 2002 | |
| 2003 | /** |
| 2004 | * @param string[] $projects |
| 2005 | * @return string |
| 2006 | */ |
| 2007 | public function listProjects( $projects ) { |
| 2008 | return $this->makeShortList( $this->getConfig()->get( 'NoticeProjects' ), $projects ); |
| 2009 | } |
| 2010 | |
| 2011 | /** |
| 2012 | * @param string[] $countries |
| 2013 | * @param string[] $regions |
| 2014 | * @return string |
| 2015 | */ |
| 2016 | public function listCountriesRegions( array $countries, array $regions ) { |
| 2017 | $allCountries = array_keys( GeoTarget::getCountriesList() ); |
| 2018 | $list = $this->makeShortList( $allCountries, $countries ); |
| 2019 | $regionsByCountry = []; |
| 2020 | foreach ( $regions as $region ) { |
| 2021 | $countryCode = substr( $region, 0, 2 ); |
| 2022 | $regionCode = substr( $region, 3 ); |
| 2023 | $regionsByCountry[$countryCode][] = $regionCode; |
| 2024 | } |
| 2025 | if ( $list !== '' && count( $regionsByCountry ) > 0 ) { |
| 2026 | $list .= '; '; |
| 2027 | } |
| 2028 | $regionsByCountryList = []; |
| 2029 | foreach ( $regionsByCountry as $countryCode => $regions ) { |
| 2030 | $all = array_keys( GeoTarget::getRegionsList( $countryCode ) ); |
| 2031 | $regionList = $this->makeShortList( $all, $regions ); |
| 2032 | $regionsByCountryList[] = "$countryCode: ($regionList)"; |
| 2033 | } |
| 2034 | $list .= $this->getContext()->getLanguage()->listToText( $regionsByCountryList ); |
| 2035 | |
| 2036 | return $list; |
| 2037 | } |
| 2038 | |
| 2039 | /** |
| 2040 | * @param string[] $languages |
| 2041 | * @return string |
| 2042 | */ |
| 2043 | public function listLanguages( $languages ) { |
| 2044 | $all = array_keys( MediaWikiServices::getInstance()->getLanguageNameUtils() |
| 2045 | ->getLanguageNames( 'en' ) ); |
| 2046 | return $this->makeShortList( $all, $languages ); |
| 2047 | } |
| 2048 | |
| 2049 | /** |
| 2050 | * @param string[] $all |
| 2051 | * @param string[] $list |
| 2052 | * @return string |
| 2053 | */ |
| 2054 | private function makeShortList( $all, $list ) { |
| 2055 | // TODO ellipsis and js/css expansion |
| 2056 | if ( count( $list ) == count( $all ) ) { |
| 2057 | return $this->getContext()->msg( 'centralnotice-all' )->text(); |
| 2058 | } |
| 2059 | if ( count( $list ) > self::LIST_COMPLEMENT_THRESHOLD * count( $all ) ) { |
| 2060 | $inverse = array_values( array_diff( $all, $list ) ); |
| 2061 | $txt = $this->getContext()->getLanguage()->listToText( $inverse ); |
| 2062 | return $this->getContext()->msg( 'centralnotice-all-except', $txt )->text(); |
| 2063 | } |
| 2064 | return $this->getContext()->getLanguage()->listToText( array_values( $list ) ); |
| 2065 | } |
| 2066 | |
| 2067 | /** |
| 2068 | * Convert comma separated list to array |
| 2069 | * @param string $list |
| 2070 | * @return array |
| 2071 | */ |
| 2072 | private function listToArray( $list ) { |
| 2073 | if ( $list ) { |
| 2074 | $array = explode( ',', $list ); |
| 2075 | } else { |
| 2076 | $array = []; |
| 2077 | } |
| 2078 | return $array; |
| 2079 | } |
| 2080 | |
| 2081 | /** @inheritDoc */ |
| 2082 | protected function getGroupName() { |
| 2083 | return 'wiki'; |
| 2084 | } |
| 2085 | |
| 2086 | /** @inheritDoc */ |
| 2087 | public function outputHeader( $summaryMsg = '' ) { |
| 2088 | // Allow users to add a custom nav bar (T138284) |
| 2089 | $navBar = $this->msg( 'centralnotice-navbar' )->inContentLanguage(); |
| 2090 | if ( !$navBar->isDisabled() ) { |
| 2091 | $this->getOutput()->addHTML( $navBar->parseAsBlock() ); |
| 2092 | } |
| 2093 | parent::outputHeader( $summaryMsg ); |
| 2094 | } |
| 2095 | } |