Merge branch 'MDL-49787_email_attachments_symlink_fix' of https://github.com/iamandre...
authorDan Poltawski <dan@moodle.com>
Tue, 26 May 2015 11:57:12 +0000 (12:57 +0100)
committerDan Poltawski <dan@moodle.com>
Tue, 26 May 2015 11:57:12 +0000 (12:57 +0100)
1  2 
lib/moodlelib.php

diff --combined lib/moodlelib.php
@@@ -408,7 -408,7 +408,7 @@@ define('FEATURE_COMPLETION_HAS_RULES', 
  
  /** True if module has no 'view' page (like label) */
  define('FEATURE_NO_VIEW_LINK', 'viewlink');
 -/** True if module supports outcomes */
 +/** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
  define('FEATURE_IDNUMBER', 'idnumber');
  /** True if module supports groups */
  define('FEATURE_GROUPS', 'groups');
@@@ -1032,16 -1032,10 +1032,16 @@@ function clean_param($param, $type) 
              // Allow http absolute, root relative and relative URLs within wwwroot.
              $param = clean_param($param, PARAM_URL);
              if (!empty($param)) {
 +
 +                // Simulate the HTTPS version of the site.
 +                $httpswwwroot = str_replace('http://', 'https://', $CFG->wwwroot);
 +
                  if (preg_match(':^/:', $param)) {
                      // Root-relative, ok!
 -                } else if (preg_match('/^'.preg_quote($CFG->wwwroot, '/').'/i', $param)) {
 +                } else if (preg_match('/^' . preg_quote($CFG->wwwroot, '/') . '/i', $param)) {
                      // Absolute, and matches our wwwroot.
 +                } else if (!empty($CFG->loginhttps) && preg_match('/^' . preg_quote($httpswwwroot, '/') . '/i', $param)) {
 +                    // Absolute, and matches our httpswwwroot.
                  } else {
                      // Relative - let's make sure there are no tricks.
                      if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?')) {
@@@ -2042,15 -2036,23 +2042,15 @@@ function get_user_preferences($name = n
   * @return int GMT timestamp
   */
  function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
 +    $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
 +    $date->setDate((int)$year, (int)$month, (int)$day);
 +    $date->setTime((int)$hour, (int)$minute, (int)$second);
  
 -    // Save input timezone, required for dst offset check.
 -    $passedtimezone = $timezone;
 -
 -    $timezone = get_user_timezone_offset($timezone);
 -
 -    if (abs($timezone) > 13) {
 -        // Server time.
 -        $time = mktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
 -    } else {
 -        $time = gmmktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
 -        $time = usertime($time, $timezone);
 +    $time = $date->getTimestamp();
  
 -        // Apply dst for string timezones or if 99 then try dst offset with user's default timezone.
 -        if ($applydst && ((99 == $passedtimezone) || !is_numeric($passedtimezone))) {
 -            $time -= dst_offset_on($time, $passedtimezone);
 -        }
 +    // Moodle BC DST stuff.
 +    if (!$applydst) {
 +        $time += dst_offset_on($time, $timezone);
      }
  
      return $time;
@@@ -2174,9 -2176,13 +2174,9 @@@ function userdate($date, $format = '', 
   * If we are running under Windows convert to Windows encoding and then back to UTF-8
   * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
   *
 - * This function does not do any calculation regarding the user preferences and should
 - * therefore receive the final date timestamp, format and timezone. Timezone being only used
 - * to differentiate the use of server time or not (strftime() against gmstrftime()).
 - *
 - * @param int $date the timestamp.
 + * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
   * @param string $format strftime format.
 - * @param int|float $tz the numerical timezone, typically returned by {@link get_user_timezone_offset()}.
 + * @param int|float|string $tz the user timezone
   * @return string the formatted date/time.
   * @since Moodle 2.3.3
   */
@@@ -2190,18 -2196,23 +2190,18 @@@ function date_format_string($date, $for
          $localewincharset = $calendartype->locale_win_charset();
      }
  
 -    if (abs($tz) > 13) {
 -        if ($localewincharset) {
 -            $format = core_text::convert($format, 'utf-8', $localewincharset);
 -            $datestring = strftime($format, $date);
 -            $datestring = core_text::convert($datestring, $localewincharset, 'utf-8');
 -        } else {
 -            $datestring = strftime($format, $date);
 -        }
 -    } else {
 -        if ($localewincharset) {
 -            $format = core_text::convert($format, 'utf-8', $localewincharset);
 -            $datestring = gmstrftime($format, $date);
 -            $datestring = core_text::convert($datestring, $localewincharset, 'utf-8');
 -        } else {
 -            $datestring = gmstrftime($format, $date);
 -        }
 +    if ($localewincharset) {
 +        $format = core_text::convert($format, 'utf-8', $localewincharset);
 +    }
 +
 +    date_default_timezone_set(core_date::get_user_timezone($tz));
 +    $datestring = strftime($format, $date);
 +    core_date::set_default_server_timezone();
 +
 +    if ($localewincharset) {
 +        $datestring = core_text::convert($datestring, $localewincharset, 'utf-8');
      }
 +
      return $datestring;
  }
  
   *
   * @package core
   * @category time
 - * @uses HOURSECS
   * @param int $time Timestamp in GMT
 - * @param float|int|string $timezone offset's time with timezone, if float and not 99, then no
 - *        dst offset is applied {@link http://docs.moodle.org/dev/Time_API#Timezone}
 + * @param float|int|string $timezone user timezone
   * @return array An array that represents the date in user time
   */
  function usergetdate($time, $timezone=99) {
 +    date_default_timezone_set(core_date::get_user_timezone($timezone));
 +    $result = getdate($time);
 +    core_date::set_default_server_timezone();
  
 -    // Save input timezone, required for dst offset check.
 -    $passedtimezone = $timezone;
 -
 -    $timezone = get_user_timezone_offset($timezone);
 -
 -    if (abs($timezone) > 13) {
 -        // Server time.
 -        return getdate($time);
 -    }
 -
 -    // Add daylight saving offset for string timezones only, as we can't get dst for
 -    // float values. if timezone is 99 (user default timezone), then try update dst.
 -    if ($passedtimezone == 99 || !is_numeric($passedtimezone)) {
 -        $time += dst_offset_on($time, $passedtimezone);
 -    }
 -
 -    $time += intval((float)$timezone * HOURSECS);
 -
 -    $datestring = gmstrftime('%B_%A_%j_%Y_%m_%w_%d_%H_%M_%S', $time);
 -
 -    // Be careful to ensure the returned array matches that produced by getdate() above.
 -    list(
 -        $getdate['month'],
 -        $getdate['weekday'],
 -        $getdate['yday'],
 -        $getdate['year'],
 -        $getdate['mon'],
 -        $getdate['wday'],
 -        $getdate['mday'],
 -        $getdate['hours'],
 -        $getdate['minutes'],
 -        $getdate['seconds']
 -    ) = explode('_', $datestring);
 -
 -    // Set correct datatype to match with getdate().
 -    $getdate['seconds'] = (int)$getdate['seconds'];
 -    $getdate['yday'] = (int)$getdate['yday'] - 1; // The function gmstrftime returns 0 through 365.
 -    $getdate['year'] = (int)$getdate['year'];
 -    $getdate['mon'] = (int)$getdate['mon'];
 -    $getdate['wday'] = (int)$getdate['wday'];
 -    $getdate['mday'] = (int)$getdate['mday'];
 -    $getdate['hours'] = (int)$getdate['hours'];
 -    $getdate['minutes'] = (int)$getdate['minutes'];
 -    return $getdate;
 +    return $result;
  }
  
  /**
   * Given a GMT timestamp (seconds since epoch), offsets it by
   * the timezone.  eg 3pm in India is 3pm GMT - 7 * 3600 seconds
   *
 + * NOTE: this function does not include DST properly,
 + *       you should use the PHP date stuff instead!
 + *
   * @package core
   * @category time
 - * @uses HOURSECS
   * @param int $date Timestamp in GMT
 - * @param float|int|string $timezone timezone to calculate GMT time offset before
 - *        calculating user time, 99 is default user timezone
 - *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
 + * @param float|int|string $timezone user timezone
   * @return int
   */
  function usertime($date, $timezone=99) {
 +    $userdate = new DateTime('@' . $date);
 +    $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
 +    $dst = dst_offset_on($date, $timezone);
  
 -    $timezone = get_user_timezone_offset($timezone);
 -
 -    if (abs($timezone) > 13) {
 -        return $date;
 -    }
 -    return $date - (int)($timezone * HOURSECS);
 +    return $date - $userdate->getOffset() + $dst;
  }
  
  /**
   * @package core
   * @category time
   * @param int $date Timestamp in GMT
 - * @param float|int|string $timezone timezone to calculate GMT time offset before
 - *        calculating user midnight time, 99 is default user timezone
 - *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
 + * @param float|int|string $timezone user timezone
   * @return int Returns a GMT timestamp
   */
  function usergetmidnight($date, $timezone=99) {
   *
   * @package core
   * @category time
 - * @param float|int|string $timezone timezone to calculate GMT time offset before
 - *        calculating user timezone, 99 is default user timezone
 - *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
 + * @param float|int|string $timezone user timezone
   * @return string
   */
  function usertimezone($timezone=99) {
 -
 -    $tz = get_user_timezone($timezone);
 -
 -    if (!is_float($tz)) {
 -        return $tz;
 -    }
 -
 -    if (abs($tz) > 13) {
 -        // Server time.
 -        return get_string('serverlocaltime');
 -    }
 -
 -    if ($tz == intval($tz)) {
 -        // Don't show .0 for whole hours.
 -        $tz = intval($tz);
 -    }
 -
 -    if ($tz == 0) {
 -        return 'UTC';
 -    } else if ($tz > 0) {
 -        return 'UTC+'.$tz;
 -    } else {
 -        return 'UTC'.$tz;
 -    }
 -
 -}
 -
 -/**
 - * Returns a float which represents the user's timezone difference from GMT in hours
 - * Checks various settings and picks the most dominant of those which have a value
 - *
 - * @package core
 - * @category time
 - * @param float|int|string $tz timezone to calculate GMT time offset for user,
 - *        99 is default user timezone
 - *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
 - * @return float
 - */
 -function get_user_timezone_offset($tz = 99) {
 -    $tz = get_user_timezone($tz);
 -
 -    if (is_float($tz)) {
 -        return $tz;
 -    } else {
 -        $tzrecord = get_timezone_record($tz);
 -        if (empty($tzrecord)) {
 -            return 99.0;
 -        }
 -        return (float)$tzrecord->gmtoff / HOURMINS;
 -    }
 -}
 -
 -/**
 - * Returns an int which represents the systems's timezone difference from GMT in seconds
 - *
 - * @package core
 - * @category time
 - * @param float|int|string $tz timezone for which offset is required.
 - *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
 - * @return int|bool if found, false is timezone 99 or error
 - */
 -function get_timezone_offset($tz) {
 -    if ($tz == 99) {
 -        return false;
 -    }
 -
 -    if (is_numeric($tz)) {
 -        return intval($tz * 60*60);
 -    }
 -
 -    if (!$tzrecord = get_timezone_record($tz)) {
 -        return false;
 -    }
 -    return intval($tzrecord->gmtoff * 60);
 +    $tz = core_date::get_user_timezone($timezone);
 +    return core_date::get_localised_timezone($tz);
  }
  
  /**
@@@ -2308,6 -2439,198 +2308,6 @@@ function get_user_timezone($tz = 99) 
      return is_numeric($tz) ? (float) $tz : $tz;
  }
  
 -/**
 - * Returns cached timezone record for given $timezonename
 - *
 - * @package core
 - * @param string $timezonename name of the timezone
 - * @return stdClass|bool timezonerecord or false
 - */
 -function get_timezone_record($timezonename) {
 -    global $DB;
 -    static $cache = null;
 -
 -    if ($cache === null) {
 -        $cache = array();
 -    }
 -
 -    if (isset($cache[$timezonename])) {
 -        return $cache[$timezonename];
 -    }
 -
 -    return $cache[$timezonename] = $DB->get_record_sql('SELECT * FROM {timezone}
 -                                                        WHERE name = ? ORDER BY year DESC', array($timezonename), IGNORE_MULTIPLE);
 -}
 -
 -/**
 - * Build and store the users Daylight Saving Time (DST) table
 - *
 - * @package core
 - * @param int $fromyear Start year for the table, defaults to 1971
 - * @param int $toyear End year for the table, defaults to 2035
 - * @param int|float|string $strtimezone timezone to check if dst should be applied.
 - * @return bool
 - */
 -function calculate_user_dst_table($fromyear = null, $toyear = null, $strtimezone = null) {
 -    global $SESSION, $DB;
 -
 -    $usertz = get_user_timezone($strtimezone);
 -
 -    if (is_float($usertz)) {
 -        // Trivial timezone, no DST.
 -        return false;
 -    }
 -
 -    if (!empty($SESSION->dst_offsettz) && $SESSION->dst_offsettz != $usertz) {
 -        // We have pre-calculated values, but the user's effective TZ has changed in the meantime, so reset.
 -        unset($SESSION->dst_offsets);
 -        unset($SESSION->dst_range);
 -    }
 -
 -    if (!empty($SESSION->dst_offsets) && empty($fromyear) && empty($toyear)) {
 -        // Repeat calls which do not request specific year ranges stop here, we have already calculated the table.
 -        // This will be the return path most of the time, pretty light computationally.
 -        return true;
 -    }
 -
 -    // Reaching here means we either need to extend our table or create it from scratch.
 -
 -    // Remember which TZ we calculated these changes for.
 -    $SESSION->dst_offsettz = $usertz;
 -
 -    if (empty($SESSION->dst_offsets)) {
 -        // If we 're creating from scratch, put the two guard elements in there.
 -        $SESSION->dst_offsets = array(1 => null, 0 => null);
 -    }
 -    if (empty($SESSION->dst_range)) {
 -        // If creating from scratch.
 -        $from = max((empty($fromyear) ? intval(date('Y')) - 3 : $fromyear), 1971);
 -        $to   = min((empty($toyear)   ? intval(date('Y')) + 3 : $toyear),   2035);
 -
 -        // Fill in the array with the extra years we need to process.
 -        $yearstoprocess = array();
 -        for ($i = $from; $i <= $to; ++$i) {
 -            $yearstoprocess[] = $i;
 -        }
 -
 -        // Take note of which years we have processed for future calls.
 -        $SESSION->dst_range = array($from, $to);
 -    } else {
 -        // If needing to extend the table, do the same.
 -        $yearstoprocess = array();
 -
 -        $from = max((empty($fromyear) ? $SESSION->dst_range[0] : $fromyear), 1971);
 -        $to   = min((empty($toyear)   ? $SESSION->dst_range[1] : $toyear),   2035);
 -
 -        if ($from < $SESSION->dst_range[0]) {
 -            // Take note of which years we need to process and then note that we have processed them for future calls.
 -            for ($i = $from; $i < $SESSION->dst_range[0]; ++$i) {
 -                $yearstoprocess[] = $i;
 -            }
 -            $SESSION->dst_range[0] = $from;
 -        }
 -        if ($to > $SESSION->dst_range[1]) {
 -            // Take note of which years we need to process and then note that we have processed them for future calls.
 -            for ($i = $SESSION->dst_range[1] + 1; $i <= $to; ++$i) {
 -                $yearstoprocess[] = $i;
 -            }
 -            $SESSION->dst_range[1] = $to;
 -        }
 -    }
 -
 -    if (empty($yearstoprocess)) {
 -        // This means that there was a call requesting a SMALLER range than we have already calculated.
 -        return true;
 -    }
 -
 -    // From now on, we know that the array has at least the two guard elements, and $yearstoprocess has the years we need
 -    // Also, the array is sorted in descending timestamp order!
 -
 -    // Get DB data.
 -
 -    static $presetscache = array();
 -    if (!isset($presetscache[$usertz])) {
 -        $presetscache[$usertz] = $DB->get_records('timezone', array('name' => $usertz),
 -            'year DESC', 'year, gmtoff, dstoff, dst_month, dst_startday, dst_weekday, dst_skipweeks, dst_time, std_month, '.
 -            'std_startday, std_weekday, std_skipweeks, std_time');
 -    }
 -    if (empty($presetscache[$usertz])) {
 -        return false;
 -    }
 -
 -    // Remove ending guard (first element of the array).
 -    reset($SESSION->dst_offsets);
 -    unset($SESSION->dst_offsets[key($SESSION->dst_offsets)]);
 -
 -    // Add all required change timestamps.
 -    foreach ($yearstoprocess as $y) {
 -        // Find the record which is in effect for the year $y.
 -        foreach ($presetscache[$usertz] as $year => $preset) {
 -            if ($year <= $y) {
 -                break;
 -            }
 -        }
 -
 -        $changes = dst_changes_for_year($y, $preset);
 -
 -        if ($changes === null) {
 -            continue;
 -        }
 -        if ($changes['dst'] != 0) {
 -            $SESSION->dst_offsets[$changes['dst']] = $preset->dstoff * MINSECS;
 -        }
 -        if ($changes['std'] != 0) {
 -            $SESSION->dst_offsets[$changes['std']] = 0;
 -        }
 -    }
 -
 -    // Put in a guard element at the top.
 -    $maxtimestamp = max(array_keys($SESSION->dst_offsets));
 -    $SESSION->dst_offsets[($maxtimestamp + DAYSECS)] = null; // DAYSECS is arbitrary, any "small" number will do.
 -
 -    // Sort again.
 -    krsort($SESSION->dst_offsets);
 -
 -    return true;
 -}
 -
 -/**
 - * Calculates the required DST change and returns a Timestamp Array
 - *
 - * @package core
 - * @category time
 - * @uses HOURSECS
 - * @uses MINSECS
 - * @param int|string $year Int or String Year to focus on
 - * @param object $timezone Instatiated Timezone object
 - * @return array|null Array dst => xx, 0 => xx, std => yy, 1 => yy or null
 - */
 -function dst_changes_for_year($year, $timezone) {
 -
 -    if ($timezone->dst_startday == 0 && $timezone->dst_weekday == 0 &&
 -        $timezone->std_startday == 0 && $timezone->std_weekday == 0) {
 -        return null;
 -    }
 -
 -    $monthdaydst = find_day_in_month($timezone->dst_startday, $timezone->dst_weekday, $timezone->dst_month, $year);
 -    $monthdaystd = find_day_in_month($timezone->std_startday, $timezone->std_weekday, $timezone->std_month, $year);
 -
 -    list($dsthour, $dstmin) = explode(':', $timezone->dst_time);
 -    list($stdhour, $stdmin) = explode(':', $timezone->std_time);
 -
 -    $timedst = make_timestamp($year, $timezone->dst_month, $monthdaydst, 0, 0, 0, 99, false);
 -    $timestd = make_timestamp($year, $timezone->std_month, $monthdaystd, 0, 0, 0, 99, false);
 -
 -    // Instead of putting hour and minute in make_timestamp(), we add them afterwards.
 -    // This has the advantage of being able to have negative values for hour, i.e. for timezones
 -    // where GMT time would be in the PREVIOUS day than the local one on which DST changes.
 -
 -    $timedst += $dsthour * HOURSECS + $dstmin * MINSECS;
 -    $timestd += $stdhour * HOURSECS + $stdmin * MINSECS;
 -
 -    return array('dst' => $timedst, 0 => $timedst, 'std' => $timestd, 1 => $timestd);
 -}
 -
  /**
   * Calculates the Daylight Saving Offset for a given date/time (timestamp)
   * - Note: Daylight saving only works for string timezones and not for float.
   * @package core
   * @category time
   * @param int $time must NOT be compensated at all, it has to be a pure timestamp
 - * @param int|float|string $strtimezone timezone for which offset is expected, if 99 or null
 - *        then user's default timezone is used. {@link http://docs.moodle.org/dev/Time_API#Timezone}
 + * @param int|float|string $strtimezone user timezone
   * @return int
   */
  function dst_offset_on($time, $strtimezone = null) {
 -    global $SESSION;
 -
 -    if (!calculate_user_dst_table(null, null, $strtimezone) || empty($SESSION->dst_offsets)) {
 -        return 0;
 -    }
 -
 -    reset($SESSION->dst_offsets);
 -    while (list($from, $offset) = each($SESSION->dst_offsets)) {
 -        if ($from <= $time) {
 -            break;
 -        }
 -    }
 -
 -    // This is the normal return path.
 -    if ($offset !== null) {
 -        return $offset;
 -    }
 -
 -    // Reaching this point means we haven't calculated far enough, do it now:
 -    // Calculate extra DST changes if needed and recurse. The recursion always
 -    // moves toward the stopping condition, so will always end.
 -
 -    if ($from == 0) {
 -        // We need a year smaller than $SESSION->dst_range[0].
 -        if ($SESSION->dst_range[0] == 1971) {
 -            return 0;
 +    $tz = core_date::get_user_timezone($strtimezone);
 +    $date = new DateTime('@' . $time);
 +    $date->setTimezone(new DateTimeZone($tz));
 +    if ($date->format('I') == '1') {
 +        if ($tz === 'Australia/Lord_Howe') {
 +            return 1800;
          }
 -        calculate_user_dst_table($SESSION->dst_range[0] - 5, null, $strtimezone);
 -        return dst_offset_on($time, $strtimezone);
 -    } else {
 -        // We need a year larger than $SESSION->dst_range[1].
 -        if ($SESSION->dst_range[1] == 2035) {
 -            return 0;
 -        }
 -        calculate_user_dst_table(null, $SESSION->dst_range[1] + 5, $strtimezone);
 -        return dst_offset_on($time, $strtimezone);
 +        return 3600;
      }
 +    return 0;
  }
  
  /**
@@@ -5605,7 -5956,7 +5605,7 @@@ function email_to_user($user, $from, $s
  
              // If the attachment is a full path to a file in the tempdir, use it as is,
              // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
-             if (strpos($attachpath, $temppath) !== 0) {
+             if (strpos($attachpath, realpath($temppath)) !== 0) {
                  $attachmentpath = $CFG->dataroot . '/' . $attachmentpath;
              }
  
@@@ -6755,6 -7106,53 +6755,6 @@@ function get_list_of_themes() 
      return $themes;
  }
  
 -/**
 - * Returns a list of timezones in the current language
 - *
 - * @return array
 - */
 -function get_list_of_timezones() {
 -    global $DB;
 -
 -    static $timezones;
 -
 -    if (!empty($timezones)) {    // This function has been called recently.
 -        return $timezones;
 -    }
 -
 -    $timezones = array();
 -
 -    if ($rawtimezones = $DB->get_records_sql("SELECT MAX(id), name FROM {timezone} GROUP BY name")) {
 -        foreach ($rawtimezones as $timezone) {
 -            if (!empty($timezone->name)) {
 -                if (get_string_manager()->string_exists(strtolower($timezone->name), 'timezones')) {
 -                    $timezones[$timezone->name] = get_string(strtolower($timezone->name), 'timezones');
 -                } else {
 -                    $timezones[$timezone->name] = $timezone->name;
 -                }
 -                if (substr($timezones[$timezone->name], 0, 1) == '[') {  // No translation found.
 -                    $timezones[$timezone->name] = $timezone->name;
 -                }
 -            }
 -        }
 -    }
 -
 -    asort($timezones);
 -
 -    for ($i = -13; $i <= 13; $i += .5) {
 -        $tzstring = 'UTC';
 -        if ($i < 0) {
 -            $timezones[sprintf("%.1f", $i)] = $tzstring . $i;
 -        } else if ($i > 0) {
 -            $timezones[sprintf("%.1f", $i)] = $tzstring . '+' . $i;
 -        } else {
 -            $timezones[sprintf("%.1f", $i)] = $tzstring;
 -        }
 -    }
 -
 -    return $timezones;
 -}
 -
  /**
   * Factory function for emoticon_manager
   *