Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4076813
CentralNotice.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
60 KB
Referenced Files
None
Subscribers
None
CentralNotice.php
View Options
<?php
use
MediaWiki\Exception\ErrorPageError
;
use
MediaWiki\Exception\ReadOnlyError
;
use
MediaWiki\Html\Html
;
use
MediaWiki\Json\FormatJson
;
use
MediaWiki\MainConfigNames
;
use
MediaWiki\MediaWikiServices
;
use
MediaWiki\Parser\ParserOptions
;
use
MediaWiki\Parser\Sanitizer
;
use
MediaWiki\Request\WebRequest
;
use
MediaWiki\SpecialPage\SpecialPage
;
use
MediaWiki\SpecialPage\UnlistedSpecialPage
;
use
MediaWiki\Xml\Xml
;
class
CentralNotice
extends
UnlistedSpecialPage
{
// TODO review usage of Xml class and unnecessary openElement() and closeElement()
// methods.
// Note: These values are not arbitrary. Higher priority is indicated by a
// higher value.
public
const
LOW_PRIORITY
=
0
;
public
const
NORMAL_PRIORITY
=
1
;
public
const
HIGH_PRIORITY
=
2
;
public
const
EMERGENCY_PRIORITY
=
3
;
// String to use in drop-down to indicate no campaign type (repesented as null in DB)
private
const
EMPTY_CAMPAIGN_TYPE_OPTION
=
'empty-campaign-type-option'
;
// When displaying a long list, display the complement "all except ~LIST"
// past a threshold, given as a proportion of the "all" list length.
private
const
LIST_COMPLEMENT_THRESHOLD
=
0.75
;
/** @var bool|null */
public
$editable
;
/** @var bool|null */
public
$centralNoticeError
;
/**
* @var Campaign
*/
private
$campaign
;
/** @var array */
private
$campaignWarnings
=
[];
public
function
__construct
()
{
// Register special page
parent
::
__construct
(
'CentralNotice'
);
}
/** @inheritDoc */
public
function
doesWrites
()
{
return
true
;
}
/**
* Handle different types of page requests
* @param string|null $sub
*/
public
function
execute
(
$sub
)
{
// Begin output
$this
->
setHeaders
();
$this
->
outputHeader
();
$out
=
$this
->
getOutput
();
$request
=
$this
->
getRequest
();
$this
->
addHelpLink
(
'//meta.wikimedia.org/wiki/Special:MyLanguage/Help:CentralNotice'
,
true
);
// Check permissions
$this
->
editable
=
$this
->
getUser
()->
isAllowed
(
'centralnotice-admin'
);
// Initialize error variable
$this
->
centralNoticeError
=
false
;
$subaction
=
$request
->
getVal
(
'subaction'
);
// Switch to campaign detail interface if requested.
// This will also handle post submissions from the detail interface.
if
(
$subaction
===
'noticeDetail'
)
{
$notice
=
$request
->
getVal
(
'notice'
);
$this
->
outputNoticeDetail
(
$notice
);
return
;
}
// Handle form submissions from "Manage campaigns" or "Add a campaign" interface
if
(
$this
->
editable
&&
$request
->
wasPosted
()
)
{
if
(
MediaWikiServices
::
getInstance
()->
getReadOnlyMode
()->
isReadOnly
()
||
CNDatabase
::
getPrimaryDb
()->
isReadOnly
()
)
{
throw
new
ReadOnlyError
();
}
// Check authentication token
if
(
$this
->
getUser
()->
matchEditToken
(
$request
->
getVal
(
'authtoken'
)
)
)
{
// Handle adding a campaign or changing existing campaign settings
// via the list interface. In either case, we'll retirect to the
// list view.
if
(
$subaction
===
'addCampaign'
)
{
$this
->
handleAddCampaignPost
();
}
else
{
$this
->
handleNoticePostFromList
();
}
// If there were no errors, reload the page to prevent duplicate form submission
if
(
!
$this
->
centralNoticeError
)
{
$out
->
redirect
(
$this
->
getPageTitle
()->
getLocalURL
()
);
return
;
}
}
else
{
$this
->
showError
(
'sessionfailure'
);
}
}
$this
->
outputListOfNotices
(
$subaction
===
'showArchived'
);
}
/**
* Output the start tag for the enclosing div we use on all subactions
*/
private
function
outputEnclosingDivStartTag
()
{
$this
->
getOutput
()->
addHTML
(
Html
::
openElement
(
'div'
,
[
'id'
=>
'preferences'
]
)
);
}
/**
* Output the end tag for the enclosing div we use on all subactions
*/
private
function
outputEnclosingDivEndTag
()
{
$this
->
getOutput
()->
addHTML
(
Html
::
closeElement
(
'div'
)
);
}
/**
* Send the list of notices (campaigns) to output and, if appropriate,
* the "Add campaign" form.
* @param bool|null $showArchived Set true to only show archived campaigns,
* false to only show unarchived campaigns
*/
private
function
outputListOfNotices
(
$showArchived
=
null
)
{
$this
->
outputEnclosingDivStartTag
();
$out
=
$this
->
getOutput
();
$out
->
addModules
(
'ext.centralNotice.adminUi'
);
$out
->
addHTML
(
Html
::
element
(
'h2'
,
[
'class'
=>
'cn-special-section'
],
$this
->
msg
(
$showArchived
?
'centralnotice-archived-campaigns'
:
'centralnotice-manage'
)->
text
()
)
);
$out
->
addModules
(
'ext.centralNotice.adminUi.campaignPager'
);
if
(
$showArchived
===
false
)
{
$out
->
addHTML
(
$this
->
getLinkRenderer
()->
makeLink
(
SpecialPage
::
getTitleFor
(
'CentralNotice'
),
$this
->
msg
(
'centralnotice-archive-show'
)->
text
(),
[],
[
'subaction'
=>
'showArchived'
]
)
);
}
$pager
=
new
CNCampaignPager
(
$this
,
$this
->
editable
,
null
,
$showArchived
);
$popts
=
ParserOptions
::
newFromContext
(
$this
->
getContext
()
);
$out
->
addParserOutputContent
(
$pager
->
getBodyOutput
(),
$popts
);
$out
->
addHTML
(
$pager
->
getNavigationBar
()
);
// If the user has edit rights, show a form for adding a campaign
if
(
$this
->
editable
&&
$showArchived
!==
true
)
{
$this
->
addNoticeForm
();
}
$this
->
outputEnclosingDivEndTag
();
}
private
function
handleNoticePostFromList
()
{
$request
=
$this
->
getRequest
();
$changes
=
json_decode
(
$request
->
getText
(
'changes'
),
true
);
$summary
=
$this
->
getSummaryFromRequest
(
$request
);
// Make the changes requested
foreach
(
$changes
as
$campaignName
=>
$campaignChanges
)
{
$initialSettings
=
Campaign
::
getCampaignSettings
(
$campaignName
);
// Next campaign if somehow this one doesn't exist
if
(
!
$initialSettings
)
{
wfLogWarning
(
'Change requested for non-existent campaign '
.
$campaignName
);
continue
;
}
// Set values as per $changes
if
(
isset
(
$campaignChanges
[
'archived'
]
)
)
{
Campaign
::
setBooleanCampaignSetting
(
$campaignName
,
'archived'
,
$campaignChanges
[
'archived'
]
);
}
if
(
isset
(
$campaignChanges
[
'locked'
]
)
)
{
Campaign
::
setBooleanCampaignSetting
(
$campaignName
,
'locked'
,
$campaignChanges
[
'locked'
]
);
}
if
(
isset
(
$campaignChanges
[
'enabled'
]
)
)
{
Campaign
::
setBooleanCampaignSetting
(
$campaignName
,
'enabled'
,
$campaignChanges
[
'enabled'
]
);
}
if
(
isset
(
$campaignChanges
[
'priority'
]
)
)
{
Campaign
::
setNumericCampaignSetting
(
$campaignName
,
'preferred'
,
intval
(
$campaignChanges
[
'priority'
]
),
self
::
EMERGENCY_PRIORITY
,
self
::
LOW_PRIORITY
);
}
if
(
isset
(
$campaignChanges
[
'campaign_type'
]
)
)
{
$type
=
$campaignChanges
[
'campaign_type'
];
$type
=
$type
===
self
::
EMPTY_CAMPAIGN_TYPE_OPTION
?
null
:
$type
;
// Sanity check: does the requested campaign type exist?
if
(
$type
&&
!
CampaignType
::
getById
(
$type
)
)
{
$this
->
showError
(
'centralnotice-non-existent-campaign-type-error'
);
return
;
}
Campaign
::
setType
(
$campaignName
,
$type
);
}
// Log any differences in settings
$newSettings
=
Campaign
::
getCampaignSettings
(
$campaignName
);
$diffs
=
array_diff_assoc
(
$initialSettings
,
$newSettings
);
if
(
$diffs
)
{
$campaignId
=
Campaign
::
getNoticeId
(
$campaignName
);
Campaign
::
processAfterCampaignChange
(
'modified'
,
$campaignId
,
$campaignName
,
$this
->
getUser
(),
$initialSettings
,
$newSettings
,
$summary
);
}
}
}
/**
* Render a field suitable for jquery.ui datepicker
* @param string $name
* @param bool $editable
* @param string|null $timestamp
* @return string
*/
protected
function
dateSelector
(
$name
,
$editable
,
$timestamp
=
null
)
{
if
(
$editable
)
{
// Normalize timestamp format. If no timestamp is passed, default to now. If -1 is
// passed, set no defaults.
if
(
$timestamp
===
-
1
)
{
$ts
=
''
;
}
else
{
$ts
=
wfTimestamp
(
TS_MW
,
$timestamp
);
}
$out
=
Html
::
element
(
'input'
,
[
'id'
=>
"{$name}Date"
,
'name'
=>
"{$name}Date"
,
'type'
=>
'text'
,
'class'
=>
'centralnotice-datepicker centralnotice-datepicker-limit_one_year'
,
]
);
$out
.=
Html
::
element
(
'input'
,
[
'id'
=>
"{$name}Date_timestamp"
,
'name'
=>
"{$name}Date_timestamp"
,
'type'
=>
'hidden'
,
'value'
=>
$ts
,
]
);
return
$out
;
}
else
{
return
htmlspecialchars
(
$this
->
getLanguage
()->
date
(
$timestamp
)
);
}
}
/**
* @param string $prefix
* @param bool $editable
* @param string|null $timestamp
* @return string
*/
private
function
timeSelectorTd
(
$prefix
,
$editable
,
$timestamp
=
null
)
{
return
Html
::
rawElement
(
'td'
,
[
// Time is left-to-right in all languages
'dir'
=>
'ltr'
,
'class'
=>
'cn-timepicker'
,
],
$this
->
timeSelector
(
$prefix
,
$editable
,
$timestamp
)
);
}
/**
* @param string $prefix
* @param bool $editable
* @param string|null $timestamp
* @return string
*/
private
function
timeSelector
(
$prefix
,
$editable
,
$timestamp
=
null
)
{
if
(
$editable
)
{
$minutes
=
$this
->
paddedRange
(
0
,
59
);
$hours
=
$this
->
paddedRange
(
0
,
23
);
// Normalize timestamp format...
$ts
=
wfTimestamp
(
TS_MW
,
$timestamp
);
$fields
=
[
[
"hour"
,
"centralnotice-hours"
,
$hours
,
substr
(
$ts
,
8
,
2
)
],
[
"min"
,
"centralnotice-min"
,
$minutes
,
substr
(
$ts
,
10
,
2
)
],
];
return
$this
->
createSelector
(
$prefix
,
$fields
);
}
else
{
return
htmlspecialchars
(
$this
->
getLanguage
()->
time
(
$timestamp
)
);
}
}
/**
* @param bool $editable
* @param string|null $selectedTypeId
* @param string|null $index The name of the campaign (used when selector is included
* in a list of campaigns by CNCampaignPager).
* @return string
*/
public
function
campaignTypeSelector
(
$editable
,
$selectedTypeId
,
$index
=
null
)
{
$types
=
CampaignType
::
getTypes
();
if
(
$editable
)
{
$options
=
Xml
::
option
(
$this
->
msg
(
'centralnotice-empty-campaign-type-option'
)->
plain
(),
self
::
EMPTY_CAMPAIGN_TYPE_OPTION
);
foreach
(
$types
as
$type
)
{
$message
=
$this
->
msg
(
$type
->
getMessageKey
()
);
$text
=
$message
->
exists
()
?
$message
->
text
()
:
$type
->
getId
();
$options
.=
Xml
::
option
(
$text
,
$type
->
getId
(),
$selectedTypeId
===
$type
->
getId
()
);
}
// Handle the case of a type removed from config but still assigned to
// a campaign in the DB.
if
(
$selectedTypeId
&&
!
CampaignType
::
getById
(
$selectedTypeId
)
)
{
$options
.=
Xml
::
option
(
$this
->
msg
(
'centralntoice-deleted-campaign-type'
,
$selectedTypeId
)->
text
(),
$selectedTypeId
,
true
);
}
// Data attributes set below (data-campaign-name and
// data-initial-value) must coordinate with CNCampaignPager and
// ext.centralNotice.adminUi.campaignPager.js
$selectAttribs
=
[
'name'
=>
'campaign_type'
,
];
if
(
$selectedTypeId
)
{
$selectAttribs
[
'data-initial-value'
]
=
$selectedTypeId
;
}
if
(
$index
)
{
$selectAttribs
[
'data-campaign-name'
]
=
$index
;
}
return
Html
::
rawElement
(
'select'
,
$selectAttribs
,
$options
);
}
else
{
if
(
$selectedTypeId
)
{
$type
=
CampaignType
::
getById
(
$selectedTypeId
);
// We might get a null type if the DB has type identifiers that are
// not currently in the configuraiton.
if
(
$type
)
{
$message
=
$this
->
msg
(
$type
->
getMessageKey
()
);
return
$message
->
exists
()
?
$message
->
escaped
()
:
htmlspecialchars
(
$type
->
getId
()
);
}
else
{
return
htmlspecialchars
(
$selectedTypeId
);
}
}
return
$this
->
msg
(
'centralnotice-empty-campaign-type-option'
)->
escaped
();
}
}
/**
* Construct the priority select list for a campaign
*
* @param string|bool $index The name of the campaign (or false if it isn't needed)
* @param bool $editable Whether or not the form is editable by the user
* @param int $priorityValue The current priority value for this campaign
*
* @return string HTML for the select list
*/
public
function
prioritySelector
(
$index
,
$editable
,
$priorityValue
)
{
$priorities
=
[
self
::
LOW_PRIORITY
=>
$this
->
msg
(
'centralnotice-priority-low'
),
self
::
NORMAL_PRIORITY
=>
$this
->
msg
(
'centralnotice-priority-normal'
),
self
::
HIGH_PRIORITY
=>
$this
->
msg
(
'centralnotice-priority-high'
),
self
::
EMERGENCY_PRIORITY
=>
$this
->
msg
(
'centralnotice-priority-emergency'
),
];
if
(
$editable
)
{
// The HTML for the select list options
$options
=
''
;
foreach
(
$priorities
as
$key
=>
$labelMsg
)
{
$options
.=
Xml
::
option
(
$labelMsg
->
text
(),
(
string
)
$key
,
$priorityValue
==
$key
);
}
// Data attributes set below (data-campaign-name and
// data-initial-value) must coordinate with CNCampaignPager and
// ext.centralNotice.adminUi.campaignPager.js
$selectAttribs
=
[
'name'
=>
'priority'
,
'data-initial-value'
=>
$priorityValue
];
if
(
$index
)
{
$selectAttribs
[
'data-campaign-name'
]
=
$index
;
}
return
Html
::
rawElement
(
'select'
,
$selectAttribs
,
$options
);
}
else
{
return
$priorities
[
$priorityValue
]->
escaped
();
}
}
/**
* Build a set of select lists. Used by timeSelector.
* @param string $prefix string to identify selector set, for example, 'start' or 'end'
* @param array $fields array of select lists to build
* @return string
*/
private
function
createSelector
(
$prefix
,
$fields
)
{
$out
=
''
;
foreach
(
$fields
as
[
$field
,
$label
,
$set
,
$current
]
)
{
$out
.=
Xml
::
listDropdown
(
"{$prefix}[{$field}]"
,
self
::
dropdownList
(
$this
->
msg
(
$label
)->
text
(),
$set
),
''
,
$current
);
}
return
$out
;
}
/**
* Output a form for adding a campaign.
*
*/
private
function
addNoticeForm
()
{
$request
=
$this
->
getRequest
();
$start
=
null
;
$campaignType
=
null
;
$noticeProjects
=
[];
$noticeLanguages
=
[];
// If there was an error, we'll need to restore the state of the form
if
(
$request
->
wasPosted
()
&&
(
$request
->
getVal
(
'subaction'
)
===
'addCampaign'
)
)
{
$start
=
$this
->
getDateTime
(
'start'
);
$noticeLanguages
=
$request
->
getArray
(
'project_languages'
,
[]
);
$noticeProjects
=
$request
->
getArray
(
'projects'
,
[]
);
$campaignType
=
$request
->
getText
(
'campaign_type'
);
}
'@phan-var array $noticeLanguages'
;
'@phan-var array $noticeProjects'
;
$htmlOut
=
''
;
// Section heading
$htmlOut
.=
Html
::
element
(
'h2'
,
[
'class'
=>
'cn-special-section'
],
$this
->
msg
(
'centralnotice-add-notice'
)->
text
()
);
// Begin Add a campaign fieldset
$htmlOut
.=
Html
::
openElement
(
'fieldset'
,
[
'class'
=>
'prefsection'
]
);
// Form for adding a campaign
$htmlOut
.=
Html
::
openElement
(
'form'
,
[
'method'
=>
'post'
]
);
$htmlOut
.=
Html
::
hidden
(
'title'
,
$this
->
getPageTitle
()->
getPrefixedText
()
);
$htmlOut
.=
Html
::
hidden
(
'subaction'
,
'addCampaign'
);
$htmlOut
.=
Html
::
openElement
(
'table'
,
[
'cellpadding'
=>
9
]
);
// Name
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
element
(
'td'
,
[],
$this
->
msg
(
'centralnotice-notice-name'
)->
text
()
)
.
Html
::
rawElement
(
'td'
,
[],
Html
::
input
(
'noticeName'
,
$request
->
getVal
(
'noticeName'
,
''
),
'text'
,
[
'size'
=>
25
]
)
)
);
// Campaign type selector
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
rawElement
(
'td'
,
[],
Xml
::
label
(
$this
->
msg
(
'centralnotice-campaign-type'
)->
text
(),
'campaign_type'
)
)
.
Html
::
rawElement
(
'td'
,
[],
$this
->
campaignTypeSelector
(
$this
->
editable
,
$campaignType
)
)
);
// Start Date
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
element
(
'td'
,
[],
$this
->
msg
(
'centralnotice-start-date'
)->
text
()
)
.
Html
::
rawElement
(
'td'
,
[],
$this
->
dateSelector
(
'start'
,
$this
->
editable
,
$start
)
)
);
// Start Time
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
element
(
'td'
,
[],
$this
->
msg
(
'centralnotice-start-time'
)->
text
()
)
.
$this
->
timeSelectorTd
(
'start'
,
$this
->
editable
,
$start
)
);
// Project
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
element
(
'td'
,
[
'valign'
=>
'top'
],
$this
->
msg
(
'centralnotice-projects'
)->
text
()
)
.
Html
::
rawElement
(
'td'
,
[],
$this
->
projectMultiSelector
(
$noticeProjects
)
)
);
// Languages
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
element
(
'td'
,
[
'valign'
=>
'top'
],
$this
->
msg
(
'centralnotice-languages'
)->
text
()
)
.
Html
::
rawElement
(
'td'
,
[],
$this
->
languageMultiSelector
(
$noticeLanguages
)
)
);
// Countries
$htmlOut
.=
Html
::
openElement
(
'tr'
);
$htmlOut
.=
Html
::
rawElement
(
'td'
,
[],
Xml
::
label
(
$this
->
msg
(
'centralnotice-geo'
)->
text
(),
'geotargeted'
)
);
$htmlOut
.=
Html
::
rawElement
(
'td'
,
[],
Html
::
check
(
'geotargeted'
,
false
,
[
'value'
=>
1
,
'id'
=>
'geotargeted'
]
)
);
$htmlOut
.=
Html
::
closeElement
(
'tr'
);
// Locations multi-selector
$htmlOut
.=
Html
::
openElement
(
'tr'
,
[
'id'
=>
'centralnotice-geo-region-multiselector'
]
);
$htmlOut
.=
Html
::
element
(
'td'
,
[
'valign'
=>
'top'
],
$this
->
msg
(
'centralnotice-location'
)->
text
()
);
$htmlOut
.=
Html
::
rawElement
(
'td'
,
[],
$this
->
geoMultiSelectorTree
()
);
$htmlOut
.=
Html
::
closeElement
(
'tr'
);
$htmlOut
.=
Html
::
closeElement
(
'table'
);
$htmlOut
.=
Html
::
hidden
(
'change'
,
'weight'
);
$htmlOut
.=
Html
::
hidden
(
'authtoken'
,
$this
->
getUser
()->
getEditToken
()
);
// Submit button
$htmlOut
.=
Html
::
rawElement
(
'div'
,
[
'class'
=>
'cn-buttons'
],
$this
->
makeSummaryField
(
true
)
.
Html
::
submitButton
(
$this
->
msg
(
'centralnotice-modify'
)->
text
()
)
);
$htmlOut
.=
Html
::
closeElement
(
'form'
);
// End Add a campaign fieldset
$htmlOut
.=
Html
::
closeElement
(
'fieldset'
);
// Output HTML
$this
->
getOutput
()->
addHTML
(
$htmlOut
);
}
private
function
handleAddCampaignPost
()
{
$request
=
$this
->
getRequest
();
$noticeName
=
$request
->
getVal
(
'noticeName'
);
$start
=
$this
->
getDateTime
(
'start'
);
$projects
=
$request
->
getArray
(
'projects'
);
$project_languages
=
$request
->
getArray
(
'project_languages'
);
$geotargeted
=
$request
->
getCheck
(
'geotargeted'
);
$geo_countries
=
$request
->
getVal
(
'geo_countries'
);
if
(
$geo_countries
)
{
$geo_countries
=
explode
(
','
,
$geo_countries
);
}
else
{
$geo_countries
=
[];
}
$geo_regions
=
$request
->
getVal
(
'geo_regions'
);
if
(
$geo_regions
)
{
$geo_regions
=
explode
(
','
,
$geo_regions
);
}
else
{
$geo_regions
=
[];
}
$campaignType
=
$request
->
getText
(
'campaign_type'
);
$campaignType
=
$campaignType
===
self
::
EMPTY_CAMPAIGN_TYPE_OPTION
?
null
:
$campaignType
;
// Sanity check: does the requested campaign type exist?
if
(
$campaignType
&&
!
CampaignType
::
getById
(
$campaignType
)
)
{
$this
->
showError
(
'centralnotice-non-existent-campaign-type-error'
);
return
;
}
if
(
$noticeName
==
''
)
{
$this
->
showError
(
'centralnotice-null-string'
);
}
else
{
$result
=
Campaign
::
addCampaign
(
$noticeName
,
false
,
$start
,
$projects
,
$project_languages
,
$geotargeted
,
$geo_countries
,
$geo_regions
,
100
,
self
::
NORMAL_PRIORITY
,
$this
->
getUser
(),
$campaignType
,
$this
->
getSummaryFromRequest
(
$request
)
);
if
(
is_string
(
$result
)
)
{
// TODO Better error handling
$this
->
showError
(
$result
);
}
}
}
/**
* Retrieve jquery.ui.datepicker date and homebrew time,
* and return as a MW timestamp string.
* @param string $prefix
* @return null|string
*/
private
function
getDateTime
(
$prefix
)
{
$request
=
$this
->
getRequest
();
// Check whether the user left the date field blank.
// Interpret any form of "empty" as a blank value.
$manual_entry
=
$request
->
getVal
(
"{$prefix}Date"
);
if
(
!
$manual_entry
)
{
return
null
;
}
$datestamp
=
$request
->
getVal
(
"{$prefix}Date_timestamp"
);
$timeArray
=
$request
->
getArray
(
$prefix
);
$timestamp
=
substr
(
$datestamp
,
0
,
8
)
.
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable
$timeArray
[
'hour'
]
.
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable
$timeArray
[
'min'
]
.
'00'
;
return
$timestamp
;
}
/**
* Show the interface for viewing/editing an individual campaign
*
* @param string $notice The name of the campaign to view
*
*/
private
function
outputNoticeDetail
(
$notice
)
{
$out
=
$this
->
getOutput
();
// Output specific ResourceLoader module
$out
->
addModules
(
'ext.centralNotice.adminUi.campaignManager'
);
// Output ResourceLoader modules for campaign mixins with custom controls
foreach
(
$this
->
getConfig
()->
get
(
'CentralNoticeCampaignMixins'
)
as
$mixinConfig
)
{
if
(
!
empty
(
$mixinConfig
[
'customAdminUIControlsModule'
]
)
)
{
$out
->
addModules
(
$mixinConfig
[
'customAdminUIControlsModule'
]
);
}
}
$this
->
outputEnclosingDivStartTag
();
// Todo: Convert the rest of this page to use this object
$this
->
campaign
=
new
Campaign
(
$notice
);
try
{
if
(
$this
->
campaign
->
isArchived
()
||
$this
->
campaign
->
isLocked
()
)
{
$out
->
setSubtitle
(
$this
->
msg
(
'centralnotice-archive-edit-prevented'
)
);
// TODO: Fix this gross hack to prevent editing
$this
->
editable
=
false
;
}
$out
->
addSubtitle
(
$this
->
getLinkRenderer
()->
makeKnownLink
(
SpecialPage
::
getTitleFor
(
'CentralNoticeLogs'
),
$this
->
msg
(
'centralnotice-campaign-view-logs'
)->
text
(),
[],
[
'log_type'
=>
'campaignSettings'
,
'campaign'
=>
$notice
]
)
);
}
catch
(
CampaignExistenceException
$ex
)
{
throw
new
ErrorPageError
(
'centralnotice'
,
'centralnotice-notice-doesnt-exist'
);
}
if
(
$this
->
editable
&&
$this
->
getRequest
()->
wasPosted
()
)
{
$this
->
handleNoticeDetailPost
(
$notice
);
}
$htmlOut
=
''
;
// Begin Campaign detail fieldset
$htmlOut
.=
Html
::
openElement
(
'fieldset'
,
[
'class'
=>
'prefsection'
]
);
if
(
$this
->
editable
)
{
$htmlOut
.=
Html
::
openElement
(
'form'
,
[
'method'
=>
'post'
,
'id'
=>
'centralnotice-notice-detail'
,
'autocomplete'
=>
'off'
,
'action'
=>
$this
->
getPageTitle
()->
getLocalURL
(
[
'subaction'
=>
'noticeDetail'
,
'notice'
=>
$notice
]
)
]
);
}
$output_detail
=
$this
->
noticeDetailForm
(
$notice
);
$output_assigned
=
$this
->
assignedTemplatesForm
(
$notice
);
$output_templates
=
$this
->
addTemplatesForm
();
$htmlOut
.=
$output_detail
;
// Catch for no banners so that we don't double message
if
(
$output_assigned
==
''
&&
$output_templates
==
''
)
{
$htmlOut
.=
$this
->
msg
(
'centralnotice-no-templates'
)->
escaped
();
$htmlOut
.=
Html
::
element
(
'p'
);
$newPage
=
$this
->
getTitleFor
(
'NoticeTemplate'
,
'add'
);
$htmlOut
.=
$this
->
getLinkRenderer
()->
makeLink
(
$newPage
,
$this
->
msg
(
'centralnotice-add-template'
)->
text
()
);
$htmlOut
.=
Html
::
element
(
'p'
);
}
elseif
(
$output_assigned
==
''
)
{
$htmlOut
.=
Xml
::
fieldset
(
$this
->
msg
(
'centralnotice-assigned-templates'
)->
text
()
);
$htmlOut
.=
$this
->
msg
(
'centralnotice-no-templates-assigned'
)->
escaped
();
$htmlOut
.=
Html
::
closeElement
(
'fieldset'
);
if
(
$this
->
editable
)
{
$htmlOut
.=
$output_templates
;
}
}
else
{
$htmlOut
.=
$output_assigned
;
if
(
$this
->
editable
)
{
$htmlOut
.=
$output_templates
;
}
}
if
(
$this
->
editable
)
{
$htmlOut
.=
Html
::
hidden
(
'authtoken'
,
$this
->
getUser
()->
getEditToken
()
);
$htmlOut
.=
$this
->
makeSummaryField
();
// Submit button
$htmlOut
.=
Html
::
rawElement
(
'div'
,
[
'class'
=>
'cn-buttons'
],
Html
::
submitButton
(
$this
->
msg
(
'centralnotice-modify'
)->
text
(),
[
'id'
=>
'noticeDetailSubmit'
]
)
);
}
if
(
$this
->
editable
)
{
$htmlOut
.=
Html
::
closeElement
(
'form'
);
}
$htmlOut
.=
Html
::
closeElement
(
'fieldset'
);
$this
->
displayCampaignWarnings
();
$out
->
addHTML
(
$htmlOut
);
$this
->
outputEnclosingDivEndTag
();
}
/**
* Process a post request from the campaign (notice) detail subaction. Make
* changes to the campaign based on the post parameters.
*
* @param string $notice
*/
private
function
handleNoticeDetailPost
(
$notice
)
{
$request
=
$this
->
getRequest
();
// If what we're doing is actually serious (ie: not updating the banner
// filter); process the request. Recall that if the serious request
// succeeds, the page will be reloaded again.
if
(
!
$request
->
getCheck
(
'template-search'
)
)
{
// Check authentication token
if
(
$this
->
getUser
()->
matchEditToken
(
$request
->
getVal
(
'authtoken'
)
)
)
{
// Handle removing campaign
if
(
$request
->
getVal
(
'archive'
)
)
{
Campaign
::
setBooleanCampaignSetting
(
$notice
,
'archived'
,
true
);
}
$initialCampaignSettings
=
Campaign
::
getCampaignSettings
(
$notice
);
// Handle locking/unlocking campaign
Campaign
::
setBooleanCampaignSetting
(
$notice
,
'locked'
,
$request
->
getCheck
(
'locked'
)
);
// Handle enabling/disabling campaign
Campaign
::
setBooleanCampaignSetting
(
$notice
,
'enabled'
,
$request
->
getCheck
(
'enabled'
)
);
// Set campaign traffic throttle
if
(
$request
->
getCheck
(
'throttle-enabled'
)
)
{
$throttle
=
$request
->
getInt
(
'throttle-cur'
,
100
);
}
else
{
$throttle
=
100
;
}
Campaign
::
setNumericCampaignSetting
(
$notice
,
'throttle'
,
$throttle
,
100
,
0
);
$config
=
$this
->
getConfig
();
$noticeNumberOfBuckets
=
$config
->
get
(
'NoticeNumberOfBuckets'
);
// Handle user bucketing setting for campaign
$numCampaignBuckets
=
min
(
$request
->
getInt
(
'buckets'
,
1
),
$noticeNumberOfBuckets
);
$numCampaignBuckets
=
pow
(
2
,
floor
(
log
(
$numCampaignBuckets
,
2
)
)
);
Campaign
::
setNumericCampaignSetting
(
$notice
,
'buckets'
,
$numCampaignBuckets
,
$noticeNumberOfBuckets
,
1
);
// Handle setting campaign priority
Campaign
::
setNumericCampaignSetting
(
$notice
,
'preferred'
,
$request
->
getInt
(
'priority'
,
self
::
NORMAL_PRIORITY
),
self
::
EMERGENCY_PRIORITY
,
self
::
LOW_PRIORITY
);
// Handle setting campaign type
$type
=
$request
->
getText
(
'campaign_type'
);
$type
=
$type
===
self
::
EMPTY_CAMPAIGN_TYPE_OPTION
?
null
:
$type
;
// Sanity check: does the requested campaign type exist?
if
(
$type
&&
!
CampaignType
::
getById
(
$type
)
)
{
$this
->
showError
(
'centralnotice-non-existent-campaign-type-error'
);
return
;
}
Campaign
::
setType
(
$notice
,
$type
);
// Handle updating geotargeting
if
(
$request
->
getCheck
(
'geotargeted'
)
)
{
Campaign
::
setBooleanCampaignSetting
(
$notice
,
'geo'
,
true
);
$countries
=
$this
->
listToArray
(
$request
->
getVal
(
'geo_countries'
)
);
Campaign
::
updateCountries
(
$notice
,
$countries
);
// Regions in format CountryCode_RegionCode
$regions
=
$this
->
listToArray
(
$request
->
getVal
(
'geo_regions'
)
);
Campaign
::
updateRegions
(
$notice
,
$regions
);
}
else
{
Campaign
::
setBooleanCampaignSetting
(
$notice
,
'geo'
,
false
);
}
// Handle updating the start and end settings
$start
=
$this
->
getDateTime
(
'start'
);
$end
=
$this
->
getDateTime
(
'end'
);
if
(
$start
&&
$end
)
{
Campaign
::
updateNoticeDate
(
$notice
,
$start
,
$end
);
}
// Handle adding of banners to the campaign
$templatesToAdd
=
$request
->
getArray
(
'addTemplates'
);
if
(
$templatesToAdd
)
{
$weight
=
$request
->
getArray
(
'weight'
);
foreach
(
$templatesToAdd
as
$templateName
)
{
$templateId
=
Banner
::
fromName
(
$templateName
)->
getId
();
$bucket
=
$request
->
getInt
(
"bucket-{$templateName}"
);
$result
=
Campaign
::
addTemplateTo
(
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable
$notice
,
$templateName
,
$weight
[
$templateId
],
$bucket
);
if
(
$result
!==
true
)
{
$this
->
showError
(
$result
);
}
}
}
// Handle removing of banners from the campaign
$templateToRemove
=
$request
->
getArray
(
'removeTemplates'
);
if
(
$templateToRemove
)
{
foreach
(
$templateToRemove
as
$template
)
{
Campaign
::
removeTemplateFor
(
$notice
,
$template
);
}
}
// Handle weight changes
$updatedWeights
=
$request
->
getArray
(
'weight'
);
$balanced
=
$request
->
getCheck
(
'balanced'
);
if
(
$updatedWeights
)
{
foreach
(
$updatedWeights
as
$templateId
=>
$weight
)
{
if
(
$balanced
)
{
$weight
=
25
;
}
Campaign
::
updateWeight
(
$notice
,
$templateId
,
$weight
);
}
}
// Handle bucket changes - keep in mind that the number of campaign buckets
// might have changed simultaneously (and might have happened server side)
$updatedBuckets
=
$request
->
getArray
(
'bucket'
);
if
(
$updatedBuckets
)
{
foreach
(
$updatedBuckets
as
$templateId
=>
$bucket
)
{
Campaign
::
updateBucket
(
$notice
,
$templateId
,
intval
(
$bucket
)
%
$numCampaignBuckets
);
}
}
// Handle new projects
$projects
=
$request
->
getArray
(
'projects'
);
if
(
$projects
)
{
Campaign
::
updateProjects
(
$notice
,
$projects
);
}
// Handle new project languages
$projectLangs
=
$request
->
getArray
(
'project_languages'
);
if
(
$projectLangs
)
{
Campaign
::
updateProjectLanguages
(
$notice
,
$projectLangs
);
}
// Handle campaign-associated mixins
foreach
(
$config
->
get
(
'CentralNoticeCampaignMixins'
)
as
$mixinName
=>
$mixinDef
)
{
$mixinControlName
=
self
::
makeNoticeMixinControlName
(
$mixinName
);
if
(
$request
->
getCheck
(
$mixinControlName
)
)
{
$params
=
[];
foreach
(
$mixinDef
[
'parameters'
]
as
$paramName
=>
$paramDef
)
{
$requestParamName
=
self
::
makeNoticeMixinControlName
(
$mixinName
,
$paramName
);
switch
(
$paramDef
[
'type'
]
)
{
case
'string'
:
case
'json'
:
$paramVal
=
Sanitizer
::
removeSomeTags
(
$request
->
getText
(
$requestParamName
)
);
break
;
case
'integer'
:
$paramVal
=
$request
->
getInt
(
$requestParamName
);
break
;
case
'float'
:
$paramVal
=
$request
->
getFloat
(
$requestParamName
);
break
;
case
'boolean'
:
$paramVal
=
$request
->
getCheck
(
$requestParamName
);
break
;
default
:
throw
new
DomainException
(
"Unknown parameter type: '{$paramDef['type']}'"
);
}
$params
[
$paramName
]
=
$paramVal
;
}
Campaign
::
updateCampaignMixins
(
$notice
,
$mixinName
,
true
,
$params
);
}
else
{
Campaign
::
updateCampaignMixins
(
$notice
,
$mixinName
,
false
);
}
}
$finalCampaignSettings
=
Campaign
::
getCampaignSettings
(
$notice
);
$campaignId
=
Campaign
::
getNoticeId
(
$notice
);
$summary
=
$this
->
getSummaryFromRequest
(
$request
);
Campaign
::
processAfterCampaignChange
(
'modified'
,
$campaignId
,
$notice
,
$this
->
getUser
(),
$initialCampaignSettings
,
$finalCampaignSettings
,
$summary
);
// If there were no errors, reload the page to prevent duplicate form submission
if
(
!
$this
->
centralNoticeError
)
{
$this
->
getOutput
()->
redirect
(
$this
->
getPageTitle
()->
getLocalURL
(
[
'subaction'
=>
'noticeDetail'
,
'notice'
=>
$notice
]
)
);
return
;
}
ChoiceDataProvider
::
invalidateCache
();
}
else
{
$this
->
showError
(
'sessionfailure'
);
}
}
}
/**
* Output stored campaign warnings
*/
private
function
displayCampaignWarnings
()
{
foreach
(
$this
->
campaignWarnings
as
$message
)
{
$this
->
getOutput
()->
wrapWikiMsg
(
"<div class='cn-error'>
\n
$1
\n
</div>"
,
$message
);
}
}
/**
* Create form for managing campaign settings (start date, end date, languages, etc.)
* @param string $notice
* @return string HTML
*/
private
function
noticeDetailForm
(
$notice
)
{
if
(
$this
->
editable
)
{
$readonly
=
[];
}
else
{
$readonly
=
[
'disabled'
=>
'disabled'
];
}
$campaign
=
Campaign
::
getCampaignSettings
(
$notice
);
if
(
$campaign
)
{
// If there was an error, we'll need to restore the state of the form
$request
=
$this
->
getRequest
();
if
(
$request
->
wasPosted
()
)
{
$start
=
$this
->
getDateTime
(
'start'
);
$end
=
$this
->
getDateTime
(
'end'
);
$isEnabled
=
$request
->
getCheck
(
'enabled'
);
$priority
=
$request
->
getInt
(
'priority'
,
self
::
NORMAL_PRIORITY
);
$throttle
=
$request
->
getInt
(
'throttle'
,
100
);
$isLocked
=
$request
->
getCheck
(
'locked'
);
$isArchived
=
$request
->
getCheck
(
'archived'
);
$noticeProjects
=
$request
->
getArray
(
'projects'
,
[]
);
$noticeLanguages
=
$request
->
getArray
(
'project_languages'
,
[]
);
$isGeotargeted
=
$request
->
getCheck
(
'geotargeted'
);
$numBuckets
=
$request
->
getInt
(
'buckets'
,
1
);
$countries
=
$this
->
listToArray
(
$request
->
getVal
(
'geo_countries'
)
);
$regions
=
$this
->
listToArray
(
$request
->
getVal
(
'geo_regions'
)
);
$type
=
$request
->
getText
(
'type'
);
}
else
{
// Defaults
$start
=
$campaign
[
'start'
];
$end
=
$campaign
[
'end'
];
$isEnabled
=
(
bool
)
$campaign
[
'enabled'
];
$priority
=
$campaign
[
'preferred'
];
$throttle
=
intval
(
$campaign
[
'throttle'
]
);
$isLocked
=
(
bool
)
$campaign
[
'locked'
];
$isArchived
=
(
bool
)
$campaign
[
'archived'
];
$noticeProjects
=
Campaign
::
getNoticeProjects
(
$notice
);
$noticeLanguages
=
Campaign
::
getNoticeLanguages
(
$notice
);
$isGeotargeted
=
(
bool
)
$campaign
[
'geo'
];
$numBuckets
=
intval
(
$campaign
[
'buckets'
]
);
$countries
=
Campaign
::
getNoticeCountries
(
$notice
);
$regions
=
Campaign
::
getNoticeRegions
(
$notice
);
$type
=
$campaign
[
'type'
];
}
'@phan-var array $noticeLanguages'
;
'@phan-var array $noticeProjects'
;
$isThrottled
=
(
$throttle
<
100
);
$type
=
$type
===
self
::
EMPTY_CAMPAIGN_TYPE_OPTION
?
null
:
$type
;
// Build Html
$htmlOut
=
''
;
$htmlOut
.=
Html
::
rawElement
(
'h2'
,
[],
$this
->
msg
(
'centralnotice-notice-heading'
,
$notice
)->
parse
()
);
$htmlOut
.=
Html
::
openElement
(
'table'
,
[
'cellpadding'
=>
9
]
);
// Rows
// Campaign type selector
$htmlOut
.=
Html
::
openElement
(
'tr'
);
$htmlOut
.=
Html
::
rawElement
(
'td'
,
[],
Xml
::
label
(
$this
->
msg
(
'centralnotice-campaign-type'
)->
text
(),
'campaign_type'
)
);
$htmlOut
.=
Html
::
rawElement
(
'td'
,
[],
$this
->
campaignTypeSelector
(
$this
->
editable
,
$type
)
);
$htmlOut
.=
Html
::
closeElement
(
'tr'
);
// Start Date
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
element
(
'td'
,
[],
$this
->
msg
(
'centralnotice-start-date'
)->
text
()
)
.
Html
::
rawElement
(
'td'
,
[],
$this
->
dateSelector
(
'start'
,
$this
->
editable
,
$start
)
)
);
// Start Time
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
element
(
'td'
,
[],
$this
->
msg
(
'centralnotice-start-time'
)->
text
()
)
.
$this
->
timeSelectorTd
(
'start'
,
$this
->
editable
,
$start
)
);
// End Date
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
element
(
'td'
,
[],
$this
->
msg
(
'centralnotice-end-date'
)->
text
()
)
.
Html
::
rawElement
(
'td'
,
[],
$this
->
dateSelector
(
'end'
,
$this
->
editable
,
$end
)
)
);
// End Time
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
element
(
'td'
,
[],
$this
->
msg
(
'centralnotice-end-time'
)->
text
()
)
.
$this
->
timeSelectorTd
(
'end'
,
$this
->
editable
,
$end
)
);
// Project
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
element
(
'td'
,
[
'valign'
=>
'top'
],
$this
->
msg
(
'centralnotice-projects'
)->
text
()
)
.
Html
::
rawElement
(
'td'
,
[],
$this
->
projectMultiSelector
(
$noticeProjects
)
)
);
// Languages
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
element
(
'td'
,
[
'valign'
=>
'top'
],
$this
->
msg
(
'centralnotice-languages'
)->
text
()
)
.
Html
::
rawElement
(
'td'
,
[],
$this
->
languageMultiSelector
(
$noticeLanguages
)
)
);
// Countries
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
rawElement
(
'td'
,
[],
Xml
::
label
(
$this
->
msg
(
'centralnotice-geo'
)->
text
(),
'geotargeted'
)
)
.
Html
::
rawElement
(
'td'
,
[],
Html
::
check
(
'geotargeted'
,
$isGeotargeted
,
array_replace
(
$readonly
,
[
'value'
=>
$notice
,
'id'
=>
'geotargeted'
]
)
)
)
);
// Locations multi-selector
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[
'id'
=>
'centralnotice-geo-region-multiselector'
],
Html
::
element
(
'td'
,
[
'valign'
=>
'top'
],
$this
->
msg
(
'centralnotice-location'
)->
text
()
)
.
Html
::
rawElement
(
'td'
,
[],
$this
->
geoMultiSelectorTree
(
$countries
,
$regions
)
)
);
$config
=
$this
->
getConfig
();
// User bucketing
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
rawElement
(
'td'
,
[],
Xml
::
label
(
$this
->
msg
(
'centralnotice-buckets'
)->
text
(),
'buckets'
)
)
.
Html
::
rawElement
(
'td'
,
[],
$this
->
numBucketsDropdown
(
$config
->
get
(
'NoticeNumberOfBuckets'
),
$numBuckets
)
)
);
// Enabled
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
rawElement
(
'td'
,
[],
Xml
::
label
(
$this
->
msg
(
'centralnotice-enabled'
)->
text
(),
'enabled'
)
)
.
Html
::
rawElement
(
'td'
,
[],
Html
::
check
(
'enabled'
,
$isEnabled
,
array_replace
(
$readonly
,
[
'value'
=>
$notice
,
'id'
=>
'enabled'
]
)
)
)
);
// Preferred / Priority
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
rawElement
(
'td'
,
[],
Xml
::
label
(
$this
->
msg
(
'centralnotice-preferred'
)->
text
(),
'priority'
)
)
.
Html
::
rawElement
(
'td'
,
[],
$this
::
prioritySelector
(
false
,
$this
->
editable
,
$priority
)
)
);
// Throttle impressions
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
rawElement
(
'td'
,
[],
Xml
::
label
(
$this
->
msg
(
'centralnotice-throttle'
)->
text
(),
'throttle-enabled'
)
)
.
Html
::
rawElement
(
'td'
,
[],
Html
::
check
(
'throttle-enabled'
,
$isThrottled
,
array_replace
(
$readonly
,
[
'value'
=>
$notice
,
'id'
=>
'throttle-enabled'
]
)
)
)
);
// Throttle value
$htmlOut
.=
Html
::
openElement
(
'tr'
,
[
'class'
=>
'cn-throttle-amount'
]
);
$htmlOut
.=
Html
::
rawElement
(
'td'
,
[],
Xml
::
label
(
$this
->
msg
(
'centralnotice-throttle-amount'
)->
text
(),
'throttle'
)
);
$throttleLabel
=
$this
->
msg
(
'percent'
)->
numParams
(
$throttle
)->
text
();
if
(
$this
->
editable
)
{
$htmlOut
.=
Html
::
rawElement
(
'td'
,
[],
Html
::
element
(
'span'
,
[
'class'
=>
'cn-throttle'
,
'id'
=>
'centralnotice-throttle-echo'
],
$throttleLabel
)
.
Html
::
hidden
(
'throttle-cur'
,
$throttle
,
[
'id'
=>
'centralnotice-throttle-cur'
]
)
.
Html
::
rawElement
(
'div'
,
[
'id'
=>
'centralnotice-throttle-amount'
],
''
)
);
}
else
{
$htmlOut
.=
Html
::
element
(
'td'
,
[],
$throttleLabel
);
}
$htmlOut
.=
Html
::
closeElement
(
'tr'
);
// Locked
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
rawElement
(
'td'
,
[],
Xml
::
label
(
$this
->
msg
(
'centralnotice-locked'
)->
text
(),
'locked'
)
)
.
Html
::
rawElement
(
'td'
,
[],
Html
::
check
(
'locked'
,
$isLocked
,
array_replace
(
$readonly
,
[
'value'
=>
$notice
,
'id'
=>
'locked'
]
)
)
)
);
if
(
$this
->
editable
)
{
// Locked
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
rawElement
(
'td'
,
[],
Xml
::
label
(
$this
->
msg
(
'centralnotice-archive-campaign'
)->
text
(),
'archive'
)
)
.
Html
::
rawElement
(
'td'
,
[],
Html
::
check
(
'archive'
,
$isArchived
,
[
'value'
=>
$notice
,
'id'
=>
'archive'
]
)
)
);
}
$htmlOut
.=
Html
::
closeElement
(
'table'
);
// Create controls for campaign-associated mixins (if there are any)
$centralNoticeCampaignMixins
=
$config
->
get
(
'CentralNoticeCampaignMixins'
);
if
(
$centralNoticeCampaignMixins
)
{
$mixinsThisNotice
=
Campaign
::
getCampaignMixins
(
$notice
);
$htmlOut
.=
Xml
::
fieldset
(
$this
->
msg
(
'centralnotice-notice-mixins-fieldset'
)->
text
()
);
foreach
(
$centralNoticeCampaignMixins
as
$mixinName
=>
$mixinDef
)
{
$mixinControlName
=
self
::
makeNoticeMixinControlName
(
$mixinName
);
$attribs
=
[
'value'
=>
$notice
,
'class'
=>
'noticeMixinCheck'
,
'id'
=>
$mixinControlName
,
'data-mixin-name'
=>
$mixinName
];
if
(
isset
(
$mixinsThisNotice
[
$mixinName
]
)
)
{
// We have data on the mixin for this campaign, though
// it may not have been enabled.
$checked
=
$mixinsThisNotice
[
$mixinName
][
'enabled'
];
$attribs
[
'data-mixin-param-values'
]
=
FormatJson
::
encode
(
$mixinsThisNotice
[
$mixinName
][
'parameters'
]
);
}
else
{
// No data; it's never been enabled for this campaign
// before. Note: default settings values are set on the
// client.
$checked
=
false
;
}
$htmlOut
.=
Html
::
openElement
(
'div'
);
$htmlOut
.=
Html
::
check
(
$mixinControlName
,
$checked
,
array_replace
(
$readonly
,
$attribs
)
);
$htmlOut
.=
Xml
::
label
(
$this
->
msg
(
$mixinDef
[
'nameMsg'
]
)->
text
(),
$mixinControlName
,
[
'for'
=>
$mixinControlName
]
);
if
(
!
empty
(
$mixinDef
[
'helpMsg'
]
)
)
{
$htmlOut
.=
Html
::
element
(
'div'
,
[
'class'
=>
'htmlform-help'
],
$this
->
msg
(
$mixinDef
[
'helpMsg'
]
)->
text
()
);
}
$htmlOut
.=
Html
::
closeElement
(
'div'
);
}
$htmlOut
.=
Html
::
closeElement
(
'fieldset'
);
}
return
$htmlOut
;
}
else
{
return
''
;
}
}
private
static
function
makeNoticeMixinControlName
(
string
$mixinName
,
?
string
$mixinParam
=
null
):
string
{
return
'notice-mixin-'
.
$mixinName
.
(
$mixinParam
?
'-'
.
$mixinParam
:
''
);
}
/**
* Create form for managing banners assigned to a campaign
*
* Common campaign misconfigurations will cause warnings to appear
* at the top of this form.
* @param string $notice
* @return string HTML
*/
private
function
assignedTemplatesForm
(
$notice
)
{
$dbr
=
CNDatabase
::
getReplicaDb
();
$res
=
$dbr
->
newSelectQueryBuilder
()
// Aliases are needed to avoid problems with table prefixes
->
select
(
[
'templates.tmp_id'
,
'templates.tmp_name'
,
'assignments.tmp_weight'
,
'assignments.asn_bucket'
,
'notices.not_buckets'
,
]
)
->
from
(
'cn_notices'
,
'notices'
)
->
join
(
'cn_assignments'
,
'assignments'
,
'notices.not_id = assignments.not_id'
)
->
join
(
'cn_templates'
,
'templates'
,
'assignments.tmp_id = templates.tmp_id'
)
->
where
(
[
'notices.not_name'
=>
$notice
,
]
)
->
orderBy
(
[
'assignments.asn_bucket'
,
'notices.not_id'
]
)
->
caller
(
__METHOD__
)
->
fetchResultSet
();
// No banners found
if
(
$res
->
numRows
()
<
1
)
{
return
''
;
}
if
(
$this
->
editable
)
{
$readonly
=
[];
}
else
{
$readonly
=
[
'disabled'
=>
'disabled'
];
}
$weights
=
[];
$banners
=
[];
foreach
(
$res
as
$row
)
{
$banners
[]
=
$row
;
$weights
[]
=
$row
->
tmp_weight
;
}
$isBalanced
=
(
count
(
array_unique
(
$weights
)
)
===
1
);
// Build Assigned banners HTML
$htmlOut
=
Html
::
hidden
(
'change'
,
'weight'
);
// Prepare data about assigned banners to provide to client-side code, and
// make it available within the fieldsset element.
$bannersForJS
=
array_map
(
static
function
(
$banner
)
{
return
[
'bannerName'
=>
$banner
->
tmp_name
,
'bucket'
=>
$banner
->
asn_bucket
];
},
$banners
);
$htmlOut
.=
Xml
::
fieldset
(
$this
->
msg
(
'centralnotice-assigned-templates'
)->
text
(),
false
,
[
'data-assigned-banners'
=>
json_encode
(
$bannersForJS
),
'id'
=>
'centralnotice-assigned-banners'
]
);
// Equal weight banners
$htmlOut
.=
Html
::
rawElement
(
'tr'
,
[],
Html
::
rawElement
(
'td'
,
[],
Xml
::
label
(
$this
->
msg
(
'centralnotice-balanced'
)->
text
(),
'balanced'
)
)
.
Html
::
rawElement
(
'td'
,
[],
Html
::
check
(
'balanced'
,
$isBalanced
,
array_replace
(
$readonly
,
[
'value'
=>
$notice
,
'id'
=>
'balanced'
]
)
)
)
);
$htmlOut
.=
Html
::
openElement
(
'table'
,
[
'cellpadding'
=>
9
,
'width'
=>
'100%'
]
);
if
(
$this
->
editable
)
{
$htmlOut
.=
Html
::
element
(
'th'
,
[
'align'
=>
'left'
,
'width'
=>
'5%'
],
$this
->
msg
(
"centralnotice-remove"
)->
text
()
);
}
$htmlOut
.=
Html
::
element
(
'th'
,
[
'align'
=>
'left'
,
'width'
=>
'5%'
,
'class'
=>
'cn-weight'
],
$this
->
msg
(
'centralnotice-weight'
)->
text
()
);
$htmlOut
.=
Html
::
element
(
'th'
,
[
'align'
=>
'left'
,
'width'
=>
'5%'
],
$this
->
msg
(
'centralnotice-bucket'
)->
text
()
);
$htmlOut
.=
Html
::
element
(
'th'
,
[
'align'
=>
'left'
,
'width'
=>
'70%'
],
$this
->
msg
(
'centralnotice-templates'
)->
text
()
);
// Table rows
$noticeNumberOfBuckets
=
$this
->
getConfig
()->
get
(
'NoticeNumberOfBuckets'
);
foreach
(
$banners
as
$row
)
{
$htmlOut
.=
Html
::
openElement
(
'tr'
);
if
(
$this
->
editable
)
{
// Remove
$htmlOut
.=
Html
::
rawElement
(
'td'
,
[
'valign'
=>
'top'
],
Html
::
check
(
'removeTemplates[]'
,
false
,
[
'value'
=>
$row
->
tmp_name
,
'class'
=>
'bannerRemoveCheckbox'
]
)
);
}
// Weight
$htmlOut
.=
Html
::
rawElement
(
'td'
,
[
'valign'
=>
'top'
,
'class'
=>
'cn-weight'
],
$this
->
weightDropdown
(
"weight[$row->tmp_id]"
,
$row
->
tmp_weight
)
);
// Bucket
$numCampaignBuckets
=
min
(
intval
(
$row
->
not_buckets
),
$noticeNumberOfBuckets
);
$htmlOut
.=
Html
::
rawElement
(
'td'
,
[
'valign'
=>
'top'
],
$this
->
bucketDropdown
(
"bucket[$row->tmp_id]"
,
(
$numCampaignBuckets
==
1
?
null
:
intval
(
$row
->
asn_bucket
)
),
$numCampaignBuckets
,
$row
->
tmp_name
)
);
// Banner
$htmlOut
.=
Html
::
rawElement
(
'td'
,
[
'valign'
=>
'top'
],
BannerRenderer
::
linkToBanner
(
$row
->
tmp_name
)
);
$htmlOut
.=
Html
::
closeElement
(
'tr'
);
}
$htmlOut
.=
Html
::
closeElement
(
'table'
);
$htmlOut
.=
Html
::
closeElement
(
'fieldset'
);
// Sneak in some extra processing, to detect errors in bucket assignment.
// Test for campaign buckets without an assigned banner or with multiple banners.
$assignedBuckets
=
[];
$numBuckets
=
$this
->
campaign
->
getBuckets
();
foreach
(
$banners
as
$banner
)
{
$bannerBucket
=
$banner
->
asn_bucket
;
$bannerName
=
$banner
->
tmp_name
;
$assignedBuckets
[
$bannerBucket
]
=
$bannerName
;
}
// Do any buckets not have a banner assigned?
if
(
count
(
$assignedBuckets
)
<
$numBuckets
)
{
$this
->
campaignWarnings
[]
=
[
'centralnotice-banner-empty-bucket'
];
}
return
$htmlOut
;
}
/**
* @param string $name
* @param int|null $selected
* @return string
*/
private
function
weightDropdown
(
$name
,
$selected
)
{
$selected
=
intval
(
$selected
);
if
(
$this
->
editable
)
{
$html
=
Html
::
openElement
(
'select'
,
[
'name'
=>
$name
]
);
foreach
(
range
(
5
,
100
,
5
)
as
$value
)
{
$html
.=
Xml
::
option
(
$value
,
(
string
)
$value
,
$value
===
$selected
);
}
$html
.=
Html
::
closeElement
(
'select'
);
return
$html
;
}
else
{
return
htmlspecialchars
(
(
string
)
$selected
);
}
}
/**
* @param string $name
* @param int|null $selected
* @param int $numberCampaignBuckets
* @param string $bannerName
* @return string
*/
private
function
bucketDropdown
(
$name
,
$selected
,
$numberCampaignBuckets
,
$bannerName
)
{
$bucketLabel
=
static
fn
(
int
$val
)
=>
chr
(
ord
(
'A'
)
+
$val
);
if
(
$this
->
editable
)
{
// Default to bucket 'A'
$selected
??=
0
;
$selected
%=
$numberCampaignBuckets
;
// bucketSelector class is for all bucket selectors (for assigned or
// unassigned banners). Coordinate with CentralNoticePager::bucketDropdown().
$html
=
Html
::
openElement
(
'select'
,
[
'name'
=>
$name
,
'class'
=>
'bucketSelector bucketSelectorForAssignedBanners'
,
'data-banner-name'
=>
$bannerName
]
);
foreach
(
range
(
0
,
$this
->
getConfig
()->
get
(
'NoticeNumberOfBuckets'
)
-
1
)
as
$value
)
{
$attribs
=
[];
if
(
$value
>=
$numberCampaignBuckets
)
{
$attribs
[
'disabled'
]
=
'disabled'
;
}
$html
.=
Xml
::
option
(
$bucketLabel
(
$value
),
$value
,
$value
===
$selected
,
$attribs
);
}
$html
.=
Html
::
closeElement
(
'select'
);
return
$html
;
}
else
{
if
(
$selected
===
null
)
{
return
'-'
;
}
return
htmlspecialchars
(
$bucketLabel
(
$selected
)
);
}
}
/**
* @param int $numBuckets
* @param int|null $selected
* @return string
*/
private
function
numBucketsDropdown
(
$numBuckets
,
$selected
)
{
$selected
??=
1
;
if
(
$this
->
editable
)
{
$html
=
Html
::
openElement
(
'select'
,
[
'name'
=>
'buckets'
,
'id'
=>
'buckets'
]
);
foreach
(
range
(
0
,
intval
(
log
(
$numBuckets
,
2
)
)
)
as
$value
)
{
$value
=
pow
(
2
,
$value
);
$html
.=
Xml
::
option
(
(
string
)
$value
,
(
string
)
$value
,
$value
===
$selected
);
}
$html
.=
Html
::
closeElement
(
'select'
);
return
$html
;
}
else
{
return
htmlspecialchars
(
(
string
)
$selected
);
}
}
/**
* Create form for adding banners to a campaign
* @return string
*/
private
function
addTemplatesForm
()
{
// Sanitize input on search key and split out terms
$searchTerms
=
$this
->
sanitizeSearchTerms
(
$this
->
getRequest
()->
getText
(
'tplsearchkey'
)
);
$pager
=
new
CentralNoticePager
(
$this
,
$searchTerms
);
// Build HTML
$htmlOut
=
Xml
::
fieldset
(
$this
->
msg
(
'centralnotice-available-templates'
)->
text
()
);
// Banner search box
$htmlOut
.=
Html
::
openElement
(
'fieldset'
,
[
'id'
=>
'cn-template-searchbox'
]
);
$htmlOut
.=
Html
::
element
(
'legend'
,
[],
$this
->
msg
(
'centralnotice-filter-template-banner'
)->
text
()
);
$htmlOut
.=
Html
::
element
(
'label'
,
[
'for'
=>
'tplsearchkey'
],
$this
->
msg
(
'centralnotice-filter-template-prompt'
)->
text
()
);
$htmlOut
.=
Html
::
input
(
'tplsearchkey'
,
$searchTerms
);
$htmlOut
.=
Html
::
element
(
'input'
,
[
'type'
=>
'submit'
,
'name'
=>
'template-search'
,
'value'
=>
$this
->
msg
(
'centralnotice-filter-template-submit'
)->
text
()
]
);
$htmlOut
.=
Html
::
closeElement
(
'fieldset'
);
// And now the banners, if any
if
(
$pager
->
getNumRows
()
>
0
)
{
// Show paginated list of banners
$htmlOut
.=
Html
::
rawElement
(
'div'
,
[
'class'
=>
'cn-pager'
],
$pager
->
getNavigationBar
()
);
$htmlOut
.=
$pager
->
getBody
();
$htmlOut
.=
Html
::
rawElement
(
'div'
,
[
'class'
=>
'cn-pager'
],
$pager
->
getNavigationBar
()
);
}
else
{
$htmlOut
.=
$this
->
msg
(
'centralnotice-no-templates'
)->
escaped
();
}
$htmlOut
.=
Html
::
closeElement
(
'fieldset'
);
return
$htmlOut
;
}
/**
* Generates a multiple select list of all languages.
*
* @param array $selected The language codes of the selected languages
*
* @return string multiple select list
*/
private
function
languageMultiSelector
(
$selected
=
[]
)
{
// Retrieve the list of languages in user's language
$languages
=
MediaWikiServices
::
getInstance
()->
getLanguageNameUtils
()
->
getLanguageNames
(
$this
->
getLanguage
()->
getCode
()
);
// Make sure the site language is in the list; a custom language code
// might not have a defined name...
$languageCode
=
$this
->
getConfig
()->
get
(
MainConfigNames
::
LanguageCode
);
if
(
!
array_key_exists
(
$languageCode
,
$languages
)
)
{
$languages
[
$languageCode
]
=
$languageCode
;
}
ksort
(
$languages
);
$options
=
"
\n
"
;
foreach
(
$languages
as
$code
=>
$name
)
{
$options
.=
Xml
::
option
(
$this
->
msg
(
'centralnotice-language-listing'
,
$code
,
$name
)->
text
(),
$code
,
in_array
(
$code
,
$selected
)
)
.
"
\n
"
;
}
$properties
=
[
'multiple'
=>
'multiple'
,
'id'
=>
'project_languages'
,
'name'
=>
'project_languages[]'
,
'class'
=>
'cn-multiselect'
,
'autocomplete'
=>
'off'
];
if
(
!
$this
->
editable
)
{
$properties
[
'disabled'
]
=
'disabled'
;
}
return
Html
::
rawElement
(
'select'
,
$properties
,
$options
);
}
/**
* Generates a multiple select list of all project types.
*
* @param array $selected The name of the selected project type
*
* @return string multiple select list
*/
private
function
projectMultiSelector
(
$selected
=
[]
)
{
$options
=
"
\n
"
;
foreach
(
$this
->
getConfig
()->
get
(
'NoticeProjects'
)
as
$project
)
{
$options
.=
Xml
::
option
(
$project
,
$project
,
in_array
(
$project
,
$selected
)
)
.
"
\n
"
;
}
$properties
=
[
'multiple'
=>
'multiple'
,
'id'
=>
'projects'
,
'name'
=>
'projects[]'
,
'class'
=>
'cn-multiselect'
,
'autocomplete'
=>
'off'
];
if
(
!
$this
->
editable
)
{
$properties
[
'disabled'
]
=
'disabled'
;
}
return
Html
::
rawElement
(
'select'
,
$properties
,
$options
);
}
/**
* @param string $text
* @param string[] $values
* @return string
*/
public
static
function
dropdownList
(
$text
,
$values
)
{
$dropdown
=
"*{$text}
\n
"
;
foreach
(
$values
as
$value
)
{
$dropdown
.=
"**{$value}
\n
"
;
}
return
$dropdown
;
}
/**
* Create a string with summary label and text field.
*
* @param bool $action If true, use a placeholder message appropriate for
* a single action (such as creating a campaign).
* @return string
*/
public
function
makeSummaryField
(
$action
=
false
)
{
$placeholderMsg
=
$action
?
'centralnotice-change-summary-action-prompt'
:
'centralnotice-change-summary-prompt'
;
return
Html
::
element
(
'label'
,
[
'class'
=>
'cn-change-summary-label'
],
$this
->
msg
(
'centralnotice-change-summary-label'
)->
text
()
)
.
Html
::
element
(
'input'
,
[
'class'
=>
'cn-change-summary-input'
,
'placeholder'
=>
$this
->
msg
(
$placeholderMsg
)->
text
(),
'size'
=>
45
,
'name'
=>
'changeSummary'
]
);
}
private
function
getSummaryFromRequest
(
WebRequest
$request
):
string
{
return
static
::
truncateSummaryField
(
$request
->
getVal
(
'changeSummary'
)
);
}
/**
* @param int $begin
* @param int $end
* @return string[]
*/
private
function
paddedRange
(
$begin
,
$end
)
{
$unpaddedRange
=
range
(
$begin
,
$end
);
$paddedRange
=
[];
foreach
(
$unpaddedRange
as
$number
)
{
// pad number with 0 if needed
$paddedRange
[]
=
sprintf
(
"%02d"
,
$number
);
}
return
$paddedRange
;
}
private
function
showError
(
string
$message
)
{
$this
->
getOutput
()->
wrapWikiMsg
(
"<div class='cn-error'>
\n
$1
\n
</div>"
,
$message
);
$this
->
centralNoticeError
=
true
;
}
/**
* Generates a multiple select list of all countries.
*
* @param array $selectedCountries The country codes of the selected countries
* @param array $selectedRegions The unique region codes of the selected regions
* in format CountryCode_RegionCode
*
* @return string multiple select list
*/
private
function
geoMultiSelectorTree
(
$selectedCountries
=
[],
$selectedRegions
=
[]
)
{
$userLanguageCode
=
$this
->
getLanguage
()->
getCode
();
$countries
=
GeoTarget
::
getCountriesList
(
$userLanguageCode
);
$locationElements
=
"
\n
"
;
foreach
(
$countries
as
$countryCode
=>
$country
)
{
$regions
=
''
;
if
(
$country
->
getRegions
()
)
{
foreach
(
$country
->
getRegions
()
as
$regionCode
=>
$name
)
{
$uniqueRegionCode
=
GeoTarget
::
makeUniqueRegionCode
(
$countryCode
,
$regionCode
);
$isSelected
=
in_array
(
$uniqueRegionCode
,
$selectedRegions
);
$data
=
[
'type'
=>
'region'
,
'code'
=>
$regionCode
,
'opened'
=>
$isSelected
,
'selected'
=>
$isSelected
];
$regions
.=
Html
::
element
(
'li'
,
[
'id'
=>
$uniqueRegionCode
,
'data-jstree'
=>
json_encode
(
$data
)
],
$this
->
msg
(
'centralnotice-location-name-and-code'
,
$name
,
$regionCode
)->
text
()
);
}
}
$isSelected
=
in_array
(
$countryCode
,
$selectedCountries
);
$data
=
[
'type'
=>
'country'
,
'code'
=>
$countryCode
,
'opened'
=>
$isSelected
,
'selected'
=>
$isSelected
];
$countryNameAndCode
=
$this
->
msg
(
'centralnotice-location-name-and-code'
,
$country
->
getName
(),
$countryCode
)->
escaped
();
$locationElements
.=
Html
::
rawElement
(
'li'
,
[
'data-jstree'
=>
json_encode
(
$data
),
'id'
=>
$countryCode
],
$countryNameAndCode
.
(
$regions
?
Html
::
rawElement
(
'ul'
,
[],
$regions
)
:
''
)
);
}
$properties
=
[
'id'
=>
'geo_locations'
,
'class'
=>
'cn-tree'
];
if
(
!
$this
->
editable
)
{
$properties
[
'disabled'
]
=
'disabled'
;
}
$search
=
Html
::
rawElement
(
'input'
,
[
'type'
=>
'text'
,
'class'
=>
'cn-tree-search'
],
''
);
$searchClear
=
Html
::
element
(
'button'
,
[
'class'
=>
'cn-tree-clear'
],
$this
->
msg
(
'centralnotice-location-filter-clear'
)->
text
()
);
$searchLabelText
=
$this
->
msg
(
'centralnotice-location-filter'
)->
escaped
();
$searchLabel
=
Html
::
rawElement
(
'label'
,
[
'class'
=>
'cn-tree-search-label'
],
$searchLabelText
.
$search
.
$searchClear
);
$statusText
=
Html
::
rawElement
(
'div'
,
[
'class'
=>
'cn-tree-status'
],
''
);
$tree
=
Html
::
rawElement
(
'div'
,
$properties
,
Html
::
rawElement
(
'ul'
,
[],
$locationElements
)
);
$hiddenInputs
=
Html
::
input
(
'geo_countries'
,
implode
(
','
,
$selectedCountries
),
'text'
,
[
'type'
=>
'hidden'
,
'id'
=>
'geo_countries_value'
]
);
$hiddenInputs
.=
Html
::
input
(
'geo_regions'
,
implode
(
','
,
$selectedRegions
),
'text'
,
[
'type'
=>
'hidden'
,
'id'
=>
'geo_regions_value'
]
);
return
Html
::
rawElement
(
'div'
,
[
'class'
=>
'cn-tree-wrapper'
],
$searchLabel
.
$tree
.
$statusText
.
$hiddenInputs
);
}
/**
* Sanitizes template search terms by removing non alpha and ensuring space delimiting.
*
* @param string $terms Search terms to sanitize
*
* @return string Space delimited string
*/
public
function
sanitizeSearchTerms
(
$terms
)
{
preg_match_all
(
'/([
\w
-]+)
\S
*/s'
,
$terms
,
$matches
);
return
implode
(
' '
,
$matches
[
1
]
);
}
/**
* Truncate the summary field in a linguistically appropriate way.
* @param string|null $summary
* @return string
*/
public
static
function
truncateSummaryField
(
$summary
)
{
return
MediaWikiServices
::
getInstance
()->
getContentLanguage
()
->
truncateForDatabase
(
$summary
??
''
,
255
);
}
/**
* Provides names of sub-pages of the CentralNotice admin interface.
*
* @inheritDoc
*/
public
function
getAssociatedNavigationLinks
():
array
{
// Keys of (default value of $wgNoticeTabifyPages) are the names of the special
// pages of the admin interface without the initial 'Special:'.
// TODO Make $wgNoticeTabifyPages a constant rather than a config variable.
return
array_map
(
static
fn
(
$name
)
=>
"Special:$name"
,
array_keys
(
$this
->
getConfig
()->
get
(
'NoticeTabifyPages'
)
)
);
}
/**
* Return the localized text used for subpages of the CN admin interface (on skins
* that support navigation links).
*
* @inheritDoc
*/
public
function
getShortDescription
(
string
$path
=
''
):
string
{
$noticeTabifyPages
=
$this
->
getConfig
()->
get
(
'NoticeTabifyPages'
);
return
$this
->
msg
(
$noticeTabifyPages
[
$path
][
'message'
]
)->
parse
();
}
/**
* Loads a CentralNotice variable from session data.
*
* @param string $variable Name of the variable
* @param mixed|null $default Default value of the variable
*
* @return mixed Stored variable or default
*/
public
function
getCNSessionVar
(
$variable
,
$default
=
null
)
{
return
$this
->
getRequest
()->
getSessionData
(
"centralnotice-$variable"
)
??
$default
;
}
/**
* Sets a CentralNotice session variable. Note that this will fail silently if a
* session does not exist for the user.
*
* @param string $variable Name of the variable
* @param mixed $value Value for the variable
*/
public
function
setCNSessionVar
(
$variable
,
$value
)
{
$this
->
getRequest
()->
setSessionData
(
"centralnotice-{$variable}"
,
$value
);
}
/**
* @param string[] $projects
* @return string
*/
public
function
listProjects
(
$projects
)
{
return
$this
->
makeShortList
(
$this
->
getConfig
()->
get
(
'NoticeProjects'
),
$projects
);
}
/**
* @param string[] $countries
* @param string[] $regions
* @return string
*/
public
function
listCountriesRegions
(
array
$countries
,
array
$regions
)
{
$allCountries
=
array_keys
(
GeoTarget
::
getCountriesList
()
);
$list
=
$this
->
makeShortList
(
$allCountries
,
$countries
);
$regionsByCountry
=
[];
foreach
(
$regions
as
$region
)
{
$countryCode
=
substr
(
$region
,
0
,
2
);
$regionCode
=
substr
(
$region
,
3
);
$regionsByCountry
[
$countryCode
][]
=
$regionCode
;
}
if
(
$list
!==
''
&&
count
(
$regionsByCountry
)
>
0
)
{
$list
.=
'; '
;
}
$regionsByCountryList
=
[];
foreach
(
$regionsByCountry
as
$countryCode
=>
$regions
)
{
$all
=
array_keys
(
GeoTarget
::
getRegionsList
(
$countryCode
)
);
$regionList
=
$this
->
makeShortList
(
$all
,
$regions
);
$regionsByCountryList
[]
=
"$countryCode: ($regionList)"
;
}
$list
.=
$this
->
getContext
()->
getLanguage
()->
listToText
(
$regionsByCountryList
);
return
$list
;
}
/**
* @param string[] $languages
* @return string
*/
public
function
listLanguages
(
$languages
)
{
$all
=
array_keys
(
MediaWikiServices
::
getInstance
()->
getLanguageNameUtils
()
->
getLanguageNames
(
'en'
)
);
return
$this
->
makeShortList
(
$all
,
$languages
);
}
/**
* @param string[] $all
* @param string[] $list
* @return string
*/
private
function
makeShortList
(
$all
,
$list
)
{
// TODO ellipsis and js/css expansion
if
(
count
(
$list
)
==
count
(
$all
)
)
{
return
$this
->
getContext
()->
msg
(
'centralnotice-all'
)->
text
();
}
if
(
count
(
$list
)
>
self
::
LIST_COMPLEMENT_THRESHOLD
*
count
(
$all
)
)
{
$inverse
=
array_values
(
array_diff
(
$all
,
$list
)
);
$txt
=
$this
->
getContext
()->
getLanguage
()->
listToText
(
$inverse
);
return
$this
->
getContext
()->
msg
(
'centralnotice-all-except'
,
$txt
)->
text
();
}
return
$this
->
getContext
()->
getLanguage
()->
listToText
(
array_values
(
$list
)
);
}
/**
* Convert comma separated list to array
* @param string $list
* @return array
*/
private
function
listToArray
(
$list
)
{
if
(
$list
)
{
$array
=
explode
(
','
,
$list
);
}
else
{
$array
=
[];
}
return
$array
;
}
/** @inheritDoc */
protected
function
getGroupName
()
{
return
'wiki'
;
}
/** @inheritDoc */
public
function
outputHeader
(
$summaryMsg
=
''
)
{
// Allow users to add a custom nav bar (T138284)
$navBar
=
$this
->
msg
(
'centralnotice-navbar'
)->
inContentLanguage
();
if
(
!
$navBar
->
isDisabled
()
)
{
$this
->
getOutput
()->
addHTML
(
$navBar
->
parseAsBlock
()
);
}
return
parent
::
outputHeader
(
$summaryMsg
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Aug 18 2026, 02:16 (9 w, 3 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
c3/6a/639b046fe254a4b8395b6c0d8c2a
Default Alt Text
CentralNotice.php (60 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment