Merge branch 'MDL-67372-master' of git://github.com/rezaies/moodle
authorSara Arjona <sara@moodle.com>
Fri, 17 Jan 2020 08:30:11 +0000 (09:30 +0100)
committerSara Arjona <sara@moodle.com>
Fri, 17 Jan 2020 08:30:11 +0000 (09:30 +0100)
90 files changed:
admin/cli/upgrade.php
admin/index.php
admin/renderer.php
admin/settings/server.php
admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js
admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js.map
admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
admin/tool/policy/readme_moodle.txt
admin/tool/policy/thirdpartylibs.xml
auth/cas/CAS/CAS.php
auth/cas/CAS/CAS/Client.php
auth/cas/CAS/README.md
auth/cas/CAS/moodle_readme.txt
backup/backup.class.php
backup/backup.php
backup/controller/backup_controller.class.php
backup/controller/base_controller.class.php
backup/controller/restore_controller.class.php
backup/externallib.php
backup/restore.php
calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php
course/classes/category.php
course/lib.php
grade/report/singleview/templates/bulk_insert.mustache
install/lang/ar_wp/langconfig.php [new file with mode: 0644]
install/lang/km/admin.php
lang/en/error.php
lang/en/moodle.php
lang/en/plugin.php
lang/en/role.php
lib/antivirus/clamav/adminlib.php
lib/antivirus/clamav/classes/scanner.php
lib/antivirus/clamav/lang/en/antivirus_clamav.php
lib/antivirus/clamav/settings.php
lib/antivirus/clamav/tests/scanner_test.php
lib/antivirus/clamav/version.php
lib/classes/plugin_manager.php
lib/classes/plugininfo/base.php
lib/classes/task/manager.php
lib/datalib.php
lib/db/access.php
lib/db/install.php
lib/db/install.xml
lib/db/upgrade.php
lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-debug.js
lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-min.js
lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button.js
lib/editor/atto/plugins/accessibilitychecker/yui/src/button/js/button.js
lib/mlbackend/python/classes/processor.php
lib/mustache/readme_moodle.txt
lib/mustache/src/Mustache/Engine.php
lib/mustache/src/Mustache/Tokenizer.php
lib/scssphp/Cache.php
lib/scssphp/Colors.php
lib/scssphp/Compiler.php
lib/scssphp/Formatter.php
lib/scssphp/Formatter/Expanded.php
lib/scssphp/Formatter/Nested.php
lib/scssphp/Node/Number.php
lib/scssphp/Parser.php
lib/scssphp/SourceMap/Base64VLQ.php
lib/scssphp/SourceMap/Base64VLQEncoder.php [deleted file]
lib/scssphp/Version.php
lib/scssphp/moodle_readme.txt
lib/setup.php
lib/tests/behat/app_behat_runtime.js
lib/tests/fixtures/testable_plugin_manager.php
lib/tests/fixtures/testable_plugininfo_base.php
lib/tests/plugin_manager_test.php
lib/tests/plugininfo/base_test.php [new file with mode: 0644]
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/upgradelib.php
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-debug.js
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-min.js
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue.js
lib/yui/src/chooserdialogue/js/chooserdialogue.js
mod/assign/locallib.php
mod/forum/classes/post_form.php
mod/forum/db/upgrade.php
mod/forum/lang/en/forum.php
mod/forum/post.php
mod/forum/templates/discussion_list.mustache
mod/forum/tests/behat/behat_mod_forum.php
mod/forum/version.php
mod/quiz/addrandomform.php
mod/quiz/styles.css
report/log/classes/table_log.php
report/loglive/classes/renderable.php
version.php

index d827e6d..12904e5 100644 (file)
@@ -137,7 +137,7 @@ if (!$envstatus) {
 
 // Test plugin dependencies.
 $failed = array();
-if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed, $CFG->branch)) {
     cli_problem(get_string('pluginscheckfailed', 'admin', array('pluginslist' => implode(', ', array_unique($failed)))));
     cli_error(get_string('pluginschecktodo', 'admin'));
 }
index c34a437..c95c73c 100644 (file)
@@ -241,7 +241,7 @@ if (!core_tables_exist()) {
 
     // check plugin dependencies
     $failed = array();
-    if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+    if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed, $CFG->branch)) {
         $PAGE->navbar->add(get_string('pluginscheck', 'admin'));
         $PAGE->set_title($strinstallation);
         $PAGE->set_heading($strinstallation . ' - Moodle ' . $CFG->target_release);
@@ -508,7 +508,7 @@ if (!$cache and $version > $CFG->version) {  // upgrade
     } else {
         // Always verify plugin dependencies!
         $failed = array();
-        if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+        if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed, $CFG->branch)) {
             echo $output->unsatisfied_dependencies_page($version, $failed, $PAGE->url);
             die();
         }
@@ -682,7 +682,7 @@ if (!$cache and moodle_needs_upgrading()) {
 
         // Make sure plugin dependencies are always checked.
         $failed = array();
-        if (!$pluginman->all_plugins_ok($version, $failed)) {
+        if (!$pluginman->all_plugins_ok($version, $failed, $CFG->branch)) {
             $output = $PAGE->get_renderer('core', 'admin');
             echo $output->unsatisfied_dependencies_page($version, $failed, $PAGE->url);
             die();
index 5ba612e..4dbe8c0 100644 (file)
@@ -983,7 +983,7 @@ class core_admin_renderer extends plugin_renderer_base {
      * @return string HTML code
      */
     public function plugins_check_table(core_plugin_manager $pluginman, $version, array $options = array()) {
-
+        global $CFG;
         $plugininfo = $pluginman->get_plugins();
 
         if (empty($plugininfo)) {
@@ -1069,8 +1069,10 @@ class core_admin_renderer extends plugin_renderer_base {
                 }
 
                 $coredependency = $plugin->is_core_dependency_satisfied($version);
+                $incompatibledependency = $plugin->is_core_compatible_satisfied($CFG->branch);
+
                 $otherpluginsdependencies = $pluginman->are_dependencies_satisfied($plugin->get_other_required_plugins());
-                $dependenciesok = $coredependency && $otherpluginsdependencies;
+                $dependenciesok = $coredependency && $otherpluginsdependencies && $incompatibledependency;
 
                 $statuscode = $plugin->get_status();
                 $row->attributes['class'] .= ' status-' . $statuscode;
@@ -1120,8 +1122,11 @@ class core_admin_renderer extends plugin_renderer_base {
                 }
 
                 $status = new html_table_cell($sourcelabel.' '.$status);
-
-                $requires = new html_table_cell($this->required_column($plugin, $pluginman, $version));
+                if ($plugin->pluginsupported != null) {
+                    $requires = new html_table_cell($this->required_column($plugin, $pluginman, $version, $CFG->branch));
+                } else {
+                    $requires = new html_table_cell($this->required_column($plugin, $pluginman, $version));
+                }
 
                 $statusisboring = in_array($statuscode, array(
                         core_plugin_manager::PLUGIN_STATUS_NODB, core_plugin_manager::PLUGIN_STATUS_UPTODATE));
@@ -1452,15 +1457,17 @@ class core_admin_renderer extends plugin_renderer_base {
      * @param \core\plugininfo\base $plugin the plugin we are rendering the row for.
      * @param core_plugin_manager $pluginman provides data on all the plugins.
      * @param string $version
+     * @param int $branch the current Moodle branch
      * @return string HTML code
      */
-    protected function required_column(\core\plugininfo\base $plugin, core_plugin_manager $pluginman, $version) {
+    protected function required_column(\core\plugininfo\base $plugin, core_plugin_manager $pluginman, $version, $branch = null) {
 
         $requires = array();
         $displayuploadlink = false;
         $displayupdateslink = false;
 
-        foreach ($pluginman->resolve_requirements($plugin, $version) as $reqname => $reqinfo) {
+        $requirements = $pluginman->resolve_requirements($plugin, $version, $branch);
+        foreach ($requirements as $reqname => $reqinfo) {
             if ($reqname === 'core') {
                 if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_OK) {
                     $class = 'requires-ok';
@@ -1469,7 +1476,19 @@ class core_admin_renderer extends plugin_renderer_base {
                     $class = 'requires-failed';
                     $label = html_writer::span(get_string('dependencyfails', 'core_plugin'), 'badge badge-danger');
                 }
-                if ($reqinfo->reqver != ANY_VERSION) {
+
+                if ($branch != null && !$plugin->is_core_compatible_satisfied($branch)) {
+                    $requires[] = html_writer::tag('li',
+                    html_writer::span(get_string('incompatibleversion', 'core_plugin', $branch), 'dep dep-core').
+                    ' '.$label, array('class' => $class));
+
+                } else if ($branch != null && $plugin->pluginsupported != null) {
+                    $requires[] = html_writer::tag('li',
+                        html_writer::span(get_string('moodlebranch', 'core_plugin',
+                        array('min' => $plugin->pluginsupported[0], 'max' => $plugin->pluginsupported[1])), 'dep dep-core').
+                        ' '.$label, array('class' => $class));
+
+                } else if ($reqinfo->reqver != ANY_VERSION) {
                     $requires[] = html_writer::tag('li',
                         html_writer::span(get_string('moodleversion', 'core_plugin', $plugin->versionrequires), 'dep dep-core').
                         ' '.$label, array('class' => $class));
@@ -1556,6 +1575,13 @@ class core_admin_renderer extends plugin_renderer_base {
             );
         }
 
+        // Check if supports is present, and $branch is not in, only if $incompatible check was ok.
+        if ($plugin->pluginsupported != null && $class == 'requires-ok' && $branch != null) {
+            if ($pluginman->check_explicitly_supported($plugin, $branch) == $pluginman::VERSION_NOT_SUPPORTED) {
+                $out .= html_writer::div(get_string('notsupported', 'core_plugin', $branch));
+            }
+        }
+
         return $out;
 
     }
index 4b0f427..0d55217 100644 (file)
@@ -44,15 +44,10 @@ $temp = new admin_settingpage('sessionhandling', new lang_string('sessionhandlin
 if (empty($CFG->session_handler_class) and $DB->session_lock_supported()) {
     $temp->add(new admin_setting_configcheckbox('dbsessions', new lang_string('dbsessions', 'admin'), new lang_string('configdbsessions', 'admin'), 0));
 }
-$temp->add(new admin_setting_configselect('sessiontimeout', new lang_string('sessiontimeout', 'admin'), new lang_string('configsessiontimeout', 'admin'), 7200, array(14400 => new lang_string('numhours', '', 4),
-                                                                                                                                                      10800 => new lang_string('numhours', '', 3),
-                                                                                                                                                      7200 => new lang_string('numhours', '', 2),
-                                                                                                                                                      5400 => new lang_string('numhours', '', '1.5'),
-                                                                                                                                                      3600 => new lang_string('numminutes', '', 60),
-                                                                                                                                                      2700 => new lang_string('numminutes', '', 45),
-                                                                                                                                                      1800 => new lang_string('numminutes', '', 30),
-                                                                                                                                                      900 => new lang_string('numminutes', '', 15),
-                                                                                                                                                      300 => new lang_string('numminutes', '', 5))));
+
+$temp->add(new admin_setting_configduration('sessiontimeout', new lang_string('sessiontimeout', 'admin'),
+    new lang_string('configsessiontimeout', 'admin'), 8 * 60 * 60));
+
 $temp->add(new admin_setting_configtext('sessioncookie', new lang_string('sessioncookie', 'admin'), new lang_string('configsessioncookie', 'admin'), '', PARAM_ALPHANUM));
 $temp->add(new admin_setting_configtext('sessioncookiepath', new lang_string('sessioncookiepath', 'admin'), new lang_string('configsessioncookiepath', 'admin'), '', PARAM_RAW));
 $temp->add(new admin_setting_configtext('sessioncookiedomain', new lang_string('sessioncookiedomain', 'admin'), new lang_string('configsessioncookiedomain', 'admin'), '', PARAM_RAW, 50));
index 22045d5..3774465 100644 (file)
Binary files a/admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js and b/admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js differ
index c676cc1..0fac0c4 100644 (file)
Binary files a/admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js.map and b/admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js.map differ
index 379c3c4..80f431f 100644 (file)
@@ -13,6 +13,7 @@
  * http://www.wimagguc.com/\r
  *\r
  */\r
+\r
 define(['jquery'], function($) {\r
 \r
 // for ie9 doesn't support debug console >>>\r
@@ -171,7 +172,7 @@ $.fn.euCookieLawPopup = (function() {
                var cookies = document.cookie.split(";");\r
                for (var i = 0; i < cookies.length; i++) {\r
                        var c = cookies[i].trim();\r
-                       if (c.indexOf(_self.vars.COOKIE_NAME) == 0) {\r
+                       if (c.indexOf(_self.vars.COOKIE_NAME) !== -1) {\r
                                userAcceptedCookies = c.substring(_self.vars.COOKIE_NAME.length + 1, c.length);\r
                        }\r
                }\r
index a4241d9..e53104c 100644 (file)
@@ -1,22 +1,19 @@
-jQuery EU Cookie Law popups 1.1.2
+jQuery EU Cookie Law popups 1.1.3
 -------------
 https://github.com/wimagguc/jquery-eu-cookie-law-popup
 
 Instructions to import 'jQuery EU Cookie Law popups' into Moodle:
 
-1. Download the latest release from https://github.com/wimagguc/jquery-eu-cookie-law-popup
+1. Download the latest release from https://github.com/wimagguc/jquery-eu-cookie-law-popup/releases
 2. Copy 'js/jquery-eu-cookie-law-popup.js' into 'amd/src/jquery-eu-cookie-law-popup.js':
 
    2.a. Replace jquery reference
 ------------------
 (function($) {
 ------------------
-
-to
+with
 ------------------
-define(
-['jquery'],
-function($) {
+define(['jquery'],function($) {
 ------------------
 
    2.b. Remove initialisation code. It will be added and configured only in the pages where is needed
@@ -29,6 +26,7 @@ $(document).ready( function() {
       'popupText' : 'We use them to give you the best experience. If you continue using our website, we\'ll assume that you are happy to receive all cookies on this website.'
     });
   }
+});
 ------------------
 
    2.c. Remove code
@@ -36,9 +34,16 @@ $(document).ready( function() {
 $(document).bind("user_cookie_consent_changed", function(event, object) {
   console.log("User cookie consent changed: " + $(object).attr('consent') );
 });
+------------------
 
+   2.d. Replace
+------------------
 }(jQuery));
 ------------------
+with
+------------------
+});
+------------------
 
 3. Copy the following styles from 'css/jquery-eu-cookie-law-popup.css' into the
 "jquery-eu-cookie-law-popup styles" section in 'styles.css':
@@ -54,3 +59,5 @@ $(document).bind("user_cookie_consent_changed", function(event, object) {
 
 4. Execute grunt to compile js
    grunt amd
+
+5. Update version number in admin/tool/policy/thirdpartylibs.xml
index 9d7a636..bcc98f2 100644 (file)
@@ -4,7 +4,7 @@
     <location>amd/src/jquery-eu-cookie-law-popup.js</location>
     <name>jQuery EU Cookie Law popups</name>
     <license>MIT</license>
-    <version>1.1.2</version>
+    <version>1.1.3</version>
     <licenseversion></licenseversion>
   </library>
 </libraries>
index 5d6f881..343a3eb 100644 (file)
@@ -61,7 +61,7 @@ if (!defined('E_USER_DEPRECATED')) {
 /**
  * phpCAS version. accessible for the user by phpCAS::getVersion().
  */
-define('PHPCAS_VERSION', '1.3.7+');
+define('PHPCAS_VERSION', '1.3.8');
 
 /**
  * @addtogroup public
index 338bd50..f06c154 100644 (file)
@@ -997,7 +997,18 @@ class CAS_Client
 
         // set to callback mode if PgtIou and PgtId CGI GET parameters are provided
         if ( $this->isProxy() ) {
-            $this->_setCallbackMode(!empty($_GET['pgtIou'])&&!empty($_GET['pgtId']));
+            if(!empty($_GET['pgtIou'])&&!empty($_GET['pgtId'])) {
+                $this->_setCallbackMode(true);
+                $this->_setCallbackModeUsingPost(false);
+            } elseif (!empty($_POST['pgtIou'])&&!empty($_POST['pgtId'])) {
+                $this->_setCallbackMode(true);
+                $this->_setCallbackModeUsingPost(true);
+            } else {
+                $this->_setCallbackMode(false);
+                $this->_setCallbackModeUsingPost(false);
+            }
+
+            
         }
 
         if ( $this->_isCallbackMode() ) {
@@ -2329,6 +2340,36 @@ class CAS_Client
         return $this->_callback_mode;
     }
 
+    /**
+     * @var bool a boolean to know if the CAS client is using POST parameters when in callback mode.
+     * Written by CAS_Client::_setCallbackModeUsingPost(), read by CAS_Client::_isCallbackModeUsingPost().
+     *
+     * @hideinitializer
+     */
+    private $_callback_mode_using_post = false;
+
+    /**
+     * This method sets/unsets usage of POST parameters in callback mode (default/false is GET parameters)
+     *
+     * @param bool $callback_mode_using_post true to use POST, false to use GET (default).
+     *
+     * @return void
+     */
+    private function _setCallbackModeUsingPost($callback_mode_using_post)
+    {
+        $this->_callback_mode_using_post = $callback_mode_using_post;
+    }
+
+    /**
+     * This method returns true when the callback mode is using POST, false otherwise.
+     *
+     * @return bool A boolean.
+     */
+    private function _isCallbackModeUsingPost()
+    {
+        return $this->_callback_mode_using_post;
+    }
+
     /**
      * the URL that should be used for the PGT callback (in fact the URL of the
      * current request without any CGI parameter). Written and read by
@@ -2387,23 +2428,39 @@ class CAS_Client
     private function _callback()
     {
         phpCAS::traceBegin();
-        if (preg_match('/^PGTIOU-[\.\-\w]+$/', $_GET['pgtIou'])) {
-            if (preg_match('/^[PT]GT-[\.\-\w]+$/', $_GET['pgtId'])) {
-                $this->printHTMLHeader('phpCAS callback');
-                $pgt_iou = $_GET['pgtIou'];
-                $pgt = $_GET['pgtId'];
-                phpCAS::trace('Storing PGT `'.$pgt.'\' (id=`'.$pgt_iou.'\')');
-                echo '<p>Storing PGT `'.$pgt.'\' (id=`'.$pgt_iou.'\').</p>';
-                $this->_storePGT($pgt, $pgt_iou);
-                $this->printHTMLFooter();
+        if ($this->_isCallbackModeUsingPost()) {
+            $pgtId = $_POST['pgtId'];
+            $pgtIou = $_POST['pgtIou'];
+        } else {
+            $pgtId = $_GET['pgtId'];
+            $pgtIou = $_GET['pgtIou'];
+        }
+        if (preg_match('/^PGTIOU-[\.\-\w]+$/', $pgtIou)) {
+            if (preg_match('/^[PT]GT-[\.\-\w]+$/', $pgtId)) {
+                phpCAS::trace('Storing PGT `'.$pgtId.'\' (id=`'.$pgtIou.'\')');
+                $this->_storePGT($pgtId, $pgtIou);
+                if (array_key_exists('HTTP_ACCEPT', $_SERVER) &&
+                    (   $_SERVER['HTTP_ACCEPT'] == 'application/xml' ||
+                        $_SERVER['HTTP_ACCEPT'] == 'text/xml'
+                    )
+                ) {
+                    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\r\n";
+                    echo '<proxySuccess xmlns="http://www.yale.edu/tp/cas" />';
+                    phpCAS::traceExit("XML response sent");
+                } else {
+                    $this->printHTMLHeader('phpCAS callback');
+                    echo '<p>Storing PGT `'.$pgtId.'\' (id=`'.$pgtIou.'\').</p>';
+                    $this->printHTMLFooter();
+                    phpCAS::traceExit("HTML response sent");
+                }
                 phpCAS::traceExit("Successfull Callback");
             } else {
-                phpCAS::error('PGT format invalid' . $_GET['pgtId']);
-                phpCAS::traceExit('PGT format invalid' . $_GET['pgtId']);
+                phpCAS::error('PGT format invalid' . $pgtId);
+                phpCAS::traceExit('PGT format invalid' . $pgtId);
             }
         } else {
-            phpCAS::error('PGTiou format invalid' . $_GET['pgtIou']);
-            phpCAS::traceExit('PGTiou format invalid' . $_GET['pgtIou']);
+            phpCAS::error('PGTiou format invalid' . $pgtIou);
+            phpCAS::traceExit('PGTiou format invalid' . $pgtIou);
         }
 
         // Flush the buffer to prevent from sending anything other then a 200
index 583c1dc..f425edc 100644 (file)
@@ -4,11 +4,16 @@ phpCAS
 phpCAS is an authentication library that allows PHP applications to easily authenticate
 users via a Central Authentication Service (CAS) server.
 
-Please see the phpCAS website for more information:
+Please see the wiki website for more information:
 
 https://wiki.jasig.org/display/CASC/phpCAS
 
-[![Build Status](https://travis-ci.org/Jasig/phpCAS.png)](https://travis-ci.org/Jasig/phpCAS)
+Api documentation can be found here:
+
+https://apereo.github.io/phpCAS/
+
+
+[![Build Status](https://travis-ci.org/apereo/phpCAS.png)](https://travis-ci.org/apereo/phpCAS)
 
 
 LICENSE
index 7894d1c..11cf506 100644 (file)
@@ -1,5 +1,3 @@
-Description of phpCAS 1.3.7 library import
+Description of phpCAS 1.3.8 library import
 
-* downloaded from http://downloads.jasig.org/cas-clients/php/current/
-* applied patch https://github.com/apereo/phpCAS/pull/247 for PHP 7.2 compatibility (MDL-60280)
-* applied patch https://github.com/apereo/phpCAS/pull/278 for PHP 7.3 compatibility (MDL-63422)
+* downloaded from http://downloads.jasig.org/cas-clients/php/current/
\ No newline at end of file
index ddea089..260058d 100644 (file)
@@ -49,6 +49,11 @@ abstract class backup implements checksumable {
     const INTERACTIVE_YES = true;
     const INTERACTIVE_NO  = false;
 
+    /** Release the session during backup/restore */
+    const RELEASESESSION_YES = true;
+    /** Don't release the session during backup/restore */
+    const RELEASESESSION_NO  = false;
+
     // Predefined modes (purposes) of the backup
     const MODE_GENERAL   = 10;
 
index cfcb3ff..c68bbfa 100644 (file)
@@ -122,7 +122,7 @@ if (!async_helper::is_async_pending($id, 'course', 'backup')) {
 
     if (!($bc = backup_ui::load_controller($backupid))) {
         $bc = new backup_controller($type, $id, backup::FORMAT_MOODLE,
-                backup::INTERACTIVE_YES, $backupmode, $USER->id);
+                backup::INTERACTIVE_YES, $backupmode, $USER->id, backup::RELEASESESSION_YES);
         // The backup id did not relate to a valid controller so we made a new controller.
         // Now we need to reset the backup id to match the new controller.
         $backupid = $bc->get_backupid();
index 52793b2..c2c7fdd 100644 (file)
@@ -79,8 +79,9 @@ class backup_controller extends base_controller {
      * @param bool $interactive Whether this backup will require user interaction; backup::INTERACTIVE_YES or INTERACTIVE_NO
      * @param int $mode One of backup::MODE_GENERAL, MODE_IMPORT, MODE_SAMESITE, MODE_HUB, MODE_AUTOMATED
      * @param int $userid The id of the user making the backup
+     * @param bool $releasesession Should release the session? backup::RELEASESESSION_YES or backup::RELEASESESSION_NO
      */
-    public function __construct($type, $id, $format, $interactive, $mode, $userid){
+    public function __construct($type, $id, $format, $interactive, $mode, $userid, $releasesession = backup::RELEASESESSION_NO) {
         $this->type = $type;
         $this->id   = $id;
         $this->courseid = backup_controller_dbops::get_courseid_from_type_id($this->type, $this->id);
@@ -88,6 +89,7 @@ class backup_controller extends base_controller {
         $this->interactive = $interactive;
         $this->mode = $mode;
         $this->userid = $userid;
+        $this->releasesession = $releasesession;
 
         // Apply some defaults
         $this->operation = backup::OPERATION_BACKUP;
@@ -359,6 +361,11 @@ class backup_controller extends base_controller {
         core_php_time_limit::raise(1 * 60 * 60); // 1 hour for 1 course initially granted
         raise_memory_limit(MEMORY_EXTRA);
 
+        // Release the session so other tabs in the same session are not blocked.
+        if ($this->get_releasesession() === backup::RELEASESESSION_YES) {
+            \core\session\manager::write_close();
+        }
+
         // If the controller has decided that we can include files, then check the setting, otherwise do not include files.
         if ($this->get_include_files()) {
             $this->set_include_files((bool) $this->get_plan()->get_setting('files')->get_value());
index 0998796..32aa06c 100644 (file)
@@ -33,6 +33,9 @@ abstract class base_controller extends backup implements loggable {
      */
     protected $logger;
 
+    /** @var bool Whether this backup should release the session. */
+    protected $releasesession = backup::RELEASESESSION_NO;
+
     /**
      * Gets the progress reporter, which can be used to report progress within
      * the backup or restore process.
@@ -82,4 +85,14 @@ abstract class base_controller extends backup implements loggable {
     public function log($message, $level, $a = null, $depth = null, $display = false) {
         backup_helper::log($message, $level, $a, $depth, $display, $this->logger);
     }
+
+    /**
+     * Returns the set value of releasesession.
+     * This is used to indicate if the session should be closed during the backup/restore.
+     *
+     * @return bool Indicates whether the session should be released.
+     */
+    public function get_releasesession() {
+        return $this->releasesession;
+    }
 }
index 1f63e57..cf37e55 100644 (file)
@@ -79,15 +79,17 @@ class restore_controller extends base_controller {
      * @param int $userid
      * @param int $target backup::TARGET_[ NEW_COURSE | CURRENT_ADDING | CURRENT_DELETING | EXISTING_ADDING | EXISTING_DELETING ]
      * @param \core\progress\base $progress Optional progress monitor
+     * @param bool $releasesession Should release the session? backup::RELEASESESSION_YES or backup::RELEASESESSION_NO
      */
     public function __construct($tempdir, $courseid, $interactive, $mode, $userid, $target,
-            \core\progress\base $progress = null) {
+            \core\progress\base $progress = null, $releasesession = backup::RELEASESESSION_NO) {
         $this->tempdir = $tempdir;
         $this->courseid = $courseid;
         $this->interactive = $interactive;
         $this->mode = $mode;
         $this->userid = $userid;
         $this->target = $target;
+        $this->releasesession = $releasesession;
 
         // Apply some defaults
         $this->type = '';
@@ -357,6 +359,11 @@ class restore_controller extends base_controller {
         core_php_time_limit::raise(1 * 60 * 60); // 1 hour for 1 course initially granted
         raise_memory_limit(MEMORY_EXTRA);
 
+        // Release the session so other tabs in the same session are not blocked.
+        if ($this->get_releasesession() === backup::RELEASESESSION_YES) {
+            \core\session\manager::write_close();
+        }
+
         // Do course cleanup precheck, if required. This was originally in restore_ui. Moved to handle async backup/restore.
         if ($this->get_target() == backup::TARGET_CURRENT_DELETING || $this->get_target() == backup::TARGET_EXISTING_DELETING) {
             $options = array();
index cb40f59..dd70910 100644 (file)
@@ -91,7 +91,6 @@ class core_backup_external extends external_api {
             require_capability('moodle/backup:backupactivity', $context);
         } else {
             require_capability('moodle/backup:backupcourse', $context);
-            $instanceid = $course->id;
         }
 
         $results = array();
index 4ee134e..a6b7d76 100644 (file)
@@ -55,9 +55,11 @@ require_capability('moodle/restore:restorecourse', $context);
 if (is_null($course)) {
     $coursefullname = $SITE->fullname;
     $courseshortname = $SITE->shortname;
+    $courseurl = new moodle_url('/');
 } else {
     $coursefullname = $course->fullname;
     $courseshortname = $course->shortname;
+    $courseurl = course_get_url($course->id);
 }
 
 // Show page header.
@@ -89,7 +91,7 @@ if ($stage & restore_ui::STAGE_CONFIRM + restore_ui::STAGE_DESTINATION) {
         $restore = restore_ui::engage_independent_stage($stage/2, $contextid);
         if ($restore->process()) {
             $rc = new restore_controller($restore->get_filepath(), $restore->get_course_id(), backup::INTERACTIVE_YES,
-                    $backupmode, $USER->id, $restore->get_target());
+                    $backupmode, $USER->id, $restore->get_target(), null, backup::RELEASESESSION_YES);
         }
     }
     if ($rc) {
@@ -173,7 +175,6 @@ if ($restore->get_stage() != restore_ui::STAGE_PROCESS) {
     \core\task\manager::queue_adhoc_task($asynctask);
 
     // Add ajax progress bar and initiate ajax via a template.
-    $courseurl = new moodle_url('/course/view.php', array('id' => $course->id));
     $restoreurl = new moodle_url('/backup/restorefile.php', array('contextid' => $contextid));
     $progresssetup = array(
             'backupid' => $restoreid,
@@ -182,7 +183,6 @@ if ($restore->get_stage() != restore_ui::STAGE_PROCESS) {
             'restoreurl' => $restoreurl->out()
     );
     echo $renderer->render_from_template('core/async_backup_status', $progresssetup);
-
 }
 
 $restore->destroy();
index 37d1a80..f442a64 100644 (file)
@@ -278,7 +278,12 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
 
         // Build the WHERE condition for the sub-query.
         if (!empty($subqueryconditions)) {
-            $subquerywhere = 'WHERE ' . implode(" OR ", $subqueryconditions);
+            $unionstartquery = "SELECT modulename, instance, eventtype, priority
+                                  FROM {event} ev
+                                 WHERE ";
+            $subqueryunion = $unionstartquery . implode(" UNION $unionstartquery ", $subqueryconditions);
+        } else {
+            $subqueryunion = '{event}';
         }
 
         // Merge subquery parameters to the parameters of the main query.
@@ -291,8 +296,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
                             ev.instance,
                             ev.eventtype,
                             MIN(ev.priority) as priority
-                       FROM {event} ev
-                      $subquerywhere
+                       FROM ($subqueryunion) ev
                    GROUP BY ev.modulename, ev.instance, ev.eventtype";
 
         // Build the main query.
index 7c247c9..de9b8b7 100644 (file)
@@ -717,13 +717,49 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
      * @return mixed
      */
     protected static function get_tree($id) {
-        global $DB;
         $coursecattreecache = cache::make('core', 'coursecattree');
         $rv = $coursecattreecache->get($id);
         if ($rv !== false) {
             return $rv;
         }
+        // Might need to rebuild the tree. Put a lock in place to ensure other requests don't try and do this in parallel.
+        $lockfactory = \core\lock\lock_config::get_lock_factory('core_coursecattree');
+        $lock = $lockfactory->get_lock('core_coursecattree_cache',
+                course_modinfo::COURSE_CACHE_LOCK_WAIT, course_modinfo::COURSE_CACHE_LOCK_EXPIRY);
+        if ($lock === false) {
+            // Couldn't get a lock to rebuild the tree.
+            return [];
+        }
+        $rv = $coursecattreecache->get($id);
+        if ($rv !== false) {
+            // Tree was built while we were waiting for the lock.
+            $lock->release();
+            return $rv;
+        }
         // Re-build the tree.
+        try {
+            $all = self::rebuild_coursecattree_cache_contents();
+            $coursecattreecache->set_many($all);
+        } finally {
+            $lock->release();
+        }
+        if (array_key_exists($id, $all)) {
+            return $all[$id];
+        }
+        // Requested non-existing category.
+        return array();
+    }
+
+    /**
+     * Rebuild the course category tree as an array, including an extra "countall" field.
+     *
+     * @return array
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws moodle_exception
+     */
+    private static function rebuild_coursecattree_cache_contents() : array {
+        global $DB;
         $sql = "SELECT cc.id, cc.parent, cc.visible
                 FROM {course_categories} cc
                 ORDER BY cc.sortorder";
@@ -760,12 +796,7 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
         }
         // We must add countall to all in case it was the requested ID.
         $all['countall'] = $count;
-        $coursecattreecache->set_many($all);
-        if (array_key_exists($id, $all)) {
-            return $all[$id];
-        }
-        // Requested non-existing category.
-        return array();
+        return $all;
     }
 
     /**
index 23088e5..9eca03f 100644 (file)
@@ -2209,6 +2209,7 @@ function move_courses($courseids, $categoryid) {
     foreach ($dbcourses as $dbcourse) {
         $course = new stdClass();
         $course->id = $dbcourse->id;
+        $course->timemodified = time();
         $course->category  = $category->id;
         $course->sortorder = $category->sortorder + MAX_COURSES_IN_CATEGORY - $i++;
         if ($category->visible == 0) {
index 82ce32f..a0123d1 100644 (file)
     }
 }}
 <div class="enable">
-    <input type="checkbox" name="{{applyname}}" value="1" id="{{applyname}}">
+    <input type="checkbox" name="{{applyname}}" value="1" id="{{applyname}}" class="ml-0">
     <label for="{{applyname}}">{{applylabel}}</label>
 </div>
 <fieldset class="form-inline">
     <legend class="accesshide">{{label}}</legend>
     <label for="{{menuname}}">{{menulabel}}</label>
-    <select name="{{menuname}}" id="{{menuname}}" class="form-control">
+    <select name="{{menuname}}" id="{{menuname}}" class="form-control custom-select">
         {{#menuoptions}}
             <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
         {{/menuoptions}}
diff --git a/install/lang/ar_wp/langconfig.php b/install/lang/ar_wp/langconfig.php
new file mode 100644 (file)
index 0000000..798c70c
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['parentlanguage'] = 'ar';
+$string['thislanguage'] = 'بالعربي workplace';
index 6b5b240..71c6d8f 100644 (file)
@@ -30,5 +30,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['clianswerno'] = 'ទេ';
 $string['environmentrequireinstall'] = 'ត្រូវតែបានដំឡើង និងអនុញ្ញាត';
 $string['environmentrequireversion'] = 'ត្រូវការកំណែ {$a->needed} ហើយអ្នកកំពុងរត់ {$a->current}';
index ef92a53..61674dd 100644 (file)
@@ -467,6 +467,7 @@ $string['pagenotexist'] = 'An unusual error occurred (tried to reach a page that
 $string['pathdoesnotstartslash'] = 'No valid arguments supplied, path does not start with slash!';
 $string['pleasereport'] = 'If you have time, please let us know what you were trying to do when the error occurred:';
 $string['pluginrequirementsnotmet'] = 'Plugin "{$a->pluginname}" ({$a->pluginversion}) could not be installed.  It requires a newer version of Moodle (currently you are using {$a->currentmoodle}, you need {$a->requiremoodle}).';
+$string['pluginunsupported'] = 'Plugin "{$a->pluginname}" {$a->pluginversion} does not support this version of Moodle {$a->moodleversion}. Seek plugin information to find supported versions.';
 $string['prefixcannotbeempty'] = '<p>Error: database table prefix cannot be empty ({$a})</p>
 <p>The site administrator must fix this problem.</p>';
 $string['prefixtoolong'] = '<p>Error: database table prefix is too long ({$a->dbfamily})</p>
@@ -604,4 +605,3 @@ $string['xmldberror'] = 'XMLDB error!';
 $string['alreadyloggedin'] = 'You are already logged in as {$a}, you need to log out before logging in as different user.';
 $string['youcannotdeletecategory'] = 'You cannot delete category \'{$a}\' because you can neither delete the contents, nor move them elsewhere.';
 $string['protected_cc_not_supported'] = 'Protected cartridges not supported.';
-
index 74644c0..6c17f58 100644 (file)
@@ -1842,6 +1842,7 @@ $string['shortnameuser'] = 'User short name';
 $string['shortsitename'] = 'Short name for site (eg single word)';
 $string['show'] = 'Show';
 $string['showactions'] = 'Show actions';
+$string['showadvancededitor'] = 'Advanced';
 $string['showadvancedsettings'] = 'Show advanced settings';
 $string['showall'] = 'Show all {$a}';
 $string['showallcourses'] = 'Show all courses';
index c0d4da1..94c5616 100644 (file)
@@ -54,6 +54,7 @@ $string['err_response_http_code'] = 'Unable to fetch available updates data - un
 $string['filterall'] = 'Show all';
 $string['filtercontribonly'] = 'Show additional plugins only';
 $string['filterupdatesonly'] = 'Show updateable only';
+$string['incompatibleversion'] = 'Incompatible Moodle version: {$a}';
 $string['isenabled'] = 'Enabled?';
 $string['misdepinfoplugin'] = 'Plugin info';
 $string['misdepinfoversion'] = 'Version info';
@@ -61,12 +62,14 @@ $string['misdepsavail'] = 'Available missing dependencies';
 $string['misdepsunavail'] = 'Unavailable missing dependencies';
 $string['misdepsunavaillist'] = 'No version found to fulfill the dependency requirements: {$a}.';
 $string['misdepsunknownlist'] = 'Not in the Plugins directory: <strong>{$a}</strong>.';
+$string['moodlebranch'] = 'Moodle {$a->min} - {$a->max}';
 $string['moodleversion'] = 'Moodle {$a}';
 $string['noneinstalled'] = 'No plugins of this type are installed';
 $string['notes'] = 'Notes';
 $string['notdownloadable'] = 'Can not download the package';
 $string['notdownloadable_help'] = 'ZIP package with the update can not be downloaded automatically. Please refer to the documentation page for more help.';
 $string['notdownloadable_link'] = 'admin/mdeploy/notdownloadable';
+$string['notsupported'] = 'Plugin may not be compatible with Moodle version {$a}';
 $string['notwritable'] = 'Plugin files not writable';
 $string['notwritable_help'] = 'Plugin files are not writable by the web server. The web server process must have write access to the plugin folder and all its contents. Write access to the root folder of the given plugin type may also be required.';
 $string['otherplugin'] = '{$a->component}';
index 72edb50..96d071a 100644 (file)
@@ -423,6 +423,7 @@ $string['site:restore'] = 'Restore courses';
 $string['site:sendmessage'] = 'Send messages to any user';
 $string['site:trustcontent'] = 'Trust submitted content';
 $string['site:uploadusers'] = 'Upload new users from file';
+$string['site:viewanonymousevents'] = 'View anonymous events in reports';
 $string['site:viewfullnames'] = 'Always see full names of users';
 $string['site:viewparticipants'] = 'View participants';
 $string['site:viewreports'] = 'View reports';
index f473cea..488ce71 100644 (file)
@@ -49,17 +49,56 @@ class antivirus_clamav_runningmethod_setting extends admin_setting_configselect
     /**
      * Validate data.
      *
-     * This ensures that unix socket transport is supported by this system.
+     * This ensures that the selected socket transport is supported by this system.
      *
      * @param string $data
      * @return mixed True on success, else error message.
      */
     public function validate($data) {
+        $supportedtransports = stream_get_transports();
         if ($data === 'unixsocket') {
-            $supportedtransports = stream_get_transports();
-            if (!array_search('unix', $supportedtransports)) {
+            if (array_search('unix', $supportedtransports) === false) {
                 return get_string('errornounixsocketssupported', 'antivirus_clamav');
             }
+        } else if ($data === 'tcpsocket') {
+            if (array_search('tcp', $supportedtransports) === false) {
+                return get_string('errornotcpsocketssupported', 'antivirus_clamav');
+            }
+        }
+        return true;
+    }
+}
+
+
+/**
+ * Abstract socket checking class
+ *
+ * @package    antivirus_clamav
+ * @copyright  2015 Ruslan Kabalin, Lancaster University.
+ * @copyright  2019 Didier Raboud, Liip AG.
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class antivirus_clamav_socket_setting extends admin_setting_configtext {
+    /**
+     * Ping ClamAV socket.
+     *
+     * This ensures that a socket setting is correct and that ClamAV is running.
+     *
+     * @param string $socketaddress Address to the socket to connect to (for stream_socket_client)
+     * @return mixed True on success, else error message.
+     */
+    protected function validate_clamav_socket($socketaddress) {
+        $socket = stream_socket_client($socketaddress, $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT);
+        if (!$socket) {
+            return get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)");
+        } else {
+            // Send PING query to ClamAV socket to check its running state.
+            fwrite($socket, "nPING\n");
+            $response = stream_get_line($socket, 4);
+            fclose($socket);
+            if ($response !== 'PONG') {
+                return get_string('errorclamavnoresponse', 'antivirus_clamav');
+            }
         }
         return true;
     }
@@ -71,7 +110,7 @@ class antivirus_clamav_runningmethod_setting extends admin_setting_configselect
  * @copyright  2015 Ruslan Kabalin, Lancaster University.
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class antivirus_clamav_pathtounixsocket_setting extends admin_setting_configtext {
+class antivirus_clamav_pathtounixsocket_setting extends antivirus_clamav_socket_setting {
     /**
      * Validate data.
      *
@@ -87,19 +126,38 @@ class antivirus_clamav_pathtounixsocket_setting extends admin_setting_configtext
         }
         $runningmethod = get_config('antivirus_clamav', 'runningmethod');
         if ($runningmethod === 'unixsocket') {
-            $socket = stream_socket_client('unix://' . $data, $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT);
-            if (!$socket) {
-                return get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)");
-            } else {
-                // Send PING query to ClamAV socket to check its running state.
-                fwrite($socket, "nPING\n");
-                $response = stream_get_line($socket, 4);
-                fclose($socket);
-                if ($response !== 'PONG') {
-                    return get_string('errorclamavnoresponse', 'antivirus_clamav');
-                }
-            }
+            return $this->validate_clamav_socket('unix://' . $data);
         }
         return true;
     }
 }
+
+/**
+ * Admin setting for Internet domain socket host, adds verification.
+ *
+ * @package    antivirus_clamav
+ * @copyright  2019 Didier Raboud, Liip AG.
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class antivirus_clamav_tcpsockethost_setting extends antivirus_clamav_socket_setting {
+    /**
+     * Validate data.
+     *
+     * This ensures that Internet domain socket setting is correct and ClamAV is running.
+     *
+     * @param string $data
+     * @return mixed True on success, else error message.
+     */
+    public function validate($data) {
+        $result = parent::validate($data);
+        if ($result !== true) {
+            return $result;
+        }
+        $runningmethod = get_config('antivirus_clamav', 'runningmethod');
+        $tcpport = get_config('antivirus_clamav', 'tcpsocketport');
+        if ($runningmethod === 'tcpsocket') {
+            return $this->validate_clamav_socket('tcp://' . $data . ':' . $tcpport);
+        }
+        return true;
+    }
+}
\ No newline at end of file
index 3e55ce4..1bd1cdb 100644 (file)
@@ -34,6 +34,7 @@ define('ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE', 1024);
 /**
  * Class implementing ClamAV antivirus.
  * @copyright  2015 Ruslan Kabalin, Lancaster University.
+ * @copyright  2019 Didier Raboud, Liip AG.
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class scanner extends \core\antivirus\scanner {
@@ -47,6 +48,8 @@ class scanner extends \core\antivirus\scanner {
             return (bool)$this->get_config('pathtoclam');
         } else if ($this->get_config('runningmethod') === 'unixsocket') {
             return (bool)$this->get_config('pathtounixsocket');
+        } else if ($this->get_config('runningmethod') === 'tcpsocket') {
+            return (bool)$this->get_config('tcpsockethost') && (bool)$this->get_config('tcpsocketport');
         }
         return false;
     }
@@ -67,12 +70,22 @@ class scanner extends \core\antivirus\scanner {
             return self::SCAN_RESULT_ERROR;
         }
 
-        // Execute the scan using preferable method.
-        $method = 'scan_file_execute_' . $this->get_config('runningmethod');
-        if (!method_exists($this, $method)) {
-            throw new \coding_exception('Attempting to call non-existing method ' . $method);
+        // We can do direct stream scanning if unixsocket or tcpsocket running methods are in use,
+        // if not, use default process.
+        $runningmethod = $this->get_config('runningmethod');
+        switch ($runningmethod) {
+            case 'unixsocket':
+            case 'tcpsocket':
+                $return = $this->scan_file_execute_socket($file, $runningmethod);
+                break;
+            case 'commandline':
+                $return = $this->scan_file_execute_commandline($file);
+                break;
+            default:
+                // This should not happen.
+                debugging('Unknown running method.');
+                return self::SCAN_RESULT_ERROR;
         }
-        $return = $this->$method($file);
 
         if ($return === self::SCAN_RESULT_ERROR) {
             $this->message_admins($this->get_scanning_notice());
@@ -92,10 +105,11 @@ class scanner extends \core\antivirus\scanner {
      * @return int Scanning result constant.
      */
     public function scan_data($data) {
-        // We can do direct stream scanning if unixsocket running method is in use,
+        // We can do direct stream scanning if unixsocket or tcpsocket running methods are in use,
         // if not, use default process.
-        if ($this->get_config('runningmethod') === 'unixsocket') {
-            $return = $this->scan_data_execute_unixsocket($data);
+        $runningmethod = $this->get_config('runningmethod');
+        if (in_array($runningmethod, array('unixsocket', 'tcpsocket'))) {
+            $return = $this->scan_data_execute_socket($data, $runningmethod);
 
             if ($return === self::SCAN_RESULT_ERROR) {
                 $this->message_admins($this->get_scanning_notice());
@@ -111,6 +125,24 @@ class scanner extends \core\antivirus\scanner {
         }
     }
 
+    /**
+     * Returns a Unix domain socket destination url
+     *
+     * @return string The socket url, fit for stream_socket_client()
+     */
+    private function get_unixsocket_destination() {
+        return 'unix://' . $this->get_config('pathtounixsocket');
+    }
+
+    /**
+     * Returns a Internet domain socket destination url
+     *
+     * @return string The socket url, fit for stream_socket_client()
+     */
+    private function get_tcpsocket_destination() {
+        return 'tcp://' . $this->get_config('tcpsockethost') . ':' . $this->get_config('tcpsocketport');
+    }
+
     /**
      * Returns the string equivalent of a numeric clam error code
      *
@@ -189,13 +221,27 @@ class scanner extends \core\antivirus\scanner {
     }
 
     /**
-     * Scan file using Unix domain sockets.
+     * Scan file using sockets.
      *
      * @param string $file Full path to the file.
+     * @param string $type Either 'tcpsocket' or 'unixsocket'
      * @return int Scanning result constant.
      */
-    public function scan_file_execute_unixsocket($file) {
-        $socket = stream_socket_client('unix://' . $this->get_config('pathtounixsocket'),
+    public function scan_file_execute_socket($file, $type) {
+        switch ($type) {
+            case "tcpsocket":
+                $socketurl = $this->get_tcpsocket_destination();
+                break;
+            case "unixsocket":
+                $socketurl = $this->get_unixsocket_destination();
+                break;
+            default;
+                // This should not happen.
+                debugging('Unknown socket type.');
+                return self::SCAN_RESULT_ERROR;
+        }
+
+        $socket = stream_socket_client($socketurl,
                 $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT);
         if (!$socket) {
             // Can't open socket for some reason, notify admins.
@@ -203,26 +249,57 @@ class scanner extends \core\antivirus\scanner {
             $this->set_scanning_notice($notice);
             return self::SCAN_RESULT_ERROR;
         } else {
-            // Execute scanning. We are running SCAN command and passing file as an argument,
-            // it is the fastest option, but clamav user need to be able to access it, so
-            // we give group read permissions first and assume 'clamav' user is in web server
-            // group (in Debian the default webserver group is 'www-data').
-            // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter,
-            // this is to avoid unexpected newline characters on different systems.
-            $perms = fileperms($file);
-            chmod($file, 0640);
-            fwrite($socket, "nSCAN ".$file."\n");
-            $output = stream_get_line($socket, 4096);
+            if ($type == "unixsocket") {
+                // Execute scanning. We are running SCAN command and passing file as an argument,
+                // it is the fastest option, but clamav user need to be able to access it, so
+                // we give group read permissions first and assume 'clamav' user is in web server
+                // group (in Debian the default webserver group is 'www-data').
+                // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter,
+                // this is to avoid unexpected newline characters on different systems.
+                $perms = fileperms($file);
+                chmod($file, 0640);
+
+                // Actual scan.
+                fwrite($socket, "nSCAN ".$file."\n");
+                // Get ClamAV answer.
+                $output = stream_get_line($socket, 4096);
+
+                // After scanning we revert permissions to initial ones.
+                chmod($file, $perms);
+            } else if ($type == "tcpsocket") {
+                // Execute scanning by passing the entire file through the TCP socket.
+                // This is not fast, but is the only possibility over a network.
+                // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter,
+                // this is to avoid unexpected newline characters on different systems.
+
+                // Actual scan.
+                fwrite($socket, "nINSTREAM\n");
+
+                // Open the file for reading.
+                $fhandle = fopen($file, 'rb');
+                while (!feof($fhandle)) {
+                    // Read it by chunks; write them to the TCP socket.
+                    $chunk = fread($fhandle, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE);
+                    $size = pack('N', strlen($chunk));
+                    fwrite($socket, $size);
+                    fwrite($socket, $chunk);
+                }
+                // Terminate streaming.
+                fwrite($socket, pack('N', 0));
+                // Get ClamAV answer.
+                $output = stream_get_line($socket, 4096);
+
+                fclose($fhandle);
+            }
+            // Free up the ClamAV socket.
             fclose($socket);
-            // After scanning we revert permissions to initial ones.
-            chmod($file, $perms);
             // Parse the output.
-            return $this->parse_unixsocket_response($output);
+            return $this->parse_socket_response($output);
         }
     }
 
     /**
-     * Scan data using unix socket.
+     * Scan data socket.
      *
      * We are running INSTREAM command and passing data stream in chunks.
      * The format of the chunk is: <length><data> where <length> is the size of the following
@@ -231,11 +308,25 @@ class scanner extends \core\antivirus\scanner {
      * Do not exceed StreamMaxLength as defined in clamd.conf, otherwise clamd will
      * reply with INSTREAM size limit exceeded and close the connection.
      *
-     * @param string $data The varaible containing the data to scan.
+     * @param string $data The variable containing the data to scan.
+     * @param string $type Either 'tcpsocket' or 'unixsocket'
      * @return int Scanning result constant.
      */
-    public function scan_data_execute_unixsocket($data) {
-        $socket = stream_socket_client('unix://' . $this->get_config('pathtounixsocket'), $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT);
+    public function scan_data_execute_socket($data, $type) {
+        switch ($type) {
+            case "tcpsocket":
+                $socketurl = $this->get_tcpsocket_destination();
+                break;
+            case "unixsocket":
+                $socketurl = $this->get_unixsocket_destination();
+                break;
+            default;
+                // This should not happen.
+                debugging('Unknown socket type!');
+                return self::SCAN_RESULT_ERROR;
+        }
+
+        $socket = stream_socket_client($socketurl, $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT);
         if (!$socket) {
             // Can't open socket for some reason, notify admins.
             $notice = get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)");
@@ -261,17 +352,17 @@ class scanner extends \core\antivirus\scanner {
             fclose($socket);
 
             // Parse the output.
-            return $this->parse_unixsocket_response($output);
+            return $this->parse_socket_response($output);
         }
     }
 
     /**
-     * Parse unix socket command response.
+     * Parse socket command response.
      *
-     * @param string $output The unix socket command response.
+     * @param string $output The socket response.
      * @return int Scanning result constant.
      */
-    private function parse_unixsocket_response($output) {
+    private function parse_socket_response($output) {
         $splitoutput = explode(': ', $output);
         $message = trim($splitoutput[1]);
         if ($message === 'OK') {
@@ -289,4 +380,35 @@ class scanner extends \core\antivirus\scanner {
             }
         }
     }
+
+    /**
+     * Scan data using Unix domain socket.
+     *
+     * @deprecated since Moodle 3.9 MDL-64075 - please do not use this function any more.
+     * @see antivirus_clamav\scanner::scan_data_execute_socket()
+     *
+     * @param string $data The variable containing the data to scan.
+     * @return int Scanning result constant.
+     */
+    public function scan_data_execute_unixsocket($data) {
+        debugging('antivirus_clamav\scanner::scan_data_execute_unixsocket() is deprecated. ' .
+                  'Use antivirus_clamav\scanner::scan_data_execute_socket() instead.', DEBUG_DEVELOPER);
+        return $this->scan_data_execute_socket($data, "unixsocket");
+    }
+
+    /**
+     * Scan file using Unix domain socket.
+     *
+     * @deprecated since Moodle 3.9 MDL-64075 - please do not use this function any more.
+     * @see antivirus_clamav\scanner::scan_file_execute_socket()
+     *
+     * @param string $file Full path to the file.
+     * @return int Scanning result constant.
+     */
+    public function scan_file_execute_unixsocket($file) {
+        debugging('antivirus_clamav\scanner::scan_file_execute_unixsocket() is deprecated. ' .
+                  'Use antivirus_clamav\scanner::scan_file_execute_socket() instead.', DEBUG_DEVELOPER);
+        return $this->scan_file_execute_socket($file, "unixsocket");
+    }
+
 }
index 0e90141..a7a397f 100644 (file)
@@ -42,4 +42,9 @@ $string['runningmethod'] = 'Running method';
 $string['runningmethoddesc'] = 'Method of running ClamAV. Command line is used by default, however on Unix systems better performance can be obtained by using system sockets.';
 $string['runningmethodcommandline'] = 'Command line';
 $string['runningmethodunixsocket'] = 'Unix domain socket';
+$string['runningmethodtcpsocket'] = 'TCP socket';
+$string['tcpsockethost'] = 'TCP socket hostname';
+$string['tcpsockethostdesc'] = 'Domain name of the ClamAV server';
+$string['tcpsocketport'] = 'TCP socket port';
+$string['tcpsocketportdesc'] = 'The port to use when connecting to ClamAV';
 $string['unknownerror'] = 'There was an unknown error with ClamAV.';
index 7bf877a..2c895c9 100644 (file)
@@ -32,6 +32,7 @@ if ($ADMIN->fulltree) {
     $runningmethodchoice = array(
         'commandline' => get_string('runningmethodcommandline', 'antivirus_clamav'),
         'unixsocket' => get_string('runningmethodunixsocket', 'antivirus_clamav'),
+        'tcpsocket' => get_string('runningmethodtcpsocket', 'antivirus_clamav'),
     );
     $settings->add(new antivirus_clamav_runningmethod_setting('antivirus_clamav/runningmethod',
             get_string('runningmethod', 'antivirus_clamav'),
@@ -47,6 +48,16 @@ if ($ADMIN->fulltree) {
             new lang_string('pathtounixsocket', 'antivirus_clamav'),
             new lang_string('pathtounixsocketdesc', 'antivirus_clamav'), '', PARAM_PATH));
 
+    // Hostname to reach ClamAV tcp socket (used in tcp socket running method).
+    $settings->add(new antivirus_clamav_tcpsockethost_setting('antivirus_clamav/tcpsockethost',
+            new lang_string('tcpsockethost', 'antivirus_clamav'),
+            new lang_string('tcpsockethostdesc', 'antivirus_clamav'), '', PARAM_HOST));
+
+    // Port to reach ClamAV tcp socket (used in tcp socket running method).
+    $settings->add(new admin_setting_configtext('antivirus_clamav/tcpsocketport',
+            new lang_string('tcpsocketport', 'antivirus_clamav'),
+            new lang_string('tcpsocketportdesc', 'antivirus_clamav'), 3310, PARAM_INT));
+
     // How to act on ClamAV failure.
     $options = array(
         'donothing' => new lang_string('configclamdonothing', 'antivirus_clamav'),
index a29edb2..4a8b9c1 100644 (file)
@@ -44,8 +44,8 @@ class antivirus_clamav_scanner_testcase extends advanced_testcase {
 
     public function test_scan_file_not_exists() {
         $antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
-                ->setMethods(array('scan_file_execute_commandline', 'message_admins'))
-                ->getMock();
+            ->setMethods(array('scan_file_execute_commandline', 'message_admins'))
+            ->getMock();
 
         // Test specifying file that does not exist.
         $nonexistingfile = $this->tempfile . '_';
@@ -58,21 +58,21 @@ class antivirus_clamav_scanner_testcase extends advanced_testcase {
     public function test_scan_file_no_virus() {
         $methods = array(
             'scan_file_execute_commandline',
-            'scan_file_execute_unixsocket',
+            'scan_file_execute_socket',
             'message_admins',
             'get_config',
         );
         $antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
-                ->setMethods($methods)
-                ->getMock();
+            ->setMethods($methods)
+            ->getMock();
         // Initiate mock scanning with configuration setting to use commandline.
         $configmap = array(array('runningmethod', 'commandline'));
         $antivirus->method('get_config')->will($this->returnValueMap($configmap));
 
-        // Configure scan_file_execute_commandline and scan_file_execute_unixsocket
+        // Configure scan_file_execute_commandline and scan_file_execute_socket
         // method stubs to behave as if no virus has been found (SCAN_RESULT_OK).
         $antivirus->method('scan_file_execute_commandline')->willReturn(0);
-        $antivirus->method('scan_file_execute_unixsocket')->willReturn(0);
+        $antivirus->method('scan_file_execute_socket')->willReturn(0);
 
         // Set expectation that message_admins is NOT called.
         $antivirus->expects($this->never())->method('message_admins');
@@ -87,26 +87,33 @@ class antivirus_clamav_scanner_testcase extends advanced_testcase {
 
         // Run mock scanning.
         $this->assertEquals(0, $antivirus->scan_file($this->tempfile, ''));
+
+        // Initiate mock scanning with configuration setting to use tcpsocket.
+        $configmap = array(array('runningmethod', 'tcpsocket'));
+        $antivirus->method('get_config')->will($this->returnValueMap($configmap));
+
+        // Run mock scanning.
+        $this->assertEquals(0, $antivirus->scan_file($this->tempfile, ''));
     }
 
     public function test_scan_file_virus() {
         $methods = array(
             'scan_file_execute_commandline',
-            'scan_file_execute_unixsocket',
+            'scan_file_execute_socket',
             'message_admins',
             'get_config',
         );
         $antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
-                ->setMethods($methods)
-                ->getMock();
+            ->setMethods($methods)
+            ->getMock();
         // Initiate mock scanning with configuration setting to use commandline.
         $configmap = array(array('runningmethod', 'commandline'));
         $antivirus->method('get_config')->will($this->returnValueMap($configmap));
 
-        // Configure scan_file_execute_commandline and scan_file_execute_unixsocket
+        // Configure scan_file_execute_commandline and scan_file_execute_socket
         // method stubs to behave as if virus has been found (SCAN_RESULT_FOUND).
         $antivirus->method('scan_file_execute_commandline')->willReturn(1);
-        $antivirus->method('scan_file_execute_unixsocket')->willReturn(1);
+        $antivirus->method('scan_file_execute_socket')->willReturn(1);
 
         // Set expectation that message_admins is NOT called.
         $antivirus->expects($this->never())->method('message_admins');
@@ -121,24 +128,31 @@ class antivirus_clamav_scanner_testcase extends advanced_testcase {
 
         // Run mock scanning.
         $this->assertEquals(1, $antivirus->scan_file($this->tempfile, ''));
+
+        // Initiate mock scanning with configuration setting to use tcpsocket.
+        $configmap = array(array('runningmethod', 'tcpsocket'));
+        $antivirus->method('get_config')->will($this->returnValueMap($configmap));
+
+        // Run mock scanning.
+        $this->assertEquals(1, $antivirus->scan_file($this->tempfile, ''));
     }
 
     public function test_scan_file_error_donothing() {
         $methods = array(
             'scan_file_execute_commandline',
-            'scan_file_execute_unixsocket',
+            'scan_file_execute_socket',
             'message_admins',
             'get_config',
             'get_scanning_notice',
         );
         $antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
-                ->setMethods($methods)
-                ->getMock();
+            ->setMethods($methods)
+            ->getMock();
 
-        // Configure scan_file_execute_commandline and scan_file_execute_unixsocket
+        // Configure scan_file_execute_commandline and scan_file_execute_socket
         // method stubs to behave as if there is a scanning error (SCAN_RESULT_ERROR).
         $antivirus->method('scan_file_execute_commandline')->willReturn(2);
-        $antivirus->method('scan_file_execute_unixsocket')->willReturn(2);
+        $antivirus->method('scan_file_execute_socket')->willReturn(2);
         $antivirus->method('get_scanning_notice')->willReturn('someerror');
 
         // Set expectation that message_admins is called.
@@ -160,24 +174,32 @@ class antivirus_clamav_scanner_testcase extends advanced_testcase {
 
         // Run mock scanning.
         $this->assertEquals(2, $antivirus->scan_file($this->tempfile, ''));
+
+        // Initiate mock scanning with configuration setting to do nothing on
+        // scanning error and using tcpsocket.
+        $configmap = array(array('clamfailureonupload', 'donothing'), array('runningmethod', 'tcpsocket'));
+        $antivirus->method('get_config')->will($this->returnValueMap($configmap));
+
+        // Run mock scanning.
+        $this->assertEquals(2, $antivirus->scan_file($this->tempfile, ''));
     }
 
     public function test_scan_file_error_actlikevirus() {
         $methods = array(
             'scan_file_execute_commandline',
-            'scan_file_execute_unixsocket',
+            'scan_file_execute_socket',
             'message_admins',
             'get_config',
             'get_scanning_notice',
         );
         $antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
-                ->setMethods($methods)
-                ->getMock();
+            ->setMethods($methods)
+            ->getMock();
 
-        // Configure scan_file_execute_commandline and scan_file_execute_unixsocket
+        // Configure scan_file_execute_commandline and scan_file_execute_socket
         // method stubs to behave as if there is a scanning error (SCAN_RESULT_ERROR).
         $antivirus->method('scan_file_execute_commandline')->willReturn(2);
-        $antivirus->method('scan_file_execute_unixsocket')->willReturn(2);
+        $antivirus->method('scan_file_execute_socket')->willReturn(2);
         $antivirus->method('get_scanning_notice')->willReturn('someerror');
 
         // Set expectation that message_admins is called.
@@ -201,24 +223,43 @@ class antivirus_clamav_scanner_testcase extends advanced_testcase {
         // Run mock scanning, we expect SCAN_RESULT_FOUND since configuration
         // require us to act like virus.
         $this->assertEquals(1, $antivirus->scan_file($this->tempfile, ''));
+
+        // Initiate mock scanning with configuration setting to act like virus on
+        // scanning error and using tcpsocket.
+        $configmap = array(array('clamfailureonupload', 'actlikevirus'), array('runningmethod', 'tcpsocket'));
+        $antivirus->method('get_config')->will($this->returnValueMap($configmap));
+
+        // Run mock scanning, we expect SCAN_RESULT_FOUND since configuration
+        // require us to act like virus.
+        $this->assertEquals(1, $antivirus->scan_file($this->tempfile, ''));
     }
 
     public function test_scan_data_no_virus() {
         $methods = array(
-            'scan_data_execute_unixsocket',
+            'scan_data_execute_socket',
             'message_admins',
             'get_config',
         );
         $antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
-                ->setMethods($methods)
-                ->getMock();
+            ->setMethods($methods)
+            ->getMock();
         // Initiate mock scanning with configuration setting to use unixsocket.
         $configmap = array(array('runningmethod', 'unixsocket'));
         $antivirus->method('get_config')->will($this->returnValueMap($configmap));
 
-        // Configure scan_data_execute_unixsocket method stubs to behave as if
+        // Configure scan_data_execute_socket method stubs to behave as if
         // no virus has been found (SCAN_RESULT_OK).
-        $antivirus->method('scan_data_execute_unixsocket')->willReturn(0);
+        $antivirus->method('scan_data_execute_socket')->willReturn(0);
+
+        // Set expectation that message_admins is NOT called.
+        $antivirus->expects($this->never())->method('message_admins');
+
+        // Run mock scanning.
+        $this->assertEquals(0, $antivirus->scan_data(''));
+
+        // Re-initiate mock scanning with configuration setting to use tcpsocket.
+        $configmap = array(array('runningmethod', 'tcpsocket'));
+        $antivirus->method('get_config')->will($this->returnValueMap($configmap));
 
         // Set expectation that message_admins is NOT called.
         $antivirus->expects($this->never())->method('message_admins');
@@ -229,20 +270,30 @@ class antivirus_clamav_scanner_testcase extends advanced_testcase {
 
     public function test_scan_data_virus() {
         $methods = array(
-            'scan_data_execute_unixsocket',
+            'scan_data_execute_socket',
             'message_admins',
             'get_config',
         );
         $antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
-                ->setMethods($methods)
-                ->getMock();
+            ->setMethods($methods)
+            ->getMock();
         // Initiate mock scanning with configuration setting to use unixsocket.
         $configmap = array(array('runningmethod', 'unixsocket'));
         $antivirus->method('get_config')->will($this->returnValueMap($configmap));
 
-        // Configure scan_data_execute_unixsocket method stubs to behave as if
+        // Configure scan_data_execute_socket method stubs to behave as if
         // no virus has been found (SCAN_RESULT_FOUND).
-        $antivirus->method('scan_data_execute_unixsocket')->willReturn(1);
+        $antivirus->method('scan_data_execute_socket')->willReturn(1);
+
+        // Set expectation that message_admins is NOT called.
+        $antivirus->expects($this->never())->method('message_admins');
+
+        // Run mock scanning.
+        $this->assertEquals(1, $antivirus->scan_data(''));
+
+        // Re-initiate mock scanning with configuration setting to use tcpsocket.
+        $configmap = array(array('runningmethod', 'tcpsocket'));
+        $antivirus->method('get_config')->will($this->returnValueMap($configmap));
 
         // Set expectation that message_admins is NOT called.
         $antivirus->expects($this->never())->method('message_admins');
@@ -253,22 +304,22 @@ class antivirus_clamav_scanner_testcase extends advanced_testcase {
 
     public function test_scan_data_error_donothing() {
         $methods = array(
-            'scan_data_execute_unixsocket',
+            'scan_data_execute_socket',
             'message_admins',
             'get_config',
             'get_scanning_notice',
         );
         $antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
-                ->setMethods($methods)
-                ->getMock();
+            ->setMethods($methods)
+            ->getMock();
         // Initiate mock scanning with configuration setting to do nothing on
         // scanning error and using unixsocket.
         $configmap = array(array('clamfailureonupload', 'donothing'), array('runningmethod', 'unixsocket'));
         $antivirus->method('get_config')->will($this->returnValueMap($configmap));
 
-        // Configure scan_data_execute_unixsocket method stubs to behave as if
+        // Configure scan_data_execute_socket method stubs to behave as if
         // there is a scanning error (SCAN_RESULT_ERROR).
-        $antivirus->method('scan_data_execute_unixsocket')->willReturn(2);
+        $antivirus->method('scan_data_execute_socket')->willReturn(2);
         $antivirus->method('get_scanning_notice')->willReturn('someerror');
 
         // Set expectation that message_admins is called.
@@ -276,27 +327,38 @@ class antivirus_clamav_scanner_testcase extends advanced_testcase {
 
         // Run mock scanning.
         $this->assertEquals(2, $antivirus->scan_data(''));
+
+        // Re-initiate mock scanning with configuration setting to do nothing on
+        // scanning error and using tcsocket.
+        $configmap = array(array('clamfailureonupload', 'donothing'), array('runningmethod', 'tcpsocket'));
+        $antivirus->method('get_config')->will($this->returnValueMap($configmap));
+
+        // Set expectation that message_admins is called.
+        $antivirus->expects($this->atLeastOnce())->method('message_admins')->with($this->equalTo('someerror'));
+
+        // Run mock scanning.
+        $this->assertEquals(2, $antivirus->scan_data(''));
     }
 
     public function test_scan_data_error_actlikevirus() {
         $methods = array(
-            'scan_data_execute_unixsocket',
+            'scan_data_execute_socket',
             'message_admins',
             'get_config',
             'get_scanning_notice',
         );
         $antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
-                ->setMethods($methods)
-                ->getMock();
+            ->setMethods($methods)
+            ->getMock();
 
         // Initiate mock scanning with configuration setting to act like virus on
         // scanning error and using unixsocket.
         $configmap = array(array('clamfailureonupload', 'actlikevirus'), array('runningmethod', 'unixsocket'));
         $antivirus->method('get_config')->will($this->returnValueMap($configmap));
 
-        // Configure scan_data_execute_unixsocket method stubs to behave as if
+        // Configure scan_data_execute_socket method stubs to behave as if
         // there is a scanning error (SCAN_RESULT_ERROR).
-        $antivirus->method('scan_data_execute_unixsocket')->willReturn(2);
+        $antivirus->method('scan_data_execute_socket')->willReturn(2);
         $antivirus->method('get_scanning_notice')->willReturn('someerror');
 
         // Set expectation that message_admins is called.
@@ -305,5 +367,17 @@ class antivirus_clamav_scanner_testcase extends advanced_testcase {
         // Run mock scanning, we expect SCAN_RESULT_FOUND since configuration
         // require us to act like virus.
         $this->assertEquals(1, $antivirus->scan_data(''));
+
+        // Re-initiate mock scanning with configuration setting to act like virus on
+        // scanning error and using tcpsocket.
+        $configmap = array(array('clamfailureonupload', 'actlikevirus'), array('runningmethod', 'tcpsocket'));
+        $antivirus->method('get_config')->will($this->returnValueMap($configmap));
+
+        // Set expectation that message_admins is called.
+        $antivirus->expects($this->atLeastOnce())->method('message_admins')->with($this->equalTo('someerror'));
+
+        // Run mock scanning, we expect SCAN_RESULT_FOUND since configuration
+        // require us to act like virus.
+        $this->assertEquals(1, $antivirus->scan_data(''));
     }
 }
index d19e7a6..5c52c89 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2019111800;          // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2019122900;          // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2019111200;          // Requires this Moodle version.
 $plugin->component = 'antivirus_clamav';  // Full name of the plugin (used for diagnostics).
index 33e4a28..7ac652d 100644 (file)
@@ -59,12 +59,21 @@ class core_plugin_manager {
     const REQUIREMENT_STATUS_OUTDATED = 'outdated';
     /** the required dependency is not installed */
     const REQUIREMENT_STATUS_MISSING = 'missing';
+    /** the current Moodle version is too high for plugin. */
+    const REQUIREMENT_STATUS_NEWER = 'newer';
 
     /** the required dependency is available in the plugins directory */
     const REQUIREMENT_AVAILABLE = 'available';
     /** the required dependency is available in the plugins directory */
     const REQUIREMENT_UNAVAILABLE = 'unavailable';
 
+    /** the moodle version is explicitly supported */
+    const VERSION_SUPPORTED = 'supported';
+    /** the moodle version is not explicitly supported */
+    const VERSION_NOT_SUPPORTED = 'notsupported';
+    /** the plugin does not specify supports */
+    const VERSION_NO_SUPPORTS = 'nosupports';
+
     /** @var core_plugin_manager holds the singleton instance */
     protected static $singletoninstance;
     /** @var array of raw plugins information */
@@ -737,10 +746,21 @@ class core_plugin_manager {
      *
      * @param int $moodleversion the version from version.php.
      * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
+     * @param int $branch the current moodle branch, null if not provided
      * @return bool true if all the dependencies are satisfied for all plugins.
      */
-    public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
-
+    public function all_plugins_ok($moodleversion, &$failedplugins = array(), $branch = null) {
+        global $CFG;
+        if (empty($branch)) {
+            $branch = $CFG->branch;
+            if (empty($branch)) {
+                // During initial install there is no branch set.
+                require($CFG->dirroot . '/version.php');
+                $branch = (int)$branch;
+                // Force CFG->branch to int value during install.
+                $CFG->branch = $branch;
+            }
+        }
         $return = true;
         foreach ($this->get_plugins() as $type => $plugins) {
             foreach ($plugins as $plugin) {
@@ -754,6 +774,11 @@ class core_plugin_manager {
                     $return = false;
                     $failedplugins[] = $plugin->component;
                 }
+
+                if (!$plugin->is_core_compatible_satisfied($branch)) {
+                    $return = false;
+                    $failedplugins[] = $plugin->component;
+                }
             }
         }
 
@@ -794,7 +819,7 @@ class core_plugin_manager {
         }
 
         $reqs = array();
-        $reqcore = $this->resolve_core_requirements($plugin, $moodleversion);
+        $reqcore = $this->resolve_core_requirements($plugin, $moodleversion, $moodlebranch);
 
         if (!empty($reqcore)) {
             $reqs['core'] = $reqcore;
@@ -814,7 +839,7 @@ class core_plugin_manager {
      * @param string|int|double $moodleversion moodle core branch to check against
      * @return stdObject
      */
-    protected function resolve_core_requirements(\core\plugininfo\base $plugin, $moodleversion) {
+    protected function resolve_core_requirements(\core\plugininfo\base $plugin, $moodleversion, $moodlebranch) {
 
         $reqs = (object)array(
             'hasver' => null,
@@ -822,7 +847,6 @@ class core_plugin_manager {
             'status' => null,
             'availability' => null,
         );
-
         $reqs->hasver = $moodleversion;
 
         if (empty($plugin->versionrequires)) {
@@ -837,6 +861,14 @@ class core_plugin_manager {
             $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
         }
 
+        // Now check if there is an explicit incompatible, supersedes requires.
+        if (isset($plugin->pluginincompatible) && $plugin->pluginincompatible != null) {
+            if (!$plugin->is_core_compatible_satisfied($moodlebranch)) {
+
+                $reqs->status = self::REQUIREMENT_STATUS_NEWER;
+            }
+        }
+
         return $reqs;
     }
 
@@ -890,6 +922,49 @@ class core_plugin_manager {
         return $reqs;
     }
 
+    /**
+     * Helper method to determine whether a moodle version is explicitly supported.
+     *
+     * @param \core\plugininfo\base $plugin the plugin we are checking
+     * @param int $branch the moodle branch to check support for
+     * @return string
+     */
+    public function check_explicitly_supported($plugin, $branch) : string {
+        // Check for correctly formed supported.
+        if (isset($plugin->pluginsupported)) {
+            // Broken apart for readability.
+            $error = false;
+            if (!is_array($plugin->pluginsupported)) {
+                $error = true;
+            }
+            if (!is_int($plugin->pluginsupported[0]) || !is_int($plugin->pluginsupported[1])) {
+                $error = true;
+            }
+            if (count($plugin->pluginsupported) != 2) {
+                $error = true;
+            }
+            if ($error) {
+                throw new coding_exception(get_string('err_supported_syntax', 'core_plugin'));
+            }
+        }
+
+        if (isset($plugin->pluginsupported) && $plugin->pluginsupported != null) {
+            if ($plugin->pluginsupported[0] <= $branch && $branch <= $plugin->pluginsupported[1]) {
+                return self::VERSION_SUPPORTED;
+            } else {
+                return self::VERSION_NOT_SUPPORTED;
+            }
+        } else {
+            // If supports aren't specified, but incompatible is, return not supported if not incompatible.
+            if (!isset($plugin->pluginsupported) && isset($plugin->pluginincompatible) && !empty($plugin->pluginincompatible)) {
+                if (!$plugin->is_core_compatible_satisfied($branch)) {
+                    return self::VERSION_NOT_SUPPORTED;
+                }
+            }
+            return self::VERSION_NO_SUPPORTS;
+        }
+    }
+
     /**
      * Is the given plugin version available in the plugins directory?
      *
index cb80f29..75d980f 100644 (file)
@@ -53,6 +53,10 @@ abstract class base {
     public $versiondb;
     /** @var int|float|string required version of Moodle core  */
     public $versionrequires;
+    /** @var array explicitly supported branches of Moodle core  */
+    public $pluginsupported;
+    /** @var int first incompatible branch of Moodle core  */
+    public $pluginincompatible;
     /** @var mixed human-readable release information */
     public $release;
     /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
@@ -218,6 +222,8 @@ abstract class base {
 
         $this->versiondisk = null;
         $this->versionrequires = null;
+        $this->pluginsupported = null;
+        $this->pluginincompatible = null;
         $this->dependencies = array();
 
         if (!isset($versions[$this->name])) {
@@ -238,6 +244,28 @@ abstract class base {
         if (isset($plugin->dependencies)) {
             $this->dependencies = $plugin->dependencies;
         }
+
+        // Check that supports and incompatible are wellformed, exception otherwise.
+        if (isset($plugin->supported)) {
+            // Checks for structure of supported.
+            $isint = (is_int($plugin->supported[0]) && is_int($plugin->supported[1]));
+            $isrange = ($plugin->supported[0] <= $plugin->supported[1] && count($plugin->supported) == 2);
+
+            if (is_array($plugin->supported) && $isint && $isrange) {
+                $this->pluginsupported = $plugin->supported;
+            } else {
+                throw new coding_exception('Incorrect syntax in plugin supported declaration in '."$this->name");
+            }
+        }
+
+        if (isset($plugin->incompatible) && $plugin->incompatible !== null) {
+            if ((ctype_digit($plugin->incompatible) || is_int($plugin->incompatible)) && (int) $plugin->incompatible > 0) {
+                $this->pluginincompatible = intval($plugin->incompatible);
+            } else {
+                throw new coding_exception('Incorrect syntax in plugin incompatible declaration in '."$this->name");
+            }
+        }
+
     }
 
     /**
@@ -341,6 +369,20 @@ abstract class base {
         }
     }
 
+    /**
+     * Returns true if the the given moodle branch is not stated incompatible with the plugin
+     *
+     * @param int $branch the moodle branch number
+     * @return bool true if not incompatible with moodle branch
+     */
+    public function is_core_compatible_satisfied(int $branch) : bool {
+        if (!empty($this->pluginincompatible) && ($branch >= $this->pluginincompatible)) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
     /**
      * Returns the status of the plugin
      *
index ebe2abe..e0ab0e4 100644 (file)
@@ -566,9 +566,6 @@ class manager {
         $records = self::ensure_adhoc_task_qos($records);
 
         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
-        if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
-            throw new \moodle_exception('locktimeout');
-        }
 
         $skipclasses = array();
 
@@ -608,6 +605,13 @@ class manager {
                     }
                 }
 
+                // The global cron lock is under the most contention so request it
+                // as late as possible and release it as soon as possible.
+                if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
+                    $lock->release();
+                    throw new \moodle_exception('locktimeout');
+                }
+
                 $task->set_lock($lock);
                 if (!$task->is_blocking()) {
                     $cronlock->release();
@@ -618,8 +622,6 @@ class manager {
             }
         }
 
-        // No tasks.
-        $cronlock->release();
         return null;
     }
 
@@ -636,10 +638,6 @@ class manager {
         global $DB;
         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
 
-        if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
-            throw new \moodle_exception('locktimeout');
-        }
-
         $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
                   AND (nextruntime IS NULL OR nextruntime < :timestart2)
                   AND disabled = 0
@@ -678,6 +676,13 @@ class manager {
                     continue;
                 }
 
+                // The global cron lock is under the most contention so request it
+                // as late as possible and release it as soon as possible.
+                if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
+                    $lock->release();
+                    throw new \moodle_exception('locktimeout');
+                }
+
                 if (!$task->is_blocking()) {
                     $cronlock->release();
                 } else {
@@ -687,8 +692,6 @@ class manager {
             }
         }
 
-        // No tasks.
-        $cronlock->release();
         return null;
     }
 
index 52946fa..2ca8e54 100644 (file)
@@ -1639,6 +1639,10 @@ function print_object($object) {
     if (CLI_SCRIPT) {
         fwrite(STDERR, print_r($object, true));
         fwrite(STDERR, PHP_EOL);
+    } else if (AJAX_SCRIPT) {
+        foreach (explode("\n", print_r($object, true)) as $line) {
+            error_log($line);
+        }
     } else {
         echo html_writer::tag('pre', s(print_r($object, true)), array('class' => 'notifytiny'));
     }
index dacaf5b..8e33d64 100644 (file)
@@ -391,6 +391,17 @@ $capabilities = array(
         )
     ),
 
+    'moodle/site:viewanonymousevents' => array(
+
+        'riskbitmask' => RISK_PERSONAL,
+
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+        )
+    ),
+
     'moodle/site:viewfullnames' => array(
 
         'captype' => 'read',
index be9692d..0722a2a 100644 (file)
@@ -125,8 +125,8 @@ function xmldb_main_install() {
         'backup_version'        => 2008111700,
         'backup_release'        => '2.0 dev',
         'mnet_dispatcher_mode'  => 'off',
-        'sessiontimeout'        => 7200, // must be present during roles installation
-        'stringfilters'         => '', // These two are managed in a strange way by the filters
+        'sessiontimeout'        => 8 * 60 * 60, // Must be present during roles installation.
+        'stringfilters'         => '', // These two are managed in a strange way by the filters.
         'filterall'             => 0, // setting page, so have to be initialised here.
         'texteditors'           => 'atto,tinymce,textarea',
         'antiviruses'           => '',
index fcef7da..9a30e74 100644 (file)
         <INDEX NAME="uuid" UNIQUE="false" FIELDS="uuid"/>
         <INDEX NAME="type-timesort" UNIQUE="false" FIELDS="type, timesort"/>
         <INDEX NAME="groupid-courseid-categoryid-visible-userid" UNIQUE="false" FIELDS="groupid, courseid, categoryid, visible, userid" COMMENT="used for calendar view"/>
+        <INDEX NAME="eventtype" UNIQUE="false" FIELDS="eventtype"/>
+        <INDEX NAME="modulename-instance" UNIQUE="false" FIELDS="modulename, instance"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="cache_filters" COMMENT="For keeping information about cached data">
index 4c56b39..a80612c 100644 (file)
@@ -2145,5 +2145,22 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2019122000.01);
     }
 
+    if ($oldversion < 2020010900.02) {
+        $table = new xmldb_table('event');
+
+        // This index will improve the performance when the Events API retrieves category and group events.
+        $index = new xmldb_index('eventtype', XMLDB_INDEX_NOTUNIQUE, ['eventtype']);
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // This index improves the performance of backups, deletion and visibilty changes on activities.
+        $index = new xmldb_index('modulename-instance', XMLDB_INDEX_NOTUNIQUE, ['modulename', 'instance']);
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        upgrade_main_savepoint(true, 2020010900.02);
+    }
     return true;
 }
index cdf67fa..c8d7bb6 100644 (file)
Binary files a/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-debug.js and b/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-debug.js differ
index 2940890..e4a9c3b 100644 (file)
Binary files a/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-min.js and b/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-min.js differ
index 7c14998..89d365c 100644 (file)
Binary files a/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button.js and b/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button.js differ
index 1634ac7..ce6a4e2 100644 (file)
@@ -127,8 +127,11 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
 
             // Check for non-empty text.
             if (Y.Lang.trim(node.get('text')) !== '') {
-                foreground = node.getComputedStyle('color');
-                background = node.getComputedStyle('backgroundColor');
+                foreground = Y.Color.fromArray(
+                    this._getComputedBackgroundColor(node, node.getComputedStyle('color')),
+                    Y.Color.TYPES.RGBA
+                );
+                background = Y.Color.fromArray(this._getComputedBackgroundColor(node), Y.Color.TYPES.RGBA);
 
                 lum1 = this._getLuminanceFromCssColor(foreground);
                 lum2 = this._getLuminanceFromCssColor(background);
@@ -237,7 +240,7 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
      * Generate the HTML that lists the found warnings.
      *
      * @method _addWarnings
-     * @param {Node} A Node to append the html to.
+     * @param {Node} list Node to append the html to.
      * @param {String} description Description of this failure.
      * @param {array} nodes An array of failing nodes.
      * @param {boolean} imagewarnings true if the warnings are related to images, false if text.
@@ -307,5 +310,43 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
             b1 = part1(color[2]);
 
         return 0.2126 * r1 + 0.7152 * g1 + 0.0722 * b1;
+    },
+
+    /**
+     * Get the computed RGB converted to full alpha value, considering the node hierarchy.
+     *
+     * @method _getComputedBackgroundColor
+     * @param {Node} node
+     * @param {String} color The initial colour. If not specified, fetches the backgroundColor from the node.
+     * @return {Array} Colour in Array form (RGBA)
+     * @private
+     */
+    _getComputedBackgroundColor: function(node, color) {
+        color = color || node.getComputedStyle('backgroundColor');
+
+        if (color.toLowerCase() === 'transparent') {
+            // Y.Color doesn't handle 'transparent' properly.
+            color = 'rgba(1, 1, 1, 0)';
+        }
+
+        // Convert the colour to its constituent parts in RGBA format, then fetch the alpha.
+        var colorParts = Y.Color.toArray(color);
+        var alpha = colorParts[3];
+
+        if (alpha === 1) {
+            // If the alpha of the background is already 1, then the parent background colour does not change anything.
+            return colorParts;
+        }
+
+        // Fetch the computed background colour of the parent and use it to calculate the RGB of this item.
+        var parentColor = this._getComputedBackgroundColor(node.get('parentNode'));
+        return [
+            // RGB = (alpha * R|G|B) + (1 - alpha * solid parent colour).
+            (1 - alpha) * parentColor[0] + alpha * colorParts[0],
+            (1 - alpha) * parentColor[1] + alpha * colorParts[1],
+            (1 - alpha) * parentColor[2] + alpha * colorParts[2],
+            // We always return a colour with full alpha.
+            1
+        ];
     }
 });
index 32b971b..85d0394 100644 (file)
@@ -38,7 +38,7 @@ class processor implements  \core_analytics\classifier, \core_analytics\regresso
     /**
      * The required version of the python package that performs all calculations.
      */
-    const REQUIRED_PIP_PACKAGE_VERSION = '2.3.0';
+    const REQUIRED_PIP_PACKAGE_VERSION = '2.4.0';
 
     /**
      * The python package is installed in a server.
index 586ce76..fc61472 100644 (file)
@@ -6,9 +6,9 @@ at https://github.com/bobthecow/mustache.php/releases)
 2) Move the src/ and LICENSE file into lib/mustache
 
 e.g.
-wget https://github.com/bobthecow/mustache.php/archive/v2.12.0.zip
-unzip v2.12.0.zip
-cd mustache.php-2.12.0/
+wget https://github.com/bobthecow/mustache.php/archive/v2.13.0.zip
+unzip v2.13.0.zip
+cd mustache.php-2.13.0/
 mv src /path/to/moodle/lib/mustache/
 mv LICENSE /path/to/moodle/lib/mustache/
 
index 9110977..fe99799 100644 (file)
@@ -23,7 +23,7 @@
  */
 class Mustache_Engine
 {
-    const VERSION        = '2.12.0';
+    const VERSION        = '2.13.0';
     const SPEC_VERSION   = '1.1.2';
 
     const PRAGMA_FILTERS      = 'FILTERS';
index b1f3d73..6dbe0cd 100644 (file)
@@ -72,15 +72,20 @@ class Mustache_Tokenizer
     private $tokens;
     private $seenTag;
     private $line;
+
     private $otag;
-    private $ctag;
+    private $otagChar;
     private $otagLen;
+
+    private $ctag;
+    private $ctagChar;
     private $ctagLen;
 
     /**
      * Scan and tokenize template source.
      *
      * @throws Mustache_Exception_SyntaxException when mismatched section tags are encountered
+     * @throws Mustache_Exception_InvalidArgumentException when $delimiters string is invalid
      *
      * @param string $text       Mustache template source to tokenize
      * @param string $delimiters Optionally, pass initial opening and closing delimiters (default: null)
@@ -110,12 +115,13 @@ class Mustache_Tokenizer
         for ($i = 0; $i < $len; $i++) {
             switch ($this->state) {
                 case self::IN_TEXT:
-                    if ($this->tagChange($this->otag, $this->otagLen, $text, $i)) {
+                    $char = $text[$i];
+                    // Test whether it's time to change tags.
+                    if ($char === $this->otagChar && substr($text, $i, $this->otagLen) === $this->otag) {
                         $i--;
                         $this->flushBuffer();
                         $this->state = self::IN_TAG_TYPE;
                     } else {
-                        $char = $text[$i];
                         $this->buffer .= $char;
                         if ($char === "\n") {
                             $this->flushBuffer();
@@ -151,7 +157,9 @@ class Mustache_Tokenizer
                     break;
 
                 default:
-                    if ($this->tagChange($this->ctag, $this->ctagLen, $text, $i)) {
+                    $char = $text[$i];
+                    // Test whether it's time to change tags.
+                    if ($char === $this->ctagChar && substr($text, $i, $this->ctagLen) === $this->ctag) {
                         $token = array(
                             self::TYPE  => $this->tagType,
                             self::NAME  => trim($this->buffer),
@@ -196,7 +204,7 @@ class Mustache_Tokenizer
                         $this->state = self::IN_TEXT;
                         $this->tokens[] = $token;
                     } else {
-                        $this->buffer .= $text[$i];
+                        $this->buffer .= $char;
                     }
                     break;
             }
@@ -219,16 +227,20 @@ class Mustache_Tokenizer
      */
     private function reset()
     {
-        $this->state   = self::IN_TEXT;
-        $this->tagType = null;
-        $this->buffer  = '';
-        $this->tokens  = array();
-        $this->seenTag = false;
-        $this->line    = 0;
-        $this->otag    = '{{';
-        $this->ctag    = '}}';
-        $this->otagLen = 2;
-        $this->ctagLen = 2;
+        $this->state    = self::IN_TEXT;
+        $this->tagType  = null;
+        $this->buffer   = '';
+        $this->tokens   = array();
+        $this->seenTag  = false;
+        $this->line     = 0;
+
+        $this->otag     = '{{';
+        $this->otagChar = '{';
+        $this->otagLen  = 2;
+
+        $this->ctag     = '}}';
+        $this->ctagChar = '}';
+        $this->ctagLen  = 2;
     }
 
     /**
@@ -249,6 +261,8 @@ class Mustache_Tokenizer
     /**
      * Change the current Mustache delimiters. Set new `otag` and `ctag` values.
      *
+     * @throws Mustache_Exception_SyntaxException when delimiter string is invalid
+     *
      * @param string $text  Mustache template source
      * @param int    $index Current tokenizer index
      *
@@ -260,28 +274,44 @@ class Mustache_Tokenizer
         $close      = '=' . $this->ctag;
         $closeIndex = strpos($text, $close, $index);
 
-        $this->setDelimiters(trim(substr($text, $startIndex, $closeIndex - $startIndex)));
-
-        $this->tokens[] = array(
+        $token = array(
             self::TYPE => self::T_DELIM_CHANGE,
             self::LINE => $this->line,
         );
 
+        try {
+            $this->setDelimiters(trim(substr($text, $startIndex, $closeIndex - $startIndex)));
+        } catch (Mustache_Exception_InvalidArgumentException $e) {
+            throw new Mustache_Exception_SyntaxException($e->getMessage(), $token);
+        }
+
+        $this->tokens[] = $token;
+
         return $closeIndex + strlen($close) - 1;
     }
 
     /**
      * Set the current Mustache `otag` and `ctag` delimiters.
      *
+     * @throws Mustache_Exception_InvalidArgumentException when delimiter string is invalid
+     *
      * @param string $delimiters
      */
     private function setDelimiters($delimiters)
     {
-        list($otag, $ctag) = explode(' ', $delimiters);
-        $this->otag = $otag;
-        $this->ctag = $ctag;
-        $this->otagLen = strlen($otag);
-        $this->ctagLen = strlen($ctag);
+        if (!preg_match('/^\s*(\S+)\s+(\S+)\s*$/', $delimiters, $matches)) {
+            throw new Mustache_Exception_InvalidArgumentException(sprintf('Invalid delimiters: %s', $delimiters));
+        }
+
+        list($_, $otag, $ctag) = $matches;
+
+        $this->otag     = $otag;
+        $this->otagChar = $otag[0];
+        $this->otagLen  = strlen($otag);
+
+        $this->ctag     = $ctag;
+        $this->ctagChar = $ctag[0];
+        $this->ctagLen  = strlen($ctag);
     }
 
     /**
@@ -309,19 +339,4 @@ class Mustache_Tokenizer
 
         return $end + $this->ctagLen - 1;
     }
-
-    /**
-     * Test whether it's time to change tags.
-     *
-     * @param string $tag    Current tag name
-     * @param int    $tagLen Current tag name length
-     * @param string $text   Mustache template source
-     * @param int    $index  Current tokenizer index
-     *
-     * @return bool True if this is a closing section tag
-     */
-    private function tagChange($tag, $tagLen, $text, $index)
-    {
-        return substr($text, $index, $tagLen) === $tag;
-    }
 }
index 1cf496f..422497f 100644 (file)
@@ -57,12 +57,12 @@ class Cache
     public function __construct($options)
     {
         // check $cacheDir
-        if (isset($options['cache_dir'])) {
-            self::$cacheDir = $options['cache_dir'];
+        if (isset($options['cacheDir'])) {
+            self::$cacheDir = $options['cacheDir'];
         }
 
         if (empty(self::$cacheDir)) {
-            throw new Exception('cache_dir not set');
+            throw new Exception('cacheDir not set');
         }
 
         if (isset($options['prefix'])) {
@@ -74,7 +74,7 @@ class Cache
         }
 
         if (isset($options['forceRefresh'])) {
-            self::$forceRefresh = $options['force_refresh'];
+            self::$forceRefresh = $options['forceRefresh'];
         }
 
         self::checkCacheDir();
@@ -97,7 +97,7 @@ class Cache
     {
         $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
 
-        if ((! self::$forceRefresh || (self::$forceRefresh === 'once' &&
+        if (((self::$forceRefresh === false) || (self::$forceRefresh === 'once' &&
             isset(self::$refreshed[$fileCache]))) && file_exists($fileCache)
         ) {
             $cacheTime = filemtime($fileCache);
@@ -176,13 +176,13 @@ class Cache
         self::$cacheDir = str_replace('\\', '/', self::$cacheDir);
         self::$cacheDir = rtrim(self::$cacheDir, '/') . '/';
 
-        if (! file_exists(self::$cacheDir)) {
+        if (! is_dir(self::$cacheDir)) {
             if (! mkdir(self::$cacheDir)) {
                 throw new Exception('Cache directory couldn\'t be created: ' . self::$cacheDir);
             }
-        } elseif (! is_dir(self::$cacheDir)) {
-            throw new Exception('Cache directory doesn\'t exist: ' . self::$cacheDir);
-        } elseif (! is_writable(self::$cacheDir)) {
+        }
+
+        if (! is_writable(self::$cacheDir)) {
             throw new Exception('Cache directory isn\'t writable: ' . self::$cacheDir);
         }
     }
index ad45924..ef6409a 100644 (file)
@@ -25,7 +25,7 @@ class Colors
      *
      * @var array
      */
-    public static $cssColors = [
+    protected static $cssColors = [
         'aliceblue' => '240,248,255',
         'antiquewhite' => '250,235,215',
         'aqua' => '0,255,255',
@@ -176,4 +176,71 @@ class Colors
         'yellow' => '255,255,0',
         'yellowgreen' => '154,205,50',
     ];
+
+    /**
+     * Convert named color in a [r,g,b[,a]] array
+     *
+     * @param string $colorName
+     *
+     * @return array|null
+     */
+    public static function colorNameToRGBa($colorName)
+    {
+        if (is_string($colorName) && isset(static::$cssColors[$colorName])) {
+            $rgba = explode(',', static::$cssColors[$colorName]);
+
+            // only case with opacity is transparent, with opacity=0, so we can intval on opacity also
+            $rgba = array_map('intval', $rgba);
+
+            return $rgba;
+        }
+
+        return null;
+    }
+
+    /**
+     * Reverse conversion : from RGBA to a color name if possible
+     *
+     * @param integer $r
+     * @param integer $g
+     * @param integer $b
+     * @param integer $a
+     *
+     * @return string|null
+     */
+    public static function RGBaToColorName($r, $g, $b, $a = 1)
+    {
+        static $reverseColorTable = null;
+
+        if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b) || ! is_numeric($a)) {
+            return null;
+        }
+
+        if ($a < 1) {
+            # specific case we dont' revert according to spec
+            #if (! $a && ! $r && ! $g && ! $b) {
+            #    return 'transparent';
+            #}
+
+            return null;
+        }
+
+        if (is_null($reverseColorTable)) {
+            $reverseColorTable = [];
+
+            foreach (static::$cssColors as $name => $rgb_str) {
+                $rgb_str = explode(',', $rgb_str);
+
+                if (count($rgb_str) == 3) {
+                    $reverseColorTable[intval($rgb_str[0])][intval($rgb_str[1])][intval($rgb_str[2])] = $name;
+                }
+            }
+        }
+
+        if (isset($reverseColorTable[intval($r)][intval($g)][intval($b)])) {
+            return $reverseColorTable[intval($r)][intval($g)][intval($b)];
+        }
+
+        return null;
+    }
 }
index 75c5d67..711d338 100644 (file)
@@ -164,6 +164,8 @@ class Compiler
 
     /**
      * Constructor
+     *
+     * @param array|null $cacheOptions
      */
     public function __construct($cacheOptions = null)
     {
@@ -173,8 +175,15 @@ class Compiler
         if ($cacheOptions) {
             $this->cache = new Cache($cacheOptions);
         }
+
+        $this->stderr = fopen('php://stderr', 'w');
     }
 
+    /**
+     * Get compiler options
+     *
+     * @return array
+     */
     public function getCompileOptions()
     {
         $options = [
@@ -190,6 +199,16 @@ class Compiler
         return $options;
     }
 
+    /**
+     * Set an alternative error output stream, for testing purpose only
+     *
+     * @param resource $handle
+     */
+    public function setErrorOuput($handle)
+    {
+        $this->stderr = $handle;
+    }
+
     /**
      * Compile scss
      *
@@ -210,7 +229,7 @@ class Compiler
             if (is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) {
                 // check if any dependency file changed before accepting the cache
                 foreach ($cache['dependencies'] as $file => $mtime) {
-                    if (! file_exists($file) || filemtime($file) !== $mtime) {
+                    if (! is_file($file) || filemtime($file) !== $mtime) {
                         unset($cache);
                         break;
                     }
@@ -234,7 +253,6 @@ class Compiler
         $this->storeEnv       = null;
         $this->charsetSeen    = null;
         $this->shouldEvaluate = null;
-        $this->stderr         = fopen('php://stderr', 'w');
 
         $this->parser = $this->parserFactory($path);
         $tree         = $this->parser->parse($code);
@@ -329,9 +347,9 @@ class Compiler
     /**
      * Push extends
      *
-     * @param array     $target
-     * @param array     $origin
-     * @param \stdClass $block
+     * @param array      $target
+     * @param array      $origin
+     * @param array|null $block
      */
     protected function pushExtends($target, $origin, $block)
     {
@@ -362,12 +380,12 @@ class Compiler
     protected function makeOutputBlock($type, $selectors = null)
     {
         $out = new OutputBlock;
-        $out->type         = $type;
-        $out->lines        = [];
-        $out->children     = [];
-        $out->parent       = $this->scope;
-        $out->selectors    = $selectors;
-        $out->depth        = $this->env->depth;
+        $out->type      = $type;
+        $out->lines     = [];
+        $out->children  = [];
+        $out->parent    = $this->scope;
+        $out->selectors = $selectors;
+        $out->depth     = $this->env->depth;
 
         if ($this->env->block instanceof Block) {
             $out->sourceName   = $this->env->block->sourceName;
@@ -501,6 +519,9 @@ class Compiler
                 if (count($new) && is_string($new[count($new) - 1]) &&
                     strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
                 ) {
+                    while (count($new)>1 && substr($new[count($new) - 1], -1) !== '(') {
+                        $part = array_pop($new) . $part;
+                    }
                     $new[count($new) - 1] .= $part;
                 } else {
                     $new[] = $part;
@@ -522,13 +543,13 @@ class Compiler
     protected function matchExtends($selector, &$out, $from = 0, $initial = true)
     {
         static $partsPile = [];
-
         $selector = $this->glueFunctionSelectors($selector);
 
         if (count($selector) == 1 && in_array(reset($selector), $partsPile)) {
             return;
         }
 
+        $outRecurs = [];
         foreach ($selector as $i => $part) {
             if ($i < $from) {
                 continue;
@@ -544,11 +565,10 @@ class Compiler
                 }
             }
 
-            if ($this->matchExtendsSingle($part, $origin)) {
-                $partsPile[] = $part;
+            $partsPile[] = $part;
+            if ($this->matchExtendsSingle($part, $origin, $initial)) {
                 $after       = array_slice($selector, $i + 1);
                 $before      = array_slice($selector, 0, $i);
-
                 list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
 
                 foreach ($origin as $new) {
@@ -560,6 +580,9 @@ class Compiler
                             $k++;
                         }
                     }
+                    if (count($nonBreakableBefore) and $k == count($new)) {
+                        $k--;
+                    }
 
                     $replacement = [];
                     $tempReplacement = $k > 0 ? array_slice($new, $k) : $new;
@@ -596,36 +619,93 @@ class Compiler
                         continue;
                     }
 
-                    $out[] = $result;
+                    $this->pushOrMergeExtentedSelector($out, $result);
 
                     // recursively check for more matches
                     $startRecurseFrom = count($before) + min(count($nonBreakableBefore), count($mergedBefore));
-                    $this->matchExtends($result, $out, $startRecurseFrom, false);
+                    if (count($origin) > 1) {
+                        $this->matchExtends($result, $out, $startRecurseFrom, false);
+                    } else {
+                        $this->matchExtends($result, $outRecurs, $startRecurseFrom, false);
+                    }
 
                     // selector sequence merging
                     if (! empty($before) && count($new) > 1) {
                         $preSharedParts = $k > 0 ? array_slice($before, 0, $k) : [];
                         $postSharedParts = $k > 0 ? array_slice($before, $k) : $before;
 
-                        list($betweenSharedParts, $nonBreakable2) = $this->extractRelationshipFromFragment($afterBefore);
+                        list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore);
 
                         $result2 = array_merge(
                             $preSharedParts,
                             $betweenSharedParts,
                             $postSharedParts,
-                            $nonBreakable2,
+                            $nonBreakabl2,
                             $nonBreakableBefore,
                             $replacement,
                             $after
                         );
 
-                        $out[] = $result2;
+                        $this->pushOrMergeExtentedSelector($out, $result2);
                     }
                 }
+            }
+            array_pop($partsPile);
+        }
+        while (count($outRecurs)) {
+            $result = array_shift($outRecurs);
+            $this->pushOrMergeExtentedSelector($out, $result);
+        }
+    }
+
+    /**
+     * Test a part for being a pseudo selector
+     * @param string $part
+     * @param array $matches
+     * @return bool
+     */
+    protected function isPseudoSelector($part, &$matches)
+    {
+        if (strpos($part, ":") === 0
+            && preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)) {
+            return true;
+        }
+        return false;
+    }
 
-                array_pop($partsPile);
+    /**
+     * Push extended selector except if
+     *  - this is a pseudo selector
+     *  - same as previous
+     *  - in a white list
+     * in this case we merge the pseudo selector content
+     * @param array $out
+     * @param array $extended
+     */
+    protected function pushOrMergeExtentedSelector(&$out, $extended)
+    {
+        if (count($out) && count($extended) === 1 && count(reset($extended)) === 1) {
+            $single = reset($extended);
+            $part = reset($single);
+            if ($this->isPseudoSelector($part, $matchesExtended)
+              && in_array($matchesExtended[1], [ 'slotted' ])) {
+                $prev = end($out);
+                $prev = $this->glueFunctionSelectors($prev);
+                if (count($prev) === 1 && count(reset($prev)) === 1) {
+                    $single = reset($prev);
+                    $part = reset($single);
+                    if ($this->isPseudoSelector($part, $matchesPrev)
+                      && $matchesPrev[1] === $matchesExtended[1]) {
+                        $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2);
+                        $extended[1] = $matchesPrev[2] . ", " . $extended[1];
+                        $extended = implode($matchesExtended[1] . '(', $extended);
+                        $extended = [ [ $extended ]];
+                        array_pop($out);
+                    }
+                }
             }
         }
+        $out[] = $extended;
     }
 
     /**
@@ -633,10 +713,11 @@ class Compiler
      *
      * @param array $rawSingle
      * @param array $outOrigin
+     * @param bool $initial
      *
      * @return boolean
      */
-    protected function matchExtendsSingle($rawSingle, &$outOrigin)
+    protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true)
     {
         $counts = [];
         $single = [];
@@ -666,17 +747,41 @@ class Compiler
             $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
         }
 
-        foreach ($single as $part) {
+        $outOrigin = [];
+        $found = false;
+
+        foreach ($single as $k => $part) {
             if (isset($this->extendsMap[$part])) {
                 foreach ($this->extendsMap[$part] as $idx) {
                     $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
                 }
             }
+            if ($initial
+                && $this->isPseudoSelector($part, $matches)
+                && ! in_array($matches[1], [ 'not' ])) {
+                $buffer    = $matches[2];
+                $parser    = $this->parserFactory(__METHOD__);
+                if ($parser->parseSelector($buffer, $subSelectors)) {
+                    foreach ($subSelectors as $ksub => $subSelector) {
+                        $subExtended = [];
+                        $this->matchExtends($subSelector, $subExtended, 0, false);
+                        if ($subExtended) {
+                            $subSelectorsExtended = $subSelectors;
+                            $subSelectorsExtended[$ksub] = $subExtended;
+                            foreach ($subSelectorsExtended as $ksse => $sse) {
+                                $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse);
+                            }
+                            $subSelectorsExtended = implode(', ', $subSelectorsExtended);
+                            $singleExtended = $single;
+                            $singleExtended[$k] = str_replace("(".$buffer.")", "($subSelectorsExtended)", $part);
+                            $outOrigin[] = [ $singleExtended ];
+                            $found = true;
+                        }
+                    }
+                }
+            }
         }
 
-        $outOrigin = [];
-        $found = false;
-
         foreach ($counts as $idx => $count) {
             list($target, $origin, /* $block */) = $this->extends[$idx];
 
@@ -738,12 +843,13 @@ class Compiler
     {
         $parents = [];
         $children = [];
+
         $j = $i = count($fragment);
 
         for (;;) {
             $children = $j != $i ? array_slice($fragment, $j, $i - $j) : [];
-            $parents = array_slice($fragment, 0, $j);
-            $slice = end($parents);
+            $parents  = array_slice($fragment, 0, $j);
+            $slice    = end($parents);
 
             if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
                 break;
@@ -765,22 +871,25 @@ class Compiler
      */
     protected function combineSelectorSingle($base, $other)
     {
-        $tag = [];
-        $out = [];
-        $wasTag = true;
+        $tag    = [];
+        $out    = [];
+        $wasTag = false;
 
-        foreach ([$base, $other] as $single) {
+        foreach ([array_reverse($base), array_reverse($other)] as $single) {
             foreach ($single as $part) {
-                if (preg_match('/^[\[.:#]/', $part)) {
+                if (preg_match('/^[\[:]/', $part)) {
                     $out[] = $part;
                     $wasTag = false;
+                } elseif (preg_match('/^[\.#]/', $part)) {
+                    array_unshift($out, $part);
+                    $wasTag = false;
                 } elseif (preg_match('/^[^_-]/', $part)) {
                     $tag[] = $part;
                     $wasTag = true;
                 } elseif ($wasTag) {
                     $tag[count($tag) - 1] .= $part;
                 } else {
-                    $out[count($out) - 1] .= $part;
+                    $out[= $part;
                 }
             }
         }
@@ -832,16 +941,17 @@ class Compiler
 
             if ($needsWrap) {
                 $wrapped = new Block;
-                $wrapped->sourceName = $media->sourceName;
-                $wrapped->sourceIndex = $media->sourceIndex;
-                $wrapped->sourceLine = $media->sourceLine;
+                $wrapped->sourceName   = $media->sourceName;
+                $wrapped->sourceIndex  = $media->sourceIndex;
+                $wrapped->sourceLine   = $media->sourceLine;
                 $wrapped->sourceColumn = $media->sourceColumn;
-                $wrapped->selectors = [];
-                $wrapped->comments = [];
-                $wrapped->parent = $media;
-                $wrapped->children = $media->children;
+                $wrapped->selectors    = [];
+                $wrapped->comments     = [];
+                $wrapped->parent       = $media;
+                $wrapped->children     = $media->children;
 
                 $media->children = [[Type::T_BLOCK, $wrapped]];
+
                 if (isset($this->lineNumberStyle)) {
                     $annotation = $this->makeOutputBlock(Type::T_COMMENT);
                     $annotation->depth = 0;
@@ -898,20 +1008,29 @@ class Compiler
     /**
      * Compile directive
      *
-     * @param \ScssPhp\ScssPhp\Block $block
+     * @param \ScssPhp\ScssPhp\Block|array $block
+     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
      */
-    protected function compileDirective(Block $block)
+    protected function compileDirective($directive, OutputBlock $out)
     {
-        $s = '@' . $block->name;
+        if (is_array($directive)) {
+            $s = '@' . $directive[0];
+            if (! empty($directive[1])) {
+                $s .= ' ' . $this->compileValue($directive[1]);
+            }
+            $this->appendRootDirective($s . ';', $out);
+        } else {
+            $s = '@' . $directive->name;
 
-        if (! empty($block->value)) {
-            $s .= ' ' . $this->compileValue($block->value);
-        }
+            if (! empty($directive->value)) {
+                $s .= ' ' . $this->compileValue($directive->value);
+            }
 
-        if ($block->name === 'keyframes' || substr($block->name, -10) === '-keyframes') {
-            $this->compileKeyframeBlock($block, [$s]);
-        } else {
-            $this->compileNestedBlock($block, [$s]);
+            if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') {
+                $this->compileKeyframeBlock($directive, [$s]);
+            } else {
+                $this->compileNestedBlock($directive, [$s]);
+            }
         }
     }
 
@@ -978,6 +1097,7 @@ class Compiler
     protected function filterScopeWithWithout($scope, $with, $without)
     {
         $filteredScopes = [];
+        $childStash = [];
 
         if ($scope->type === TYPE::T_ROOT) {
             return $scope;
@@ -985,6 +1105,7 @@ class Compiler
 
         // start from the root
         while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) {
+            array_unshift($childStash, $scope);
             $scope = $scope->parent;
         }
 
@@ -996,8 +1117,8 @@ class Compiler
             if ($this->isWith($scope, $with, $without)) {
                 $s = clone $scope;
                 $s->children = [];
-                $s->lines = [];
-                $s->parent = null;
+                $s->lines    = [];
+                $s->parent   = null;
 
                 if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) {
                     $s->selectors = [];
@@ -1006,7 +1127,9 @@ class Compiler
                 $filteredScopes[] = $s;
             }
 
-            if ($scope->children) {
+            if (count($childStash)) {
+                $scope = array_shift($childStash);
+            } elseif ($scope->children) {
                 $scope = end($scope->children);
             } else {
                 $scope = null;
@@ -1027,8 +1150,9 @@ class Compiler
         while (count($filteredScopes)) {
             $s = array_shift($filteredScopes);
             $s->parent = $p;
-            $p->children[] = &$s;
-            $p = $s;
+            $p->children[] = $s;
+            $newScope = &$p->children[0];
+            $p = &$p->children[0];
         }
 
         return $newScope;
@@ -1139,8 +1263,9 @@ class Compiler
         foreach ($envs as $e) {
             if ($e->block && ! $this->isWith($e->block, $with, $without)) {
                 $ec = clone $e;
-                $ec->block = null;
+                $ec->block     = null;
                 $ec->selectors = [];
+
                 $filtered[] = $ec;
             } else {
                 $filtered[] = $e;
@@ -1169,16 +1294,26 @@ class Compiler
             if ($block->type === Type::T_DIRECTIVE) {
                 if (isset($block->name)) {
                     return $this->testWithWithout($block->name, $with, $without);
-                }
-                elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
+                } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
                     return $this->testWithWithout($m[1], $with, $without);
-                }
-                else {
+                } else {
                     return $this->testWithWithout('???', $with, $without);
                 }
             }
-        }
-        elseif (isset($block->selectors)) {
+        } elseif (isset($block->selectors)) {
+            // a selector starting with number is a keyframe rule
+            if (count($block->selectors)) {
+                $s = reset($block->selectors);
+
+                while (is_array($s)) {
+                    $s = reset($s);
+                }
+
+                if (is_object($s) && $s instanceof Node\Number) {
+                    return $this->testWithWithout('keyframes', $with, $without);
+                }
+            }
+
             return $this->testWithWithout('rule', $with, $without);
         }
 
@@ -1191,10 +1326,12 @@ class Compiler
      * @param string $what
      * @param array  $with
      * @param array  $without
-     * @return bool
+     *
+     * @return boolean
      *   true if the block should be kept, false to reject
      */
-    protected function testWithWithout($what, $with, $without) {
+    protected function testWithWithout($what, $with, $without)
+    {
 
         // if without, reject only if in the list (or 'all' is in the list)
         if (count($without)) {
@@ -1263,6 +1400,7 @@ class Compiler
                     array_unshift($child[1]->prefix[2], $prefix);
                     break;
             }
+
             $this->compileChild($child, $nested);
         }
     }
@@ -1295,15 +1433,15 @@ class Compiler
 
             if ($needWrapping) {
                 $wrapped = new Block;
-                $wrapped->sourceName = $block->sourceName;
-                $wrapped->sourceIndex = $block->sourceIndex;
-                $wrapped->sourceLine = $block->sourceLine;
+                $wrapped->sourceName   = $block->sourceName;
+                $wrapped->sourceIndex  = $block->sourceIndex;
+                $wrapped->sourceLine   = $block->sourceLine;
                 $wrapped->sourceColumn = $block->sourceColumn;
-                $wrapped->selectors = [];
-                $wrapped->comments = [];
-                $wrapped->parent = $block;
-                $wrapped->children = $block->children;
-                $wrapped->selfParent = $block->selfParent;
+                $wrapped->selectors    = [];
+                $wrapped->comments     = [];
+                $wrapped->parent       = $block;
+                $wrapped->children     = $block->children;
+                $wrapped->selfParent   = $block->selfParent;
 
                 $block->children = [[Type::T_BLOCK, $wrapped]];
             }
@@ -1380,17 +1518,50 @@ class Compiler
 
             $this->compileChildrenNoReturn($block->children, $out, $block->selfParent);
 
-            // and revert for the following childs of the same block
+            // and revert for the following children of the same block
             if ($selfParentSelectors) {
                 $block->selfParent->selectors = $selfParentSelectors;
             }
         }
 
-        $this->formatter->stripSemicolon($out->lines);
-
         $this->popEnv();
     }
 
+
+    /**
+     * Compile the value of a comment that can have interpolation
+     *
+     * @param array   $value
+     * @param boolean $pushEnv
+     *
+     * @return array|mixed|string
+     */
+    protected function compileCommentValue($value, $pushEnv = false)
+    {
+        $c = $value[1];
+
+        if (isset($value[2])) {
+            if ($pushEnv) {
+                $this->pushEnv();
+                $storeEnv = $this->storeEnv;
+                $this->storeEnv = $this->env;
+            }
+
+            try {
+                $c = $this->compileValue($value[2]);
+            } catch (\Exception $e) {
+                // ignore error in comment compilation which are only interpolation
+            }
+
+            if ($pushEnv) {
+                $this->storeEnv = $storeEnv;
+                $this->popEnv();
+            }
+        }
+
+        return $c;
+    }
+
     /**
      * Compile root level comment
      *
@@ -1399,7 +1570,7 @@ class Compiler
     protected function compileComment($block)
     {
         $out = $this->makeOutputBlock(Type::T_COMMENT);
-        $out->lines[] = is_string($block[1]) ? $block[1] : $this->compileValue($block[1]);
+        $out->lines[] = $this->compileCommentValue($block, true);
 
         $this->scope->children[] = $out;
     }
@@ -1420,8 +1591,8 @@ class Compiler
         // after evaluating interpolates, we might need a second pass
         if ($this->shouldEvaluate) {
             $selectors = $this->revertSelfSelector($selectors);
-            $buffer = $this->collapseSelectors($selectors);
-            $parser = $this->parserFactory(__METHOD__);
+            $buffer    = $this->collapseSelectors($selectors);
+            $parser    = $this->parserFactory(__METHOD__);
 
             if ($parser->parseSelector($buffer, $newSelectors)) {
                 $selectors = array_map([$this, 'evalSelector'], $newSelectors);
@@ -1505,6 +1676,7 @@ class Compiler
                     } else {
                         $output[] = $compound;
                     }
+
                     $glueNext = true;
                 } elseif ($glueNext) {
                     $output[count($output) - 1] .= ' ' . $compound;
@@ -1518,6 +1690,7 @@ class Compiler
                 foreach ($output as &$o) {
                     $o = [Type::T_STRING, '', [$o]];
                 }
+
                 $output = [Type::T_LIST, ' ', $output];
             } else {
                 $output = implode(' ', $output);
@@ -1675,6 +1848,7 @@ class Compiler
             // not displayed but you can var_dump it to deep debug
             $msg = $this->callStackMessage(true, 100);
             $msg = "Infinite calling loop";
+
             $this->throwError($msg);
         }
     }
@@ -1729,7 +1903,7 @@ class Compiler
                 $stm[1]->selfParent = $selfParent;
                 $ret = $this->compileChild($stm, $out);
                 $stm[1]->selfParent = null;
-            } elseif ($selfParent && $stm[0] === TYPE::T_INCLUDE) {
+            } elseif ($selfParent && in_array($stm[0], [TYPE::T_INCLUDE, TYPE::T_EXTEND])) {
                 $stm['selfParent'] = $selfParent;
                 $ret = $this->compileChild($stm, $out);
                 unset($stm['selfParent']);
@@ -1758,9 +1932,12 @@ class Compiler
     protected function evaluateMediaQuery($queryList)
     {
         static $parser = null;
+
         $outQueryList = [];
+
         foreach ($queryList as $kql => $query) {
             $shouldReparse = false;
+
             foreach ($query as $kq => $q) {
                 for ($i = 1; $i < count($q); $i++) {
                     $value = $this->compileValue($q[$i]);
@@ -1779,24 +1956,31 @@ class Compiler
                     $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value];
                 }
             }
+
             if ($shouldReparse) {
                 if (is_null($parser)) {
                     $parser = $this->parserFactory(__METHOD__);
                 }
+
                 $queryString = $this->compileMediaQuery([$queryList[$kql]]);
                 $queryString = reset($queryString);
+
                 if (strpos($queryString, '@media ') === 0) {
                     $queryString = substr($queryString, 7);
                     $queries = [];
+
                     if ($parser->parseMediaQueryList($queryString, $queries)) {
                         $queries = $this->evaluateMediaQuery($queries[2]);
+
                         while (count($queries)) {
                             $outQueryList[] = array_shift($queries);
                         }
+
                         continue;
                     }
                 }
             }
+
             $outQueryList[] = $queryList[$kql];
         }
 
@@ -1812,9 +1996,9 @@ class Compiler
      */
     protected function compileMediaQuery($queryList)
     {
-        $start = '@media ';
+        $start   = '@media ';
         $default = trim($start);
-        $out = [];
+        $out     = [];
         $current = "";
 
         foreach ($queryList as $query) {
@@ -1834,6 +2018,7 @@ class Compiler
                 switch ($q[0]) {
                     case Type::T_MEDIA_TYPE:
                         $newType = array_map([$this, 'compileValue'], array_slice($q, 1));
+
                         // combining not and anything else than media type is too risky and should be avoided
                         if (! $mediaTypeOnly) {
                             if (in_array(Type::T_NOT, $newType) || ($type && in_array(Type::T_NOT, $type) )) {
@@ -1854,8 +2039,8 @@ class Compiler
                                 }
 
                                 $current = "";
-                                $type = null;
-                                $parts = [];
+                                $type    = null;
+                                $parts   = [];
                             }
                         }
 
@@ -1987,23 +2172,19 @@ class Compiler
             return $type1;
         }
 
-        $m1 = '';
-        $t1 = '';
-
         if (count($type1) > 1) {
-            $m1= strtolower($type1[0]);
-            $t1= strtolower($type1[1]);
+            $m1 = strtolower($type1[0]);
+            $t1 = strtolower($type1[1]);
         } else {
+            $m1 = '';
             $t1 = strtolower($type1[0]);
         }
 
-        $m2 = '';
-        $t2 = '';
-
         if (count($type2) > 1) {
             $m2 = strtolower($type2[0]);
             $t2 = strtolower($type2[1]);
         } else {
+            $m2 = '';
             $t2 = strtolower($type2[0]);
         }
 
@@ -2058,6 +2239,8 @@ class Compiler
                 return true;
             }
 
+            $this->appendRootDirective('@import ' . $this->compileValue($rawPath). ';', $out);
+
             return false;
         }
 
@@ -2069,17 +2252,21 @@ class Compiler
 
             foreach ($rawPath[2] as $path) {
                 if ($path[0] !== Type::T_STRING) {
+                    $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
+
                     return false;
                 }
             }
 
             foreach ($rawPath[2] as $path) {
-                $this->compileImport($path, $out);
+                $this->compileImport($path, $out, $once);
             }
 
             return true;
         }
 
+        $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
+
         return false;
     }
 
@@ -2119,9 +2306,9 @@ class Compiler
 
         // insert the directive as a comment
         $child = $this->makeOutputBlock(Type::T_COMMENT);
-        $child->lines[] = $line;
-        $child->sourceName = $this->sourceNames[$this->sourceIndex];
-        $child->sourceLine = $this->sourceLine;
+        $child->lines[]      = $line;
+        $child->sourceName   = $this->sourceNames[$this->sourceIndex];
+        $child->sourceLine   = $this->sourceLine;
         $child->sourceColumn = $this->sourceColumn;
 
         $root->children[] = $child;
@@ -2133,12 +2320,12 @@ class Compiler
     }
 
     /**
-     * Append lines to the courrent output block:
+     * Append lines to the current output block:
      * directly to the block or through a child if necessary
      *
      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
      * @param string                                 $type
-     * @param string                                 $line
+     * @param string|mixed                           $line
      */
     protected function appendOutputLine(OutputBlock $out, $type, $line)
     {
@@ -2148,20 +2335,20 @@ class Compiler
             $parent = $out->parent;
 
             if (end($parent->children) !== $out) {
-                $outWrite = &$parent->children[count($parent->children)-1];
+                $outWrite = &$parent->children[count($parent->children) - 1];
             }
         }
 
         // check if it's a flat output or not
         if (count($out->children)) {
-            $lastChild = &$out->children[count($out->children) -1];
+            $lastChild = &$out->children[count($out->children) - 1];
 
             if ($lastChild->depth === $out->depth && is_null($lastChild->selectors) && ! count($lastChild->children)) {
                 $outWrite = $lastChild;
             } else {
                 $nextLines = $this->makeOutputBlock($type);
                 $nextLines->parent = $out;
-                $nextLines->depth = $out->depth;
+                $nextLines->depth  = $out->depth;
 
                 $out->children[] = $nextLines;
                 $outWrite = &$nextLines;
@@ -2182,16 +2369,17 @@ class Compiler
     protected function compileChild($child, OutputBlock $out)
     {
         if (isset($child[Parser::SOURCE_LINE])) {
-            $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
-            $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
+            $this->sourceIndex  = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
+            $this->sourceLine   = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
             $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
         } elseif (is_array($child) && isset($child[1]->sourceLine)) {
-            $this->sourceIndex = $child[1]->sourceIndex;
-            $this->sourceLine = $child[1]->sourceLine;
+            $this->sourceIndex  = $child[1]->sourceIndex;
+            $this->sourceLine   = $child[1]->sourceLine;
             $this->sourceColumn = $child[1]->sourceColumn;
         } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
-            $this->sourceLine = $out->sourceLine;
-            $this->sourceIndex = array_search($out->sourceName, $this->sourceNames);
+            $this->sourceLine   = $out->sourceLine;
+            $this->sourceIndex  = array_search($out->sourceName, $this->sourceNames);
+            $this->sourceColumn = $out->sourceColumn;
 
             if ($this->sourceIndex === false) {
                 $this->sourceIndex = null;
@@ -2202,21 +2390,17 @@ class Compiler
             case Type::T_SCSSPHP_IMPORT_ONCE:
                 $rawPath = $this->reduce($child[1]);
 
-                if (! $this->compileImport($rawPath, $out, true)) {
-                    $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
-                }
+                $this->compileImport($rawPath, $out, true);
                 break;
 
             case Type::T_IMPORT:
                 $rawPath = $this->reduce($child[1]);
 
-                if (! $this->compileImport($rawPath, $out)) {
-                    $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
-                }
+                $this->compileImport($rawPath, $out);
                 break;
 
             case Type::T_DIRECTIVE:
-                $this->compileDirective($child[1]);
+                $this->compileDirective($child[1], $out);
                 break;
 
             case Type::T_AT_ROOT:
@@ -2242,9 +2426,9 @@ class Compiler
                 list(, $name, $value) = $child;
 
                 if ($name[0] === Type::T_VARIABLE) {
-                    $flags = isset($child[3]) ? $child[3] : [];
+                    $flags     = isset($child[3]) ? $child[3] : [];
                     $isDefault = in_array('!default', $flags);
-                    $isGlobal = in_array('!global', $flags);
+                    $isGlobal  = in_array('!global', $flags);
 
                     if ($isGlobal) {
                         $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
@@ -2252,7 +2436,7 @@ class Compiler
                     }
 
                     $shouldSet = $isDefault &&
-                        (($result = $this->get($name[1], false)) === null ||
+                        (is_null($result = $this->get($name[1], false)) ||
                         $result === static::$null);
 
                     if (! $isDefault || $shouldSet) {
@@ -2263,28 +2447,77 @@ class Compiler
 
                 $compiledName = $this->compileValue($name);
 
-                // handle shorthand syntax: size / line-height
-                if ($compiledName === 'font' || $compiledName === 'grid-row' || $compiledName === 'grid-column') {
+                // handle shorthand syntaxes : size / line-height...
+                if (in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) {
                     if ($value[0] === Type::T_VARIABLE) {
                         // if the font value comes from variable, the content is already reduced
                         // (i.e., formulas were already calculated), so we need the original unreduced value
                         $value = $this->get($value[1], true, null, true);
                     }
 
-                    $fontValue=&$value;
+                    $shorthandValue=&$value;
+
+                    $shorthandDividerNeedsUnit = false;
+                    $maxListElements           = null;
+                    $maxShorthandDividers      = 1;
 
-                    if ($value[0] === Type::T_LIST && $value[1]==',') {
+                    switch ($compiledName) {
+                        case 'border-radius':
+                            $maxListElements = 4;
+                            $shorthandDividerNeedsUnit = true;
+                            break;
+                    }
+
+                    if ($compiledName === 'font' and $value[0] === Type::T_LIST && $value[1]==',') {
                         // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica"
                         // we need to handle the first list element
-                        $fontValue=&$value[2][0];
+                        $shorthandValue=&$value[2][0];
                     }
 
-                    if ($fontValue[0] === Type::T_EXPRESSION && $fontValue[1] === '/') {
-                        $fontValue = $this->expToString($fontValue);
-                    } elseif ($fontValue[0] === Type::T_LIST) {
-                        foreach ($fontValue[2] as &$item) {
+                    if ($shorthandValue[0] === Type::T_EXPRESSION && $shorthandValue[1] === '/') {
+                        $revert = true;
+
+                        if ($shorthandDividerNeedsUnit) {
+                            $divider = $shorthandValue[3];
+
+                            if (is_array($divider)) {
+                                $divider = $this->reduce($divider, true);
+                            }
+
+                            if (intval($divider->dimension) and !count($divider->units)) {
+                                $revert = false;
+                            }
+                        }
+
+                        if ($revert) {
+                            $shorthandValue = $this->expToString($shorthandValue);
+                        }
+                    } elseif ($shorthandValue[0] === Type::T_LIST) {
+                        foreach ($shorthandValue[2] as &$item) {
                             if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
-                                $item = $this->expToString($item);
+                                if ($maxShorthandDividers > 0) {
+                                    $revert = true;
+                                    // if the list of values is too long, this has to be a shorthand,
+                                    // otherwise it could be a real division
+                                    if (is_null($maxListElements) or count($shorthandValue[2]) <= $maxListElements) {
+                                        if ($shorthandDividerNeedsUnit) {
+                                            $divider = $item[3];
+
+                                            if (is_array($divider)) {
+                                                $divider = $this->reduce($divider, true);
+                                            }
+
+                                            if (intval($divider->dimension) and !count($divider->units)) {
+                                                $revert = false;
+                                            }
+                                        }
+                                    }
+
+                                    if ($revert) {
+                                        $item = $this->expToString($item);
+                                        $maxShorthandDividers--;
+                                    }
+                                }
                             }
                         }
                     }
@@ -2315,13 +2548,15 @@ class Compiler
                     break;
                 }
 
-                $this->appendOutputLine($out, Type::T_COMMENT, $child[1]);
+                $line = $this->compileCommentValue($child, true);
+                $this->appendOutputLine($out, Type::T_COMMENT, $line);
                 break;
 
             case Type::T_MIXIN:
             case Type::T_FUNCTION:
                 list(, $block) = $child;
-
+                // the block need to be able to go up to it's parent env to resolve vars
+                $block->parentEnv = $this->getStoreEnv();
                 $this->set(static::$namespaces[$block->type] . $block->name, $block, true);
                 break;
 
@@ -2332,8 +2567,13 @@ class Compiler
                     foreach ($results as $result) {
                         // only use the first one
                         $result = current($result);
+                        $selectors = $out->selectors;
 
-                        $this->pushExtends($result, $out->selectors, $child);
+                        if (! $selectors && isset($child['selfParent'])) {
+                            $selectors = $this->multiplySelectors($this->env, $child['selfParent']);
+                        }
+
+                        $this->pushExtends($result, $selectors, $child);
                     }
                 }
                 break;
@@ -2465,7 +2705,7 @@ class Compiler
 
             case Type::T_INCLUDE:
                 // including a mixin
-                list(, $name, $argValues, $content) = $child;
+                list(, $name, $argValues, $content, $argUsing) = $child;
 
                 $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
 
@@ -2508,19 +2748,32 @@ class Compiler
                 // i.e., recursive @include of the same mixin
                 if (isset($content)) {
                     $copyContent = clone $content;
-                    $copyContent->scope = $callingScope;
+                    $copyContent->scope = clone $callingScope;
 
                     $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
                 } else {
                     $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env);
                 }
 
+                // save the "using" argument list for applying it to when "@content" is invoked
+                if (isset($argUsing)) {
+                    $this->setRaw(static::$namespaces['special'] . 'using', $argUsing, $this->env);
+                } else {
+                    $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env);
+                }
+
                 if (isset($mixin->args)) {
                     $this->applyArguments($mixin->args, $argValues);
                 }
 
                 $this->env->marker = 'mixin';
 
+                if (! empty($mixin->parentEnv)) {
+                    $this->env->declarationScopeParent = $mixin->parentEnv;
+                } else {
+                    $this->throwError("@mixin $name() without parentEnv");
+                }
+
                 $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . " " . $name);
 
                 $this->storeEnv = $storeEnv;
@@ -2529,18 +2782,35 @@ class Compiler
                 break;
 
             case Type::T_MIXIN_CONTENT:
-                $env = isset($this->storeEnv) ? $this->storeEnv : $this->env;
-                $content = $this->get(static::$namespaces['special'] . 'content', false, $env);
+                $env        = isset($this->storeEnv) ? $this->storeEnv : $this->env;
+                $content    = $this->get(static::$namespaces['special'] . 'content', false, $env);
+                $argUsing   = $this->get(static::$namespaces['special'] . 'using', false, $env);
+                $argContent = $child[1];
 
                 if (! $content) {
                     $content = new \stdClass();
-                    $content->scope = new \stdClass();
+                    $content->scope    = new \stdClass();
                     $content->children = $env->parent->block->children;
                     break;
                 }
 
                 $storeEnv = $this->storeEnv;
+                $varsUsing = [];
+
+                if (isset($argUsing) && isset($argContent)) {
+                    // Get the arguments provided for the content with the names provided in the "using" argument list
+                    $this->storeEnv = $this->env;
+                    $varsUsing = $this->applyArguments($argUsing, $argContent, false);
+                }
+
+                // restore the scope from the @content
                 $this->storeEnv = $content->scope;
+
+                // append the vars from using if any
+                foreach ($varsUsing as $name => $val) {
+                    $this->set($name, $val, true, $this->storeEnv);
+                }
+
                 $this->compileChildrenNoReturn($content->children, $out);
 
                 $this->storeEnv = $storeEnv;
@@ -2550,8 +2820,9 @@ class Compiler
                 list(, $value) = $child;
 
                 $fname = $this->sourceNames[$this->sourceIndex];
-                $line = $this->sourceLine;
+                $line  = $this->sourceLine;
                 $value = $this->compileValue($this->reduce($value, true));
+
                 fwrite($this->stderr, "File $fname on line $line DEBUG: $value\n");
                 break;
 
@@ -2559,8 +2830,9 @@ class Compiler
                 list(, $value) = $child;
 
                 $fname = $this->sourceNames[$this->sourceIndex];
-                $line = $this->sourceLine;
+                $line  = $this->sourceLine;
                 $value = $this->compileValue($this->reduce($value, true));
+
                 fwrite($this->stderr, "File $fname on line $line WARN: $value\n");
                 break;
 
@@ -2568,8 +2840,9 @@ class Compiler
                 list(, $value) = $child;
 
                 $fname = $this->sourceNames[$this->sourceIndex];
-                $line = $this->sourceLine;
+                $line  = $this->sourceLine;
                 $value = $this->compileValue($this->reduce($value, true));
+
                 $this->throwError("File $fname on line $line ERROR: $value\n");
                 break;
 
@@ -2664,11 +2937,10 @@ class Compiler
      * @param array   $value
      * @param boolean $inExp
      *
-     * @return null|array|\ScssPhp\ScssPhp\Node\Number
+     * @return null|string|array|\ScssPhp\ScssPhp\Node\Number
      */
     protected function reduce($value, $inExp = false)
     {
-
         if (is_null($value)) {
             return null;
         }
@@ -2694,9 +2966,8 @@ class Compiler
                     return $this->expToString($value);
                 }
 
-                $left = $this->coerceForExpression($left);
+                $left  = $this->coerceForExpression($left);
                 $right = $this->coerceForExpression($right);
-
                 $ltype = $left[0];
                 $rtype = $right[0];
 
@@ -2837,6 +3108,7 @@ class Compiler
 
             case Type::T_INTERPOLATE:
                 $value[1] = $this->reduce($value[1]);
+
                 if ($inExp) {
                     return $value[1];
                 }
@@ -2849,6 +3121,7 @@ class Compiler
             case Type::T_SELF:
                 $selfSelector = $this->multiplySelectors($this->env);
                 $selfSelector = $this->collapseSelectors($selfSelector, true);
+
                 return $selfSelector;
 
             default:
@@ -2923,6 +3196,10 @@ class Compiler
                     $value[2][$key] = $this->normalizeValue($item);
                 }
 
+                if (! empty($value['enclosing'])) {
+                    unset($value['enclosing']);
+                }
+
                 return $value;
 
             case Type::T_STRING:
@@ -3339,7 +3616,7 @@ class Compiler
      *
      * @param array $value
      *
-     * @return string
+     * @return string|array
      */
     public function compileValue($value)
     {
@@ -3356,14 +3633,38 @@ class Compiler
                 // [4] - optional alpha component
                 list(, $r, $g, $b) = $value;
 
-                $r = round($r);
-                $g = round($g);
-                $b = round($b);
+                $r = $this->compileRGBAValue($r);
+                $g = $this->compileRGBAValue($g);
+                $b = $this->compileRGBAValue($b);
+
+                if (count($value) === 5) {
+                    $alpha = $this->compileRGBAValue($value[4], true);
 
-                if (count($value) === 5 && $value[4] !== 1) { // rgba
-                    $a = new Node\Number($value[4], '');
+                    if (! is_numeric($alpha) || $alpha < 1) {
+                        $colorName = Colors::RGBaToColorName($r, $g, $b, $alpha);
+
+                        if (! is_null($colorName)) {
+                            return $colorName;
+                        }
+
+                        if (is_numeric($alpha)) {
+                            $a = new Node\Number($alpha, '');
+                        } else {
+                            $a = $alpha;
+                        }
 
-                    return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
+                        return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
+                    }
+                }
+
+                if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b)) {
+                    return 'rgb(' . $r . ', ' . $g . ', ' . $b . ')';
+                }
+
+                $colorName = Colors::RGBaToColorName($r, $g, $b);
+
+                if (! is_null($colorName)) {
+                    return $colorName;
                 }
 
                 $h = sprintf('#%02x%02x%02x', $r, $g, $b);
@@ -3394,9 +3695,28 @@ class Compiler
                 }
 
                 list(, $delim, $items) = $value;
+                $pre = $post = "";
+                if (! empty($value['enclosing'])) {
+                    switch ($value['enclosing']) {
+                        case 'parent':
+                            //$pre = "(";
+                            //$post = ")";
+                            break;
+                        case 'forced_parent':
+                            $pre = "(";
+                            $post = ")";
+                            break;
+                        case 'bracket':
+                        case 'forced_bracket':
+                            $pre = "[";
+                            $post = "]";
+                            break;
+                    }
+                }
 
+                $prefix_value = '';
                 if ($delim !== ' ') {
-                    $delim .= ' ';
+                    $prefix_value = ' ';
                 }
 
                 $filtered = [];
@@ -3406,14 +3726,18 @@ class Compiler
                         continue;
                     }
 
-                    $filtered[] = $this->compileValue($item);
+                    $compiled = $this->compileValue($item);
+                    if ($prefix_value && strlen($compiled)) {
+                        $compiled = $prefix_value . $compiled;
+                    }
+                    $filtered[] = $compiled;
                 }
 
-                return implode("$delim", $filtered);
+                return $pre . substr(implode("$delim", $filtered), strlen($prefix_value)) . $post;
 
             case Type::T_MAP:
-                $keys = $value[1];
-                $values = $value[2];
+                $keys     = $value[1];
+                $values   = $value[2];
                 $filtered = [];
 
                 for ($i = 0, $s = count($keys); $i < $s; $i++) {
@@ -3431,11 +3755,23 @@ class Compiler
                 list(, $interpolate, $left, $right) = $value;
                 list(,, $whiteLeft, $whiteRight) = $interpolate;
 
+                $delim = $left[1];
+
+                if ($delim && $delim !== ' ' && ! $whiteLeft) {
+                    $delim .= ' ';
+                }
+
                 $left = count($left[2]) > 0 ?
-                    $this->compileValue($left) . $whiteLeft : '';
+                    $this->compileValue($left) . $delim . $whiteLeft: '';
+
+                $delim = $right[1];
+
+                if ($delim && $delim !== ' ') {
+                    $delim .= ' ';
+                }
 
                 $right = count($right[2]) > 0 ?
-                    $whiteRight . $this->compileValue($right) : '';
+                    $whiteRight . $delim . $this->compileValue($right) : '';
 
                 return $left . $this->compileValue($interpolate) . $right;
 
@@ -3465,6 +3801,7 @@ class Compiler
                             }
 
                             $temp = $this->compileValue([Type::T_KEYWORD, $item]);
+
                             if ($temp[0] === Type::T_STRING) {
                                 $filtered[] = $this->compileStringContent($temp);
                             } elseif ($temp[0] === Type::T_KEYWORD) {
@@ -3490,6 +3827,9 @@ class Compiler
             case Type::T_NULL:
                 return 'null';
 
+            case Type::T_COMMENT:
+                return $this->compileCommentValue($value);
+
             default:
                 $this->throwError("unknown value type: ".json_encode($value));
         }
@@ -3580,9 +3920,9 @@ class Compiler
             $selectors = $env->selectors;
 
             do {
-                $stillHasSelf = false;
+                $stillHasSelf  = false;
                 $prevSelectors = $selectors;
-                $selectors = [];
+                $selectors     = [];
 
                 foreach ($prevSelectors as $selector) {
                     foreach ($parentSelectors as $parent) {
@@ -3648,9 +3988,11 @@ class Compiler
                         foreach ($parentPart as $pp) {
                             if (is_array($pp)) {
                                 $flatten = [];
+
                                 array_walk_recursive($pp, function ($a) use (&$flatten) {
                                     $flatten[] = $a;
                                 });
+
                                 $pp = implode($flatten);
                             }
 
@@ -3694,12 +4036,14 @@ class Compiler
             : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
 
         $store = [$this->env, $this->storeEnv];
-        $this->env = $env;
+
+        $this->env      = $env;
         $this->storeEnv = null;
-        $parentQueries = $this->evaluateMediaQuery($parentQueries);
+        $parentQueries  = $this->evaluateMediaQuery($parentQueries);
+
         list($this->env, $this->storeEnv) = $store;
 
-        if ($childQueries === null) {
+        if (is_null($childQueries)) {
             $childQueries = $parentQueries;
         } else {
             $originalQueries = $childQueries;
@@ -3891,7 +4235,6 @@ class Compiler
             $env = $this->getStoreEnv();
         }
 
-        $nextIsRoot = false;
         $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
 
         $maxDepth = 10000;
@@ -3910,12 +4253,16 @@ class Compiler
             }
 
             if (! $hasNamespace && isset($env->marker)) {
-                if (! $nextIsRoot && ! empty($env->store[$specialContentKey])) {
+                if (! empty($env->store[$specialContentKey])) {
                     $env = $env->store[$specialContentKey]->scope;
                     continue;
                 }
 
-                $env = $this->rootEnv;
+                if (! empty($env->declarationScopeParent)) {
+                    $env = $env->declarationScopeParent;
+                } else {
+                    $env = $this->rootEnv;
+                }
                 continue;
             }
 
@@ -3927,7 +4274,7 @@ class Compiler
         }
 
         if ($shouldThrow) {
-            $this->throwError("Undefined variable \$$name" . ($maxDepth<=0 ? " (infinite recursion)" : ""));
+            $this->throwError("Undefined variable \$$name" . ($maxDepth <= 0 ? " (infinite recursion)" : ""));
         }
 
         // found nothing
@@ -3944,7 +4291,7 @@ class Compiler
      */
     protected function has($name, Environment $env = null)
     {
-        return $this->get($name, false, $env) !== null;
+        return ! is_null($this->get($name, false, $env));
     }
 
     /**
@@ -4018,7 +4365,7 @@ class Compiler
      */
     public function addParsedFile($path)
     {
-        if (isset($path) && file_exists($path)) {
+        if (isset($path) && is_file($path)) {
             $this->parsedFiles[realpath($path)] = filemtime($path);
         }
     }
@@ -4183,6 +4530,7 @@ class Compiler
         }
 
         $pi = pathinfo($path);
+
         array_unshift($this->importPaths, $pi['dirname']);
         $this->compileChildrenNoReturn($tree->children, $out);
         array_shift($this->importPaths);
@@ -4202,9 +4550,9 @@ class Compiler
         $urls = [];
 
         // for "normal" scss imports (ignore vanilla css and external requests)
-        if (! preg_match('/\.css$|^https?:\/\//', $url)) {
+        if (! preg_match('~\.css$|^https?://~', $url)) {
             // try both normal and the _partial filename
-            $urls = [$url, preg_replace('/[^\/]+$/', '_\0', $url)];
+            $urls = [$url, preg_replace('~[^/]+$~', '_\0', $url)];
         }
 
         $hasExtension = preg_match('/[.]s?css$/', $url);
@@ -4220,8 +4568,8 @@ class Compiler
                     ) ? '/' : '';
                     $full = $dir . $separator . $full;
 
-                    if ($this->fileExists($file = $full . '.scss') ||
-                        ($hasExtension && $this->fileExists($file = $full))
+                    if (is_file($file = $full . '.scss') ||
+                        ($hasExtension && is_file($file = $full))
                     ) {
                         return $file;
                     }
@@ -4230,7 +4578,7 @@ class Compiler
                 // check custom callback for import path
                 $file = call_user_func($dir, $url);
 
-                if ($file !== null) {
+                if (! is_null($file)) {
                     return $file;
                 }
             }
@@ -4325,9 +4673,10 @@ class Compiler
                           ? $this->sourceNames[$call[Parser::SOURCE_INDEX]]
                           : '(unknown file)');
                     $msg .= " on line " . $call[Parser::SOURCE_LINE];
+
                     $callStackMsg[] = $msg;
 
-                    if (! is_null($limit) && $ncall>$limit) {
+                    if (! is_null($limit) && $ncall > $limit) {
                         break;
                     }
                 }
@@ -4347,6 +4696,10 @@ class Compiler
     protected function handleImportLoop($name)
     {
         for ($env = $this->env; $env; $env = $env->parent) {
+            if (! $env->block) {
+                continue;
+            }
+
             $file = $this->sourceNames[$env->block->sourceIndex];
 
             if (realpath($file) === $name) {
@@ -4356,18 +4709,6 @@ class Compiler
         }
     }
 
-    /**
-     * Does file exist?
-     *
-     * @param string $name
-     *
-     * @return boolean
-     */
-    protected function fileExists($name)
-    {
-        return file_exists($name) && is_file($name);
-    }
-
     /**
      * Call SCSS @function
      *
@@ -4401,6 +4742,11 @@ class Compiler
         $tmp->children = [];
 
         $this->env->marker = 'function';
+        if (! empty($func->parentEnv)) {
+            $this->env->declarationScopeParent = $func->parentEnv;
+        } else {
+            $this->throwError("@function $name() without parentEnv");
+        }
 
         $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . " " . $name);
 
@@ -4437,11 +4783,17 @@ class Compiler
             return false;
         }
 
-        @list($sorted, $kwargs) = $this->sortArgs($prototype, $args);
+        @list($sorted, $kwargs) = $this->sortNativeFunctionArgs($libName, $prototype, $args);
 
         if ($name !== 'if' && $name !== 'call') {
+            $inExp = true;
+
+            if ($name === 'join') {
+                $inExp = false;
+            }
+
             foreach ($sorted as &$val) {
-                $val = $this->reduce($val, true);
+                $val = $this->reduce($val, $inExp);
             }
         }
 
@@ -4479,12 +4831,13 @@ class Compiler
     /**
      * Sorts keyword arguments
      *
-     * @param array $prototype
-     * @param array $args
+     * @param string $functionName
+     * @param array  $prototypes
+     * @param array  $args
      *
      * @return array
      */
-    protected function sortArgs($prototypes, $args)
+    protected function sortNativeFunctionArgs($functionName, $prototypes, $args)
     {
         static $parser = null;
 
@@ -4508,6 +4861,23 @@ class Compiler
             return [$posArgs, $keyArgs];
         }
 
+        // specific cases ?
+        if (in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) {
+            // notation 100 127 255 / 0 is in fact a simple list of 4 values
+            foreach ($args as $k => $arg) {
+                if ($arg[1][0] === Type::T_LIST && count($arg[1][2]) === 3) {
+                    $last = end($arg[1][2]);
+
+                    if ($last[0] === Type::T_EXPRESSION && $last[1] === '/') {
+                        array_pop($arg[1][2]);
+                        $arg[1][2][] = $last[2];
+                        $arg[1][2][] = $last[3];
+                        $args[$k] = $arg;
+                    }
+                }
+            }
+        }
+
         $finalArgs = [];
 
         if (! is_array(reset($prototypes))) {
@@ -4554,7 +4924,7 @@ class Compiler
             }
 
             try {
-                $vars = $this->applyArguments($argDef, $args, false);
+                $vars = $this->applyArguments($argDef, $args, false, false);
 
                 // ensure all args are populated
                 foreach ($prototype as $i => $p) {
@@ -4602,14 +4972,22 @@ class Compiler
     /**
      * Apply argument values per definition
      *
-     * @param array $argDef
-     * @param array $argValues
+     * @param array   $argDef
+     * @param array   $argValues
+     * @param boolean $storeInEnv
+     * @param boolean $reduce
+     *   only used if $storeInEnv = false
+     *
+     * @return array
      *
      * @throws \Exception
      */
-    protected function applyArguments($argDef, $argValues, $storeInEnv = true)
+    protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true)
     {
         $output = [];
+        if (is_array($argValues) && count($argValues) && end($argValues) === static::$null) {
+            array_pop($argValues);
+        }
 
         if ($storeInEnv) {
             $storeEnv = $this->getStoreEnv();
@@ -4639,18 +5017,27 @@ class Compiler
             if (! empty($arg[0])) {
                 $hasKeywordArgument = true;
 
-                if (! isset($args[$arg[0][1]]) || $args[$arg[0][1]][3]) {
+                $name = $arg[0][1];
+                if (! isset($args[$name])) {
+                    foreach (array_keys($args) as $an) {
+                        if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
+                            $name = $an;
+                            break;
+                        }
+                    }
+                }
+                if (! isset($args[$name]) || $args[$name][3]) {
                     if ($hasVariable) {
-                        $deferredKeywordArgs[$arg[0][1]] = $arg[1];
+                        $deferredKeywordArgs[$name] = $arg[1];
                     } else {
                         $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
                         break;
                     }
-                } elseif ($args[$arg[0][1]][0] < count($remaining)) {
+                } elseif ($args[$name][0] < count($remaining)) {
                     $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
                     break;
                 } else {
-                    $keywordArgs[$arg[0][1]] = $arg[1];
+                    $keywordArgs[$name] = $arg[1];
                 }
             } elseif ($arg[2] === true) {
                 $val = $this->reduce($arg[1], true);
@@ -4658,7 +5045,7 @@ class Compiler
                 if ($val[0] === Type::T_LIST) {
                     foreach ($val[2] as $name => $item) {
                         if (! is_numeric($name)) {
-                            if (!isset($args[$name])) {
+                            if (! isset($args[$name])) {
                                 foreach (array_keys($args) as $an) {
                                     if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
                                         $name = $an;
@@ -4676,6 +5063,7 @@ class Compiler
                             if (is_null($splatSeparator)) {
                                 $splatSeparator = $val[1];
                             }
+
                             $remaining[] = $item;
                         }
                     }
@@ -4685,7 +5073,7 @@ class Compiler
                         $item = $val[2][$i];
 
                         if (! is_numeric($name)) {
-                            if (!isset($args[$name])) {
+                            if (! isset($args[$name])) {
                                 foreach (array_keys($args) as $an) {
                                     if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
                                         $name = $an;
@@ -4693,6 +5081,7 @@ class Compiler
                                     }
                                 }
                             }
+
                             if ($hasVariable) {
                                 $deferredKeywordArgs[$name] = $item;
                             } else {
@@ -4702,6 +5091,7 @@ class Compiler
                             if (is_null($splatSeparator)) {
                                 $splatSeparator = $val[1];
                             }
+
                             $remaining[] = $item;
                         }
                     }
@@ -4743,7 +5133,7 @@ class Compiler
             if ($storeInEnv) {
                 $this->set($name, $this->reduce($val, true), true, $env);
             } else {
-                $output[$name] = $val;
+                $output[$name] = ($reduce ? $this->reduce($val, true) : $val);
             }
         }
 
@@ -4761,7 +5151,7 @@ class Compiler
             if ($storeInEnv) {
                 $this->set($name, $this->reduce($default, true), true);
             } else {
-                $output[$name] = $default;
+                $output[$name] = ($reduce ? $this->reduce($default, true) : $default);
             }
         }
 
@@ -4785,7 +5175,7 @@ class Compiler
             return $this->toBool($value);
         }
 
-        if ($value === null) {
+        if (is_null($value)) {
             return static::$null;
         }
 
@@ -4797,30 +5187,14 @@ class Compiler
             return static::$emptyString;
         }
 
-        if (preg_match('/^(#([0-9a-f]{6})|#([0-9a-f]{3}))$/i', $value, $m)) {
-            $color = [Type::T_COLOR];
-
-            if (isset($m[3])) {
-                $num = hexdec($m[3]);
-
-                foreach ([3, 2, 1] as $i) {
-                    $t = $num & 0xf;
-                    $color[$i] = $t << 4 | $t;
-                    $num >>= 4;
-                }
-            } else {
-                $num = hexdec($m[2]);
-
-                foreach ([3, 2, 1] as $i) {
-                    $color[$i] = $num & 0xff;
-                    $num >>= 8;
-                }
-            }
+        $value = [Type::T_KEYWORD, $value];
+        $color = $this->coerceColor($value);
 
+        if ($color) {
             return $color;
         }
 
-        return [Type::T_KEYWORD, $value];
+        return $value;
     }
 
     /**
@@ -4836,7 +5210,9 @@ class Compiler
             return $item;
         }
 
-        if ($item === static::$emptyList) {
+        if ($item[0] === static::$emptyList[0]
+            && $item[1] === static::$emptyList[1]
+            && $item[2] === static::$emptyList[2]) {
             return static::$emptyMap;
         }
 
@@ -4869,6 +5245,7 @@ class Compiler
                 switch ($key[0]) {
                     case Type::T_LIST:
                     case Type::T_MAP:
+                    case Type::T_STRING:
                         break;
 
                     default:
@@ -4912,21 +5289,106 @@ class Compiler
      *
      * @return array|null
      */
-    protected function coerceColor($value)
+    protected function coerceColor($value, $inRGBFunction = false)
     {
         switch ($value[0]) {
             case Type::T_COLOR:
+                for ($i = 1; $i <= 3; $i++) {
+                    if (! is_numeric($value[$i])) {
+                        $cv = $this->compileRGBAValue($value[$i]);
+
+                        if (! is_numeric($cv)) {
+                            return null;
+                        }
+
+                        $value[$i] = $cv;
+                    }
+
+                    if (isset($value[4])) {
+                        if (! is_numeric($value[4])) {
+                            $cv = $this->compileRGBAValue($value[4], true);
+
+                            if (! is_numeric($cv)) {
+                                return null;
+                            }
+
+                            $value[4] = $cv;
+                        }
+                    }
+                }
+
                 return $value;
 
+            case Type::T_LIST:
+                if ($inRGBFunction) {
+                    if (count($value[2]) == 3 || count($value[2]) == 4) {
+                        $color = $value[2];
+                        array_unshift($color, Type::T_COLOR);
+
+                        return $this->coerceColor($color);
+                    }
+                }
+
+                return null;
+
             case Type::T_KEYWORD:
+                if (! is_string($value[1])) {
+                    return null;
+                }
+
                 $name = strtolower($value[1]);
+                // hexa color?
+                if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) {
+                    $nofValues = strlen($m[1]);
+
+                    if (in_array($nofValues, [3, 4, 6, 8])) {
+                        $nbChannels = 3;
+                        $color      = [];
+                        $num        = hexdec($m[1]);
+
+                        switch ($nofValues) {
+                            case 4:
+                                $nbChannels = 4;
+                                // then continuing with the case 3:
+                            case 3:
+                                for ($i = 0; $i < $nbChannels; $i++) {
+                                    $t = $num & 0xf;
+                                    array_unshift($color, $t << 4 | $t);
+                                    $num >>= 4;
+                                }
+
+                                break;
 
-                if (isset(Colors::$cssColors[$name])) {
-                    $rgba = explode(',', Colors::$cssColors[$name]);
+                            case 8:
+                                $nbChannels = 4;
+                                // then continuing with the case 6:
+                            case 6:
+                                for ($i = 0; $i < $nbChannels; $i++) {
+                                    array_unshift($color, $num & 0xff);
+                                    $num >>= 8;
+                                }
+
+                                break;
+                        }
 
+                        if ($nbChannels === 4) {
+                            if ($color[3] === 255) {
+                                $color[3] = 1; // fully opaque
+                            } else {
+                                $color[3] = round($color[3] / 255, 3);
+                            }
+                        }
+
+                        array_unshift($color, Type::T_COLOR);
+
+                        return $color;
+                    }
+                }
+
+                if ($rgba = Colors::colorNameToRGBa($name)) {
                     return isset($rgba[3])
-                        ? [Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3]]
-                        : [Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]];
+                        ? [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2], $rgba[3]]
+                        : [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2]];
                 }
 
                 return null;
@@ -4935,6 +5397,88 @@ class Compiler
         return null;
     }
 
+    /**
+     * @param integer|\ScssPhp\ScssPhp\Node\Number $value
+     * @param boolean                              $isAlpha
+     *
+     * @return integer|mixed
+     */
+    protected function compileRGBAValue($value, $isAlpha = false)
+    {
+        if ($isAlpha) {
+            return $this->compileColorPartValue($value, 0, 1, false);
+        }
+
+        return $this->compileColorPartValue($value, 0, 255, true);
+    }
+
+    /**
+     * @param mixed         $value
+     * @param integer|float $min
+     * @param integer|float $max
+     * @param boolean       $isInt
+     * @param boolean       $clamp
+     * @param boolean       $modulo
+     *
+     * @return integer|mixed
+     */
+    protected function compileColorPartValue($value, $min, $max, $isInt = true, $clamp = true, $modulo = false)
+    {
+        if (! is_numeric($value)) {
+            if (is_array($value)) {
+                $reduced = $this->reduce($value);
+
+                if (is_object($reduced) && $value->type === Type::T_NUMBER) {
+                    $value = $reduced;
+                }
+            }
+
+            if (is_object($value) && $value->type === Type::T_NUMBER) {
+                $num = $value->dimension;
+
+                if (count($value->units)) {
+                    $unit = array_keys($value->units);
+                    $unit = reset($unit);
+
+                    switch ($unit) {
+                        case '%':
+                            $num *= $max / 100;
+                            break;
+                        default:
+                            break;
+                    }
+                }
+
+                $value = $num;
+            } elseif (is_array($value)) {
+                $value = $this->compileValue($value);
+            }
+        }
+
+        if (is_numeric($value)) {
+            if ($isInt) {
+                $value = round($value);
+            }
+
+            if ($clamp) {
+                $value = min($max, max($min, $value));
+            }
+
+            if ($modulo) {
+                $value = $value % $max;
+
+                // still negative?
+                while ($value < $min) {
+                    $value += $max;
+                }
+            }
+
+            return $value;
+        }
+
+        return $value;
+    }
+
     /**
      * Coerce value to string
      *
@@ -5246,30 +5790,68 @@ class Compiler
         return false === $key ? static::$null : $key + 1;
     }
 
-    protected static $libRgb = ['red', 'green', 'blue'];
-    protected function libRgb($args)
+    protected static $libRgb = [
+        ['color'],
+        ['color', 'alpha'],
+        ['channels'],
+        ['red', 'green', 'blue'],
+        ['red', 'green', 'blue', 'alpha'] ];
+    protected function libRgb($args, $kwargs, $funcName = 'rgb')
     {
-        list($r, $g, $b) = $args;
+        switch (count($args)) {
+            case 1:
+                if (! $color = $this->coerceColor($args[0], true)) {
+                    $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
+                }
+                break;
 
-        return [Type::T_COLOR, $r[1], $g[1], $b[1]];
-    }
+            case 3:
+                $color = [Type::T_COLOR, $args[0], $args[1], $args[2]];
 
-    protected static $libRgba = [
-        ['color', 'alpha:1'],
-        ['red', 'green', 'blue', 'alpha:1'] ];
-    protected function libRgba($args)
-    {
-        if ($color = $this->coerceColor($args[0])) {
-            $num = isset($args[3]) ? $args[3] : $args[1];
-            $alpha = $this->assertNumber($num);
-            $color[4] = $alpha;
+                if (! $color = $this->coerceColor($color)) {
+                    $color = [Type::T_STRING, '', [$funcName .'(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
+                }
 
-            return $color;
+                return $color;
+
+            case 2:
+                if ($color = $this->coerceColor($args[0], true)) {
+                    $alpha = $this->compileRGBAValue($args[1], true);
+
+                    if (is_numeric($alpha)) {
+                        $color[4] = $alpha;
+                    } else {
+                        $color = [Type::T_STRING, '',
+                            [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']];
+                    }
+                } else {
+                    $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
+                }
+                break;
+
+            case 4:
+            default:
+                $color = [Type::T_COLOR, $args[0], $args[1], $args[2], $args[3]];
+
+                if (! $color = $this->coerceColor($color)) {
+                    $color = [Type::T_STRING, '',
+                        [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
+                }
+                break;
         }
 
-        list($r, $g, $b, $a) = $args;
+        return $color;
+    }
 
-        return [Type::T_COLOR, $r[1], $g[1], $b[1], $a[1]];
+    protected static $libRgba = [
+        ['color'],
+        ['color', 'alpha'],
+        ['channels'],
+        ['red', 'green', 'blue'],
+        ['red', 'green', 'blue', 'alpha'] ];
+    protected function libRgba($args, $kwargs)
+    {
+        return $this->libRgb($args, $kwargs, 'rgba');
     }
 
     // helper function for adjust_color, change_color, and scale_color
@@ -5277,21 +5859,25 @@ class Compiler
     {
         $color = $this->assertColor($args[0]);
 
-        foreach ([1, 2, 3, 7] as $i) {
-            if (isset($args[$i])) {
-                $val = $this->assertNumber($args[$i]);
-                $ii = $i === 7 ? 4 : $i; // alpha
-                $color[$ii] = call_user_func($fn, isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
+        foreach ([1 => 1, 2 => 2, 3 => 3, 7 => 4] as $iarg => $irgba) {
+            if (isset($args[$iarg])) {
+                $val = $this->assertNumber($args[$iarg]);
+
+                if (! isset($color[$irgba])) {
+                    $color[$irgba] = (($irgba < 4) ? 0 : 1);
+                }
+
+                $color[$irgba] = call_user_func($fn, $color[$irgba], $val, $iarg);
             }
         }
 
         if (! empty($args[4]) || ! empty($args[5]) || ! empty($args[6])) {
             $hsl = $this->toHSL($color[1], $color[2], $color[3]);
 
-            foreach ([4, 5, 6] as $i) {
-                if (! empty($args[$i])) {
-                    $val = $this->assertNumber($args[$i]);
-                    $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
+            foreach ([4 => 1, 5 => 2, 6 => 3] as $iarg => $ihsl) {
+                if (! empty($args[$iarg])) {
+                    $val = $this->assertNumber($args[$iarg]);
+                    $hsl[$ihsl] = call_user_func($fn, $hsl[$ihsl], $val, $iarg);
                 }
             }
 
@@ -5374,7 +5960,7 @@ class Compiler
         $color = $this->coerceColor($args[0]);
         $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
 
-        return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
+        return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]];
     }
 
     protected static $libRed = ['color'];
@@ -5461,25 +6047,56 @@ class Compiler
         return $this->fixColor($new);
     }
 
-    protected static $libHsl = ['hue', 'saturation', 'lightness'];
-    protected function libHsl($args)
+    protected static $libHsl =[
+        ['channels'],
+        ['hue', 'saturation', 'lightness'],
+        ['hue', 'saturation', 'lightness', 'alpha'] ];
+    protected function libHsl($args, $kwargs, $funcName = 'hsl')
     {
-        list($h, $s, $l) = $args;
+        if (count($args) == 1) {
+            if ($args[0][0] !== Type::T_LIST || count($args[0][2]) < 3 || count($args[0][2]) > 4) {
+                return [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
+            }
 
-        return $this->toRGB($h[1], $s[1], $l[1]);
-    }
+            $args = $args[0][2];
+        }
 
-    protected static $libHsla = ['hue', 'saturation', 'lightness', 'alpha'];
-    protected function libHsla($args)
-    {
-        list($h, $s, $l, $a) = $args;
+        $hue = $this->compileColorPartValue($args[0], 0, 360, false, false, true);
+        $saturation = $this->compileColorPartValue($args[1], 0, 100, false);
+        $lightness = $this->compileColorPartValue($args[2], 0, 100, false);
 
-        $color = $this->toRGB($h[1], $s[1], $l[1]);
-        $color[4] = $a[1];
+        $alpha = null;
+
+        if (count($args) === 4) {
+            $alpha = $this->compileColorPartValue($args[3], 0, 100, false);
+
+            if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness) || ! is_numeric($alpha)) {
+                return [Type::T_STRING, '',
+                    [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
+            }
+        } else {
+            if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness)) {
+                return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
+            }
+        }
+
+        $color = $this->toRGB($hue, $saturation, $lightness);
+
+        if (! is_null($alpha)) {
+            $color[4] = $alpha;
+        }
 
         return $color;
     }
 
+    protected static $libHsla = [
+            ['channels'],
+            ['hue', 'saturation', 'lightness', 'alpha:1'] ];
+    protected function libHsla($args, $kwargs)
+    {
+        return $this->libHsl($args, $kwargs, 'hsla');
+    }
+
     protected static $libHue = ['color'];
     protected function libHue($args)
     {
@@ -5589,21 +6206,32 @@ class Compiler
         return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
     }
 
-    protected static $libInvert = ['color'];
+    protected static $libInvert = ['color', 'weight:1'];
     protected function libInvert($args)
     {
-        $value = $args[0];
+        list($value, $weight) = $args;
+
+        if (! isset($weight)) {
+            $weight = 1;
+        } else {
+            $weight = $this->coercePercent($weight);
+        }
 
         if ($value[0] === Type::T_NUMBER) {
             return null;
         }
 
         $color = $this->assertColor($value);
-        $color[1] = 255 - $color[1];
-        $color[2] = 255 - $color[2];
-        $color[3] = 255 - $color[3];
+        $inverted = $color;
+        $inverted[1] = 255 - $inverted[1];
+        $inverted[2] = 255 - $inverted[2];
+        $inverted[3] = 255 - $inverted[3];
 
-        return $color;
+        if ($weight < 1) {
+            return $this->libMix([$inverted, $color, [Type::T_NUMBER, $weight]]);
+        }
+
+        return $inverted;
     }
 
     // increases opacity by amount
@@ -5712,7 +6340,7 @@ class Compiler
         $min = null;
 
         foreach ($numbers as $key => $number) {
-            if (null === $min || $number[1] <= $min[1]) {
+            if (is_null($min) || $number[1] <= $min[1]) {
                 $min = [$key, $number[1]];
             }
         }
@@ -5726,7 +6354,7 @@ class Compiler
         $max = null;
 
         foreach ($numbers as $key => $number) {
-            if (null === $max || $number[1] >= $max[1]) {
+            if (is_null($max) || $number[1] >= $max[1]) {
                 $max = [$key, $number[1]];
             }
         }
@@ -5743,9 +6371,9 @@ class Compiler
      */
     protected function getNormalizedNumbers($args)
     {
-        $unit = null;
+        $unit         = null;
         $originalUnit = null;
-        $numbers = [];
+        $numbers      = [];
 
         foreach ($args as $key => $item) {
             if ($item[0] !== Type::T_NUMBER) {
@@ -5755,7 +6383,7 @@ class Compiler
 
             $number = $item->normalize();
 
-            if (null === $unit) {
+            if (is_null($unit)) {
                 $unit = $number[2];
                 $originalUnit = $item->unitStr();
             } elseif ($number[1] && $unit !== $number[2]) {
@@ -5843,6 +6471,7 @@ class Compiler
 
         if (! is_null($key)) {
             $key = $this->compileStringContent($this->coerceString($key));
+
             for ($i = count($map[1]) - 1; $i >= 0; $i--) {
                 if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
                     return $map[2][$i];
@@ -5941,6 +6570,18 @@ class Compiler
         return [Type::T_MAP, $keys, $values];
     }
 
+    protected static $libIsBracketed = ['list'];
+    protected function libIsBracketed($args)
+    {
+        $list = $args[0];
+        $this->coerceList($list, ' ');
+        if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') {
+            return true;
+        }
+        return false;
+    }
+
+
     protected function listSeparatorForJoin($list1, $sep)
     {
         if (! isset($sep)) {
@@ -5952,23 +6593,53 @@ class Compiler
                 return ',';
 
             case 'space':
-                return '';
+                return ' ';
 
             default:
                 return $list1[1];
         }
     }
 
-    protected static $libJoin = ['list1', 'list2', 'separator:null'];
+    protected static $libJoin = ['list1', 'list2', 'separator:null', 'bracketed:auto'];
     protected function libJoin($args)
     {
-        list($list1, $list2, $sep) = $args;
+        list($list1, $list2, $sep, $bracketed) = $args;
 
         $list1 = $this->coerceList($list1, ' ');
         $list2 = $this->coerceList($list2, ' ');
-        $sep = $this->listSeparatorForJoin($list1, $sep);
+        $sep   = $this->listSeparatorForJoin($list1, $sep);
+
+        if ($bracketed === static::$true) {
+            $bracketed = true;
+        } elseif ($bracketed === static::$false) {
+            $bracketed = false;
+        } elseif ($bracketed === [Type::T_KEYWORD, 'auto']) {
+            $bracketed = 'auto';
+        } elseif ($bracketed === static::$null) {
+            $bracketed = false;
+        } else {
+            $bracketed = $this->compileValue($bracketed);
+            $bracketed = ! ! $bracketed;
+            if ($bracketed === true) {
+                $bracketed = true;
+            }
+        }
+
+        if ($bracketed === 'auto') {
+            $bracketed = false;
+            if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') {
+                $bracketed = true;
+            }
+        }
 
-        return [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
+        $res = [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
+        if (isset($list1['enclosing'])) {
+            $res['enlcosing'] = $list1['enclosing'];
+        }
+        if ($bracketed) {
+            $res['enclosing'] = 'bracket';
+        }
+        return $res;
     }
 
     protected static $libAppend = ['list', 'val', 'separator:null'];
@@ -5979,13 +6650,17 @@ class Compiler
         $list1 = $this->coerceList($list1, ' ');
         $sep = $this->listSeparatorForJoin($list1, $sep);
 
-        return [Type::T_LIST, $sep, array_merge($list1[2], [$value])];
+        $res = [Type::T_LIST, $sep, array_merge($list1[2], [$value])];
+        if (isset($list1['enclosing'])) {
+            $res['enclosing'] = $list1['enclosing'];
+        }
+        return $res;
     }
 
     protected function libZip($args)
     {
-        foreach ($args as $arg) {
-            $this->assertList($arg);
+        foreach ($args as $key => $arg) {
+            $args[$key] = $this->coerceList($arg);
         }
 
         $lists = [];
@@ -6116,10 +6791,10 @@ class Compiler
         return new Node\Number(strlen($stringContent), '');
     }
 
-    protected static $libStrSlice = ['string', 'start-at', 'end-at:null'];
+    protected static $libStrSlice = ['string', 'start-at', 'end-at:-1'];
     protected function libStrSlice($args)
     {
-        if (isset($args[2]) && $args[2][1] == 0) {
+        if (isset($args[2]) && ! $args[2][1]) {
             return static::$nullString;
         }
 
@@ -6132,7 +6807,7 @@ class Compiler
             $start--;
         }
 
-        $end    = (int) $args[2][1];
+        $end    = isset($args[2]) ? (int) $args[2][1] : -1;
         $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end);
 
         $string[2] = $length
@@ -6270,14 +6945,40 @@ class Compiler
         return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
     }
 
-    protected static $libInspect = ['value'];
-    protected function libInspect($args)
+    protected function inspectFormatValue($value, $force_enclosing_display = false)
     {
-        if ($args[0] === static::$null) {
-            return [Type::T_KEYWORD, 'null'];
+        if ($value === static::$null) {
+            $value = [Type::T_KEYWORD, 'null'];
+        }
+        $stringValue = [$value];
+        if ($value[0] === Type::T_LIST) {
+            if (end($value[2]) === static::$null) {
+                array_pop($value[2]);
+                $value[2][] = [Type::T_STRING, '', ['']];
+                $force_enclosing_display = true;
+            }
+            if (! empty($value['enclosing'])) {
+                if ($force_enclosing_display
+                    || ($value['enclosing'] === 'bracket' )
+                    || !count($value[2])) {
+                    $value['enclosing'] = 'forced_'.$value['enclosing'];
+                    $force_enclosing_display = true;
+                }
+            }
+            foreach ($value[2] as $k => $listelement) {
+                $value[2][$k] = $this->inspectFormatValue($listelement, $force_enclosing_display);
+            }
+            $stringValue = [$value];
         }
 
-        return $args[0];
+        return [Type::T_STRING, '', $stringValue];
+    }
+
+    protected static $libInspect = ['value'];
+    protected function libInspect($args)
+    {
+        $value = $args[0];
+        return $this->inspectFormatValue($value);
     }
 
     /**
@@ -6431,6 +7132,7 @@ class Compiler
         // get the selector... list
         $args = reset($args);
         $args = $args[2];
+
         if (count($args) < 1) {
             $this->throwError("selector-append() needs at least 1 argument");
         }
@@ -6497,8 +7199,8 @@ class Compiler
         list($selectors, $extendee, $extender) = $args;
 
         $selectors = $this->getSelectorArg($selectors);
-        $extendee = $this->getSelectorArg($extendee);
-        $extender = $this->getSelectorArg($extender);
+        $extendee  = $this->getSelectorArg($extendee);
+        $extender  = $this->getSelectorArg($extender);
 
         if (! $selectors || ! $extendee || ! $extender) {
             $this->throwError("selector-extend() invalid arguments");
@@ -6514,8 +7216,8 @@ class Compiler
     {
         list($selectors, $original, $replacement) = $args;
 
-        $selectors = $this->getSelectorArg($selectors);
-        $original = $this->getSelectorArg($original);
+        $selectors   = $this->getSelectorArg($selectors);
+        $original    = $this->getSelectorArg($original);
         $replacement = $this->getSelectorArg($replacement);
 
         if (! $selectors || ! $original || ! $replacement) {
@@ -6580,13 +7282,14 @@ class Compiler
         // get the selector... list
         $args = reset($args);
         $args = $args[2];
+
         if (count($args) < 1) {
             $this->throwError("selector-nest() needs at least 1 argument");
         }
 
         $selectorsMap = array_map([$this, 'getSelectorArg'], $args);
-
         $envs = [];
+
         foreach ($selectorsMap as $selectors) {
             $env = new Environment();
             $env->selectors = $selectors;
@@ -6594,8 +7297,8 @@ class Compiler
             $envs[] = $env;
         }
 
-        $envs = array_reverse($envs);
-        $env = $this->extractEnv($envs);
+        $envs            = array_reverse($envs);
+        $env             = $this->extractEnv($envs);
         $outputSelectors = $this->multiplySelectors($env);
 
         return $this->formatOutputSelector($outputSelectors);
@@ -6638,6 +7341,7 @@ class Compiler
      *
      * @param array $compound1
      * @param array $compound2
+     *
      * @return array|mixed
      */
     protected function unifyCompoundSelectors($compound1, $compound2)
@@ -6653,7 +7357,7 @@ class Compiler
         // check that last part are compatible
         $lastPart1 = array_pop($compound1);
         $lastPart2 = array_pop($compound2);
-        $last = $this->mergeParts($lastPart1, $lastPart2);
+        $last      = $this->mergeParts($lastPart1, $lastPart2);
 
         if (! $last) {
             return [[]];
@@ -6676,6 +7380,7 @@ class Compiler
 
                 $c = $this->mergeParts($part1, $part2);
                 $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
+
                 $part1 = $part2 = null;
 
                 array_pop($compound1);
@@ -6690,6 +7395,7 @@ class Compiler
 
                 $c = $this->mergeParts($part2, $part1);
                 $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
+
                 $part1 = $part2 = null;
 
                 array_pop($compound2);
@@ -6701,9 +7407,9 @@ class Compiler
                 array_pop($compound1);
                 array_pop($compound2);
 
-                $s = $this->prependSelectors($unifiedSelectors, [$part2]);
+                $s   = $this->prependSelectors($unifiedSelectors, [$part2]);
                 $new = array_merge($new, $this->prependSelectors($s, [$part1]));
-                $s = $this->prependSelectors($unifiedSelectors, [$part1]);
+                $s   = $this->prependSelectors($unifiedSelectors, [$part1]);
                 $new = array_merge($new, $this->prependSelectors($s, [$part2]));
             } elseif ($part1) {
                 array_pop($compound1);
@@ -6757,8 +7463,8 @@ class Compiler
     protected function matchPartInCompound($part, $compound)
     {
         $partTag = $this->findTagName($part);
-        $before = $compound;
-        $after = [];
+        $before  = $compound;
+        $after   = [];
 
         // try to find a match by tag name first
         while (count($before)) {
@@ -6804,7 +7510,7 @@ class Compiler
     {
         $tag1 = $this->findTagName($parts1);
         $tag2 = $this->findTagName($parts2);
-        $tag = $this->checkCompatibleTags($tag1, $tag2);
+        $tag  = $this->checkCompatibleTags($tag1, $tag2);
 
         // not compatible tags
         if ($tag === false) {
@@ -6855,12 +7561,12 @@ class Compiler
         $tags = array_unique($tags);
         $tags = array_filter($tags);
 
-        if (count($tags)>1) {
+        if (count($tags) > 1) {
             $tags = array_diff($tags, ['*']);
         }
 
         // not compatible nodes
-        if (count($tags)>1) {
+        if (count($tags) > 1) {
             return false;
         }
 
index 478aa6a..e177704 100644 (file)
@@ -81,6 +81,11 @@ abstract class Formatter
      */
     protected $sourceMapGenerator;
 
+    /**
+     * @var string
+     */
+    protected $strippedSemicolon;
+
     /**
      * Initialize formatter
      *
@@ -113,24 +118,6 @@ abstract class Formatter
         return rtrim($name) . $this->assignSeparator . $value . ';';
     }
 
-    /**
-     * Strip semi-colon appended by property(); it's a separator, not a terminator
-     *
-     * @api
-     *
-     * @param array $lines
-     */
-    public function stripSemicolon(&$lines)
-    {
-        if ($this->keepSemicolons) {
-            return;
-        }
-
-        if (($count = count($lines)) && substr($lines[$count - 1], -1) === ';') {
-            $lines[$count - 1] = substr($lines[$count - 1], 0, -1);
-        }
-    }
-
     /**
      * Output lines inside a block
      *
@@ -207,6 +194,10 @@ abstract class Formatter
         if (! empty($block->selectors)) {
             $this->indentLevel--;
 
+            if (! $this->keepSemicolons) {
+                $this->strippedSemicolon = '';
+            }
+
             if (empty($block->children)) {
                 $this->write($this->break);
             }
@@ -217,8 +208,10 @@ abstract class Formatter
 
     /**
      * Test and clean safely empty children
+     *
      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
-     * @return bool
+     *
+     * @return boolean
      */
     protected function testEmptyChildren($block)
     {
@@ -228,14 +221,16 @@ abstract class Formatter
             foreach ($block->children as $k => &$child) {
                 if (! $this->testEmptyChildren($child)) {
                     $isEmpty = false;
-                } else {
-                    if ($child->type === Type::T_MEDIA || $child->type === Type::T_DIRECTIVE) {
-                        $child->children = [];
-                        $child->selectors = null;
-                    }
+                    continue;
+                }
+
+                if ($child->type === Type::T_MEDIA || $child->type === Type::T_DIRECTIVE) {
+                    $child->children = [];
+                    $child->selectors = null;
                 }
             }
         }
+
         return $isEmpty;
     }
 
@@ -254,8 +249,8 @@ abstract class Formatter
         $this->sourceMapGenerator = null;
 
         if ($sourceMapGenerator) {
-            $this->currentLine = 1;
-            $this->currentColumn = 0;
+            $this->currentLine        = 1;
+            $this->currentColumn      = 0;
             $this->sourceMapGenerator = $sourceMapGenerator;
         }
 
@@ -271,10 +266,32 @@ abstract class Formatter
     }
 
     /**
+     * Output content
+     *
      * @param string $str
      */
     protected function write($str)
     {
+        if (! empty($this->strippedSemicolon)) {
+            echo $this->strippedSemicolon;
+
+            $this->strippedSemicolon = '';
+        }
+
+        /*
+         * Maybe Strip semi-colon appended by property(); it's a separator, not a terminator
+         * will be striped for real before a closing, otherwise displayed unchanged starting the next write
+         */
+        if (! $this->keepSemicolons &&
+            $str &&
+            (strpos($str, ';') !== false) &&
+            (substr($str, -1) === ';')
+        ) {
+            $str = substr($str, 0, -1);
+
+            $this->strippedSemicolon = ';';
+        }
+
         if ($this->sourceMapGenerator) {
             $this->sourceMapGenerator->addMapping(
                 $this->currentLine,
index 8eec475..9549c6c 100644 (file)
@@ -55,7 +55,7 @@ class Expanded extends Formatter
 
         foreach ($block->lines as $index => $line) {
             if (substr($line, 0, 2) === '/*') {
-                $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line);
+                $block->lines[$index] = preg_replace('/[\r\n]+/', $glue, $line);
             }
         }
 
index 50a70ce..f9e7f03 100644 (file)
@@ -63,24 +63,13 @@ class Nested extends Formatter
 
         foreach ($block->lines as $index => $line) {
             if (substr($line, 0, 2) === '/*') {
-                $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line);
+                $block->lines[$index] = preg_replace('/[\r\n]+/', $glue, $line);
             }
         }
 
         $this->write($inner . implode($glue, $block->lines));
     }
 
-    protected function hasFlatChild($block)
-    {
-        foreach ($block->children as $child) {
-            if (empty($child->selectors)) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
     /**
      * {@inheritdoc}
      */
@@ -109,7 +98,7 @@ class Nested extends Formatter
             array_pop($depths);
             $this->depth--;
 
-            if (!$this->depth && ($block->depth <= 1 || (!$this->indentLevel && $block->type === Type::T_COMMENT)) &&
+            if (! $this->depth && ($block->depth <= 1 || (! $this->indentLevel && $block->type === Type::T_COMMENT)) &&
                 (($block->selectors && ! $isMediaOrDirective) || $previousHasSelector)
             ) {
                 $downLevel = $this->break;
@@ -170,15 +159,18 @@ class Nested extends Formatter
             }
 
             $this->blockLines($block);
+
             $closeBlock = $this->break;
         }
 
         if (! empty($block->children)) {
-            if ($this->depth>0 && ($isMediaOrDirective || ! $this->hasFlatChild($block))) {
+            if ($this->depth > 0 && ($isMediaOrDirective || ! $this->hasFlatChild($block))) {
                 array_pop($depths);
+
                 $this->depth--;
                 $this->blockChildren($block);
                 $this->depth++;
+
                 $depths[] = $block->depth;
             } else {
                 $this->blockChildren($block);
@@ -193,13 +185,19 @@ class Nested extends Formatter
         if (! empty($block->selectors)) {
             $this->indentLevel--;
 
+            if (! $this->keepSemicolons) {
+                $this->strippedSemicolon = '';
+            }
+
             $this->write($this->close);
+
             $closeBlock = $this->break;
 
             if ($this->depth > 1 && ! empty($block->children)) {
                 array_pop($depths);
                 $this->depth--;
             }
+
             if (! $isMediaOrDirective) {
                 $previousHasSelector = true;
             }
@@ -209,4 +207,22 @@ class Nested extends Formatter
             $this->write($this->break);
         }
     }
+
+    /**
+     * Block has flat child
+     *
+     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
+     *
+     * @return boolean
+     */
+    private function hasFlatChild($block)
+    {
+        foreach ($block->children as $child) {
+            if (empty($child->selectors)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
 }
index 1e83bf8..acbabff 100644 (file)
@@ -141,11 +141,11 @@ class Number extends Node implements \ArrayAccess
     public function offsetExists($offset)
     {
         if ($offset === -3) {
-            return $this->sourceColumn !== null;
+            return ! is_null($this->sourceColumn);
         }
 
         if ($offset === -2) {
-            return $this->sourceLine !== null;
+            return ! is_null($this->sourceLine);
         }
 
         if ($offset === -1 ||
index ed9b28f..6a30af1 100644 (file)
@@ -218,8 +218,8 @@ class Parser
      *
      * @api
      *
-     * @param string $buffer
-     * @param string $out
+     * @param string       $buffer
+     * @param string|array $out
      *
      * @return boolean
      */
@@ -245,8 +245,8 @@ class Parser
      *
      * @api
      *
-     * @param string $buffer
-     * @param string $out
+     * @param string       $buffer
+     * @param string|array $out
      *
      * @return boolean
      */
@@ -272,10 +272,10 @@ class Parser
      *
      * @api
      *
-     * @param string $buffer
-     * @param string $out
+     * @param string       $buffer
+     * @param string|array $out
      *
-     * @return array
+     * @return boolean
      */
     public function parseMediaQueryList($buffer, &$out)
     {
@@ -287,7 +287,6 @@ class Parser
 
         $this->saveEncoding();
 
-
         $isMediaQuery = $this->mediaQueryList($out);
 
         $this->restoreEncoding();
@@ -343,11 +342,14 @@ class Parser
             if ($this->literal('@at-root', 8) &&
                 ($this->selectors($selector) || true) &&
                 ($this->map($with) || true) &&
+                (($this->matchChar('(')
+                    && $this->interpolation($with)
+                    && $this->matchChar(')')) || true) &&
                 $this->matchChar('{', false)
             ) {
                 $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
                 $atRoot->selector = $selector;
-                $atRoot->with = $with;
+                $atRoot->with     = $with;
 
                 return true;
             }
@@ -383,9 +385,18 @@ class Parser
                     ($this->argValues($argValues) || true) &&
                     $this->matchChar(')') || true) &&
                 ($this->end() ||
+                    ($this->literal('using', 5) &&
+                        $this->argumentDef($argUsing) &&
+                        ($this->end() || $this->matchChar('{') && $hasBlock = true)) ||
                     $this->matchChar('{') && $hasBlock = true)
             ) {
-                $child = [Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null];
+                $child = [
+                    Type::T_INCLUDE,
+                    $mixinName,
+                    isset($argValues) ? $argValues : null,
+                    null,
+                    isset($argUsing) ? $argUsing : null
+                ];
 
                 if (! empty($hasBlock)) {
                     $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
@@ -524,9 +535,9 @@ class Parser
                 $this->matchChar('{', false)
             ) {
                 $for = $this->pushSpecialBlock(Type::T_FOR, $s);
-                $for->var = $varName[1];
+                $for->var   = $varName[1];
                 $for->start = $start;
-                $for->end = $end;
+                $for->end   = $end;
                 $for->until = isset($forUntil);
 
                 return true;
@@ -536,7 +547,13 @@ class Parser
 
             if ($this->literal('@if', 3) && $this->valueList($cond) && $this->matchChar('{', false)) {
                 $if = $this->pushSpecialBlock(Type::T_IF, $s);
-                $if->cond = $cond;
+                while ($cond[0] === Type::T_LIST
+                    && !empty($cond['enclosing'])
+                    && $cond['enclosing'] === 'parent'
+                    && count($cond[2]) == 1) {
+                    $cond = reset($cond[2]);
+                }
+                $if->cond  = $cond;
                 $if->cases = [];
 
                 return true;
@@ -577,8 +594,15 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@content', 8) && $this->end()) {
-                $this->append([Type::T_MIXIN_CONTENT], $s);
+            #if ($this->literal('@content', 8))
+
+            if ($this->literal('@content', 8) &&
+                ($this->end() ||
+                    $this->matchChar('(') &&
+                        $this->argValues($argContent) &&
+                        $this->matchChar(')') &&
+                    $this->end())) {
+                $this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s);
 
                 return true;
             }
@@ -633,9 +657,10 @@ class Parser
 
             if ($this->literal('@supports', 9) &&
                 ($t1=$this->supportsQuery($supportQuery)) &&
-                ($t2=$this->matchChar('{', false)) ) {
+                ($t2=$this->matchChar('{', false))
+            ) {
                 $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
-                $directive->name = 'supports';
+                $directive->name  = 'supports';
                 $directive->value = $supportQuery;
 
                 return true;
@@ -665,6 +690,19 @@ class Parser
 
             $this->seek($s);
 
+            // maybe it's a generic blockless directive
+            if ($this->matchChar('@', false) &&
+                $this->keyword($dirName) &&
+                $this->valueList($dirValue) &&
+                $this->end()
+            ) {
+                $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue]], $s);
+
+                return true;
+            }
+
+            $this->seek($s);
+
             return false;
         }
 
@@ -709,7 +747,7 @@ class Parser
 
             if ($this->eatWhiteDefault) {
                 $this->whitespace();
-                $this->append(null); // collect comments at the begining if needed
+                $this->append(null); // collect comments at the beginning if needed
             }
 
             return true;
@@ -751,7 +789,7 @@ class Parser
         if ($this->matchChar('}', false)) {
             $block = $this->popBlock();
 
-            if (!isset($block->type) || $block->type !== Type::T_IF) {
+            if (! isset($block->type) || $block->type !== Type::T_IF) {
                 if ($this->env->parent) {
                     $this->append(null); // collect comments before next statement if needed
                 }
@@ -823,7 +861,7 @@ class Parser
 
         $this->env = $b;
 
-        // collect comments at the begining of a block if needed
+        // collect comments at the beginning of a block if needed
         if ($this->eatWhiteDefault) {
             $this->whitespace();
 
@@ -1055,16 +1093,17 @@ class Parser
             if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
                 // comment that are kept in the output CSS
                 $comment = [];
+                $startCommentCount = $this->count;
                 $endCommentCount = $this->count + strlen($m[1]);
 
                 // find interpolations in comment
                 $p = strpos($this->buffer, '#{', $this->count);
 
                 while ($p !== false && $p < $endCommentCount) {
-                    $c = substr($this->buffer, $this->count, $p - $this->count);
-                    $comment[] = $c;
+                    $c           = substr($this->buffer, $this->count, $p - $this->count);
+                    $comment[]   = $c;
                     $this->count = $p;
-                    $out = null;
+                    $out         = null;
 
                     if ($this->interpolation($out)) {
                         // keep right spaces in the following string part
@@ -1076,7 +1115,7 @@ class Parser
                             $out[3] = '';
                         }
 
-                        $comment[] = $out;
+                        $comment[] = [Type::T_COMMENT, substr($this->buffer, $p, $this->count - $p), $out];
                     } else {
                         $comment[] = substr($this->buffer, $this->count, 2);
 
@@ -1094,10 +1133,11 @@ class Parser
                     $this->appendComment([Type::T_COMMENT, $c]);
                 } else {
                     $comment[] = $c;
-                    $this->appendComment([Type::T_COMMENT, [Type::T_STRING, '', $comment]]);
+                    $staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount);
+                    $this->appendComment([Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]]);
                 }
 
-                $this->commentsSeen[$this->count] = true;
+                $this->commentsSeen[$startCommentCount] = true;
                 $this->count = $endCommentCount;
             } else {
                 // comment that are ignored and not kept in the output css
@@ -1118,8 +1158,21 @@ class Parser
     protected function appendComment($comment)
     {
         if (! $this->discardComments) {
-            if ($comment[0] === Type::T_COMMENT && is_string($comment[1])) {
-                $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
+            if ($comment[0] === Type::T_COMMENT) {
+                if (is_string($comment[1])) {
+                    $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
+                }
+                if (isset($comment[2]) and is_array($comment[2]) and $comment[2][0] === Type::T_STRING) {
+                    foreach ($comment[2][2] as $k => $v) {
+                        if (is_string($v)) {
+                            $p = strpos($v, "\n");
+                            if ($p !== false) {
+                                $comment[2][2][$k] = substr($v, 0, $p + 1)
+                                    . preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], substr($v, $p+1));
+                            }
+                        }
+                    }
+                }
             }
 
             $this->env->comments[] = $comment;
@@ -1135,7 +1188,7 @@ class Parser
     protected function append($statement, $pos = null)
     {
         if (! is_null($statement)) {
-            if ($pos !== null) {
+            if (! is_null($pos)) {
                 list($line, $column) = $this->getSourcePosition($pos);
 
                 $statement[static::SOURCE_LINE]   = $line;
@@ -1247,12 +1300,14 @@ class Parser
         $s = $this->count;
 
         $not = false;
+
         if (($this->literal('not', 3) && ($not = true) || true) &&
             $this->matchChar('(') &&
             ($this->expression($property)) &&
             $this->literal(': ', 2) &&
             $this->valueList($value) &&
-            $this->matchChar(')')) {
+            $this->matchChar(')')
+         ) {
             $support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]];
             $support[2][] = $property;
             $support[2][] = [Type::T_KEYWORD, ': '];
@@ -1267,7 +1322,8 @@ class Parser
 
         if ($this->matchChar('(') &&
             $this->supportsQuery($subQuery) &&
-            $this->matchChar(')')) {
+            $this->matchChar(')')
+        ) {
             $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, '('], $subQuery, [Type::T_KEYWORD, ')']]];
             $s = $this->count;
         } else {
@@ -1275,7 +1331,8 @@ class Parser
         }
 
         if ($this->literal('not', 3) &&
-            $this->supportsQuery($subQuery)) {
+            $this->supportsQuery($subQuery)
+        ) {
             $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]];
             $s = $this->count;
         } else {
@@ -1284,12 +1341,15 @@ class Parser
 
         if ($this->literal('selector(', 9) &&
             $this->selector($selector) &&
-            $this->matchChar(')')) {
+            $this->matchChar(')')
+        ) {
             $support = [Type::T_STRING, '', [[Type::T_KEYWORD, 'selector(']]];
 
             $selectorList = [Type::T_LIST, '', []];
+
             foreach ($selector as $sc) {
                 $compound = [Type::T_STRING, '', []];
+
                 foreach ($sc as $scp) {
                     if (is_array($scp)) {
                         $compound[2][] = $scp;
@@ -1297,6 +1357,7 @@ class Parser
                         $compound[2][] = [Type::T_KEYWORD, $scp];
                     }
                 }
+
                 $selectorList[2][] = $compound;
             }
             $support[2][] = $selectorList;
@@ -1317,6 +1378,7 @@ class Parser
         if ($this->literal('and', 3) &&
             $this->genericList($expressions, 'supportsQuery', ' and', false)) {
             array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
+
             $parts = [$expressions];
             $s = $this->count;
         } else {
@@ -1326,6 +1388,7 @@ class Parser
         if ($this->literal('or', 2) &&
             $this->genericList($expressions, 'supportsQuery', ' or', false)) {
             array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
+
             $parts = [$expressions];
             $s = $this->count;
         } else {
@@ -1336,7 +1399,9 @@ class Parser
             if ($this->eatWhiteDefault) {
                 $this->whitespace();
             }
+
             $out = [Type::T_STRING, '', $parts];
+
             return true;
         }
 
@@ -1408,6 +1473,7 @@ class Parser
 
         if (! $this->variable($keyword) || ! $this->matchChar(':')) {
             $this->seek($s);
+
             $keyword = null;
         }
 
@@ -1436,7 +1502,12 @@ class Parser
      */
     protected function valueList(&$out)
     {
-        return $this->genericList($out, 'spaceList', ',');
+        $discardComments = $this->discardComments;
+        $this->discardComments = true;
+        $res = $this->genericList($out, 'spaceList', ',');
+        $this->discardComments = $discardComments;
+
+        return $res;
     }
 
     /**
@@ -1463,17 +1534,19 @@ class Parser
      */
     protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
     {
-        $s = $this->count;
+        $s     = $this->count;
         $items = [];
         $value = null;
 
         while ($this->$parseItem($value)) {
+            $trailing_delim = false;
             $items[] = $value;
 
             if ($delim) {
                 if (! $this->literal($delim, strlen($delim))) {
                     break;
                 }
+                $trailing_delim = true;
             }
         }
 
@@ -1483,6 +1556,9 @@ class Parser
             return false;
         }
 
+        if ($trailing_delim) {
+            $items[] = [Type::T_NULL];
+        }
         if ($flatten && count($items) === 1) {
             $out = $items[0];
         } else {
@@ -1496,41 +1572,58 @@ class Parser
      * Parse expression
      *
      * @param array $out
+     * @param bool $listOnly
+     * @param bool $lookForExp
      *
      * @return boolean
      */
-    protected function expression(&$out)
+    protected function expression(&$out, $listOnly = false, $lookForExp = true)
     {
         $s = $this->count;
         $discard = $this->discardComments;
         $this->discardComments = true;
+        $allowedTypes = ($listOnly ? [Type::T_LIST] : [Type::T_LIST, Type::T_MAP]);
 
         if ($this->matchChar('(')) {
-            if ($this->parenExpression($out, $s, ")")) {
+            if ($this->enclosedExpression($lhs, $s, ")", $allowedTypes)) {
+                if ($lookForExp) {
+                    $out = $this->expHelper($lhs, 0);
+                } else {
+                    $out = $lhs;
+                }
+
                 $this->discardComments = $discard;
+
                 return true;
             }
 
             $this->seek($s);
         }
 
-        if ($this->matchChar('[')) {
-            if ($this->parenExpression($out, $s, "]", [Type::T_LIST, Type::T_KEYWORD])) {
-                if ($out[0] !== Type::T_LIST && $out[0] !== Type::T_MAP) {
-                    $out = [Type::T_STRING, '', [ '[', $out, ']' ]];
+        if (in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) {
+            if ($this->enclosedExpression($lhs, $s, "]", [Type::T_LIST])) {
+                if ($lookForExp) {
+                    $out = $this->expHelper($lhs, 0);
+                } else {
+                    $out = $lhs;
                 }
-
                 $this->discardComments = $discard;
+
                 return true;
             }
 
             $this->seek($s);
         }
 
-        if ($this->value($lhs)) {
-            $out = $this->expHelper($lhs, 0);
+        if (!$listOnly && $this->value($lhs)) {
+            if ($lookForExp) {
+                $out = $this->expHelper($lhs, 0);
+            } else {
+                $out = $lhs;
+            }
 
             $this->discardComments = $discard;
+
             return true;
         }
 
@@ -1548,15 +1641,35 @@ class Parser
      *
      * @return boolean
      */
-    protected function parenExpression(&$out, $s, $closingParen = ")", $allowedTypes = [Type::T_LIST, Type::T_MAP])
+    protected function enclosedExpression(&$out, $s, $closingParen = ")", $allowedTypes = [Type::T_LIST, Type::T_MAP])
     {
-        if ($this->matchChar($closingParen)) {
+        if ($this->matchChar($closingParen) && in_array(Type::T_LIST, $allowedTypes)) {
             $out = [Type::T_LIST, '', []];
-
+            switch ($closingParen) {
+                case ")":
+                    $out['enclosing'] = 'parent'; // parenthesis list
+                    break;
+                case "]":
+                    $out['enclosing'] = 'bracket'; // bracketed list
+                    break;
+            }
             return true;
         }
 
-        if ($this->valueList($out) && $this->matchChar($closingParen) && in_array($out[0], $allowedTypes)) {
+        if ($this->valueList($out) && $this->matchChar($closingParen)
+            && in_array($out[0], [Type::T_LIST, Type::T_KEYWORD])
+            && in_array(Type::T_LIST, $allowedTypes)) {
+            if ($out[0] !== Type::T_LIST || ! empty($out['enclosing'])) {
+                $out = [Type::T_LIST, '', [$out]];
+            }
+            switch ($closingParen) {
+                case ")":
+                    $out['enclosing'] = 'parent'; // parenthesis list
+                    break;
+                case "]":
+                    $out['enclosing'] = 'bracket'; // bracketed list
+                    break;
+            }
             return true;
         }
 
@@ -1600,7 +1713,7 @@ class Parser
                 break;
             }
 
-            if (! $this->value($rhs)) {
+            if (! $this->value($rhs) && ! $this->expression($rhs, true, false)) {
                 break;
             }
 
@@ -1655,7 +1768,7 @@ class Parser
 
         $this->seek($s);
 
-       &n