* Added feature #13998: Introduce automatic versioning of CSS and JS files (thanks...
authorErnesto Baschny <ernst@cron-it.de>
Tue, 13 Apr 2010 18:48:51 +0000 (18:48 +0000)
committerErnesto Baschny <ernst@cron-it.de>
Tue, 13 Apr 2010 18:48:51 +0000 (18:48 +0000)
git-svn-id: https://svn.typo3.org/TYPO3v4/Core/trunk@7346 709f56b5-9817-0410-a4d7-c38de5d9e867

ChangeLog
NEWS.txt
t3lib/class.t3lib_div.php
t3lib/class.t3lib_pagerenderer.php
t3lib/config_default.php
typo3/alt_main.php
typo3/sysext/cms/layout/db_layout.php
typo3/sysext/cms/tslib/class.tslib_content.php
typo3/sysext/recycler/mod1/index.php
typo3/sysext/t3editor/classes/class.tx_t3editor.php

index 7f92a7c..b8a659e 100755 (executable)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,7 @@
+2010-04-13  Ernesto Baschny  <ernst@cron-it.de>
+
+       * Added feature #13998: Introduce automatic versioning of CSS and JS files (thanks to Lars Houmark)
+
 2010-04-13  Benjamin Mack  <benni@typo3.org>
 
        * Added feature #13865: Possibility to configure alternative parameter for jumpurl filelinks (Thanks to Sebastian Gebhard)
index 8b3df5d..adba071 100644 (file)
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -18,6 +18,14 @@ General
                5. Color palettes become ExtJS ColorPalettes.
                6. All dialogue windows become ExtJS windows.
 
+       * Automatic version-numbers of CSS and JS files to avoid caching problems: This feature provides automatic numbering of CSS and JS files using the files modified timestamp. This way the file reference will change when a CSS or JS files is changed, and by this the browser and proxy will re-cache the file. Can be configured to include the timestamp within the the filename (before .ext) or as a parameter to the file (default).
+         If versioning is done inside the filename (by setting TYPO3_CONF_VARS[BE][versionNumberInFilename] to true) you need this line as the first rewrite rule in .htaccess:
+               # Rule for versioned static files (see TYPO3_CONF_VARS[BE][versionNumberInFilename])
+               RewriteCond %{REQUEST_FILENAME} !-f
+               RewriteCond %{REQUEST_FILENAME} !-d
+               RewriteRule ^(.+)\.(\d+)\.(php|js|css|png|jpg|gif)$ $1.$3 [L]
+         Developers can use this API for versioning of files in the own backend mods, by calling t3lib_div::createVersionNumberedFilename or using the core API for including files in the page renderer class.
+
 Backend
 =======
 
index 0f9fc0a..73be877 100644 (file)
@@ -3351,7 +3351,68 @@ final class t3lib_div {
        }
 
 
+       /**
+        * Function for static version numbers on files, based on the filemtime
+        *
+        * This will make the filename automatically change when a file is
+        * changed, and by that re-cached by the browser. If the file does not
+        * exist physically the original file passed to the function is
+        * returned without the timestamp.
+        * 
+        * Behaviour is influenced by the setting 
+        * TYPO3_CONF_VARS[TYPO3_MODE][versionNumberInFilename]
+        * = true (BE) / "embed" (FE) : modify filename
+        * = false (BE) / "querystring" (FE) : add timestamp as parameter
+        *
+        * @param string $file Relative path to file including all potential query parameters (not htmlspecialchared yet)
+        * @param boolean $forceQueryString If settings would suggest to embed in filename, this parameter allows us to force the versioning to occur in the query string. This is needed for scriptaculous.js which cannot have a different filename in order to load its modules (?load=...)
+        * @return Relative path with version filename including the timestamp
+        * @author Lars Houmark <lars@houmark.com>
+        */
+       public static function createVersionNumberedFilename($file, $forceQueryString = FALSE) {
+               $lookupFile = explode('?', $file);
+               $path = self::resolveBackPath(self::dirname(PATH_thisScript) .'/'. $lookupFile[0]);
+
+               if (TYPO3_MODE == 'FE') {
+                       $mode = strtolower($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['versionNumberInFilename']);
+                       if ($mode === 'embed') {
+                               $mode = TRUE;
+                       } else if ($mode === 'querystring') {
+                               $mode = FALSE;
+                       } else {
+                               $doNothing = TRUE;
+                       }
+               } else {
+                       $mode = $GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['versionNumberInFilename'];
+               }
+
+               if (! file_exists($path) || $doNothing) {
+                               // File not found, return filename unaltered
+                       $fullName = $file;
 
+               } else if (! $mode || $forceQueryString) {
+                               // If use of .htaccess rule is not configured,
+                               // we use the default query-string method
+                       if ($lookupFile[1]) {
+                               $separator = '&';
+                       } else {
+                               $separator = '?';
+                       }
+                       $fullName = $file . $separator . filemtime($path);
+
+               } else {
+                               // Change the filename
+                       $name = explode('.', $lookupFile[0]);
+                       $extension = array_pop($name);
+               
+                       array_push($name, filemtime($path), $extension);
+                       $fullName = implode('.', $name);
+                               // append potential query string
+                       $fullName .= $lookupFile[1] ? '?' . $lookupFile[1] : '';
+               }
+
+               return $fullName;
+       }
 
 
 
index d55b5ac..b5d2450 100644 (file)
@@ -1000,8 +1000,12 @@ class t3lib_PageRenderer implements t3lib_Singleton {
 
                if (count($this->cssFiles)) {
                        foreach ($this->cssFiles as $file => $properties) {
-                               $file = htmlspecialchars(t3lib_div::resolveBackPath($file));
-                               $tag = '<link rel="' . $properties['rel'] . '" type="text/css" href="' . $file . '" media="' . $properties['media'] . '"' . ($properties['title'] ? ' title="' . $properties['title'] . '"' : '') . ' />';
+                               $file = t3lib_div::resolveBackPath($file);
+                               $file = t3lib_div::createVersionNumberedFilename($file);
+                               $tag = '<link rel="' . $properties['rel'] . '" type="text/css" href="' .
+                                       htmlspecialchars($file) . '" media="' . $properties['media'] . '"' .
+                                       ($properties['title'] ? ' title="' . $properties['title'] . '"' : '') .
+                                       ' />';
                                if ($properties['allWrap'] && strpos($properties['allWrap'], '|') !== FALSE) {
                                        $tag = str_replace('|', $tag, $properties['allWrap']);
                                }
@@ -1028,10 +1032,9 @@ class t3lib_PageRenderer implements t3lib_Singleton {
 
                if (count($this->jsLibs)) {
                        foreach ($this->jsLibs as $name => $properties) {
-                               $properties['file'] = htmlspecialchars(
-                                       t3lib_div::resolveBackPath($properties['file'])
-                               );
-                               $tag = '<script src="' . $properties['file'] . '" type="' . $properties['type'] . '"></script>';
+                               $properties['file'] = t3lib_div::resolveBackPath($properties['file']);
+                               $properties['file'] = t3lib_div::createVersionNumberedFilename($properties['file']);
+                               $tag = '<script src="' . htmlspecialchars($properties['file']) . '" type="' . $properties['type'] . '"></script>';
                                if ($properties['allWrap'] && strpos($properties['allWrap'], '|') !== FALSE) {
                                        $tag = str_replace('|', $tag, $properties['allWrap']);
                                }
@@ -1054,8 +1057,9 @@ class t3lib_PageRenderer implements t3lib_Singleton {
 
                if (count($this->jsFiles)) {
                        foreach ($this->jsFiles as $file => $properties) {
-                                       $file = htmlspecialchars(t3lib_div::resolveBackPath($file));
-                                       $tag = '<script src="' . $file . '" type="' . $properties['type'] . '"></script>';
+                                       $file = t3lib_div::resolveBackPath($file);
+                                       $file = t3lib_div::createVersionNumberedFilename($file);
+                                       $tag = '<script src="' . htmlspecialchars($file) . '" type="' . $properties['type'] . '"></script>';
                                        if ($properties['allWrap'] && strpos($properties['allWrap'], '|') !== FALSE) {
                                                $tag = str_replace('|', $tag, $properties['allWrap']);
                                        }
@@ -1162,7 +1166,8 @@ class t3lib_PageRenderer implements t3lib_Singleton {
                $out = '';
 
                if ($this->addPrototype) {
-                       $out .= '<script src="' . $this->backPath . 'contrib/prototype/prototype.js" type="text/javascript"></script>' . LF;
+                       $out .= '<script src="' . t3lib_div::createVersionNumberedFilename($this->backPath .
+                               'contrib/prototype/prototype.js') . '" type="text/javascript"></script>' . LF;
                        unset($this->jsFiles[$this->backPath . 'contrib/prototype/prototype.js']);
                }
 
@@ -1181,22 +1186,30 @@ class t3lib_PageRenderer implements t3lib_Singleton {
                        if (count($mods)) {
                                $moduleLoadString = '?load=' . implode(',', $mods);
                        }
-
-                       $out .= '<script src="' . $this->backPath . 'contrib/scriptaculous/scriptaculous.js' . $moduleLoadString . '" type="text/javascript"></script>' . LF;
+                       $out .= '<script src="' . t3lib_div::createVersionNumberedFilename($this->backPath .
+                               'contrib/scriptaculous/scriptaculous.js' . $moduleLoadString, TRUE) .
+                               '" type="text/javascript"></script>' . LF;
                        unset($this->jsFiles[$this->backPath . 'contrib/scriptaculous/scriptaculous.js' . $moduleLoadString]);
                }
 
                        // include extCore
                if ($this->addExtCore) {
-                       $out .= '<script src="' . $this->backPath . 'contrib/extjs/ext-core' . ($this->enableExtCoreDebug ? '-debug' : '') . '.js" type="text/javascript"></script>' . LF;
+                       $out .= '<script src="' . t3lib_div::createVersionNumberedFilename($this->backPath .
+                               'contrib/extjs/ext-core' . ($this->enableExtCoreDebug ? '-debug' : '') . '.js') .
+                               '" type="text/javascript"></script>' . LF;
                        unset($this->jsFiles[$this->backPath . 'contrib/extjs/ext-core' . ($this->enableExtCoreDebug ? '-debug' : '') . '.js']);
                }
 
                        // include extJS
                if ($this->addExtJS) {
                                // use the base adapter all the time
-                       $out .= '<script src="' . $this->backPath . 'contrib/extjs/adapter/' . ($this->enableExtJsDebug ? str_replace('.js', '-debug.js', $this->extJSadapter) : $this->extJSadapter) . '" type="text/javascript"></script>' . LF;
-                       $out .= '<script src="' . $this->backPath . 'contrib/extjs/ext-all' . ($this->enableExtJsDebug ? '-debug' : '') . '.js" type="text/javascript"></script>' . LF;
+                       $out .= '<script src="' . t3lib_div::createVersionNumberedFilename($this->backPath .
+                               'contrib/extjs/adapter/' . ($this->enableExtJsDebug ?
+                                       str_replace('.js', '-debug.js', $this->extJSadapter) : $this->extJSadapter)) .
+                               '" type="text/javascript"></script>' . LF;
+                       $out .= '<script src="' . t3lib_div::createVersionNumberedFilename($this->backPath .
+                               'contrib/extjs/ext-all' . ($this->enableExtJsDebug ? '-debug' : '') . '.js') .
+                               '" type="text/javascript"></script>' . LF;
 
                                // add extJS localization
                        $localeMap = $this->csConvObj->isoArray; // load standard ISO mapping and modify for use with ExtJS
@@ -1211,7 +1224,8 @@ class t3lib_PageRenderer implements t3lib_Singleton {
                                // TODO autoconvert file from UTF8 to current BE charset if necessary!!!!
                        $extJsLocaleFile = 'contrib/extjs/locale/ext-lang-' . $extJsLang . '.js';
                        if (file_exists(PATH_typo3 . $extJsLocaleFile)) {
-                               $out .= '<script src="' . $this->backPath . $extJsLocaleFile . '" type="text/javascript" charset="utf-8"></script>' . LF;
+                               $out .= '<script src="' . t3lib_div::createVersionNumberedFilename($this->backPath .
+                                       $extJsLocaleFile) . '" type="text/javascript" charset="utf-8"></script>' . LF;
                        }
 
 
index f8c0806..9725a91 100644 (file)
@@ -257,6 +257,7 @@ $TYPO3_CONF_VARS = array(
                'flexformForceCDATA' => 0,                              // Boolean:  If set, will add CDATA to Flexform XML. Some versions of libxml have a bug that causes HTML entities to be stripped from any XML content and this setting will avoid the bug by adding CDATA.
                'explicitConfirmationOfTranslation' => FALSE,   // If set, then the diff-data of localized records is not saved automatically when updated but requires that a translator clicks the special finish_translation/save/close button that becomes available.
                'elementVersioningOnly' => FALSE,               // If true, only element versioning is allowed in the backend. This is recommended for new installations of TYPO3 4.2+ since "page" and "branch" versioning types are known for the drawbacks of loosing ids and "element" type versions supports moving now.
+               'versionNumberInFilename' => FALSE, // Boolean. If true, included CSS and JS files will have the timestamp embedded in the filename, ie. filename.1269312081.js. This will make browsers and proxies reload the files if they change (thus avoiding caching issues). IMPORTANT: this feature requires this .htaccess rule to work: RewriteCond %{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_FILENAME} !-d - RewriteRule ^(.+)\.(\d+)\.(php|js|css|png|jpg|gif)$ $1.$3 [L]. If false the filemtime will be appended as a query-string.
                'AJAX' => array(                                // array of key-value pairs for a unified use of AJAX calls in the TYPO3 backend. Keys are the unique ajaxIDs where the value will be resolved to call a method in an object. See ajax.php and the classes/class.typo3ajax.php for more information.
                        'SC_alt_db_navframe::expandCollapse'                => 'typo3/alt_db_navframe.php:SC_alt_db_navframe->ajaxExpandCollapse',
                        'SC_alt_file_navframe::expandCollapse'              => 'typo3/alt_file_navframe.php:SC_alt_file_navframe->ajaxExpandCollapse',
@@ -338,6 +339,7 @@ $TYPO3_CONF_VARS = array(
                'eID_include' => array(),                               // Array of key/value pairs where key is "tx_[ext]_[optional suffix]" and value is relative filename of class to include. Key is used as "?eID=" for index_ts.php to include the code file which renders the page from that point. (Useful for functionality that requires a low initialization footprint, eg. frontend ajax applications)
                'disableNoCacheParameter' => FALSE,             // Boolean. If set, the no_cache request parameter will become ineffective. This is currently still an experimental feature and will require a website only with plugins that don't use this parameter. However, using "&no_cache=1" should be avoided anyway because there are better ways to disable caching for a certain part of the website (see COA_INT/USER_INT documentation in TSref).
                'workspacePreviewLogoutTemplate' => '', // If set, points to an HTML file relative to the TYPO3_site root which will be read and outputted as template for this message. Example: fileadmin/templates/template_workspace_preview_logout.html. Inside you can put the marker %1$s to insert the URL to go back to. Use this in <a href="%1$s">Go back...</a> links
+               'versionNumberInFilename' => '', // String. Allows to automatically include a version number (timestamp of the file) to referred CSS and JS filenames on the rendered page. This will make browsers and proxies reload the files if they change (thus avoiding caching issues). Set to 'embed' will have the timestamp embedded in the filename, ie. filename.1269312081.js. IMPORTANT: 'embed' requires this .htaccess rule to work: RewriteCond %{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_FILENAME} !-d - RewriteRule ^(.+)\.(\d+)\.(php|js|css|png|jpg|gif)$ $1.$3 [L]. Set to 'querystring' to append the version number as a query parameter (doesn't require mod_rewrite). Default setting of '' will turn this functionality off.
                'XCLASS' => array(),                                    // See 'Inside TYPO3' document for more information.
        ),
        'MODS' => array(                // Backend Module Configuration (obsolete, make extension instead)
index 0f57bc5..49a3be6 100644 (file)
@@ -498,8 +498,8 @@ class SC_alt_main {
                $this->generateJScode();
                $GLOBALS['TBE_TEMPLATE']->JScode= '
                        <script type="text/javascript" src="md5.js"></script>
-                       <script type="text/javascript" src="../t3lib/jsfunc.evalfield.js"></script>
-                       <script type="text/javascript" src="js/backend.js"></script>
+                       <script type="text/javascript" src="' . t3lib_div::createVersionNumberedFilename('../t3lib/jsfunc.evalfield.js') . '"></script>
+                       <script type="text/javascript" src="' . t3lib_div::createVersionNumberedFilename('js/backend.js') . '"></script>
                        ';
                $GLOBALS['TBE_TEMPLATE']->JScode.=$GLOBALS['TBE_TEMPLATE']->wrapScriptTags($this->mainJScode);
 
index 318d191..6da6e76 100755 (executable)
@@ -426,7 +426,9 @@ class SC_db_layout {
                        $this->doc->setModuleTemplate('templates/db_layout.html');
 
                                // JavaScript:
-                       $this->doc->JScode = '<script type="text/javascript" src="'.$BACK_PATH.'../t3lib/jsfunc.updateform.js"></script>';
+                       $this->doc->JScode = '<script type="text/javascript" ' .
+                               'src="' . t3lib_div::createVersionNumberedFilename($BACK_PATH . '../t3lib/jsfunc.updateform.js') . '">' .
+                               '</script>';
                        $this->doc->JScode.= $this->doc->wrapScriptTags('
                                if (top.fsMod) top.fsMod.recentIds["web"] = '.intval($this->id).';
                                if (top.fsMod) top.fsMod.navFrameHighlightedID["web"] = "pages'.intval($this->id).'_"+top.fsMod.currentBank; '.intval($this->id).';
index f514d0f..edf4b7d 100644 (file)
@@ -2204,9 +2204,19 @@ class tslib_cObj {
                $hiddenfields = '<div style="display:none;">'.$hiddenfields.'</div>';
 
                if ($conf['REQ'])       {
-                       $validateForm=' onsubmit="return validateForm(\''.$formname.'\',\''.implode(',',$fieldlist).'\','.t3lib_div::quoteJSvalue($conf['goodMess']).','.t3lib_div::quoteJSvalue($conf['badMess']).','.t3lib_div::quoteJSvalue($conf['emailMess']).')"';
-                       $GLOBALS['TSFE']->additionalHeaderData['JSFormValidate'] = '<script type="text/javascript" src="'.$GLOBALS['TSFE']->absRefPrefix.'t3lib/jsfunc.validateform.js"></script>';
-               } else $validateForm='';
+                       $validateForm = ' onsubmit="return validateForm(\'' .
+                               $formname . '\',\'' . implode(',',$fieldlist) . '\',' .
+                               t3lib_div::quoteJSvalue($conf['goodMess']) . ',' .
+                               t3lib_div::quoteJSvalue($conf['badMess']) . ',' .
+                               t3lib_div::quoteJSvalue($conf['emailMess']) . ')"';
+                       $GLOBALS['TSFE']->additionalHeaderData['JSFormValidate'] =
+                               '<script type="text/javascript" src="' .
+                               t3lib_div::createVersionNumberedFilename($GLOBALS['TSFE']->absRefPrefix .
+                                       't3lib/jsfunc.validateform.js') .
+                               '"</script>';
+               } else {
+                       $validateForm = '';
+               }
 
                        // Create form tag:
                $theTarget = ($theRedirect?$LD['target']:$LD_A['target']);
@@ -6989,7 +6999,11 @@ class tslib_cObj {
        /*]]>*/
 </script>
 ';
-               $GLOBALS['TSFE']->additionalHeaderData['JSincludeFormupdate']='<script type="text/javascript" src="'.$GLOBALS['TSFE']->absRefPrefix.'t3lib/jsfunc.updateform.js"></script>';
+               $GLOBALS['TSFE']->additionalHeaderData['JSincludeFormupdate'] =
+                       '<script type="text/javascript" src="'
+                       . t3lib_div::createVersionNumberedFilename($GLOBALS['TSFE']->absRefPrefix
+                               . 't3lib/jsfunc.updateform.js')
+                       . '"></script>';
                return $JSPart;
        }
 
index 260935b..56d47d7 100644 (file)
@@ -158,7 +158,7 @@ class  tx_recycler_module1 extends t3lib_SCbase {
         */
        protected function loadStylesheet($fileName) {
                $fileName = t3lib_div::resolveBackPath($this->doc->backPath . $fileName);
-               $this->doc->JScode.= TAB . '<link rel="stylesheet" type="text/css" href="' . $fileName . '" />' . LF;
+               $this->doc->JScode .= TAB . '<link rel="stylesheet" type="text/css" href="' . t3lib_div::createVersionNumberedFilename($fileName) . '" />' . LF;
        }
 
        /**
@@ -169,7 +169,7 @@ class  tx_recycler_module1 extends t3lib_SCbase {
         */
        protected function loadJavaScript($fileName) {
                $fileName = t3lib_div::resolveBackPath($this->doc->backPath . $fileName);
-               $this->doc->JScode.= TAB . '<script language="javascript" type="text/javascript" src="' . $fileName . '"></script>' . LF;
+               $this->doc->JScode .= TAB . '<script language="javascript" type="text/javascript" src="' . t3lib_div::createVersionNumberedFilename($fileName) . '"></script>' . LF;
        }
 
        /**
index 4216058..4151ab6 100644 (file)
@@ -126,9 +126,9 @@ class tx_t3editor implements t3lib_Singleton {
 
                                // include editor-css
                        $content .= '<link href="' .
-                               $GLOBALS['BACK_PATH'] .
+                               t3lib_div::createVersionNumberedFilename($GLOBALS['BACK_PATH'] .
                                t3lib_extmgm::extRelPath('t3editor') .
-                               'res/css/t3editor.css' .
+                               'res/css/t3editor.css') .
                                '" type="text/css" rel="stylesheet" />';
 
                                // include editor-js-lib