[BUGFIX] replace non-free jsmin.php with hook
authorChristian Welzel <gawain@camlann.de>
Tue, 15 Nov 2011 20:09:30 +0000 (21:09 +0100)
committerChristian Kuhn <lolli@schwarzbu.ch>
Tue, 14 Aug 2012 11:16:23 +0000 (13:16 +0200)
jsmin.php has a non free license. See referenced bug report for
details. This patch replaces jsmin.php with a hook to provide a
way for extensions to implement own compression algorithms.

Change-Id: I1fc5bfe29aaa20692c4323dd28d5c0a95863cf3d
Fixes: #31832
Releases: 6.0
Reviewed-on: http://review.typo3.org/6682
Reviewed-by: Marcus Schwemer
Tested-by: Marcus Schwemer
Reviewed-by: Stefan Neufeind
Reviewed-by: Wouter Wolters
Tested-by: Wouter Wolters
Reviewed-by: Christian Kuhn
Tested-by: Christian Kuhn
NEWS.txt
t3lib/class.t3lib_div.php
tests/Unit/t3lib/class.t3lib_divTest.php
typo3/contrib/jsmin/jsmin.php [deleted file]

index bd92a65..082b79b 100644 (file)
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -134,6 +134,14 @@ There are tons of solutions on the net and on server basis that can do a
 better job than the core implementation ever did. Therefor the complete
 code was dropped and all TypoScript config.stat* options are obsolete.
 
+* Removed compression of javascript files with jsmin
+
+The default compression of certain javascript files in frontend and backend
+with the jsmin library was removed from the core due to license issues. The
+code segment was substituted with a hook, so extensions can now deliver
+compression solutions if needed. In general, it is a good idea to configure
+a webserver to compress javascript and css files on the webserver with gzip.
+
 ===============================================================================
 Changes and Improvements
 ===============================================================================
index 50387ea..0eca690 100644 (file)
@@ -41,7 +41,7 @@
  * @package TYPO3
  * @subpackage t3lib
  */
-final class t3lib_div {
+class t3lib_div {
 
                // Severity constants used by t3lib_div::sysLog()
        const SYSLOG_SEVERITY_INFO = 0;
@@ -2415,15 +2415,27 @@ final class t3lib_div {
         * @return string Minified script or source string if error happened
         */
        public static function minifyJavaScript($script, &$error = '') {
-               require_once(PATH_typo3 . 'contrib/jsmin/jsmin.php');
-               try {
-                       $error = '';
-                       $script = trim(JSMin::minify(str_replace(CR, '', $script)));
-               }
-               catch (JSMinException $e) {
-                       $error = 'Error while minifying JavaScript: ' . $e->getMessage();
-                       self::devLog($error, 't3lib_div', 2,
-                               array('JavaScript' => $script, 'Stack trace' => $e->getTrace()));
+               if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_div.php']['minifyJavaScript'])) {
+                       $fakeThis = FALSE;
+                       foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_div.php']['minifyJavaScript'] as $hookMethod) {
+                               try {
+                                       $parameters = array('script' => $script);
+                                       $script = static::callUserFunction($hookMethod, $parameters, $fakeThis);
+                               } catch (Exception $e) {
+                                       $errorMessage = 'Error minifying java script: ' . $e->getMessage();
+                                       $error .= $errorMessage;
+                                       static::devLog(
+                                               $errorMessage,
+                                               't3lib_div',
+                                               2,
+                                               array(
+                                                       'JavaScript' => $script,
+                                                       'Stack trace' => $e->getTrace(),
+                                                       'hook' => $hookMethod
+                                               )
+                                       );
+                               }
+                       }
                }
                return $script;
        }
@@ -5326,4 +5338,4 @@ final class t3lib_div {
        }
 }
 
-?>
+?>
\ No newline at end of file
index 3434513..41216cb 100644 (file)
@@ -2739,6 +2739,138 @@ class t3lib_divTest extends tx_phpunit_testcase {
                );
        }
 
+       ///////////////////////////
+       // Tests concerning minifyJavaScript
+       ///////////////////////////
+
+       /**
+        * @test
+        */
+       public function minifyJavaScriptReturnsInputStringIfNoHookIsRegistered() {
+               unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_div.php']['minifyJavaScript']);
+               $testString = uniqid('string');
+               $this->assertSame($testString, t3lib_div::minifyJavaScript($testString));
+       }
+
+       /**
+        * Create an own hook callback class, register as hook, and check
+        * if given string to compress is given to hook method
+        *
+        * @test
+        */
+       public function minifyJavaScriptCallsRegisteredHookWithInputString() {
+               $hookClassName = uniqid('tx_coretest');
+               $minifyHookMock = $this->getMock(
+                       'stdClass',
+                       array('minify'),
+                       array(),
+                       $hookClassName
+               );
+
+               $functionName = '&' . $hookClassName . '->minify';
+               $GLOBALS['T3_VAR']['callUserFunction'][$functionName] = array();
+               $GLOBALS['T3_VAR']['callUserFunction'][$functionName]['obj'] = $minifyHookMock;
+               $GLOBALS['T3_VAR']['callUserFunction'][$functionName]['method'] = 'minify';
+               $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_div.php']['minifyJavaScript'][] = $functionName;
+
+               $minifyHookMock->expects($this->once())->method('minify')
+                       ->will($this->returnCallback(array($this, 'isMinifyJavaScriptHookCalledCallback')));
+
+               t3lib_div::minifyJavaScript('foo');
+       }
+
+       /**
+        * Callback function used in minifyJavaScriptCallsRegisteredHookWithInputString test
+        *
+        * @param array $params
+        */
+       public function isMinifyJavaScriptHookCalledCallback(array $params) {
+                       // We can not throw an exception here, because that would be caught by the
+                       // minifyJavaScript method under test itself. Thus, we just die if the
+                       // input string is not ok.
+               if ($params['script'] !== 'foo') {
+                       die('broken');
+               }
+       }
+
+       /**
+        * Create a hook callback, use callback to throw an exception and check
+        * if the exception is given as error parameter to the calling method.
+        *
+        * @test
+        */
+       public function minifyJavaScriptReturnsErrorStringOfHookException() {
+               $hookClassName = uniqid('tx_coretest');
+               $minifyHookMock = $this->getMock(
+                       'stdClass',
+                       array('minify'),
+                       array(),
+                       $hookClassName
+               );
+
+               $functionName = '&' . $hookClassName . '->minify';
+               $GLOBALS['T3_VAR']['callUserFunction'][$functionName] = array();
+               $GLOBALS['T3_VAR']['callUserFunction'][$functionName]['obj'] = $minifyHookMock;
+               $GLOBALS['T3_VAR']['callUserFunction'][$functionName]['method'] = 'minify';
+               $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_div.php']['minifyJavaScript'][] = $functionName;
+
+               $minifyHookMock->expects($this->any())->method('minify')
+                       ->will($this->returnCallback(array($this, 'minifyJavaScriptErroneousCallback')));
+
+               $error = '';
+               t3lib_div::minifyJavaScript('string to compress', $error);
+               $this->assertSame('Error minifying java script: foo', $error);
+       }
+
+       /**
+        * Check if the error message that is returned by the hook callback
+        * is logged to t3lib_div::devLog.
+        *
+        * @test
+        */
+       public function minifyJavaScriptWritesExceptionMessageToDevLog() {
+               $t3libDivMock = uniqid('t3lib_div');
+               eval(
+                       'class ' . $t3libDivMock . ' extends t3lib_div {' .
+                       '  public static function devLog($errorMessage) {' .
+                       '    if (!($errorMessage === \'Error minifying java script: foo\')) {' .
+                       '      throw new UnexpectedValue(\'broken\');' .
+                       '    }' .
+                       '    throw new RuntimeException();' .
+                       '  }' .
+                       '}'
+               );
+               $hookClassName = uniqid('tx_coretest');
+               $minifyHookMock = $this->getMock(
+                       'stdClass',
+                       array('minify'),
+                       array(),
+                       $hookClassName
+               );
+
+               $functionName = '&' . $hookClassName . '->minify';
+               $GLOBALS['T3_VAR']['callUserFunction'][$functionName] = array();
+               $GLOBALS['T3_VAR']['callUserFunction'][$functionName]['obj'] = $minifyHookMock;
+               $GLOBALS['T3_VAR']['callUserFunction'][$functionName]['method'] = 'minify';
+               $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_div.php']['minifyJavaScript'][] = $functionName;
+
+               $minifyHookMock->expects($this->any())->method('minify')
+                       ->will($this->returnCallback(array($this, 'minifyJavaScriptErroneousCallback')));
+
+               $this->setExpectedException('RuntimeException');
+               $t3libDivMock::minifyJavaScript('string to compress');
+       }
+
+       /**
+        * Callback function used in
+        *      minifyJavaScriptReturnsErrorStringOfHookException and
+        *      minifyJavaScriptWritesExceptionMessageToDevLog
+        *
+        * @throws RuntimeException
+        */
+       public function minifyJavaScriptErroneousCallback() {
+               throw new RuntimeException('foo', 1344888548);
+       }
 
        ///////////////////////////
        // Tests concerning getUrl
diff --git a/typo3/contrib/jsmin/jsmin.php b/typo3/contrib/jsmin/jsmin.php
deleted file mode 100644 (file)
index d77d083..0000000
+++ /dev/null
@@ -1,296 +0,0 @@
-<?php
-/**
- * jsmin.php - PHP implementation of Douglas Crockford's JSMin.
- *
- * This is pretty much a direct port of jsmin.c to PHP with just a few
- * PHP-specific performance tweaks. Also, whereas jsmin.c reads from stdin and
- * outputs to stdout, this library accepts a string as input and returns another
- * string as output.
- *
- * PHP 5 or higher is required.
- *
- * Permission is hereby granted to use this version of the library under the
- * same terms as jsmin.c, which has the following license:
- *
- * --
- * Copyright (c) 2002 Douglas Crockford  (www.crockford.com)
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy of
- * this software and associated documentation files (the "Software"), to deal in
- * the Software without restriction, including without limitation the rights to
- * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
- * of the Software, and to permit persons to whom the Software is furnished to do
- * so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * The Software shall be used for Good, not Evil.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- * --
- *
- * @package JSMin
- * @author Ryan Grove <ryan@wonko.com>
- * @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c)
- * @copyright 2007 Ryan Grove <ryan@wonko.com> (PHP port)
- * @license http://opensource.org/licenses/mit-license.php MIT License
- * @version 1.1.0 (2007-06-01)
- * @link http://code.google.com/p/jsmin-php/
- */
-
-class JSMin {
-  const ORD_LF    = 10;
-  const ORD_SPACE = 32;
-
-  protected $a           = '';
-  protected $b           = '';
-  protected $input       = '';
-  protected $inputIndex  = 0;
-  protected $inputLength = 0;
-  protected $lookAhead   = null;
-  protected $output      = array();
-
-  // -- Public Static Methods --------------------------------------------------
-
-  public static function minify($js) {
-    $jsmin = new JSMin($js);
-    return $jsmin->min();
-  }
-
-  // -- Public Instance Methods ------------------------------------------------
-
-  public function __construct($input) {
-    $this->input       = str_replace("\r\n", "\n", $input);
-    $this->inputLength = strlen($this->input);
-  }
-
-  // -- Protected Instance Methods ---------------------------------------------
-
-  protected function action($d) {
-    switch($d) {
-      case 1:
-        $this->output[] = $this->a;
-
-      case 2:
-        $this->a = $this->b;
-
-        if ($this->a === "'" || $this->a === '"') {
-          for (;;) {
-            $this->output[] = $this->a;
-            $this->a        = $this->get();
-
-            if ($this->a === $this->b) {
-              break;
-            }
-
-            if (ord($this->a) <= self::ORD_LF) {
-              throw new JSMinException('Unterminated string literal.');
-            }
-
-            if ($this->a === '\\') {
-              $this->output[] = $this->a;
-              $this->a        = $this->get();
-            }
-          }
-        }
-
-      case 3:
-        $this->b = $this->next();
-
-        if ($this->b === '/' && (
-            $this->a === '(' || $this->a === ',' || $this->a === '=' ||
-            $this->a === ':' || $this->a === '[' || $this->a === '!' ||
-            $this->a === '&' || $this->a === '|' || $this->a === '?')) {
-
-          $this->output[] = $this->a;
-          $this->output[] = $this->b;
-
-          for (;;) {
-            $this->a = $this->get();
-
-            if ($this->a === '/') {
-              break;
-            }
-            elseif ($this->a === '\\') {
-              $this->output[] = $this->a;
-              $this->a        = $this->get();
-            }
-            elseif (ord($this->a) <= self::ORD_LF) {
-              throw new JSMinException('Unterminated regular expression '.
-                  'literal.');
-            }
-
-            $this->output[] = $this->a;
-          }
-
-          $this->b = $this->next();
-        }
-    }
-  }
-
-  protected function get() {
-    $c = $this->lookAhead;
-    $this->lookAhead = null;
-
-    if ($c === null) {
-      if ($this->inputIndex < $this->inputLength) {
-        $c = $this->input[$this->inputIndex];
-        $this->inputIndex += 1;
-      }
-      else {
-        $c = null;
-      }
-    }
-
-    if ($c === "\r") {
-      return "\n";
-    }
-
-    if ($c === null || $c === "\n" || ord($c) >= self::ORD_SPACE) {
-      return $c;
-    }
-
-    return ' ';
-  }
-
-  protected function isAlphaNum($c) {
-    return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1;
-  }
-
-  protected function min() {
-    $this->a = "\n";
-    $this->action(3);
-
-    while ($this->a !== null) {
-      switch ($this->a) {
-        case ' ':
-          if ($this->isAlphaNum($this->b)) {
-            $this->action(1);
-          }
-          else {
-            $this->action(2);
-          }
-          break;
-
-        case "\n":
-          switch ($this->b) {
-            case '{':
-            case '[':
-            case '(':
-            case '+':
-            case '-':
-              $this->action(1);
-              break;
-
-            case ' ':
-              $this->action(3);
-              break;
-
-            default:
-              if ($this->isAlphaNum($this->b)) {
-                $this->action(1);
-              }
-              else {
-                $this->action(2);
-              }
-          }
-          break;
-
-        default:
-          switch ($this->b) {
-            case ' ':
-              if ($this->isAlphaNum($this->a)) {
-                $this->action(1);
-                break;
-              }
-
-              $this->action(3);
-              break;
-
-            case "\n":
-              switch ($this->a) {
-                case '}':
-                case ']':
-                case ')':
-                case '+':
-                case '-':
-                case '"':
-                case "'":
-                  $this->action(1);
-                  break;
-
-                default:
-                  if ($this->isAlphaNum($this->a)) {
-                    $this->action(1);
-                  }
-                  else {
-                    $this->action(3);
-                  }
-              }
-              break;
-
-            default:
-              $this->action(1);
-              break;
-          }
-      }
-    }
-
-    return implode('', $this->output);
-  }
-
-  protected function next() {
-    $c = $this->get();
-
-    if ($c === '/') {
-      switch($this->peek()) {
-        case '/':
-          for (;;) {
-            $c = $this->get();
-
-            if (ord($c) <= self::ORD_LF) {
-              return $c;
-            }
-          }
-
-        case '*':
-          $this->get();
-
-          for (;;) {
-            switch($this->get()) {
-              case '*':
-                if ($this->peek() === '/') {
-                  $this->get();
-                  return ' ';
-                }
-                break;
-
-              case null:
-                throw new JSMinException('Unterminated comment.');
-            }
-          }
-
-        default:
-          return $c;
-      }
-    }
-
-    return $c;
-  }
-
-  protected function peek() {
-    $this->lookAhead = $this->get();
-    return $this->lookAhead;
-  }
-}
-
-// -- Exceptions ---------------------------------------------------------------
-class JSMinException extends Exception {}
-?>
\ No newline at end of file