From 016fbe7a81aaf51a4c37e3f69ff7bc81f0037834 Mon Sep 17 00:00:00 2001 From: Trevor Parscal Date: Wed, 29 Sep 2010 19:04:04 +0000 Subject: [PATCH] Made ResourceLoader an instantiable object, rather than a static one, making it more modular and testable. --- includes/MessageBlobStore.php | 42 +++--- includes/OutputPage.php | 37 ++--- includes/ResourceLoader.php | 126 +++++++----------- includes/ResourceLoaderContext.php | 8 +- includes/ResourceLoaderModule.php | 14 +- load.php | 3 +- .../phpunit/includes/ResourceLoaderTest.php | 1 + 7 files changed, 108 insertions(+), 123 deletions(-) diff --git a/includes/MessageBlobStore.php b/includes/MessageBlobStore.php index 45a8b27f39..604b1d4638 100644 --- a/includes/MessageBlobStore.php +++ b/includes/MessageBlobStore.php @@ -31,7 +31,7 @@ class MessageBlobStore { /** * Get the message blobs for a set of modules - * @param $modules array Array of module names + * @param $modules array Array of module objects keyed by module name * @param $lang string Language code * @return array An array mapping module names to message blobs */ @@ -43,14 +43,14 @@ class MessageBlobStore { return array(); } // Try getting from the DB first - $blobs = self::getFromDB( $modules, $lang ); + $blobs = self::getFromDB( array_keys( $modules ), $lang ); // Generate blobs for any missing modules and store them in the DB - $missing = array_diff( $modules, array_keys( $blobs ) ); - foreach ( $missing as $module ) { - $blob = self::insertMessageBlob( $module, $lang ); + $missing = array_diff( array_keys( $modules ), array_keys( $blobs ) ); + foreach ( $missing as $name ) { + $blob = self::insertMessageBlob( $name, $modules[$name], $lang ); if ( $blob ) { - $blobs[$module] = $blob; + $blobs[$name] = $blob; } } @@ -66,8 +66,8 @@ class MessageBlobStore { * @param $lang string Language code * @return mixed Message blob or false if the module has no messages */ - public static function insertMessageBlob( $module, $lang ) { - $blob = self::generateMessageBlob( $module, $lang ); + public static function insertMessageBlob( $name, ResourceLoaderModule $module, $lang ) { + $blob = self::generateMessageBlob( $name, $module, $lang ); if ( !$blob ) { return false; @@ -76,7 +76,7 @@ class MessageBlobStore { $dbw = wfGetDB( DB_MASTER ); $success = $dbw->insert( 'msg_resource', array( 'mr_lang' => $lang, - 'mr_resource' => $module, + 'mr_resource' => $name, 'mr_blob' => $blob, 'mr_timestamp' => $dbw->timestamp() ), @@ -88,7 +88,7 @@ class MessageBlobStore { if ( $dbw->affectedRows() == 0 ) { // Blob was already present, fetch it $blob = $dbw->selectField( 'msg_resource', 'mr_blob', array( - 'mr_resource' => $module, + 'mr_resource' => $name, 'mr_lang' => $lang, ), __METHOD__ @@ -96,11 +96,10 @@ class MessageBlobStore { } else { // Update msg_resource_links $rows = array(); - $mod = ResourceLoader::getModule( $module ); - foreach ( $mod->getMessages() as $key ) { + foreach ( $module->getMessages() as $key ) { $rows[] = array( - 'mrl_resource' => $module, + 'mrl_resource' => $name, 'mrl_message' => $key ); } @@ -120,14 +119,14 @@ class MessageBlobStore { * @return mixed If $lang is set, the new message blob for that language is * returned if present. Otherwise, null is returned. */ - public static function updateModule( $module, $lang = null ) { + public static function updateModule( $name, ResourceLoaderModule $module, $lang = null ) { $retval = null; // Find all existing blobs for this module $dbw = wfGetDB( DB_MASTER ); $res = $dbw->select( 'msg_resource', array( 'mr_lang', 'mr_blob' ), - array( 'mr_resource' => $module ), + array( 'mr_resource' => $name ), __METHOD__ ); @@ -139,13 +138,13 @@ class MessageBlobStore { foreach ( $res as $row ) { $oldBlob = $row->mr_blob; - $newBlob = self::generateMessageBlob( $module, $row->mr_lang ); + $newBlob = self::generateMessageBlob( $name, $module, $row->mr_lang ); if ( $row->mr_lang === $lang ) { $retval = $newBlob; } $newRows[] = array( - 'mr_resource' => $module, + 'mr_resource' => $name, 'mr_lang' => $row->mr_lang, 'mr_blob' => $newBlob, 'mr_timestamp' => $now @@ -166,7 +165,7 @@ class MessageBlobStore { // Delete removed messages, insert added ones if ( $removed ) { $dbw->delete( 'msg_resource_links', array( - 'mrl_resource' => $module, + 'mrl_resource' => $name, 'mrl_message' => $removed ), __METHOD__ ); @@ -176,7 +175,7 @@ class MessageBlobStore { foreach ( $added as $message ) { $newLinksRows[] = array( - 'mrl_resource' => $module, + 'mrl_resource' => $name, 'mrl_message' => $message ); } @@ -343,11 +342,10 @@ class MessageBlobStore { * @param $lang string Language code * @return string JSON object */ - private static function generateMessageBlob( $module, $lang ) { - $mod = ResourceLoader::getModule( $module ); + private static function generateMessageBlob( $name, ResourceLoaderModule $module, $lang ) { $messages = array(); - foreach ( $mod->getMessages() as $key ) { + foreach ( $module->getMessages() as $key ) { $messages[$key] = wfMsgExt( $key, array( 'language' => $lang ) ); } diff --git a/includes/OutputPage.php b/includes/OutputPage.php index a32b04e98a..94edee8a01 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -25,6 +25,7 @@ class OutputPage { var $mScripts = '', $mLinkColours, $mPageLinkTitle = '', $mHeadItems = array(); var $mModules = array(), $mModuleScripts = array(), $mModuleStyles = array(), $mModuleMessages = array(); + var $mResourceLoader; var $mInlineMsg = array(); var $mTemplateIds = array(); @@ -2281,9 +2282,13 @@ class OutputPage { } // TODO: Document - static function makeResourceLoaderLink( $skin, $modules, $only, $useESI = false ) { + protected function makeResourceLoaderLink( $skin, $modules, $only, $useESI = false ) { global $wgUser, $wgLang, $wgRequest, $wgLoadScript, $wgResourceLoaderDebug, $wgResourceLoaderUseESI, $wgResourceLoaderInlinePrivateModules; + // Lazy-load ResourceLoader + if ( is_null( $this->mResourceLoader ) ) { + $this->mResourceLoader = new ResourceLoader(); + } // TODO: Should this be a static function of ResourceLoader instead? // TODO: Divide off modules starting with "user", and add the user parameter to them $query = array( @@ -2299,7 +2304,7 @@ class OutputPage { // Create keyed-by-group list of module objects from modules list $groups = array(); foreach ( (array) $modules as $name ) { - $module = ResourceLoader::getModule( $name ); + $module = $this->mResourceLoader->getModule( $name ); $group = $module->getGroup(); if ( !isset( $groups[$group] ) ) { $groups[$group] = array(); @@ -2315,17 +2320,17 @@ class OutputPage { } // Support inlining of private modules if configured as such if ( $group === 'private' && $wgResourceLoaderInlinePrivateModules ) { - $context = new ResourceLoaderContext( new FauxRequest( $query ) ); + $context = new ResourceLoaderContext( $this->mResourceLoader, new FauxRequest( $query ) ); if ( $only == 'styles' ) { $links .= Html::inlineStyle( ResourceLoader::makeLoaderConditionalScript( - ResourceLoader::makeModuleResponse( $context, $modules ) + $this->mResourceLoader->makeModuleResponse( $context, $modules ) ) ); } else { $links .= Html::inlineScript( ResourceLoader::makeLoaderConditionalScript( - ResourceLoader::makeModuleResponse( $context, $modules ) + $this->mResourceLoader->makeModuleResponse( $context, $modules ) ) ); } @@ -2336,7 +2341,7 @@ class OutputPage { // we can ensure cache misses on change if ( $group === 'user' || $group === 'site' ) { // Create a fake request based on the one we are about to make so modules return correct times - $context = new ResourceLoaderContext( new FauxRequest( $query ) ); + $context = new ResourceLoaderContext( $this->mResourceLoader, new FauxRequest( $query ) ); // Get the maximum timestamp $timestamp = 0; foreach ( $modules as $module ) { @@ -2380,7 +2385,7 @@ class OutputPage { global $wgUser, $wgRequest, $wgUseSiteJs, $wgResourceLoaderDebug; // Startup - this will immediately load jquery and mediawiki modules - $scripts = self::makeResourceLoaderLink( $sk, 'startup', 'scripts', true ); + $scripts = $this->makeResourceLoaderLink( $sk, 'startup', 'scripts', true ); // Configuration -- This could be merged together with the load and go, but makeGlobalVariablesScript returns a // whole script tag -- grumble grumble... @@ -2390,20 +2395,20 @@ class OutputPage { if ( $wgRequest->getFuzzyBool( 'debug', $wgResourceLoaderDebug ) ) { // Scripts foreach ( $this->getModuleScripts() as $name ) { - $scripts .= self::makeResourceLoaderLink( $sk, $name, 'scripts' ); + $scripts .= $this->makeResourceLoaderLink( $sk, $name, 'scripts' ); } // Messages foreach ( $this->getModuleMessages() as $name ) { - $scripts .= self::makeResourceLoaderLink( $sk, $name, 'messages' ); + $scripts .= $this->makeResourceLoaderLink( $sk, $name, 'messages' ); } } else { // Scripts if ( count( $this->getModuleScripts() ) ) { - $scripts .= self::makeResourceLoaderLink( $sk, $this->getModuleScripts(), 'scripts' ); + $scripts .= $this->makeResourceLoaderLink( $sk, $this->getModuleScripts(), 'scripts' ); } // Messages if ( count( $this->getModuleMessages() ) ) { - $scripts .= self::makeResourceLoaderLink( $sk, $this->getModuleMessages(), 'messages' ); + $scripts .= $this->makeResourceLoaderLink( $sk, $this->getModuleMessages(), 'messages' ); } } @@ -2423,18 +2428,18 @@ class OutputPage { # XXX: additional security check/prompt? $this->addInlineScript( $wgRequest->getText( 'wpTextbox1' ) ); } else { - $scripts .= self::makeResourceLoaderLink( $sk, array( 'user', 'user.options' ), 'scripts' ); + $scripts .= $this->makeResourceLoaderLink( $sk, array( 'user', 'user.options' ), 'scripts' ); $userOptionsAdded = true; } } if ( !$userOptionsAdded ) { - $scripts .= self::makeResourceLoaderLink( $sk, 'user.options', 'scripts' ); + $scripts .= $this->makeResourceLoaderLink( $sk, 'user.options', 'scripts' ); } $scripts .= "\n" . $this->mScripts; // Add site JS if enabled if ( $wgUseSiteJs ) { - $scripts .= self::makeResourceLoaderLink( $sk, 'site', 'scripts' ); + $scripts .= $this->makeResourceLoaderLink( $sk, 'site', 'scripts' ); } return $scripts; @@ -2558,11 +2563,11 @@ class OutputPage { // Support individual script requests in debug mode if ( $wgRequest->getFuzzyBool( 'debug', $wgResourceLoaderDebug ) ) { foreach ( $this->getModuleStyles() as $name ) { - $tags[] = self::makeResourceLoaderLink( $sk, $name, 'styles' ); + $tags[] = $this->makeResourceLoaderLink( $sk, $name, 'styles' ); } } else { if ( count( $this->getModuleStyles() ) ) { - $tags[] = self::makeResourceLoaderLink( $sk, $this->getModuleStyles(), 'styles' ); + $tags[] = $this->makeResourceLoaderLink( $sk, $this->getModuleStyles(), 'styles' ); } } diff --git a/includes/ResourceLoader.php b/includes/ResourceLoader.php index 5647f69407..1f2538bd8e 100644 --- a/includes/ResourceLoader.php +++ b/includes/ResourceLoader.php @@ -30,30 +30,11 @@ class ResourceLoader { /* Protected Static Members */ // @var array list of module name/ResourceLoaderModule object pairs - protected static $modules = array(); - protected static $initialized = false; + protected $modules = array(); - /* Protected Static Methods */ - - /** - * Registers core modules and runs registration hooks - */ - protected static function initialize() { - global $IP; - - // Safety check - this should never be called more than once - if ( !self::$initialized ) { - wfProfileIn( __METHOD__ ); - // This needs to be first, because hooks might call ResourceLoader - // public interfaces which will call this - self::$initialized = true; - self::register( include( "$IP/resources/Resources.php" ) ); - wfRunHooks( 'ResourceLoaderRegisterModules' ); - wfProfileOut( __METHOD__ ); - } - } + /* Protected Methods */ - /* + /** * Loads information stored in the database about modules * * This is not inside the module code because it's so much more performant to request all of the information at once @@ -62,7 +43,7 @@ class ResourceLoader { * @param $modules array list of module names to preload information for * @param $context ResourceLoaderContext context to load the information within */ - protected static function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) { + protected function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) { $dbr = wfGetDb( DB_SLAVE ); $skin = $context->getSkin(); $lang = $context->getLanguage(); @@ -76,21 +57,21 @@ class ResourceLoader { $modulesWithDeps = array(); foreach ( $res as $row ) { - self::$modules[$row->md_module]->setFileDependencies( $skin, + $this->modules[$row->md_module]->setFileDependencies( $skin, FormatJson::decode( $row->md_deps, true ) ); $modulesWithDeps[] = $row->md_module; } // Register the absence of a dependencies row too foreach ( array_diff( $modules, $modulesWithDeps ) as $name ) { - self::$modules[$name]->setFileDependencies( $skin, array() ); + $this->modules[$name]->setFileDependencies( $skin, array() ); } // Get message blob mtimes. Only do this for modules with messages $modulesWithMessages = array(); $modulesWithoutMessages = array(); foreach ( $modules as $name ) { - if ( count( self::$modules[$name]->getMessages() ) ) { + if ( count( $this->modules[$name]->getMessages() ) ) { $modulesWithMessages[] = $name; } else { $modulesWithoutMessages[] = $name; @@ -103,11 +84,11 @@ class ResourceLoader { ), __METHOD__ ); foreach ( $res as $row ) { - self::$modules[$row->mr_resource]->setMsgBlobMtime( $lang, $row->mr_timestamp ); + $this->modules[$row->mr_resource]->setMsgBlobMtime( $lang, $row->mr_timestamp ); } } foreach ( $modulesWithoutMessages as $name ) { - self::$modules[$name]->setMsgBlobMtime( $lang, 0 ); + $this->modules[$name]->setMsgBlobMtime( $lang, 0 ); } } @@ -119,7 +100,7 @@ class ResourceLoader { * @param $file String: path to file being filtered, (optional: only required for CSS to resolve paths) * @return String: filtered data */ - protected static function filter( $filter, $data ) { + protected function filter( $filter, $data ) { global $wgMemc; wfProfileIn( __METHOD__ ); @@ -166,7 +147,23 @@ class ResourceLoader { return $result; } - /* Static Methods */ + /* Methods */ + + /** + * Registers core modules and runs registration hooks + */ + public function __construct() { + global $IP; + + wfProfileIn( __METHOD__ ); + + // Register core modules + $this->register( include( "$IP/resources/Resources.php" ) ); + // Register extension modules + wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) ); + + wfProfileOut( __METHOD__ ); + } /** * Registers a module with the ResourceLoader system. @@ -184,14 +181,13 @@ class ResourceLoader { * happened, but in bringing errors to the client in a way that they can * easily see them if they want to, such as by using FireBug */ - public static function register( $name, ResourceLoaderModule $object = null ) { + public function register( $name, ResourceLoaderModule $object = null ) { wfProfileIn( __METHOD__ ); - self::initialize(); // Allow multiple modules to be registered in one call if ( is_array( $name ) && !isset( $object ) ) { foreach ( $name as $key => $value ) { - self::register( $key, $value ); + $this->register( $key, $value ); } wfProfileOut( __METHOD__ ); @@ -199,13 +195,14 @@ class ResourceLoader { } // Disallow duplicate registrations - if ( isset( self::$modules[$name] ) ) { + if ( isset( $this->modules[$name] ) ) { // A module has already been registered by this name throw new MWException( 'Another module has already been registered as ' . $name ); } // Attach module - self::$modules[$name] = $object; + $this->modules[$name] = $object; $object->setName( $name ); + wfProfileOut( __METHOD__ ); } @@ -214,11 +211,8 @@ class ResourceLoader { * * @return Array: array( modulename => ResourceLoaderModule ) */ - public static function getModules() { - - self::initialize(); - - return self::$modules; + public function getModules() { + return $this->modules; } /** @@ -227,31 +221,8 @@ class ResourceLoader { * @param $name String: module name * @return mixed ResourceLoaderModule or null if not registered */ - public static function getModule( $name ) { - - self::initialize(); - - return isset( self::$modules[$name] ) ? self::$modules[$name] : null; - } - - /** - * Get the highest modification time of all modules, based on a given - * combination of language code, skin name and debug mode flag. - * - * @param $context ResourceLoaderContext object - * @return Integer: UNIX timestamp - */ - public static function getHighestModifiedTime( ResourceLoaderContext $context ) { - - self::initialize(); - - $time = 1; // wfTimestamp() treats 0 as 'now', so that's not a suitable choice - - foreach ( self::$modules as $module ) { - $time = max( $time, $module->getModifiedTime( $context ) ); - } - - return $time; + public function getModule( $name ) { + return isset( $this->modules[$name] ) ? $this->modules[$name] : null; } /** @@ -259,19 +230,18 @@ class ResourceLoader { * * @param $context ResourceLoaderContext object */ - public static function respond( ResourceLoaderContext $context ) { + public function respond( ResourceLoaderContext $context ) { global $wgResourceLoaderMaxage; wfProfileIn( __METHOD__ ); - self::initialize(); // Split requested modules into two groups, modules and missing $modules = array(); $missing = array(); foreach ( $context->getModules() as $name ) { - if ( isset( self::$modules[$name] ) ) { - $modules[$name] = self::$modules[$name]; + if ( isset( $this->modules[$name] ) ) { + $modules[$name] = $this->modules[$name]; } else { $missing[] = $name; } @@ -291,7 +261,7 @@ class ResourceLoader { } // Preload information needed to the mtime calculation below - self::preloadModuleInfo( array_keys( $modules ), $context ); + $this->preloadModuleInfo( array_keys( $modules ), $context ); // To send Last-Modified and support If-Modified-Since, we need to detect // the last modified time @@ -326,10 +296,10 @@ class ResourceLoader { wfProfileOut( __METHOD__ ); } - public static function makeModuleResponse( ResourceLoaderContext $context, array $modules, $missing = null ) { + public function makeModuleResponse( ResourceLoaderContext $context, array $modules, $missing = null ) { // Pre-fetch blobs $blobs = $context->shouldIncludeMessages() ? - MessageBlobStore::get( array_keys( $modules ), $context->getLanguage() ) : array(); + MessageBlobStore::get( $modules, $context->getLanguage() ) : array(); // Generate output $out = ''; @@ -349,9 +319,9 @@ class ResourceLoader { ( count( $styles = $module->getStyles( $context ) ) ) ) { // Flip CSS on a per-module basis - if ( self::$modules[$name]->getFlip( $context ) ) { + if ( $this->modules[$name]->getFlip( $context ) ) { foreach ( $styles as $media => $style ) { - $styles[$media] = self::filter( 'flip-css', $style ); + $styles[$media] = $this->filter( 'flip-css', $style ); } } } @@ -374,7 +344,7 @@ class ResourceLoader { // Minify CSS before embedding in mediaWiki.loader.implement call (unless in debug mode) if ( !$context->getDebug() ) { foreach ( $styles as $media => $style ) { - $styles[$media] = self::filter( 'minify-css', $style ); + $styles[$media] = $this->filter( 'minify-css', $style ); } } $out .= self::makeLoaderImplementScript( $name, $scripts, $styles, $messages ); @@ -400,14 +370,14 @@ class ResourceLoader { return $out; } else { if ( $context->getOnly() === 'styles' ) { - return self::filter( 'minify-css', $out ); + return $this->filter( 'minify-css', $out ); } else { - return self::filter( 'minify-js', $out ); + return $this->filter( 'minify-js', $out ); } } } - // Client code generation methods + /* Static Methods */ public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) { if ( is_array( $scripts ) ) { diff --git a/includes/ResourceLoaderContext.php b/includes/ResourceLoaderContext.php index c8b3baf235..1dca623e66 100644 --- a/includes/ResourceLoaderContext.php +++ b/includes/ResourceLoaderContext.php @@ -30,6 +30,7 @@ class ResourceLoaderContext { /* Protected Members */ + protected $resourceLoader; protected $request; protected $modules; protected $language; @@ -43,9 +44,10 @@ class ResourceLoaderContext { /* Methods */ - public function __construct( WebRequest $request ) { + public function __construct( ResourceLoader $resourceLoader, WebRequest $request ) { global $wgLang, $wgDefaultSkin, $wgResourceLoaderDebug; + $this->resourceLoader = $resourceLoader; $this->request = $request; // Interperet request $this->modules = explode( '|', $request->getVal( 'modules' ) ); @@ -71,6 +73,10 @@ class ResourceLoaderContext { } } + public function getResourceLoader() { + return $this->resourceLoader; + } + public function getRequest() { return $this->request; } diff --git a/includes/ResourceLoaderModule.php b/includes/ResourceLoaderModule.php index 751cb1bba2..8e0464310c 100644 --- a/includes/ResourceLoaderModule.php +++ b/includes/ResourceLoaderModule.php @@ -1054,7 +1054,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { $out = ''; $registrations = array(); - foreach ( ResourceLoader::getModules() as $name => $module ) { + foreach ( $context->getResourceLoader()->getModules() as $name => $module ) { // Support module loader scripts if ( ( $loader = $module->getLoaderScript() ) !== false ) { $deps = FormatJson::encode( $module->getDependencies() ); @@ -1104,8 +1104,8 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'skin' => $context->getSkin(), 'debug' => $context->getDebug() ? 'true' : 'false', 'version' => wfTimestamp( TS_ISO_8601, round( max( - ResourceLoader::getModule( 'jquery' )->getModifiedTime( $context ), - ResourceLoader::getModule( 'mediawiki' )->getModifiedTime( $context ) + $context->getResourceLoader()->getModule( 'jquery' )->getModifiedTime( $context ), + $context->getResourceLoader()->getModule( 'mediawiki' )->getModifiedTime( $context ) ), -2 ) ) ); // Ensure uniform query order @@ -1132,10 +1132,14 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { return $this->modifiedTime[$hash]; } $this->modifiedTime[$hash] = filemtime( "$IP/resources/startup.js" ); - + // ATTENTION!: Because of the line above, this is not going to cause infinite recursion - think carefully // before making changes to this code! - return $this->modifiedTime[$hash] = ResourceLoader::getHighestModifiedTime( $context ); + $time = 1; // wfTimestamp() treats 0 as 'now', so that's not a suitable choice + foreach ( $context->getResourceLoader()->getModules() as $module ) { + $time = max( $time, $module->getModifiedTime( $context ) ); + } + return $this->modifiedTime[$hash] = $time; } public function getFlip( $context ) { diff --git a/load.php b/load.php index 85c4f67b82..9f594e5f11 100644 --- a/load.php +++ b/load.php @@ -45,7 +45,8 @@ if ( $wgRequest->isPathInfoBad() ) { } // Respond to resource loading request -ResourceLoader::respond( new ResourceLoaderContext( $wgRequest ) ); +$resourceLoader = new ResourceLoader(); +$resourceLoader->respond( new ResourceLoaderContext( $resourceLoader, $wgRequest ) ); wfProfileOut( 'load.php' ); wfLogProfilingData(); diff --git a/maintenance/tests/phpunit/includes/ResourceLoaderTest.php b/maintenance/tests/phpunit/includes/ResourceLoaderTest.php index 73c923fabc..98f5f0a098 100644 --- a/maintenance/tests/phpunit/includes/ResourceLoaderTest.php +++ b/maintenance/tests/phpunit/includes/ResourceLoaderTest.php @@ -1,6 +1,7 @@