<?php
/**
 * Class regrouping basic methods for download page.
 *
 * @copyright 2009-2011  Romain d'Alverny <rda>
 * @license GPL-3+
 *
*/
class Downloads
{
    /**
    */
    function __construct()
    {
    }
    /**
     * @param string $ua
     *
     * @return array
     *
     * @todo unit tests or use something else
     * Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; fr) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/4.1.2 Safari/533.18.5
     * Mozilla/5.0 (X11; Linux i686; rv:2.0) Gecko/20110330 Firefox/4.0
     * other old ones
     *
     * @todo refactor this
    */
    public static function get_platform($ua = null)
    {
        if ($ua == '')
            return array(
                'arch'    => 'i586',
                'system'  => 'unknown',
                'locale'  => 'en',
                'browser' => null
            );

        $locale = null;

        if (preg_match_all('/([.^\(^\)]*) \((.*)\) (.*)/', $ua, $r))
        {
            $r = $r[2][0];
            $r = explode(';', $r);
            if (isset($r[3])) {
                $r = explode(')', trim($r[3]));
                if (strlen($r[0]) > 5)
                    $r = substr($r[0], 0, 5);
                else
                    $r = $r[0];
            }
            else
                $r = null;

            $locale = $r;
        }

        $arch = 'i586';
        if (strpos($ua, 'x86_64') !== false)
            $arch = 'x86_64';

        $sys = null;
        if (strpos($ua, 'Windows') !== false)
            $sys = 'win';
        elseif (strpos($ua, 'Macintosh') !== false
            || strpos($ua, 'Mac OS X') !== false)
            $sys = 'mac';
        elseif (strpos($ua, 'Linux') !== false)
            $sys = 'linux';

        $browser = null;
        if (strpos($ua, 'Firefox') !== false)
            $browser = 'firefox';
        elseif (strpos($ua, 'MSIE') !== false)
            $browser = 'msie';
        elseif (strpos($ua, 'Safari') !== false)
            $browser = 'safari';
        elseif (strpos($ua, 'Opera') !== false)
            $browser = 'opera';

        return array(
            'arch'    => $arch,
            'system'  => $sys,
            'locale'  => $locale, // FIXME (rda) use Accept-Language instead
            'browser' => $browser
        );
    }

    /**
     * Sort 2D array by multiple associative or numeric keys.
     * $sorted_array = self::sort_2d_array_by_multiple_keys($unsorted_array, 'first key', 'second', ...);
     * 
     * Note that keys are preserved!
     *
     * @param array $unsorted_array
     *
     * @param string first key to order by
     *
     * @param string second key to order by
     *
     * @param string add as many keys to order by as needed
     *
     * @return array $sorted_array
    */
    public static function sort_2d_array_by_multiple_keys()
    {
        $arguments      = func_get_args();
        $unsorted_array = array_shift($arguments);
        $sort_order     = $arguments;
        uasort($unsorted_array, function($first_value, $second_value) use($sort_order) {
            $result = 0;
            for($argument_num = 1, $num_of_arguments = count($sort_order); $argument_num <= $num_of_arguments; $argument_num += 1) {
                $key    = $sort_order[$argument_num - 1];
                $first_compare_value  = $first_value[$key];
                $second_compare_value = $second_value[$key];
                $comparison = strcoll($first_compare_value, $second_compare_value);
                if(0 == $comparison) {
                    $temp_result = 0;
                } else {
                    $temp_result = (0 > $comparison) ? -1 : 1;
                }
                $result += $temp_result;
                $result = 10 * $result;
            }
            return $result;
        });
        return $unsorted_array;
    }

    /**
     * Get mirrors list from mirrors.mageia.org,
     * store/cache it in a different key/value format
     * (keys are: "$country" and "_C:$continent"),
     * and return it.
     *
     * Note that the mirrors list doesn't change with versions, for now;
     * it's a full or nothing list.
     *
     * @param boolean $prod
     * @param boolean $documentation
     * @param boolean $mirrorlist
     *
     * @return array
    */
    public static function get_all_mirrors($prod = true, $documentation = false, $mirrorlist = false)
    {
        if ($documentation) {
            $cache_file = realpath(__DIR__ . '/cached.list_doc.php');
        } else if ($mirrorlist) {
            $cache_file = realpath(__DIR__ . '/cached.list_mirrorlist.php');
        } else {
            $cache_file = realpath(__DIR__ . '/cached.list.php');
        }

        if ($prod) {
            require $cache_file;

        } else {
            $data    = file('http://mirrors.mageia.org/api/mageia.9.i586.list');
            $mirrors = array();
            $num_up  = 0;
            $num_dn  = 0;
            $mirror_cities    = array();
            $mirror_countries = array();
            $refresh_country_and_city_arrays = false;
            $faults = array();
            if ($prod == false && $documentation == false && $mirrorlist == false) {
                function _r($passthru) {return '_r("' . $passthru . '")';};
                include_once(realpath(__DIR__) . '/../en/downloads/get/lib.php');
                $refresh_country_and_city_arrays = true;
            }
            $num_of_all_mirrs = count($data);
            $num_of_tested_mirrs = 0;
            $common_patern = '/distrib/8/i586';
            $common_patern = '/distrib/9/i586';
            //~ $common_patern = '/distrib/cauldron/i586';
            // this huge regex magic achieved with a lot of help from great https://regex101.com/
            $single_mirror_parsing_regex  = '/\s*continent\s*=\s*(?<continent>\w*)\s*,\s*';
            $single_mirror_parsing_regex .= '\s*zone\s*=\s*(?<zone>\w*)\s*,\s*';
            $single_mirror_parsing_regex .= '\s*country\s*=\s*(?<country>\w*)\s*,\s*';
            $single_mirror_parsing_regex .= '(?:\s*city\s*=\s*(?<city>[\S ,]*)\s*,\s*)?';
            $single_mirror_parsing_regex .= '\s*latitude\s*=\s*[-]?\d*\.?\d*\s*,\s*';
            $single_mirror_parsing_regex .= '\s*longitude\s*=\s*[-]?\d*\.?\d*\s*,\s*';
            $single_mirror_parsing_regex .= '\s*version\s*=\s*\w*\s*,\s*arch\s*=\s*\w*\s*,\s*type\s*=\s*\w*\s*,\s*';
            $single_mirror_parsing_regex .= '\s*url\s*=\s*(?<url>\S*)\s*/m';
            foreach ($data as $line) {
                $num_of_tested_mirrs++;
                $mirrs_processed = sprintf("%.0f %%", $num_of_tested_mirrs / $num_of_all_mirrs * 100);
                $regex_error = preg_match_all($single_mirror_parsing_regex, $line, $matches, PREG_SET_ORDER, 0);
                if (false === $regex_error) {
                    $faults['error: regex parse failed'][] = $line;
                    echo 'Regex parse failed error in line ' . $line;
                    continue;
                }
                $m = $matches[0];
                if ('' == $m['url']) {
                    $faults['error: parse url'][] = $line;
                    echo 'Url parse error in line ' . $line;
                    continue;
                }
                if ('' == $m['continent']) {
                    $faults['error: parse continent'][] = $line;
                    echo 'Continent parse error in line ' . $line;
                    continue;
                } else {
                    $mirr_continent = $m['continent'];
                }
                if (false === strpos($m['url'], $common_patern)) {
                    $faults['error: url distrib missing'][] = $line;
                    echo "Url no $common_patern error in line $line";
                    continue;
                }
                $pu = parse_url($m['url']);
                if (in_array($pu['scheme'], array('http', 'https', 'ftp'))) {
                    $item = array(
                        'zone'      => ('' != $m['zone']) ? $m['zone'] : '?',
                        'country'   => ('' != $m['country']) ? $m['country'] : '?',
                        'city'      => ('' != $m['city']) ? trim($m['city']) : '-',
                        // BEWARE of the path substitution here. Must match.
                        'url'       => str_replace($common_patern, '', $m['url'])
                    );
                    if ($refresh_country_and_city_arrays == true) {
                        // prepare details for i18n
                        if ('-' != $item['city']) {
                            if (isset($cities_i18n[$item['city']])) {
                                $mirror_cities[$item['city']] = $cities_i18n[$item['city']];
                            } else {
                                $mirror_cities[$item['city']] = '_r("' . $item['city'] . '") // new city (not yet in /en/downloads/get/lib.php)';
                            }
                        } else {
                            $faults['notice: no city given'][] = $line;
                        }
                        if (isset($countries[$item['country']])) {
                            $mirror_countries[$item['country']] = $countries[$item['country']];
                        } else if (isset($item['country'])) {
                            $mirror_countries[$item['country']] = " // new country (not yet in /en/downloads/get/lib.php)";
                        }
                    }

                    if ($documentation) {
                        $test_file = $item['url'].'/doc/mga8/date.txt';
                    } else if ($mirrorlist) {
                        $test_file = $item['url'].'/distrib/9/x86_64/media/core/updates/repodata/repomd.xml'; // when changing, please change $common_patern too
                        $test_file = $item['url'].'/distrib/9/x86_64/media/core/release/meta-task-9-2.mga9.noarch.rpm'; // when changing, please change $common_patern too
                    } else {
                        $test_file = $item['url'].'/iso/9/torrents/Mageia-9-Live-Xfce-i586.torrent';
                    }
                    if (false === @file_get_contents($test_file)) {
                        $num_dn++;
                        echo "Down $num_dn (up: $num_up, about $mirrs_processed of _all_ mirrors tested) $test_file \n";
                    } else {
                        $num_up++;
                        echo "Up $num_up (down: $num_dn, about $mirrs_processed of _all_ mirrors tested) $test_file \n";
                        $mirrors['_C:' . $mirr_continent][] = $item;
                    }
                }
            }
            ksort($mirrors);
            foreach ($mirrors as &$continent) {
                $sorted_continent = self::sort_2d_array_by_multiple_keys($continent, 'zone', 'country', 'city', 'url');
                $continent = array_values($sorted_continent);
            }
            unset($continent);

            echo "\nThere are $num_up servers with the file and $num_dn with some kind of issue.\n";
            file_put_contents($cache_file,
                sprintf('<?php $mirrors = %s; ?>' . PHP_EOL, var_export($mirrors, true)));

            if ($refresh_country_and_city_arrays == true) {
                foreach ($countries as $ISO_3166_country_code => $lib_country) {
                    if (!isset($mirror_countries[$ISO_3166_country_code])) {
                        $mirror_countries[$ISO_3166_country_code] = $lib_country;
                    }
                }
                foreach ($cities_i18n as $english_name => $translated_name) {
                    if (!isset($mirror_cities[$english_name])) {
                        $mirror_cities[$english_name] = '_r("' . $english_name . '")';
                    }
                }
                ksort($mirror_cities);
                $countries_u = array_unique($mirror_countries);
                ksort($countries_u);
                echo "\nArray of cities for i18n: ";
                var_export($mirror_cities);
                echo ";\nArray of countries for i18n: ";
                var_export($countries_u);
                echo ";\nArray of faults and warnings: ";
                var_export($faults);
                echo ";\n";
            }
            echo "\nRemove char ' at each value on both arrays for cities and countries above : replace '_r(\" with just _r( and also behind \")', with \"),\n";
            echo "and update arrays in file /en/downloads/get/lib.php with that.\n";
        }
        return $mirrors;
    }

    /**
     * Get mirrors from stored dictionary and find best matching mirror:
     * - if it exists in the country otherwise
     * - on continent if it exists otherwise
     * - random mirror
     * It can also prepare a list of mirrors for mirrorlist page
     *
     * @param string $country
     * @param string $continent
     * @param boolean $prod
     * @param boolean $documentation
     * @param boolean $mirrorlist
     *
     * @return array
    */
    function get_mirror($country, $continent = null, $prod = true, $documentation = false, $mirrorlist = false, $https_only = false)
    {
        $mirs      = self::get_all_mirrors($prod, $documentation, $mirrorlist);
        $continent = '_C:' . $continent;

        $mirrors       = array();
        $fr_mirr_asist = array();
        $list_of_mirrs = array(
            'country'                 => array(),
            'continent_minus_country' => array(),
            'other_continents'        => array()
        );
        $hostnames     = array();
        $list_of_mirrs['country_alternate_protocol'] = array();
        $continent_https_only = array();
        $other_continents_https_only = array();
        foreach ($mirs as $curr_continent => $continent_mirrors) {
            foreach ($continent_mirrors as $mirror) {
                if ($continent != $curr_continent)
                {
                    if ($mirrorlist)
                    {
                        $list_of_mirrs['other_continents'][] = $mirror['url'];
                    }
                    elseif (true === $https_only)
                    {
                        $pu = parse_url($mirror['url']);
                        if ('https' == $pu['scheme'])
                        {
                            $other_continents_https_only[$curr_continent][] = $mirror;
                        }
                    }
                }
                else
                {
                    if (strpos($mirror['url'], 'distrib-coffee.ipsl.jussieu.fr') !== false)
                    {
                        // exclude source server to drop it's DL load
                        continue;
                    }
                    // keep assisting the french mirrors with german ones
                    if ($country == 'FR' && $mirror['country'] == 'DE')
                    {
                        $pu = parse_url($mirror['url']);
                        if ('https' == $pu['scheme']) {
                            $fr_mirr_asist[] = $mirror;
                        }
                    }
                    // only add german mirrors when french are on turn
                    // sorting of mirror db cache must be kept to work properly (DE before FR)
                    if ($country == 'FR' && $mirror['country'] == 'FR' && count($fr_mirr_asist) > 0)
                    {
                        $mirrors[$continent] = $fr_mirr_asist;
                        $fr_mirr_asist = array();
                    }
                    if ($mirrorlist)
                    {
                        $pu = parse_url($mirror['url']);
                        if (in_array($pu['host'], $hostnames))
                        {
                            $list_of_mirrs['country_alternate_protocol'][] = $mirror['url'];
                        }
                        else
                        {
                            $hostnames[] = $pu['host'];
                            if ($mirror['country'] == $country)
                            {
                                $list_of_mirrs['country'][] = $mirror['url'];
                            }
                            else
                            {
                                $list_of_mirrs['continent_minus_country'][] = $mirror['url'];
                            }
                        }
                    }
                    else
                    {
                        if ($mirror['country'] == $country)
                        {
                            if (true === $https_only)
                            {
                                $pu = parse_url($mirror['url']);
                                if ('https' == $pu['scheme']) {
                                    $mirrors[$continent][] = $mirror;
                                }
                            }
                            else
                            {
                                $mirrors[$continent][] = $mirror;
                            }
                        }
                        else
                        {
                            if (true === $https_only)
                            {
                                $pu = parse_url($mirror['url']);
                                if ('https' == $pu['scheme']) {
                                    $continent_https_only[$curr_continent][] = $mirror;
                                }
                            }
                        }
                    }
                }
            }
        }
        if (count($mirrors) == 0)
        {
            if (true === $https_only)
            {
                if (count($continent_https_only) > 0)
                {
                    $mirrors = $continent_https_only;
                }
                if (count($mirrors) == 0 && count($other_continents_https_only) > 0)
                {
                    $mirrors = $other_continents_https_only;
                }
            }
            else
            {
                // add all continent mirrors if country doesn't have any
                $mirrors[$continent] = $continent_mirrors;
            }
        }
        // falback if selection fails
        if (count($mirrors) > 0)
        {
            $mirs = $mirrors;
        }

        // a workaround as shuffle() doesn't preserve assoc. keys
        if (count($mirs) > 1)
        {
            $mirs_keys = array_keys($mirs);
            shuffle($mirs_keys);
            foreach($mirs_keys as $key) {
                $shuffled[$key] = $mirs[$key];
            }
            $mirs = $shuffled;
        }
        
        $mirr_continent = array_keys($mirs)[0];
        $mirs = array_shift($mirs);
        shuffle($mirs);
        $one_mirror = array_shift($mirs);
        $one_mirror['continent'] = $mirr_continent;

        if ($mirrorlist)
        {
            shuffle($list_of_mirrs['country']);
            shuffle($list_of_mirrs['continent_minus_country']);
            shuffle($list_of_mirrs['other_continents']);
            $resulting_mirrs = array_merge(
                $list_of_mirrs['country'],
                $list_of_mirrs['continent_minus_country'],
                $list_of_mirrs['country_alternate_protocol'],
                $list_of_mirrs['other_continents']
            );
            $one_mirror['mirrors_list'] = array_slice($resulting_mirrs, 0, 10);
        }

        return $one_mirror;
    }

    function prepare_download($force = false, $country = null, $prod = true, $documentation = false, $mirrorlist = false, $https_only = false)
    {
        return $this->get_one_mirror($force, $country, $prod, $documentation, $mirrorlist, $https_only);
    }

    /**
     * Setup session data about current visitor for downloads.
     *
     * @param boolean $force
     * @param string $country
     * @param boolean $prod
     * @param boolean $documentation
     * @param boolean $mirrorlist
     *
     * @return array
     *
     * TODO extract as much as possible $_SESSION(read) and $_SERVER and $_GET
    */
    function get_one_mirror($force = false, $country = null, $prod = true, $documentation = false, $mirrorlist = false, $https_only = false)
    {
        $fuzzy_mirror = false;

        if (!is_null($country))
            $force = true;

        // FIXME break this into smaller parts and extract globals so we can test st
        if (!$force && isset($_SESSION['dl-data']))
        {
            //error_log(sprintf('Got session data: %s', print_r($_SESSION['dl-data'], true)));
            $system  = $_SESSION['dl-data']['system'];
            if (isset($_GET['mirror']))
            {
                $mirror                        = array('url' => $_GET['mirror']);
                $mirror['purl']                = parse_url($mirror['url']);
                $_SESSION['dl-data']['mirror'] = $mirror;
                $country                       = '';
            }
            else
            {
                $country = $_SESSION['dl-data']['country'];
                $mirror  = $_SESSION['dl-data']['mirror'];
            }
        }
        else
        {
            //error_log('getting platform');
            $system = self::get_platform($_SERVER['HTTP_USER_AGENT']);
            if (isset($_GET['mirror']))
            {
                $mirror         = array('url' => $_GET['mirror']);
                $mirror['purl'] = parse_url($mirror['url']);
                $country        = null;
            }
            else
            {
                //error_log('no mirror set yet');
                if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])
                    && $str = $_SERVER['HTTP_X_FORWARDED_FOR'])
                {
                    $arr = explode(', ', $str);
                    $ip  = $arr[0];
                }
                else
                    $ip = $_SERVER['REMOTE_ADDR'];

                $_SESSION['ip'] = $ip;
                if (is_null($country))
                {
                    require_once realpath(__DIR__ . '/mga_geoip.php');
                    $country      = MGA_Geoip::mga_geoip_country_by_ip($ip, false);
                    $continent    = MGA_Geoip::mga_geoip_continent_by_country($country);
                    $fuzzy_mirror = true;
                    $_SESSION['country']   = $country;
                    $_SESSION['continent'] = $continent;
                }
                if (!isset($continent))
                {
                    $continent = null;
                }

                $mirror         = $this->get_mirror($country, $continent, $prod, $documentation, $mirrorlist, $https_only);
                $mirror['purl'] = parse_url($mirror['url']);

                // reassign country, as get_one_mirror() may have decided
                // to return a mirror from another country than the one
                // requested initially - @see get_one_mirror()
                $country   = $mirror['zone'];
                $continent = $mirror['continent'];

                if (is_null($mirror)) {
                    // @todo?
                }
            }

            // write to session
            $_SESSION['dl-data'] = array(
                'system'    => $system,
                'country'   => $country,
                'continent' => $continent,
                'mirror'    => $mirror
            );
        }
        if (!isset($mirror['mirrors_list']))
        {
            $mirror['mirrors_list'] = array();
        }
        return array(
            'arch'          => $system['arch'],
            'mirror_host'   => $mirror['purl']['host'],
            'mirror_scheme' => $mirror['purl']['scheme'],
            'mirror_url'    => $mirror['url'],
            'country'       => $country,
            'continent'     => $continent,
            'city'          => $mirror['city'],
            'fuzzy_mirror'  => $fuzzy_mirror,
            'mirrors_list'  => $mirror['mirrors_list'],
        );
    }
}