<?php
// SPDX-License-Identifier: GPL-3.0-or-later
// ================================================================================================
// Steam Removed Game Scanner
// <https://fietkau.software/steam_removed_game_scanner>
//
// Copyright (C) 2022 Julian Fietkau
//
// This program 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.
//
// This program 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 this program. If
// not, see <https://www.gnu.org/licenses/>.
// ================================================================================================
// Steam Removed Game Scanner includes a copy of LightOpenID by Mewp, which has its own copyright
// notice and may be redistributed under the MIT license. See the "LightOpenID" subdirectory.
// ================================================================================================

// Make sure the working directory is the main script's location - this gives
// consistency for file accesses. Under most circumstances this will happen
// automatically, but not all.
chdir(__DIR__);

// There's no point in having search engines and other crawlers index any part
// of the Steam Profile Scanner, let alone follow sign-in URLs, so we direct
// them not to.
header('X-Robots-Tag: noindex, nofollow', true);

require_once 'components/configuration.php';
require_once 'components/diagnostics.php';

// If the configuration is set to force a specific hostname, but it's not the
// one we are currently on, immediately force a redirect.
if(!Configuration\is_empty('force_hostname') && $_SERVER['SERVER_NAME'] != Configuration\get('force_hostname')) {
    header($_SERVER['SERVER_PROTOCOL'].' 307 Temporary Redirect', true, 307);
    header('Location: '.$_SERVER['REQUEST_SCHEME'].'://'.Configuration\get('force_hostname').$_SERVER['REQUEST_URI']);
    die();
}

// There are some circumstances where we need to stop the script right away.
function halt_with_errors($problems, $http_status = 500, $http_status_text = 'Internal Server Error') {
    header($_SERVER['SERVER_PROTOCOL'].' '.$http_status.' '.$http_status_text, true, $http_status);
    foreach($problems as $problem) {
        error_log('Steam Profile Scanner Fatal error: '.$problem);
    }
    $view['title'] = 'Server Error';
    include 'view/header.php';
    include 'view/error.php';
    die();
}

$fatal_problems = Diagnostics\check_fatal();
if(count($fatal_problems) > 0) {
    halt_with_errors($fatal_problems);
}

require_once 'components/authentication.php';

// If $_GET['signin'] exists, that means the user has just clicked the sign-in
// button. This is the point where we generate an OpenID log-in URL for Steam.
// In theory it would be possible to do this earlier and just have the
// sign-in button point to the OpenID URL directly, but there are two reasons
// why we do it this way: 1. the OpenID URLs are generally ugly, and 2. the
// creation of one takes a round-trip to Steam's servers, so it delays the
// displaying of the page. This way the greeting page loads much faster.
if(isset($_GET['signin'])) {
    // We chop off the query string because if we don't, LightOpenID will use
    // the full URL to redirect back to, landing us in an infinite loop.
    $_SERVER['REQUEST_URI'] = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
    // This redirect has to be 307 and not 301 to avoid the client caching it.
    header($_SERVER['SERVER_PROTOCOL'].' 307 Temporary Redirect', true, 307);
    header('Location: '.Authentication\get_signin_url());
    die();
}

$auth_error = Authentication\process_session_data(Configuration\get('api_key'));
if($auth_error != null) {
    unset($_SESSION['steam_id']);
    halt_with_errors([$auth_error]);
}

// If one of the following is true, that means the user has either just
// logged out or has returned from the OpenID provider. In either case, the
// session has already been processed, so we can do a little redirect to
// prettify the URL.
if(isset($_GET['signout']) || isset($_GET['openid_ns'])) {
    header($_SERVER['SERVER_PROTOCOL'].' 307 Temporary Redirect', true, 307);
    // We chop off the entire query string, whatever it may have been.
    // This redirect has to be 307 and not 301 to avoid the client caching it.
    header('Location: '.parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
    die();
}

require_once 'components/database.php';
require_once 'components/profile_scan.php';

// The following are the parameters that broadly determine what is sent to
// the client.
$view = [
    'destination' => 'index',
    'signed_in' => isset($_SESSION) && isset($_SESSION['steam_id']) && preg_match('/^[0-9]{5,25}$/', $_SESSION['steam_id']),
    'is_admin' => isset($_SESSION) && isset($_SESSION['steam_id']) && Database\is_admin($_SESSION['steam_id']),
    'bare' => isset($_GET['bare']), // skip the page header and footer
    'fresh' => isset($_POST['start_scan']), // user just clicked the "scan my profile" button
    'base_path' => parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH),
];

// We have two special pages we need to account for. Notably, there is a difference
// whether their respective config setting is a full HTML page or just a URL.
foreach(['terms', 'contact'] as $special_page) {
    // If the config setting doesn't exist, simply ignore the GET parameter and
    // probably end up on the main page.
    if(isset($_GET[$special_page]) && !Configuration\is_empty('footer_'.$special_page)) {
        $content = trim(Configuration\get('footer_'.$special_page));
        if(substr_count($content, "\n") == 0) {
            // If the config setting is just a URL, we still want the links with
            // the GET parameter to be valid just in case. We simply redirect to the 
            // configured URL.
            header($_SERVER['SERVER_PROTOCOL'].' 307 Temporary Redirect', true, 307);
            header('Location: '.Configuration\get('footer_'.$special_page));
            die();
        } else {
            // If the config setting is a full page, set things up so it can be
            // rendered later.
            $view['destination'] = $special_page;
            switch($special_page) {
                case 'contact':
                    $view['title'] = 'Contact';
                    break;
                case 'terms':
                    $view['title'] = 'Terms of Use';
                    break;
            }
        }
    }
}

// If the session times out while the user has a tab open in the admin view,
// we need to make sure that any bare requests return an error. If that happens,
// the client-side JS attempts to redirect to the full URL instead of showing
// the bare reply. In this case this means that the user will see the landing page.
// The two special pages are exempt from this.
if(!$view['signed_in'] && isset($_GET['bare']) && $view['destination'] == 'index') {
    halt_with_errors(['This page is only accessible if you are signed in.'], 401, 'Unauthorized');
}

// The bulk of input/output mapping and content preparation happens in the following sequence.
if($view['is_admin']) {
    if(isset($_GET['scan'])) {
        $scan_data = Database\get_scan_result($_GET['scan']);
        if($scan_data !== null) {
            $view['destination'] = 'scan';
            $view['scan_id'] = $_GET['scan'];
            $view['scan_data'] = $scan_data;
        } else {
            $view['error_message'] = 'Scan ID "'.$_GET['scan'].'" not found in database';
            $view['destination'] = 'index';
            // If the scan was requested as a bare page and is not found, reply with a
            // 404 so the dynamic client can react sensibly.
            if($view['bare']) {
                halt_with_errors([$view['error_message']], 404, 'Not Found');
            }
        }
    }
    foreach(['admin', 'ban'] as $topic) {
        if(isset($_GET['add_'.$topic])) {
            $profile = ProfileScan\get_profile($_GET['add_'.$topic]);
            if($profile[0] == 'success') {
                $result = call_user_func('Database\add_'.$topic, $profile[1]);
                if($result[0] != 'success') {
                    $view['error_message'] = $result[1];
                }
            } else {
                $view['error_message'] = $profile[1];
            }
        } elseif(isset($_GET['remove_'.$topic])) {
            if($topic != 'admin' || $_GET['remove_'.$topic] != $_SESSION['steam_id']) {
                $result = call_user_func('Database\remove_'.$topic, $_GET['remove_'.$topic]);
                if($result[0] != 'success') {
                    $view['error_message'] = $result[1];
                }
            }
        }
    }
    if(isset($_GET['new_scan'])) {
        $profile = ProfileScan\get_profile($_GET['new_scan']);
        if($profile[0] == 'success') {
            $scan_result = Database\add_scan($profile[1], $_SESSION['display_name']);
            if($scan_result[0] == 'success') {
                $scan_id = $scan_result[1];
            } else {
                $scan_id = null;
                $view['error_message'] = $scan_result[1];
            }
        } else {
            $scan_id = null;
            $view['error_message'] = $profile[1];
        }
    }
    if(isset($_GET['json'])) {
        header('Content-Type: application/json', true);
        if(isset($view['error_message'])) {
            // Originally we were sending HTTP 500 here instead of 200, but it turns
            // out that makes it more difficult to parse the JSON response.
            echo json_encode(['error' => $view['error_message']]);
        } else {
            // Basically we throw everything we have into the reply so the client
            // can extract what it needs.
            $result = array_merge($_GET);
            if(isset($scan_id) && $scan_id != null) {
                $result['scan_id'] = $scan_id;
            }
            if(isset($profile)) {
                $result = array_merge($result, $profile[1]);
            }
            echo json_encode($result);
        }
        die();
    }
}

// At this point in the control flow, if the user (who is an admin) has requested
// a new scan, it is all finished and ready to go, so we prettify the client's URL.
if($view['is_admin'] && isset($_GET['new_scan']) && $scan_id !== null) {
    header($_SERVER['SERVER_PROTOCOL'].' 307 Temporary Redirect', true, 307);
    header('Location: '.parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH).'?scan='.$scan_id);
    die();
}

// This is the case where a non-admin user has initiated a self-scan.
if($view['signed_in'] && !$view['is_admin'] && isset($_POST['start_scan'])) {
    if(Database\get_self_scan_eligibility($_SESSION['steam_id']) == null) {
        $target_profile = ProfileScan\get_profile($_SESSION['steam_id']);
        if($target_profile[0] == 'success') {
            $scan_result = Database\add_scan($target_profile[1], null);
            if($scan_result[0] != 'success') {
                  $view['error_message'] = $scan_result[1];
            }
        } else {
            $view['error_message'] = $target_profile[1];
        }
    } else {
        $view['error_message'] = 'User is not eligible for another scan yet';
    }
}

// In the standard view, the user gets shown their most resent self-scan.
if($view['signed_in'] && !$view['is_admin']) {
    $scan_id = Database\get_recent_self_scan($_SESSION['steam_id']);
    if($scan_id !== null) {
        $view['scan_data'] = Database\get_scan_result($scan_id);
    }
    $view['next_scan_time'] = Database\get_self_scan_eligibility($_SESSION['steam_id']);
}

// From here it pretty much comes down to which view templates to show.
if(!$view['bare']) {
    include 'view/header.php';
}

if($view['is_admin'] && in_array($view['destination'], ['index', 'scan'])) {
    if(!$view['bare']) {
        include 'view/admin_start.php';
    }
    if($view['destination'] == 'index') {
        include 'view/admin_index.php';
    } elseif($view['destination'] == 'scan') {
        include 'view/admin_scan.php';
    }
    if(!$view['bare']) {
        include 'view/admin_end.php';
    }
}
if($view['signed_in'] && !$view['is_admin'] && in_array($view['destination'], ['index', 'scan']) && Database\is_banned($_SESSION['steam_id'])) {
    include 'view/banned.php';
}
if($view['signed_in'] && !$view['is_admin'] && $view['destination'] == 'index') {
    include 'view/user_start.php';
    if(isset($view['scan_data'])) {
        include 'view/user_scan_summary.php';
    } else {
        include 'view/user_welcome.php';
    }
    include 'view/user_end.php';
}
if(!$view['signed_in'] && $view['destination'] == 'index') {
    include 'view/landing.php';
}
if(in_array($view['destination'], ['contact', 'terms'])) {
    include 'view/special_page.php';
}
if(!$view['bare']) {
    include 'view/footer.php';
}
?>