jenkins-bot has submitted this change and it was merged. Change subject: Add DeviceDetectorService class ......................................................................
Add DeviceDetectorService class In order to remove knowledge about device detection from MobileContext, including the order of the methods we use to detect devices, it needs to be moved somewhere: MobileFrontend\Devices\DeviceDetectorService. If device detection is enabled, then DeviceDetectorService iterates through a set of child device detectors, the default set of which are defined in DeviceDetectorService::factory. Changes: * Extract DeviceDetectorService from MobileContext#isMobileDevice and #getDevice. * Make MobileContext#isMobileDevice use DeviceDetectorService * Remove MobileContext#getAMF and #getDevice. Bug: T143891 Change-Id: I09547448f7eb1a56cea718a6751de227d2890987 --- M extension.json M includes/MobileContext.php A includes/devices/DeviceDetectorService.php A tests/phpunit/devices/DeviceDetectorServiceIntegrationTest.php A tests/phpunit/devices/DeviceDetectorServiceTest.php 5 files changed, 288 insertions(+), 57 deletions(-) Approvals: Jdlrobson: Looks good to me, approved jenkins-bot: Verified diff --git a/extension.json b/extension.json index 0bd1aad..c4efee7 100644 --- a/extension.json +++ b/extension.json @@ -96,7 +96,8 @@ "MobileFrontend\\Devices\\DeviceProperties": "includes/devices/DeviceProperties.php", "MobileFrontend\\Devices\\AMFDeviceDetector": "includes/devices/AMFDeviceDetector.php", "MobileFrontend\\Devices\\CustomHeaderDeviceDetector": "includes/devices/CustomHeaderDeviceDetector.php", - "MobileFrontend\\Devices\\UADeviceDetector": "includes/devices/UADeviceDetector.php" + "MobileFrontend\\Devices\\UADeviceDetector": "includes/devices/UADeviceDetector.php", + "MobileFrontend\\Devices\\DeviceDetectorService": "includes/devices/DeviceDetectorService.php" }, "ResourceModules": { "skins.minerva.base.reset": { diff --git a/includes/MobileContext.php b/includes/MobileContext.php index 2cb8e63..52823d2 100644 --- a/includes/MobileContext.php +++ b/includes/MobileContext.php @@ -4,6 +4,7 @@ */ use MediaWiki\MediaWikiServices; +use MobileFrontend\Devices\DeviceDetectorService; /** * Provide various request-dependant methods to use in mobile context @@ -53,8 +54,17 @@ */ protected $analyticsLogItems = []; - /** @var IDeviceProperties $device Saves current device description */ - private $device; + /** + * The memoized result of `MobileContext#isMobileDevice`. + * + * This defaults to `null`, meaning that `MobileContext#isMobileDevice` has + * yet to be called. + * + * @see MobileContext#isMobileDevice + * + * @var {bool|null} $isMobileDevice + **/ + private $isMobileDevice = null; /** * @var string $action MediaWiki 'action' @@ -144,31 +154,6 @@ } /** - * Gets the current device description - * @return IDeviceProperties - */ - public function getDevice() { - $mobileHeader = $this->getMFConfig()->get( 'MFMobileHeader' ); - - if ( $this->device ) { - return $this->device; - } - $detector = DeviceDetection::factory(); - $request = $this->getRequest(); - - if ( $mobileHeader && $this->getRequest()->getHeader( $mobileHeader ) !== false ) { - $this->device = new HtmlDeviceProperties(); - } else { - $userAgent = $request->getHeader( 'User-agent' ); - $acceptHeader = $request->getHeader( 'Accept' ); - $acceptHeader = $acceptHeader === false ? '' : $acceptHeader; - $this->device = $detector->detectDeviceProperties( $userAgent, $acceptHeader ); - } - - return $this->device; - } - - /** * Checks whether references should be lazy loaded for the current user * @return bool */ @@ -214,42 +199,40 @@ } /** - * Check whether the device is a mobile device + * Detects whether the UA is sending the request from a device and, if so, + * whether to display the mobile view to that device. + * + * The mobile view will always be displayed to mobile devices. However, it + * will only be displayed to tablet devices if `$wgMFShowMobileViewToTablets` + * is truthy. + * + * @FIXME: This should be renamed to something more appropriate, e.g. + * `shouldDisplayMobileViewToDevice`. + * + * @see MobileContext::shouldDisplayMobileView + * * @return bool */ public function isMobileDevice() { + if ( $this->isMobileDevice !== null ) { + return $this->isMobileDevice; + } + + $this->isMobileDevice = false; + $config = $this->getMFConfig(); + $properties = DeviceDetectorService::factory( $config ) + ->detectDeviceProperties( $this->getRequest(), $_SERVER ); - if ( !$config->get( 'MFAutodetectMobileView' ) ) { - return false; + if ( $properties ) { + $showMobileViewToTablets = $config->get( 'MFShowMobileViewToTablets' ); + + $this->isMobileDevice = + $properties->isMobileDevice() + || ( $properties->isTabletDevice() && $showMobileViewToTablets ); } - if ( $this->getAMF() ) { - return true; - } - $device = $this->getDevice(); - return $device->isMobileDevice() - && !( !$config->get( 'MFShowMobileViewToTablets' ) && $device->isTablet() ); - } - - /** - * Check for mobile device when using Apache Mobile Filter (AMF) - * - * IF AMF is enabled, make sure we use it to detect mobile devices. - * Tablets are currently served desktop site. - * - * AMF docs: http://wiki.apachemobilefilter.org/ - * - * @return bool - */ - public function getAMF() { - $showMobileViewToTablets = $this->getMFConfig()->get( 'MFShowMobileViewToTablets' ); - - $amf = isset( $_SERVER['AMF_DEVICE_IS_MOBILE'] ) && $_SERVER['AMF_DEVICE_IS_MOBILE'] === 'true'; - if ( !$showMobileViewToTablets && $amf ) { - $amf &= $_SERVER['AMF_DEVICE_IS_TABLET'] === 'false'; - } - return $amf; + return $this->isMobileDevice; } /** diff --git a/includes/devices/DeviceDetectorService.php b/includes/devices/DeviceDetectorService.php new file mode 100644 index 0000000..121bdd2 --- /dev/null +++ b/includes/devices/DeviceDetectorService.php @@ -0,0 +1,64 @@ +<?php + +namespace MobileFrontend\Devices; + +use Config; +use WebRequest; + +/** + * MobileFrontend's device detector. + * + * Detects the properties of a device by iterating over a list, //in order//, + * child device detectors until one returns a positive result. + * + * The order of the child device detectors is defined in + * `DeviceDetectorService::factory`. + */ +class DeviceDetectorService implements DeviceDetector { + + /** + * Given MobileFrontend's configuration, creates a new instance of the device + * detector. + * + * If `$wgMFAutodetectMobileView` is falsy, then no device detection will + * occur. + * + * @return DeviceDetectorService + */ + public static function factory( Config $config ) { + $children = []; + + if ( $config->get( 'MFAutodetectMobileView' ) ) { + array_push( + $children, + new AMFDeviceDetector(), + new CustomHeaderDeviceDetector( $config ), + new UADeviceDetector() + ); + } + + return new self( $children ); + } + + /** + * @var DeviceDetector[] + */ + private $children; + + /** + * @param DeviceDetector[] $children + */ + public function __construct( array $children ) { + $this->children = $children; + } + + public function detectDeviceProperties( WebRequest $request, array $server ) { + foreach ( $this->children as $child ) { + $properties = $child->detectDeviceProperties( $request, $server ); + + if ( $properties ) { + return $properties; + } + } + } +} diff --git a/tests/phpunit/devices/DeviceDetectorServiceIntegrationTest.php b/tests/phpunit/devices/DeviceDetectorServiceIntegrationTest.php new file mode 100644 index 0000000..b297fe6 --- /dev/null +++ b/tests/phpunit/devices/DeviceDetectorServiceIntegrationTest.php @@ -0,0 +1,110 @@ +<?php + +namespace Tests\MobileFrontend\Devices; + +use MediaWikiTestCase; +use FauxRequest; +use GlobalVarConfig; +use MobileFrontend\Devices\DeviceDetectorService; + +/** + * This suite of test cases tests + * `MobileFrontend\Devices\DeviceDetectorService`'s behaviour with no stubbed + * dependencies. + * + * @group integration + */ +class DeviceDetectorServiceIntegrationTest extends MediaWikiTestCase { + public function setUp() { + parent::setUp(); + + $this->setMwGlobals( 'wgMFAutodetectMobileView', true ); + + $this->request = new FauxRequest(); + $this->request->setHeader( + 'User-Agent', + + // A Macbook Air running Google Chrome (53.0.2785.116). + // @codingStandardsIgnoreStart + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36' + // @codingStandardsIgnoreEnd + ); + + $this->server = []; + } + + private function detectDeviceProperties() { + $config = new GlobalVarConfig(); + + return DeviceDetectorService::factory( $config ) + ->detectDeviceProperties( $this->request, $this->server ); + } + + private function whenTheRequestIsFromAMobileUA() { + $this->request->setHeader( + 'User-Agent', + + // An iPhone running iOS 8.0. + // @codingStandardsIgnoreStart + 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25' + // @codingStandardsIgnoreEnd + ); + + } + + public function test_it_should_handle_requests_from_mobile_UAs() { + $this->whenTheRequestIsFromAMobileUA(); + + $properties = $this->detectDeviceProperties(); + + $this->assertTrue( $properties->isMobileDevice() ); + $this->assertFalse( $properties->isTabletDevice() ); + } + + public function test_it_should_handle_a_request_from_desktop_browsers() { + $properties = $this->detectDeviceProperties(); + + $this->assertFalse( $properties->isMobileDevice() ); + $this->assertFalse( $properties->isTabletDevice() ); + } + + public function test_it_should_prioritize_the_custom_request_header() { + // @codingStandardsIgnoreStart + // The custom header //should// either be M or ZERO, per + // <https://github.com/wikimedia/operations-puppet/blob/2a2714c28eab25eed469375dc5322ea6a6ef85df/modules/varnish/templates/text-frontend.inc.vcl.erb#L74-L78>. + // @codingStandardsIgnoreEnd + + // @FIXME Change the default value of $wgMFMobileHeader to "X-Subdomain". + $this->request->setHeader( 'X-WAP', 'M' ); + + $properties = $this->detectDeviceProperties(); + + $this->assertTrue( $properties->isMobileDevice() ); + $this->assertFalse( $properties->isTabletDevice() ); + } + + /** + * @FIXME Should this really be the case? + */ + public function test_it_should_prioritize_the_amf_environment_variables() { + $this->request->setHeader( 'X-WAP', 'M' ); + + $this->server[ 'AMF_DEVICE_IS_TABLET' ] = 'true'; + + $properties = $this->detectDeviceProperties(); + + $this->assertFalse( + $properties->isMobileDevice(), + 'Apache Mobile Filter environment variables are prioritized above the custom request header.' + ); + $this->assertTrue( $properties->isTabletDevice() ); + } + + public function test_it_should_handle_device_detection_being_disabled() { + $this->setMwGlobals( 'wgMFAutodetectMobileView', false ); + + $this->whenTheRequestIsFromAMobileUA(); + + $this->assertNull( $this->detectDeviceProperties() ); + } +} diff --git a/tests/phpunit/devices/DeviceDetectorServiceTest.php b/tests/phpunit/devices/DeviceDetectorServiceTest.php new file mode 100644 index 0000000..3c066a1 --- /dev/null +++ b/tests/phpunit/devices/DeviceDetectorServiceTest.php @@ -0,0 +1,73 @@ +<?php + +namespace Tests\MobileFrontend\Devices; + +use MediaWikiTestCase; +use MobileFrontend\Devices\DeviceDetector; +use WebRequest; +use FauxRequest; +use MobileFrontend\Devices\DeviceProperties; +use MobileFrontend\Devices\DeviceDetectorService; + +class StubDeviceDetector implements DeviceDetector { + public function __construct( $result ) { + $this->result = $result; + } + + public function detectDeviceProperties( WebRequest $request, array $server ) { + return $this->result; + } +} + +class DeviceDetectorServiceTest extends MediaWikiTestCase { + protected function setUp() { + parent::setUp(); + + $this->request = new FauxRequest(); + } + + /** + * Creates a list of child device detectors from a list of results, which are + * then used to create an instance of `CompositeDeviceDetector`. + * + * @param array $results + * @return CompositeDeviceDetector + */ + private function createDetector( array $results ) { + $childFactory = function ( $result ) { + return new StubDeviceDetector( $result ); + }; + + return new DeviceDetectorService( array_map( $childFactory, $results ) ); + } + + public function test_it_should_handle_one_child() { + $expectedProperties = new DeviceProperties( true, false ); + $detector = $this->createDetector( [ $expectedProperties ] ); + + $properties = $detector->detectDeviceProperties( $this->request, [] ); + + $this->assertSame( $expectedProperties, $properties ); + } + + public function test_it_should_handle_many_children() { + $expectedProperties = new DeviceProperties( true, false ); + $detector = $this->createDetector( [ + null, + null, + $expectedProperties + ] ); + + $properties = $detector->detectDeviceProperties( $this->request, [] ); + + $this->assertSame( $expectedProperties, $properties ); + } + + public function test_it_should_handle_zero_children() { + $detector = $this->createDetector( [] ); + + $properties = $detector->detectDeviceProperties( $this->request, [] ); + + $this->assertEquals( null, $properties ); + } +} -- To view, visit https://gerrit.wikimedia.org/r/314000 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I09547448f7eb1a56cea718a6751de227d2890987 Gerrit-PatchSet: 8 Gerrit-Project: mediawiki/extensions/MobileFrontend Gerrit-Branch: master Gerrit-Owner: Phuedx <samsm...@wikimedia.org> Gerrit-Reviewer: Bmansurov <bmansu...@wikimedia.org> Gerrit-Reviewer: Jdlrobson <jrob...@wikimedia.org> Gerrit-Reviewer: Phuedx <samsm...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits