Page MenuHomePhabricator

Clean up usage of $_SESSION in WMF-deployed extensions
Open, NormalPublic

Description

The session channel has a bunch of MediaWiki\Session\PHPSessionHandler::write: Key "XXX" added in both Session and $_SESSION! messages (at a glance, CentralAuth is the main culprit). We should probably fix these to have cleaner logs.


At first glance, it looks like all of these log entries are probably false positives fixed by rMW46a565d6b001: Avoid false "added in both Session and $_SESSION" when value is null. But this should still be cleaned up at some point, as well as usage of session_*() functions and wfSetupSession().

As for actually fixing things,

  • Anywhere you have a WebRequest, you have a Session via $webRequest->getSession(). So it should be easy to get one. If you really don't have a WebRequest, \MediaWiki\Session\SessionManager::getGlobalSession() is available.
  • $webRequest->getSessionData() and $webRequest->setSessionData() may be used without any back-compat testing to replace direct accesses of $_SESSION, although they're inefficient if you're getting/setting many variables and can't be iterated over. The Session object can be iterated over.
  • session_id() !== '' is replaced with $session->isPersistent().
  • wfSetupSession() is replaced with $session->persist()
    • Also, if ( session_id() === '' ) { wfSetupSession(); } may be replaced with simply $session->persist();.

Event Timeline

Tgr created this task.Jan 21 2016, 11:24 PM
Tgr raised the priority of this task from to Needs Triage.
Tgr updated the task description. (Show Details)
Tgr added a subscriber: Tgr.
Restricted Application added subscribers: StudiesWorld, Aklapper. · View Herald TranscriptJan 21 2016, 11:24 PM
Reedy added a subscriber: Reedy.Jan 21 2016, 11:26 PM
reedy@tin:/srv/mediawiki-staging/php-1.27.0-wmf.11/extensions$ grep -R _SESSION *
AbuseFilter/AbuseFilter.class.php:				if ( !isset( $_SESSION[$warnKey] ) || !$_SESSION[$warnKey] ) {
AbuseFilter/AbuseFilter.class.php:					$_SESSION[$warnKey] = true;
AbuseFilter/AbuseFilter.class.php:					$_SESSION[$warnKey] = false;
CentralAuth/includes/session/CentralAuthSessionCompat.php:		if ( $token != @$_SESSION['globalloggedin'] ) { // FIXME: Usage of @
CentralAuth/includes/session/CentralAuthSessionCompat.php:			$_SESSION['globalloggedin'] = $token;
CodeEditor/modules/ace/mode-php.js:        ('$GLOBALS|$_SERVER|$_GET|$_POST|$_FILES|$_REQUEST|$_SESSION|$_ENV|$_COOKIE|$php_errormsg|$HTTP_RAW_POST_DATA|' +
CodeEditor/modules/ace/mode-php.js:    "$_SESSION": {
CodeEditor/modules/ace/snippets/php.js:	$_SESSION['${1:variable}']${2}\n\
Collection/Collection.session.php:		return isset( $_SESSION['wsCollection'] );
Collection/Collection.session.php:		$collection = $_SESSION['wsCollection'];
Collection/Collection.session.php:		$_SESSION['wsCollection'] = $collection;
Collection/Collection.session.php:		$_SESSION['wsCollection'] = array(
Collection/Collection.session.php:			$_SESSION['wsCollection']['enabled'] = true;
Collection/Collection.session.php:		$_SESSION['wsCollection']['enabled'] = false;
Collection/Collection.session.php:		return self::hasSession() && isset( $_SESSION['wsCollection']['enabled'] ) &&
Collection/Collection.session.php:			$_SESSION['wsCollection']['enabled'];
Collection/Collection.session.php:		return self::hasSession() && isset( $_SESSION['wsCollection']['items'] );
Collection/Collection.session.php:		foreach ( $_SESSION['wsCollection']['items'] as $item ) {
Collection/Collection.session.php:		foreach ( $_SESSION['wsCollection']['items'] as $index => $item ) {
Collection/Collection.session.php:		$coll = $_SESSION['wsCollection'];
Collection/Collection.session.php:		$_SESSION['wsCollection'] = $coll;
Collection/Collection.session.php:		return self::purge() ? $_SESSION['wsCollection'] : array();
Collection/Collection.session.php:		$_SESSION['wsCollection'] = $collection;
Collection/Collection.suggest.php:		if ( isset( $_SESSION['wsCollectionSuggestBan'] ) ) {
Collection/Collection.suggest.php:		 	unset( $_SESSION['wsCollectionSuggestBan'] );
Collection/Collection.suggest.php:		if ( isset( $_SESSION['wsCollectionSuggestProp'] ) ) {
Collection/Collection.suggest.php:			unset( $_SESSION['wsCollectionSuggestProp'] );
Collection/Collection.suggest.php:		if ( !isset( $_SESSION['wsCollectionSuggestBan'] ) ) {
Collection/Collection.suggest.php:		$bans = $_SESSION['wsCollectionSuggestBan'];
Collection/Collection.suggest.php:		$_SESSION['wsCollectionSuggestBan'] = $newbans;
Collection/Collection.suggest.php:		if ( !isset( $_SESSION['wsCollectionSuggestBan'] ) || $mode == 'resetbans' ) {
Collection/Collection.suggest.php:			$_SESSION['wsCollectionSuggestBan'] = array();
Collection/Collection.suggest.php:		if ( !isset( $_SESSION['wsCollectionSuggestProp'] ) ) {
Collection/Collection.suggest.php:			$_SESSION['wsCollectionSuggestProp'] = array();
Collection/Collection.suggest.php:				$_SESSION['wsCollectionSuggestBan'][] = $param;
Collection/Collection.suggest.php:				$_SESSION['wsCollectionSuggestBan'][] = $param;
Collection/Collection.suggest.php:			$_SESSION['wsCollection'],
Collection/Collection.suggest.php:			$_SESSION['wsCollectionSuggestBan'],
Collection/Collection.suggest.php:			$_SESSION['wsCollectionSuggestProp']
Collection/Collection.suggest.php:		$template->set( 'collection', $_SESSION['wsCollection'] );
Collection/Collection.suggest.php:		$_SESSION['wsCollectionSuggestProp'] = $proposals->getLinkList();
Collection/Collection.suggest.php:		$prop->setCollection( $_SESSION['wsCollection'] );
Collection/Collection.php:	if ( isset( $_SESSION['wsCollection'] ) ) {
Collection/Collection.php:		$collection = $_SESSION['wsCollection'];
Collection/Collection.php:	$_SESSION['wsCollection'] = $collection;
Collection/Collection.php:	$collection = $_SESSION['wsCollection'];
Collection/Collection.hooks.php:			|| !isset( $_SESSION['wsCollection']['enabled'] )
Collection/Collection.hooks.php:			|| !$_SESSION['wsCollection']['enabled'] ) {
Collection/Collection.hooks.php:			$modifiedTimes['collection'] = $_SESSION['wsCollection']['timestamp'];
ConfirmEdit/includes/CaptchaStore.php:		$_SESSION['captcha' . $info['index']] = $info;
ConfirmEdit/includes/CaptchaStore.php:		if ( isset( $_SESSION['captcha' . $index] ) ) {
ConfirmEdit/includes/CaptchaStore.php:			return $_SESSION['captcha' . $index];
ConfirmEdit/includes/CaptchaStore.php:		unset( $_SESSION['captcha' . $index] );
DonationInterface/extras/session_velocity/session_velocity.body.php:		if ( !array_key_exists( self::SESS_ROOT, $_SESSION ) ) {
DonationInterface/extras/session_velocity/session_velocity.body.php:			$_SESSION[self::SESS_ROOT] = array();
DonationInterface/extras/session_velocity/session_velocity.body.php:		if ( !array_key_exists( $gateway, $_SESSION[self::SESS_ROOT] ) ) {
DonationInterface/extras/session_velocity/session_velocity.body.php:			$_SESSION[self::SESS_ROOT][$gateway] = array();
DonationInterface/extras/session_velocity/session_velocity.body.php:		if ( !array_key_exists( $transaction, $_SESSION[self::SESS_ROOT][$gateway] ) ) {
DonationInterface/extras/session_velocity/session_velocity.body.php:			$_SESSION[self::SESS_ROOT][$gateway][$transaction] = array(
DonationInterface/extras/session_velocity/session_velocity.body.php:		$lastTime = $_SESSION[self::SESS_ROOT][$gateway][$transaction][self::SESS_TIME];
DonationInterface/extras/session_velocity/session_velocity.body.php:		$score = $_SESSION[self::SESS_ROOT][$gateway][$transaction][self::SESS_SCORE];
DonationInterface/extras/session_velocity/session_velocity.body.php:			$_SESSION[self::SESS_ROOT][$gateway][$transaction][$this::SESS_SCORE] = $score;
DonationInterface/extras/session_velocity/session_velocity.body.php:			$_SESSION[self::SESS_ROOT][$gateway][$transaction][$this::SESS_TIME] = $cRequestTime;
DonationInterface/amazon_gateway/amazon.adapter.php:		$_SESSION['order_refs'][$orderReferenceId] = true;
DonationInterface/amazon_gateway/amazon.adapter.php:		$_SESSION['billing_agreements'][$billingAgreementId] = true;
DonationInterface/globalcollect_gateway/globalcollect_resultswitcher.body.php:			if ( !$_SESSION ) {
DonationInterface/globalcollect_gateway/globalcollect_resultswitcher.body.php:					if ( array_key_exists( 'pending', $_SESSION ) ){
DonationInterface/globalcollect_gateway/globalcollect_resultswitcher.body.php:						$started = $_SESSION['pending'];
DonationInterface/globalcollect_gateway/globalcollect_resultswitcher.body.php:					$_SESSION['pending'] = microtime( true ); //We couldn't have gotten this far if the server wasn't sticky.
DonationInterface/globalcollect_gateway/globalcollect_resultswitcher.body.php:					$_SESSION['order_status'][$oid] = $session_info;
DonationInterface/globalcollect_gateway/globalcollect_resultswitcher.body.php:					unset( $_SESSION['pending'] );
DonationInterface/globalcollect_gateway/globalcollect_resultswitcher.body.php:					$_SESSION['order_status'][$oid]['data']['count'] = 0;
DonationInterface/globalcollect_gateway/globalcollect_resultswitcher.body.php:					$_SESSION['order_status'][$oid]['data']['count'] = $_SESSION['order_status'][$oid]['data']['count'] + 1;
DonationInterface/globalcollect_gateway/globalcollect_resultswitcher.body.php:					$this->logger->error( "Resultswitcher: Multiple attempts to process. " . $_SESSION['order_status'][$oid]['data']['count'] );
DonationInterface/globalcollect_gateway/globalcollect_resultswitcher.body.php:					$result->setData( $_SESSION['order_status'][$oid]['data'] );
DonationInterface/globalcollect_gateway/globalcollect_resultswitcher.body.php:					$result->setMessage( $_SESSION['order_status'][$oid]['message'] );
DonationInterface/globalcollect_gateway/globalcollect_resultswitcher.body.php:					$result->setErrors( $_SESSION['order_status'][$oid]['errors'] );
DonationInterface/globalcollect_gateway/globalcollect_resultswitcher.body.php:			if ( !$_SESSION ) {
DonationInterface/globalcollect_gateway/globalcollect_resultswitcher.body.php:			$_SESSION['order_status'][$this->qs_oid] = 'liberated';
DonationInterface/gateway_common/DonationData.php:		if ( $this->gateway->session_exists() && array_key_exists( 'Donor', $_SESSION ) ) {
DonationInterface/gateway_common/DonationData.php:			foreach ( $_SESSION['Donor'] as $key => $val ) {
DonationInterface/gateway_common/GatewayPage.php:		if ( array_key_exists( 'Donor', $_SESSION ) ) {
DonationInterface/gateway_common/GatewayPage.php:			foreach ( $_SESSION['Donor'] as $key => $val ) {
DonationInterface/gateway_common/GatewayPage.php:			$_SESSION[ 'order_status' ][ $oid ] = 'liberated';
DonationInterface/gateway_common/gateway.adapter.php:	 *	$_POST, $_SESSION (though they should be expressed in the arary
DonationInterface/gateway_common/gateway.adapter.php:		$_SESSION['numAttempt'] = $attempts;
DonationInterface/gateway_common/gateway.adapter.php:		$_SESSION['sequence'] = $sequence;
DonationInterface/gateway_common/gateway.adapter.php:		if ( is_array( $_SESSION ) && array_key_exists( $key, $_SESSION ) ) {
DonationInterface/gateway_common/gateway.adapter.php:				return $_SESSION[$key];
DonationInterface/gateway_common/gateway.adapter.php:				if ( is_array( $_SESSION[$key] ) && array_key_exists( $subkey, $_SESSION[$key] ) ) {
DonationInterface/gateway_common/gateway.adapter.php:					return $_SESSION[$key][$subkey];
DonationInterface/gateway_common/gateway.adapter.php:			unset( $_SESSION['Donor'] );
DonationInterface/gateway_common/gateway.adapter.php:		$_SESSION['Donor'] = array ( );
DonationInterface/gateway_common/gateway.adapter.php:			$_SESSION['Donor'][$item] = $this->getData_Unstaged_Escaped( $item );
DonationInterface/gateway_common/gateway.adapter.php:			$_SESSION['numAttempt'] = 0;
DonationInterface/gateway_common/gateway.adapter.php:			foreach ( $_SESSION as $key => $value ) {
DonationInterface/gateway_common/gateway.adapter.php:					unset( $_SESSION[$key] );
DonationInterface/gateway_common/gateway.adapter.php:				unset( $_SESSION['Donor'][$reset_me] );
DonationInterface/gateway_common/gateway.adapter.php:					unset( $_SESSION['Donor']['payment_submethod'] );
DonationInterface/gateway_common/gateway.adapter.php:				unset( $_SESSION['Donor']['order_id'] );
DonationInterface/gateway_common/gateway.adapter.php:			$_SESSION['PaymentForms'] = array ( );
DonationInterface/gateway_common/gateway.adapter.php:			$_SESSION['PaymentForms'][] = $form_key;
DonationInterface/gateway_common/gateway.adapter.php:			$ffname = end( $_SESSION['PaymentForms'] );
DonationInterface/gateway_common/gateway.adapter.php:		if ( !isset( $_SESSION[$gateway_ident . 'EditToken'] ) ) {
DonationInterface/gateway_common/gateway.adapter.php:			$_SESSION[$gateway_ident . 'EditToken'] = $token;
DonationInterface/gateway_common/gateway.adapter.php:			$token = $_SESSION[$gateway_ident . 'EditToken'];
DonationInterface/gateway_common/gateway.adapter.php:		$_SESSION[$gateway_ident . 'EditToken'] = $unsalted;
DonationInterface/gateway_common/gateway.adapter.php:			'_SESSION' => array ( 'Donor' => 'order_id' ),
DonationInterface/gateway_common/gateway.adapter.php:				case "_SESSION" :
DonationInterface/gateway_common/gateway.adapter.php:								if ( array_key_exists( $subkey, $_SESSION ) && array_key_exists( $subvalue, $_SESSION[$subkey] ) ) {
DonationInterface/gateway_common/gateway.adapter.php:									$oid_candidates['_SESSION' . $subkey . $subvalue] = $_SESSIO[$subkey][$subvalue];
DonationInterface/gateway_common/gateway.adapter.php:							if ( array_key_exists( $key, $_SESSION ) ) {
DonationInterface/gateway_common/gateway.adapter.php:								$oid_candidates[$var] = $_SESSION[$key];
DonationInterface/tests/IntegrationTest.php:		$this->assertEquals( 'paypal', $_SESSION['PaymentForms'][0], "Paypal didn't load its form." );
DonationInterface/tests/IntegrationTest.php:		$this->assertEquals( '1', $_SESSION['numAttempt'], "We failed to record the initial paypal attempt in the session" );
DonationInterface/tests/Adapter/GatewayAdapterTest.php:		$this->assertEquals( 'globalcollect', $_SESSION['Donor']['gateway'], 'Test setup failed.' );
DonationInterface/tests/Adapter/GatewayAdapterTest.php:		$_SESSION['sequence'] = 2;
DonationInterface/tests/Adapter/GatewayAdapterTest.php:		$expected_order_id = "{$init['contribution_tracking_id']}.{$_SESSION['sequence']}";
DonationInterface/tests/Adapter/GatewayAdapterTest.php:		$this->assertEquals( '', $_SESSION['Donor']['recurring'], 'Test setup failed.' );
DonationInterface/tests/Adapter/GatewayAdapterTest.php:		$this->assertEquals( '1', $_SESSION['Donor']['recurring'], 'Test setup failed.' );
DonationInterface/tests/Adapter/GatewayAdapterTest.php:		$this->assertEquals( 'itau', $_SESSION['Donor']['payment_submethod'], 'Test setup failed.' );
DonationInterface/tests/Adapter/Worldpay/WorldpayTest.php:        unset( $_SESSION['Donor']['order_id'] );
DonationInterface/tests/Adapter/Worldpay/WorldpayTest.php:        $_SESSION['sequence'] = 2;
DonationInterface/tests/Adapter/Worldpay/WorldpayTest.php:		$expected_order_id = "{$init['contribution_tracking_id']}.{$_SESSION['sequence']}";
DonationInterface/tests/Adapter/GlobalCollect/GlobalCollectTest.php:		$_SESSION['Donor']['order_id'] = '44444';
DonationInterface/tests/Adapter/GlobalCollect/GlobalCollectTest.php:		unset( $_SESSION['Donor']['order_id'] );
DonationInterface/tests/Adapter/Amazon/AmazonTest.php:		$_SESSION['Donor'] = $init;
DonationInterface/tests/Adapter/Astropay/AstropayTest.php:		$_SESSION['Donor']['order_id'] = '123456789';
DonationInterface/tests/Adapter/Astropay/AstropayTest.php:		$_SESSION['Donor']['order_id'] = '123456789';
DonationInterface/tests/Adapter/Astropay/AstropayTest.php:		$_SESSION['Donor']['order_id'] = '123456789';
DonationInterface/tests/Adapter/Astropay/AstropayTest.php:		$_SESSION['Donor']['order_id'] = '123456789';
DonationInterface/tests/Adapter/Astropay/AstropayTest.php:		$_SESSION['Donor']['order_id'] = '123456789';
DonationInterface/tests/Adapter/Astropay/AstropayTest.php:		$_SESSION['Donor']['order_id'] = '123456789';
DonationInterface/tests/Adapter/Astropay/AstropayTest.php:		$_SESSION['Donor']['order_id'] = '123456789';
DonationInterface/tests/Adapter/PayPal/PayPalTest.php:		$_SESSION['Donor'] = $init;
DonationInterface/tests/Adapter/PayPal/PayPalTest.php:		$_SESSION['Donor'] = $init;
DonationInterface/tests/Adapter/PayPal/PayPalResultSwitcherTest.php:		$_SESSION['Donor'] = $init;
DonationInterface/tests/DonationInterfaceTestCase.php:		$_SESSION = array ( );
DonationInterface/worldpay_gateway/worldpay.adapter.php:		$_SESSION['Donor']['wp_one_time_token'] = $this->getData_Unstaged_Escaped( 'wp_one_time_token' );
DonationInterface/worldpay_gateway/worldpay.adapter.php:			$_SESSION['Donor_protected']['cvv'] = $cvv;
DonationInterface/worldpay_gateway/worldpay.adapter.php:			unset( $_SESSION['Donor_protected']['cvv'] );
DonationInterface/worldpay_gateway/worldpay.adapter.php:		if ( isset( $_SESSION['Donor_protected']['cvv'] ) ) {
DonationInterface/worldpay_gateway/worldpay.adapter.php:			return $_SESSION['Donor_protected']['cvv'];
LdapAuthentication/LdapAuthentication.php:		$_SESSION['wsDomain'] = $domain;
LdapAuthentication/LdapAuthentication.php:		if ( isset( $_SESSION['wsDomain'] ) && $_SESSION['wsDomain'] != 'invaliddomain' ) {
LdapAuthentication/LdapAuthentication.php:			return $_SESSION['wsDomain'];
LdapAuthentication/LdapAuthentication.php:			self::saveDomain( $user, $_SESSION['wsDomain'] );
LiquidThreads/classes/View.php:		if ( !isset( $_SESSION ) ) {
LiquidThreads/classes/View.php:		foreach ( $_SESSION as $k => $v ) {
OpenStackManager/nova/OpenStackNovaUser.php:		if ( $wikiUser->getToken( false ) && isset( $_SESSION['wsOpenStackToken'] ) ) {
OpenStackManager/nova/OpenStackNovaUser.php:			self::saveToken( $wikiUser, $_SESSION['wsOpenStackToken'] );
OpenStackManager/nova/OpenStackNovaUser.php:			$_SESSION['wsOpenStackToken'] = $token;
Scribunto/common/ApiScribuntoConsole.php:	const SC_SESSION_EXPIRY = 3600;
Scribunto/common/ApiScribuntoConsole.php:		$cache->set( $sessionKey, $newSession, self::SC_SESSION_EXPIRY );
SecurePoll/SecurePoll.php:	$_SESSION['securepoll_voter'] = null;
SecurePoll/includes/user/Auth.php:		if ( isset( $_SESSION['securepoll_voter'][$election->getId()] ) ) {
SecurePoll/includes/user/Auth.php:			$voterId = $_SESSION['securepoll_voter'][$election->getId()];
SecurePoll/includes/user/Auth.php:					$_SESSION['securepoll_voter'][$election->getId()] = $otherVoter->getId();
SecurePoll/includes/user/Auth.php:			$_SESSION['securepoll_voter'][$election->getId()] = $voter->getId();
SecurePoll/includes/user/Auth.php:		if ( isset( $_SESSION['securepoll_voter'][$election->getId()] ) ) {
SecurePoll/includes/user/Auth.php:			$otherVoterId = $_SESSION['securepoll_voter'][$election->getId()];
SecurePoll/includes/user/Auth.php:		$_SESSION['securepoll_voter'][$election->getId()] = $voter->getId();
SecurePoll/includes/main/SpecialSecurePoll.php:		if ( !isset( $_SESSION['spToken'] ) ) {
SecurePoll/includes/main/SpecialSecurePoll.php:			$_SESSION['spToken'] = sha1( mt_rand() . mt_rand() . mt_rand() );
SecurePoll/includes/main/SpecialSecurePoll.php:		return $_SESSION['spToken'];
SemanticForms/includes/SF_AutoeditAPI.php:			if ( isset( $_SESSION ) ) {
SemanticForms/includes/SF_AutoeditAPI.php:				$wgRequest = new FauxRequest( $this->mOptions, true, $_SESSION );
SemanticForms/includes/SF_AutoeditAPI.php:		if ( isset( $_SESSION ) ) {
SemanticForms/includes/SF_AutoeditAPI.php:			$wgRequest = new FauxRequest( $this->mOptions, true, $_SESSION );
reedy@tin:/srv/mediawiki-staging/php-1.27.0-wmf.11/extensions$

Change 266545 had a related patch set uploaded (by Anomie):
Avoid false "added in both Session and $_SESSION" when value is null

https://gerrit.wikimedia.org/r/266545

Change 266547 had a related patch set uploaded (by BryanDavis):
Avoid false "added in both Session and $_SESSION" when value is null

https://gerrit.wikimedia.org/r/266547

Change 266545 merged by jenkins-bot:
Avoid false "added in both Session and $_SESSION" when value is null

https://gerrit.wikimedia.org/r/266545

Change 266547 merged by jenkins-bot:
Avoid false "added in both Session and $_SESSION" when value is null

https://gerrit.wikimedia.org/r/266547

Anomie updated the task description. (Show Details)Jan 26 2016, 6:59 PM
Anomie set Security to None.
Anomie removed a project: Patch-For-Review.
bd808 triaged this task as Normal priority.Feb 11 2016, 2:08 AM
bd808 added a subscriber: bd808.
Anomie updated the task description. (Show Details)Jan 26 2017, 2:55 AM

Change 415760 had a related patch set uploaded (by Jforrester; owner: Jforrester):
[mediawiki/extensions/Collection@master] Avoid calls to deprecated wfSetupSession, $_SESSION, and session_id

https://gerrit.wikimedia.org/r/415760

Change 415762 had a related patch set uploaded (by Jforrester; owner: Jforrester):
[mediawiki/extensions/AbuseFilter@master] Avoid calls to deprecated wfSetupSession, $_SESSION, and session_id

https://gerrit.wikimedia.org/r/415762

Jdforrester-WMF updated the task description. (Show Details)

Change 415760 merged by jenkins-bot:
[mediawiki/extensions/Collection@master] Avoid calls to deprecated wfSetupSession, $_SESSION, and session_id

https://gerrit.wikimedia.org/r/415760

Change 415762 merged by jenkins-bot:
[mediawiki/extensions/AbuseFilter@master] Avoid calls to deprecated wfSetupSession, $_SESSION, and session_id

https://gerrit.wikimedia.org/r/415762