Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4099688
bucketer.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
9 KB
Referenced Files
None
Subscribers
None
bucketer.js
View Options
/**
* Storage, retrieval and other processing of buckets. Provides
* cn.internal.bucketer.
*
* Bucket assignments are stored using the kvStore, in LocalStorage or a cookie.
* To maximize concision for users that fall back to cookies, the value consists
* of '*'-separated campaigns, each of which is made up of '!'-separated fields.
* The format is:
*
* NAME!START!END!VALUE[*NAME!START!END!VALUE..]
*
* - Start is stored as a second offset from UNIX timestamp 1400000000
* (March 2014).
* - End is stored as second offset from start.
*
* For example:
*
* 'WikiConference_USA!39942400!3729600!2'
*
* ...would be deserialized to:
*
* { WikiConference_USA: { start: 1439942400, end: 1443672000, val: 2 } }
*
*/
(
function
()
{
var
bucketer
,
// Bucket objects by campaign; properties are campaign names.
// Retrieved from kvStore (which uses LocalStorage or a fallback cookie)
// or from a legacy cookie.
buckets
=
null
,
// The campaign we're working with.
campaign
=
null
,
kvStore
=
mw
.
centralNotice
.
kvStore
,
multiStorageOption
,
// Name of the legacy cookie for CentralNotice buckets. Its value is
// a compact serialization of buckets in the same format as is
// currently used here.
LEGACY_COOKIE
=
'CN'
,
STORAGE_KEY
=
'buckets'
;
/**
* Escape '*' and '!' in a campaign name to make it safe for serialization.
*
* @param name
*/
function
escapeCampaignName
(
name
)
{
return
name
.
replace
(
/[*!]/g
,
function
(
match
)
{
return
'&#'
+
match
.
charCodeAt
(
0
);
}
);
}
/**
* Decode any escaped '*' and '!' characters in a serialized campaign name.
*
* @param name
*/
function
decodeCampaignName
(
name
)
{
return
name
.
replace
(
/&#(33|42)/
,
function
(
match
,
$1
)
{
return
String
.
fromCharCode
(
$1
);
}
);
}
function
parseSerializedBuckets
(
serialized
)
{
var
parsedBuckets
=
{};
serialized
.
split
(
'*'
).
forEach
(
function
(
strBucket
)
{
var
parts
=
strBucket
.
split
(
'!'
),
key
=
decodeCampaignName
(
parts
[
0
]
),
start
=
parseInt
(
parts
[
1
],
10
)
+
14e8
,
end
=
start
+
parseInt
(
parts
[
2
],
10
),
val
=
parseInt
(
parts
[
3
],
10
);
if
(
key
&&
start
&&
end
&&
!
isNaN
(
val
)
)
{
parsedBuckets
[
key
]
=
{
start
:
start
,
end
:
end
,
val
:
val
};
}
}
);
return
parsedBuckets
;
}
/**
* Check legacy bucket cookie, and try to migrate. If a legacy cookie is
* found, load buckets from there.
*
* @return {boolean} true if a legacy cookie was migrated, false if not
*/
function
possiblyLoadAndMigrateLegacyBuckets
()
{
var
cookieVal
=
$
.
cookie
(
LEGACY_COOKIE
);
if
(
cookieVal
)
{
// We need to deserialize and store again to determine ttl
buckets
=
parseSerializedBuckets
(
cookieVal
);
storeBuckets
();
$
.
removeCookie
(
LEGACY_COOKIE
,
{
path
:
'/'
}
);
return
true
;
}
return
false
;
}
/**
* Attempt to get buckets from the storage. If no stored buckets are
* found, set buckets to an empty object.
*/
function
loadBuckets
()
{
var
val
=
kvStore
.
getItem
(
STORAGE_KEY
,
kvStore
.
contexts
.
GLOBAL
,
multiStorageOption
);
buckets
=
(
val
?
parseSerializedBuckets
(
val
)
:
{}
);
}
/**
* Store buckets using the kvStore. The storage item will be set to
* expire after the all the buckets it contains do.
*
* Though compact serialization is no longer needed for the majority
* of users, who'll get LocalStorage, it's useful for those who fall
* back to cookies, and it seems preferable to keep things consistent.
*/
function
storeBuckets
()
{
var
expires
=
Math
.
ceil
(
Date
.
now
()
/
1000
),
// eslint-disable-next-line no-jquery/no-map-util
serialized
=
$
.
map
(
buckets
,
function
(
opts
,
key
)
{
var
parts
=
[
escapeCampaignName
(
key
),
Math
.
floor
(
opts
.
start
-
14e8
),
Math
.
ceil
(
opts
.
end
-
opts
.
start
),
opts
.
val
];
if
(
opts
.
end
>
expires
)
{
expires
=
Math
.
ceil
(
opts
.
end
);
}
return
parts
.
join
(
'!'
);
}
).
join
(
'*'
);
kvStore
.
setItem
(
STORAGE_KEY
,
serialized
,
kvStore
.
contexts
.
GLOBAL
,
// Convert expires to ttl in days
Math
.
ceil
(
(
expires
-
(
Date
.
now
()
/
1000
)
)
/
86400
),
multiStorageOption
);
}
/**
* Get a random bucket (integer greater or equal to 0 and less than
* wgNoticeNumberOfControllerBuckets).
*
* @return {number}
*/
function
getRandomBucket
()
{
return
Math
.
floor
(
Math
.
random
()
*
mw
.
config
.
get
(
'wgNoticeNumberOfControllerBuckets'
)
);
}
/**
* Do all things bucket:
* - Get buckets from the kvStore or legacy cookie, if available.
* - If necessary, generate a random bucket for the campaign.
* - Ensure the stored end date for this campaign is up-to-date.
* - Go through all the buckets, purging expired buckets.
* - Store the updated bucket data using the kvStore.
*
* This should be called before a bucket is requested but after
* setCampaign() has been called.
*/
function
retrieveProcessAndGet
()
{
var
campaignName
=
campaign
.
name
,
campaignStartDate
,
bucket
,
bucketEndDate
,
retrievedBucketEndDate
,
val
,
extension
=
mw
.
config
.
get
(
'wgCentralNoticePerCampaignBucketExtension'
),
now
=
new
Date
(),
bucketsModified
=
false
;
campaignStartDate
=
new
Date
();
campaignStartDate
.
setTime
(
campaign
.
start
*
1000
);
// Buckets should end the time indicated by extension after
// the campaign's end
bucketEndDate
=
new
Date
();
bucketEndDate
.
setTime
(
campaign
.
end
*
1000
);
bucketEndDate
.
setUTCDate
(
bucketEndDate
.
getUTCDate
()
+
extension
);
// Check if and how we can store buckets. Allow cookie fallback in all
// cases in which localStorage isn't available.
// If we have no storage options (cookies and localStorage disabled),
// loading and storing buckets will be no-ops, and a new bucket will be
// chosen every time.
multiStorageOption
=
kvStore
.
getMultiStorageOption
(
true
);
// In all cases, check for a legacy cookie and try to migrate if one
// was found. Otherwise, load normally.
if
(
!
possiblyLoadAndMigrateLegacyBuckets
()
)
{
loadBuckets
();
}
bucket
=
buckets
[
campaignName
];
// If we have a valid bucket, just check and possibly update its
// expiry.
// Note that buckets that are expired but that were retrieved from
// storage (because they didn't have the chance to get purged) are
// not considered valid. In that case, for consistency, we choose a
// new random bucket, just as if no bucket had been found.
if
(
bucket
&&
bucketEndDate
>
now
)
{
retrievedBucketEndDate
=
new
Date
();
retrievedBucketEndDate
.
setTime
(
bucket
.
end
*
1000
);
if
(
retrievedBucketEndDate
.
getTime
()
!==
bucketEndDate
.
getTime
()
)
{
bucket
.
end
=
bucketEndDate
.
getTime
()
/
1000
;
bucketsModified
=
true
;
}
}
else
{
// We always use wgNoticeNumberOfControllerBuckets, and
// not the campaign's number of buckets, to determine
// how many possible buckets to randomly choose from. If
// the campaign actually has less buckets than that,
// the value is mapped down as necessary. This lets
// campaigns modify the number of buckets they use.
val
=
getRandomBucket
();
bucket
=
{
val
:
val
,
start
:
campaignStartDate
.
getTime
()
/
1000
,
end
:
bucketEndDate
.
getTime
()
/
1000
};
buckets
[
campaignName
]
=
bucket
;
bucketsModified
=
true
;
}
// Purge any expired buckets
for
(
campaignName
in
buckets
)
{
bucketEndDate
=
new
Date
();
bucketEndDate
.
setTime
(
buckets
[
campaignName
].
end
*
1000
);
if
(
bucketEndDate
<
now
)
{
delete
buckets
[
campaignName
];
bucketsModified
=
true
;
}
}
// Store the buckets if there were changes
if
(
bucketsModified
)
{
storeBuckets
();
}
}
/**
* Bucketer object (intended for access from within this RL module).
*/
bucketer
=
mw
.
centralNotice
.
internal
.
bucketer
=
{
/**
* @param {Object} c A campaign object. Note: we don't check that the
* object is valid.
*/
setCampaign
:
function
(
c
)
{
campaign
=
c
;
},
/**
* This should only be called once. setCampaign() must have been called
* first.
*/
process
:
function
()
{
retrieveProcessAndGet
();
},
/**
* Get the bucket for this user for the campaign sent in setCampaign().
* setCampaign() and process() must have been called first. (Normally
* they're called by mw.centralNotice.chooseAndMaybeDisplay(). Calls
* to this method from mixin hooks don't have to worry about this.)
*
* @return {number}
*/
getBucket
:
function
()
{
return
buckets
[
campaign
.
name
].
val
;
},
/**
* Return the bucket pared down to fit within the number of buckets in the
* campaign. Note: setCampaign() must have been called first.
*
* @return {number}
*/
getReducedBucket
:
function
()
{
return
bucketer
.
getBucket
()
%
campaign
.
bucket_count
;
},
/**
* Store this bucket value for this user for the campaign sent in
* setCampaign().
* setCampaign() and process() must have been called first. (Normally
* they're called by mw.centralNotice.chooseAndMaybeDisplay(). Calls
* to this method from mixin hooks don't have to worry about this.)
*
* @param {number} val The numeric bucket value to store
*/
setBucket
:
function
(
val
)
{
buckets
[
campaign
.
name
].
val
=
val
;
storeBuckets
();
}
};
}()
);
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Tue, Aug 18, 16:02 (3 w, 5 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
9d/0a/c1a72a9248018a6a9f2c79478482
Default Alt Text
bucketer.js (9 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment