/** * Get the footer with user information (when joined, how * many edits/uploads, visit user page and talk page) * @return string */ protected function getUserFooterData() { $fromDate = $this->targetUser->getRegistration(); $ts = new MWTimestamp(wfTimestamp(TS_UNIX, $fromDate)); $diff = $ts->diff(new MWTimestamp()); if ($fromDate === null) { // User was registered in pre-historic times when registration wasn't recorded $msg = 'mobile-frontend-profile-footer-ancient'; $units = 0; $fromDate = '20010115000000'; // No users before that date } elseif ($diff->y) { $msg = 'mobile-frontend-profile-footer-years'; $units = $diff->y; } elseif ($diff->m) { $msg = 'mobile-frontend-profile-footer-months'; $units = $diff->m; } else { $msg = 'mobile-frontend-profile-footer-days'; $units = $diff->d; } $editCount = $this->targetUser->getEditCount(); $uploadCount = $this->userInfo->countRecentUploads($fromDate); // Ensure that the upload count is compatible with the i18n message if ($uploadCount > 500) { $uploadCount = 500; } $username = $this->targetUser->getName(); return array('editsSummary' => $this->msg($msg, $username)->numParams($units, $editCount, $uploadCount)->parse(), 'linkUserPage' => Linker::link($this->targetUser->getUserPage(), $this->msg('mobile-frontend-profile-userpage-link', $username)->escaped()), 'linkTalk' => $this->getTalkLink()); }
/** * @covers LogFormatter::newFromEntry */ public function testNormalLogParams() { $entry = $this->newLogEntry('test', array()); $formatter = LogFormatter::newFromEntry($entry); $formatter->setContext($this->context); $formatter->setShowUserToolLinks(false); $paramsWithoutTools = $formatter->getMessageParametersForTesting(); unset($formatter->parsedParameters); $formatter->setShowUserToolLinks(true); $paramsWithTools = $formatter->getMessageParametersForTesting(); $userLink = Linker::userLink($this->user->getId(), $this->user->getName()); $userTools = Linker::userToolLinksRedContribs($this->user->getId(), $this->user->getName(), $this->user->getEditCount()); $titleLink = Linker::link($this->title, null, array(), array()); // $paramsWithoutTools and $paramsWithTools should be only different // in index 0 $this->assertEquals($paramsWithoutTools[1], $paramsWithTools[1]); $this->assertEquals($paramsWithoutTools[2], $paramsWithTools[2]); $this->assertEquals($userLink, $paramsWithoutTools[0]['raw']); $this->assertEquals($userLink . $userTools, $paramsWithTools[0]['raw']); $this->assertEquals($this->user->getName(), $paramsWithoutTools[1]); $this->assertEquals($titleLink, $paramsWithoutTools[2]['raw']); }
/** * Function for logging the event for Schema:Echo * @param $user User being notified. * @param $event EchoEvent to log detail about. * @param $deliveryMethod string containing either 'web' or 'email' */ public static function logSchemaEcho(User $user, EchoEvent $event, $deliveryMethod) { global $wgEchoConfig, $wgEchoNotifications; if (!$wgEchoConfig['eventlogging']['Echo']['enabled']) { // Only attempt event logging if Echo schema is enabled return; } // Notifications under system category should have -1 as sender id if ($event->getCategory() === 'system') { $sender = -1; } else { $agent = $event->getAgent(); if ($agent) { $sender = $agent->isAnon() ? $agent->getName() : $agent->getId(); } else { $sender = -1; } } if (isset($wgEchoNotifications[$event->getType()]['group'])) { $group = $wgEchoNotifications[$event->getType()]['group']; } else { $group = 'neutral'; } $data = array('version' => $wgEchoConfig['version'], 'eventId' => $event->getId(), 'notificationType' => $event->getType(), 'notificationGroup' => $group, 'sender' => (string) $sender, 'recipientUserId' => $user->getId(), 'recipientEditCount' => (int) $user->getEditCount()); // Add the source if it exists. (This is mostly for the Thanks extension.) $extra = $event->getExtra(); if (isset($extra['source'])) { $data['eventSource'] = (string) $extra['source']; } if ($deliveryMethod == 'email') { $data['deliveryMethod'] = 'email'; } else { // whitelist valid delivery methods so it is always valid $data['deliveryMethod'] = 'web'; } // Add revision ID if it exists $rev = $event->getRevision(); if ($rev) { $data['revisionId'] = $rev->getId(); } self::actuallyLogTheEvent('Echo', $data); }
protected function makeUserLink(User $user, $toolFlags = 0) { if ($this->plaintext) { $element = $user->getName(); } else { $element = Linker::userLink($user->getId(), $user->getName()); if ($this->linkFlood) { $element .= Linker::userToolLinks($user->getId(), $user->getName(), true, $toolFlags, $user->getEditCount()); } } return $element; }
/** * As recCheckCondition, but *not* recursive. The only valid conditions * are those whose first element is APCOND_EMAILCONFIRMED/APCOND_EDITCOUNT/ * APCOND_AGE. Other types will throw an exception if no extension evalu- * ates them. * * @param $cond Array: A condition, which must not contain other conditions * @param $user User The user to check the condition against * @return bool Whether the condition is true for the user */ private static function checkCondition($cond, User $user) { global $wgEmailAuthentication, $wgEnableEditCountLocal; if (count($cond) < 1) { return false; } switch ($cond[0]) { case APCOND_EMAILCONFIRMED: if (Sanitizer::validateEmail($user->getEmail())) { if ($wgEmailAuthentication) { return (bool) $user->getEmailAuthenticationTimestamp(); } else { return true; } } return false; case APCOND_EDITCOUNT: if (!empty($wgEnableEditCountLocal)) { return $user->getEditCountLocal() >= $cond[1]; } else { return $user->getEditCount() >= $cond[1]; } case APCOND_AGE: $age = time() - wfTimestampOrNull(TS_UNIX, $user->getRegistration()); return $age >= $cond[1]; case APCOND_AGE_FROM_EDIT: $age = time() - wfTimestampOrNull(TS_UNIX, $user->getFirstEditTimestamp()); return $age >= $cond[1]; case APCOND_INGROUPS: $groups = array_slice($cond, 1); return count(array_intersect($groups, $user->getGroups())) == count($groups); case APCOND_ISIP: return $cond[1] == $user->getRequest()->getIP(); case APCOND_IPINRANGE: return IP::isInRange($user->getRequest()->getIP(), $cond[1]); case APCOND_BLOCKED: return $user->isBlocked(); case APCOND_ISBOT: return in_array('bot', User::getGroupPermissions($user->getGroups())); default: $result = null; wfRunHooks('AutopromoteCondition', array($cond[0], array_slice($cond, 1), $user, &$result)); if ($result === null) { throw new MWException("Unrecognized condition {$cond[0]} for autopromotion!"); } return (bool) $result; } }
/** * @param User $user * @param IContextSource $context * @param array $defaultPreferences * @return void */ static function profilePreferences($user, IContextSource $context, &$defaultPreferences) { global $wgAuth, $wgContLang, $wgParser; $config = $context->getConfig(); // retrieving user name for GENDER and misc. $userName = $user->getName(); # # User info ##################################### // Information panel $defaultPreferences['username'] = array('type' => 'info', 'label-message' => array('username', $userName), 'default' => $userName, 'section' => 'personal/info'); # Get groups to which the user belongs $userEffectiveGroups = $user->getEffectiveGroups(); $userGroups = $userMembers = array(); foreach ($userEffectiveGroups as $ueg) { if ($ueg == '*') { // Skip the default * group, seems useless here continue; } $groupName = User::getGroupName($ueg); $userGroups[] = User::makeGroupLinkHTML($ueg, $groupName); $memberName = User::getGroupMember($ueg, $userName); $userMembers[] = User::makeGroupLinkHTML($ueg, $memberName); } asort($userGroups); asort($userMembers); $lang = $context->getLanguage(); $defaultPreferences['usergroups'] = array('type' => 'info', 'label' => $context->msg('prefs-memberingroups')->numParams(count($userGroups))->params($userName)->parse(), 'default' => $context->msg('prefs-memberingroups-type')->rawParams($lang->commaList($userGroups), $lang->commaList($userMembers))->escaped(), 'raw' => true, 'section' => 'personal/info'); $editCount = Linker::link(SpecialPage::getTitleFor("Contributions", $userName), $lang->formatNum($user->getEditCount())); $defaultPreferences['editcount'] = array('type' => 'info', 'raw' => true, 'label-message' => 'prefs-edits', 'default' => $editCount, 'section' => 'personal/info'); if ($user->getRegistration()) { $displayUser = $context->getUser(); $userRegistration = $user->getRegistration(); $defaultPreferences['registrationdate'] = array('type' => 'info', 'label-message' => 'prefs-registration', 'default' => $context->msg('prefs-registration-date-time', $lang->userTimeAndDate($userRegistration, $displayUser), $lang->userDate($userRegistration, $displayUser), $lang->userTime($userRegistration, $displayUser))->parse(), 'section' => 'personal/info'); } $canViewPrivateInfo = $user->isAllowed('viewmyprivateinfo'); $canEditPrivateInfo = $user->isAllowed('editmyprivateinfo'); // Actually changeable stuff $defaultPreferences['realname'] = array('type' => $canEditPrivateInfo && $wgAuth->allowPropChange('realname') ? 'text' : 'info', 'default' => $user->getRealName(), 'section' => 'personal/info', 'label-message' => 'yourrealname', 'help-message' => 'prefs-help-realname'); if ($canEditPrivateInfo && $wgAuth->allowPasswordChange()) { $link = Linker::link(SpecialPage::getTitleFor('ChangePassword'), $context->msg('prefs-resetpass')->escaped(), array(), array('returnto' => SpecialPage::getTitleFor('Preferences')->getPrefixedText())); $defaultPreferences['password'] = array('type' => 'info', 'raw' => true, 'default' => $link, 'label-message' => 'yourpassword', 'section' => 'personal/info'); } // Only show prefershttps if secure login is turned on if ($config->get('SecureLogin') && wfCanIPUseHTTPS($context->getRequest()->getIP())) { $defaultPreferences['prefershttps'] = array('type' => 'toggle', 'label-message' => 'tog-prefershttps', 'help-message' => 'prefs-help-prefershttps', 'section' => 'personal/info'); } // Language $languages = Language::fetchLanguageNames(null, 'mw'); $languageCode = $config->get('LanguageCode'); if (!array_key_exists($languageCode, $languages)) { $languages[$languageCode] = $languageCode; } ksort($languages); $options = array(); foreach ($languages as $code => $name) { $display = wfBCP47($code) . ' - ' . $name; $options[$display] = $code; } $defaultPreferences['language'] = array('type' => 'select', 'section' => 'personal/i18n', 'options' => $options, 'label-message' => 'yourlanguage'); $defaultPreferences['gender'] = array('type' => 'radio', 'section' => 'personal/i18n', 'options' => array($context->msg('parentheses')->params($context->msg('gender-unknown')->plain())->escaped() => 'unknown', $context->msg('gender-female')->escaped() => 'female', $context->msg('gender-male')->escaped() => 'male'), 'label-message' => 'yourgender', 'help-message' => 'prefs-help-gender'); // see if there are multiple language variants to choose from if (!$config->get('DisableLangConversion')) { foreach (LanguageConverter::$languagesWithVariants as $langCode) { if ($langCode == $wgContLang->getCode()) { $variants = $wgContLang->getVariants(); if (count($variants) <= 1) { continue; } $variantArray = array(); foreach ($variants as $v) { $v = str_replace('_', '-', strtolower($v)); $variantArray[$v] = $lang->getVariantname($v, false); } $options = array(); foreach ($variantArray as $code => $name) { $display = wfBCP47($code) . ' - ' . $name; $options[$display] = $code; } $defaultPreferences['variant'] = array('label-message' => 'yourvariant', 'type' => 'select', 'options' => $options, 'section' => 'personal/i18n', 'help-message' => 'prefs-help-variant'); } else { $defaultPreferences["variant-{$langCode}"] = array('type' => 'api'); } } } // Stuff from Language::getExtraUserToggles() // FIXME is this dead code? $extraUserToggles doesn't seem to be defined for any language $toggles = $wgContLang->getExtraUserToggles(); foreach ($toggles as $toggle) { $defaultPreferences[$toggle] = array('type' => 'toggle', 'section' => 'personal/i18n', 'label-message' => "tog-{$toggle}"); } // show a preview of the old signature first $oldsigWikiText = $wgParser->preSaveTransform('~~~', $context->getTitle(), $user, ParserOptions::newFromContext($context)); $oldsigHTML = $context->getOutput()->parseInline($oldsigWikiText, true, true); $defaultPreferences['oldsig'] = array('type' => 'info', 'raw' => true, 'label-message' => 'tog-oldsig', 'default' => $oldsigHTML, 'section' => 'personal/signature'); $defaultPreferences['nickname'] = array('type' => $wgAuth->allowPropChange('nickname') ? 'text' : 'info', 'maxlength' => $config->get('MaxSigChars'), 'label-message' => 'yournick', 'validation-callback' => array('Preferences', 'validateSignature'), 'section' => 'personal/signature', 'filter-callback' => array('Preferences', 'cleanSignature')); $defaultPreferences['fancysig'] = array('type' => 'toggle', 'label-message' => 'tog-fancysig', 'help-message' => 'prefs-help-signature', 'section' => 'personal/signature'); # # Email stuff if ($config->get('EnableEmail')) { if ($canViewPrivateInfo) { $helpMessages[] = $config->get('EmailConfirmToEdit') ? 'prefs-help-email-required' : 'prefs-help-email'; if ($config->get('EnableUserEmail')) { // additional messages when users can send email to each other $helpMessages[] = 'prefs-help-email-others'; } $emailAddress = $user->getEmail() ? htmlspecialchars($user->getEmail()) : ''; if ($canEditPrivateInfo && $wgAuth->allowPropChange('emailaddress')) { $link = Linker::link(SpecialPage::getTitleFor('ChangeEmail'), $context->msg($user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail')->escaped(), array(), array('returnto' => SpecialPage::getTitleFor('Preferences')->getPrefixedText())); $emailAddress .= $emailAddress == '' ? $link : $context->msg('word-separator')->escaped() . $context->msg('parentheses')->rawParams($link)->escaped(); } $defaultPreferences['emailaddress'] = array('type' => 'info', 'raw' => true, 'default' => $emailAddress, 'label-message' => 'youremail', 'section' => 'personal/email', 'help-messages' => $helpMessages); } $disableEmailPrefs = false; if ($config->get('EmailAuthentication')) { $emailauthenticationclass = 'mw-email-not-authenticated'; if ($user->getEmail()) { if ($user->getEmailAuthenticationTimestamp()) { // date and time are separate parameters to facilitate localisation. // $time is kept for backward compat reasons. // 'emailauthenticated' is also used in SpecialConfirmemail.php $displayUser = $context->getUser(); $emailTimestamp = $user->getEmailAuthenticationTimestamp(); $time = $lang->userTimeAndDate($emailTimestamp, $displayUser); $d = $lang->userDate($emailTimestamp, $displayUser); $t = $lang->userTime($emailTimestamp, $displayUser); $emailauthenticated = $context->msg('emailauthenticated', $time, $d, $t)->parse() . '<br />'; $disableEmailPrefs = false; $emailauthenticationclass = 'mw-email-authenticated'; } else { $disableEmailPrefs = true; $emailauthenticated = $context->msg('emailnotauthenticated')->parse() . '<br />' . Linker::linkKnown(SpecialPage::getTitleFor('Confirmemail'), $context->msg('emailconfirmlink')->escaped()) . '<br />'; $emailauthenticationclass = "mw-email-not-authenticated"; } } else { $disableEmailPrefs = true; $emailauthenticated = $context->msg('noemailprefs')->escaped(); $emailauthenticationclass = 'mw-email-none'; } if ($canViewPrivateInfo) { $defaultPreferences['emailauthentication'] = array('type' => 'info', 'raw' => true, 'section' => 'personal/email', 'label-message' => 'prefs-emailconfirm-label', 'default' => $emailauthenticated, 'cssclass' => $emailauthenticationclass); } } if ($config->get('EnableUserEmail') && $user->isAllowed('sendemail')) { $defaultPreferences['disablemail'] = array('type' => 'toggle', 'invert' => true, 'section' => 'personal/email', 'label-message' => 'allowemail', 'disabled' => $disableEmailPrefs); $defaultPreferences['ccmeonemails'] = array('type' => 'toggle', 'section' => 'personal/email', 'label-message' => 'tog-ccmeonemails', 'disabled' => $disableEmailPrefs); } if ($config->get('EnotifWatchlist')) { $defaultPreferences['enotifwatchlistpages'] = array('type' => 'toggle', 'section' => 'personal/email', 'label-message' => 'tog-enotifwatchlistpages', 'disabled' => $disableEmailPrefs); } if ($config->get('EnotifUserTalk')) { $defaultPreferences['enotifusertalkpages'] = array('type' => 'toggle', 'section' => 'personal/email', 'label-message' => 'tog-enotifusertalkpages', 'disabled' => $disableEmailPrefs); } if ($config->get('EnotifUserTalk') || $config->get('EnotifWatchlist')) { $defaultPreferences['enotifminoredits'] = array('type' => 'toggle', 'section' => 'personal/email', 'label-message' => 'tog-enotifminoredits', 'disabled' => $disableEmailPrefs); if ($config->get('EnotifRevealEditorAddress')) { $defaultPreferences['enotifrevealaddr'] = array('type' => 'toggle', 'section' => 'personal/email', 'label-message' => 'tog-enotifrevealaddr', 'disabled' => $disableEmailPrefs); } } } }
/** * Prepares array of user fields needed to be passed by API * @param User $oUser * @return array */ public function prepareUserParams(\User $oUser) { $aUserParams = ['user_id' => $oUser->getId(), 'user_name' => $oUser->getName(), 'user_real_name' => $oUser->getRealName(), 'user_email' => $oUser->getEmail(), 'user_email_authenticated' => $oUser->getEmailAuthenticationTimestamp(), 'user_registration' => $oUser->getRegistration(), 'user_editcount' => (int) $oUser->getEditCount(), 'user_touched' => $oUser->getTouched()]; return $aUserParams; }
/** * Returns the survey user types the provided user has. * * @since 0.1 * * @param User $user * * @return array of Survey::$USER_ */ public static function getTypesForUser(User $user) { $userTypes = array(Survey::$USER_ALL); $userTypes[] = $user->isLoggedIn() ? Survey::$USER_LOGGEDIN : Survey::$USER_ANON; if ($user->isEmailConfirmed()) { $userTypes[] = Survey::$USER_CONFIRMED; } if ($user->getEditCount() > 0) { $userTypes[] = Survey::$USER_EDITOR; } return $userTypes; }
/** * As recCheckCondition, but *not* recursive. The only valid conditions * are those whose first element is APCOND_EMAILCONFIRMED/APCOND_EDITCOUNT/ * APCOND_AGE. Other types will throw an exception if no extension evalu- * ates them. * * @param array $cond A condition, which must not contain other conditions * @param User $user The user to check the condition against * @return bool Whether the condition is true for the user */ private static function checkCondition($cond, User $user) { if (count($cond) < 1) { return false; } switch ($cond[0]) { case APCOND_EMAILCONFIRMED: if (User::isValidEmailAddr($user->getEmail())) { global $wgEmailAuthentication; if ($wgEmailAuthentication) { return $user->getEmailAuthenticationTimestamp() ? true : false; } else { return true; } } return false; case APCOND_EDITCOUNT: return $user->getEditCount() >= $cond[1]; case APCOND_AGE: $age = time() - wfTimestampOrNull(TS_UNIX, $user->getRegistration()); return $age >= $cond[1]; case APCOND_INGROUPS: $groups = array_slice($cond, 1); return count(array_intersect($groups, $user->getGroups())) == count($groups); default: $result = null; wfRunHooks('AutopromoteCondition', array($cond[0], array_slice($cond, 1), $user, &$result)); if ($result === null) { throw new MWException("Unrecognized condition {$cond[0]} for autopromotion!"); } return $result ? true : false; } }
/** * Perform the migration of a given uncyclopedia account * * @param User $user */ private function migrateUser(User $user) { $this->output(sprintf("%d: %s <%s>", $user->getId(), $user->getName(), $user->mEmail ?: 'MISSING_EMAIL')); $then = microtime(true); // keep data that will be saved to CSV file $action = false; $userName = $user->getName(); $isMerged = false; // check if the uncyclo account has a valid email set $isValidEmail = Sanitizer::validateEmail($user->mEmail); $uncycloEdits = (int) $user->getEditCount(); $uncycloEditsSince = $this->getEditsCountAfter($user, self::UNCYCLO_EDITS_AFTER); if (!$isValidEmail) { $this->accountsWithNoEmail++; if ($uncycloEditsSince > 0) { $this->accountsWithNoEmailWithEdits++; $this->output(sprintf("\n\tEmail missing for %s [active after Jan 2014]\n", $userName)); } } // check the shared users database $globalUser = $this->getGlobalUserByName($user->getName()); $globalEdits = null; if ($globalUser instanceof User) { // HACK: calling getEmail() on global user will trigger a SQL query on uncyclo database $this->output(sprintf(' - conflicts with the global user #%d <%s>', $globalUser->getId(), $globalUser->mEmail ?: 'MISSING_EMAIL')); $globalEdits = (int) $globalUser->getEditCount(); // global and shared DB accounts match if ($isValidEmail && strtolower($globalUser->mEmail) === strtolower($user->mEmail)) { $this->output(' - emails match'); $this->output("\n\tmerging accounts..."); $this->mergedAccounts++; $isMerged = true; $action = 'merge accounts'; $this->doChangeUncycloUserId($user, $globalUser->getId()); } else { // resolve conflicts $this->output("\n\tresolving account conflicts..."); $this->output(sprintf(" uncyclo edits: %d / global edits: %d\n\t", $uncycloEdits, $globalEdits)); /** Sannse says: No edits on either wikia = rename Uncyclopedia account No edits on Wikia/edits on Uncyclopedia = rename Wikia account < 1000 edits on Uncyclopedia = rename Uncyclopedia account > 1000 edits on Uncyclopedia = rename account with least edits **/ if ($uncycloEdits === 0 && $globalEdits === 0) { $user = $this->doRenameUncycloUser($user); $action = 'rename uncyclo account [no edits on either wikia]'; } elseif ($uncycloEdits > 0 && $globalEdits === 0) { $this->doRenameGlobalUser($globalUser); $action = 'rename wikia account [uncyclo edits, no global edits]'; } elseif ($uncycloEdits < 1000) { $user = $this->doRenameUncycloUser($user); $action = 'rename uncyclo account [uncyclo edits < 1k]'; } else { // rename the one with the least edits if ($uncycloEdits > $globalEdits) { $this->doRenameGlobalUser($globalUser); $action = 'rename wikia account [more uncyclo edits than global ones]'; } else { $user = $this->doRenameUncycloUser($user); $action = 'rename uncyclo account [less uncyclo edits than global ones]'; } } // now create a shared account using the "local" uncyclo user object $this->doCreateGlobalUser($user); } } else { // there's no accounts conflict - create a shared account and update the uncyclopedia user_id entries $this->createdAccounts++; $action = 'create a global account'; // now create a shared account using the "local" uncyclo user object $this->doCreateGlobalUser($user); } // add an entry to CSV file if (is_resource($this->csv)) { fputcsv($this->csv, [$user->getId(), $userName, $user->getName(), $user->mEmail, $isValidEmail ? 'Y' : 'N', $globalUser ? $globalUser->mEmail : 'none', $isMerged ? 'Y' : 'N', $uncycloEdits, $uncycloEditsSince, is_int($globalEdits) ? $globalEdits : 'none', $action]); } $this->info(__METHOD__, ['took' => microtime(true) - $then, 'user_id' => $user->getId(), 'user_name' => $userName, 'action' => $action]); $this->output("\n"); }
/** * Build a value to store in memcached based on the PST content and parser output * * This makes a simple version of WikiPage::prepareContentForEdit() as stash info * * @param Content $pstContent Pre-Save transformed content * @param ParserOutput $parserOutput * @param string $timestamp TS_MW * @param User $user * @return array (stash info array, TTL in seconds, info code) or (null, 0, info code) */ private static function buildStashValue(Content $pstContent, ParserOutput $parserOutput, $timestamp, User $user) { // If an item is renewed, mind the cache TTL determined by config and parser functions. // Put an upper limit on the TTL for sanity to avoid extreme template/file staleness. $since = time() - wfTimestamp(TS_UNIX, $parserOutput->getTimestamp()); $ttl = min($parserOutput->getCacheExpiry() - $since, self::MAX_CACHE_TTL); if ($ttl <= 0) { return [null, 0, 'no_ttl']; } // Only store what is actually needed $stashInfo = (object) ['pstContent' => $pstContent, 'output' => $parserOutput, 'timestamp' => $timestamp, 'edits' => $user->getEditCount()]; return [$stashInfo, $ttl, 'ok']; }