<?php

/**
 * @author WP Cloud Plugins
 * @copyright Copyright (c) 2023, WP Cloud Plugins
 *
 * @since       2.0
 * @see https://www.wpcloudplugins.com
 */

namespace TheLion\ShareoneDrive;

defined('ABSPATH') || exit; // Exit if accessed directly.

if (!function_exists('onedrive_api_php_client_autoload')) {
    require_once SHAREONEDRIVE_ROOTDIR.'/vendors/API/autoload.php';
}

class Client
{
    /**
     * The single instance of the class.
     *
     * @var Client
     */
    protected static $_instance;

    /**
     * Client Instance.
     *
     * Ensures only one instance is loaded or can be loaded.
     *
     * @return Client - Client instance
     *
     * @static
     */
    public static function instance()
    {
        if (is_null(self::$_instance)) {
            self::$_instance = new self();
        }

        return self::$_instance;
    }

    /**
     * Get folders and files.
     *
     * @param string $id
     * @param bool   $checkauthorized
     *
     * @return array|bool
     */
    public function get_folder($id = false, $checkauthorized = true)
    {
        if (false === $id) {
            $id = Processor::instance()->get_requested_entry();
        }

        try {
            $cached_node = API::get_folder($id);
        } catch (\Exception $ex) {
            return false;
        }

        // Check if folder is in the shortcode-set rootfolder
        if (true === $checkauthorized && !Processor::instance()->_is_entry_authorized($cached_node)) {
            return false;
        }

        return ['folder' => $cached_node, 'contents' => $cached_node->get_children()];
    }

    // Get entry

    /**
     * @param type $id
     * @param type $checkauthorized
     *
     * @return bool|CacheNode
     */
    public function get_entry($id = false, $checkauthorized = true)
    {
        if (false === $id) {
            $id = Processor::instance()->get_requested_entry();
        }

        try {
            $cached_node = API::get_entry($id);
        } catch (\Exception $ex) {
            return false;
        }

        if (true === $checkauthorized && ('root' !== $id && !Processor::instance()->_is_entry_authorized($cached_node))) {
            return false;
        }

        return $cached_node;
    }

    public function update_expired_entry(CacheNode $cached_node)
    {
        $entry = $cached_node->get_entry();

        try {
            $api_entry = App::instance()->get_drive()->items->get($entry->get_id(), ['driveId' => $cached_node->get_drive_id(), 'expand' => API::$apilistfilesexpirefields]);
            $entry = new Entry($api_entry);
        } catch (\Exception $ex) {
            Helpers::log_error('Client is receiving API error.', 'API', null, __LINE__, $ex);

            return false;
        }

        return Cache::instance()->add_to_cache($entry);
    }

    // Search entry by name
    public function search_by_name($query)
    {
        if ('parent' === Processor::instance()->get_shortcode_option('searchfrom')) {
            $searched_folder_id = Processor::instance()->get_requested_entry();
        } else {
            $searched_folder_id = Processor::instance()->get_root_folder();
        }

        $searched_folder = $this->get_folder($searched_folder_id, true);
        $search_contents = ('1' === Processor::instance()->get_shortcode_option('searchcontents'));

        $search = new Search();
        $results = $search->do_search($query, $searched_folder['folder'], $search_contents);

        return ['folder' => $searched_folder['folder'], 'contents' => $results];
    }

    // Delete multiple files from OneDrive

    public function delete_entries($entries_to_delete = [])
    {
        foreach ($entries_to_delete as $key => $target_entry_path) {
            $target_cached_entry = $this->get_entry($target_entry_path);

            if (false === $target_cached_entry) {
                unset($entries_to_delete[$key]);

                continue;
            }

            $target_entry = $target_cached_entry->get_entry();

            if ($target_entry->is_file() && false === User::can_delete_files()) {
                Helpers::log_error('Failed to delete entry as user is not allowed to remove files', 'Client', ['entry_id' => $target_entry->get_id], __LINE__);

                unset($entries_to_delete[$key]);

                continue;
            }

            if ($target_entry->is_dir() && false === User::can_delete_folders()) {
                Helpers::log_error('Failed to delete entry as user is not allowed to remove folders', 'Client', ['entry_id' => $target_entry->get_id], __LINE__);
                unset($entries_to_delete[$key]);

                continue;
            }

            if ('1' === Processor::instance()->get_shortcode_option('demo')) {
                unset($entries_to_delete[$key]);
            }
        }

        try {
            $deleted_entries = API::delete($entries_to_delete);
        } catch (\Exception $ex) {
            Helpers::log_error('Client is receiving API error.', 'API', null, __LINE__, $ex);

            return [];
        }

        if ('1' === Processor::instance()->get_shortcode_option('notificationdeletion')) {
            Processor::instance()->send_notification_email('deletion', $deleted_entries);
        }

        return $deleted_entries;
    }

    // Rename entry from OneDrive

    public function rename_entry($new_filename = null)
    {
        if ('1' === Processor::instance()->get_shortcode_option('demo')) {
            return new \WP_Error('broke', esc_html__('Failed to rename file.', 'wpcloudplugins'));
        }

        if (null === $new_filename) {
            return new \WP_Error('broke', esc_html__('No new name set', 'wpcloudplugins'));
        }

        // Get entry meta data
        $cached_node = Cache::instance()->is_cached(Processor::instance()->get_requested_entry(), App::get_current_drive_id());

        if (false === $cached_node) {
            $cached_node = $this->get_entry(Processor::instance()->get_requested_entry());
            if (false === $cached_node) {
                return new \WP_Error('broke', esc_html__('Failed to rename file.', 'wpcloudplugins'));
            }
        }

        // Check if user is allowed to delete from this dir
        if (!$cached_node->is_in_folder(Processor::instance()->get_last_folder())) {
            return new \WP_Error('broke', esc_html__('You are not authorized to rename files in this directory', 'wpcloudplugins'));
        }

        $entry = $cached_node->get_entry();

        // Check user permission
        if (!$entry->get_permission('canrename')) {
            return new \WP_Error('broke', esc_html__('You are not authorized to rename this file or folder', 'wpcloudplugins'));
        }

        // Check if entry is allowed
        if (!Processor::instance()->_is_entry_authorized($cached_node)) {
            return new \WP_Error('broke', esc_html__('You are not authorized to rename this file or folder', 'wpcloudplugins'));
        }

        if ($entry->is_dir() && (false === User::can_rename_folders())) {
            return new \WP_Error('broke', esc_html__('You are not authorized to rename folder', 'wpcloudplugins'));
        }

        if ($entry->is_file() && (false === User::can_rename_files())) {
            return new \WP_Error('broke', esc_html__('You are not authorized to rename this file', 'wpcloudplugins'));
        }

        $extension = $entry->get_extension();
        $name = (!empty($extension)) ? $new_filename.'.'.$extension : $new_filename;
        $updaterequest = ['name' => $name];

        try {
            API::set_drive_by_id($cached_node->get_drive_id());
            $renamed_entry = API::patch($entry->get_id(), $updaterequest);

            if (false !== $renamed_entry && null !== $renamed_entry) {
                Cache::instance()->update_cache();
            }

            do_action('shareonedrive_log_event', 'shareonedrive_renamed_entry', $renamed_entry, ['old_name' => $entry->get_name()]);
        } catch (\Exception $ex) {
            Helpers::log_error('Client is receiving API error.', 'API', null, __LINE__, $ex);

            return new \WP_Error('broke', esc_html__('Failed to rename file.', 'wpcloudplugins'));
        }

        Cache::instance()->pull_for_changes(true);

        return $renamed_entry;
    }

    // Move & Copy
    public function move_entries($entries, $target, $copy = false)
    {
        $entries_to_move = [];

        $cached_target = $this->get_entry($target);
        $cached_current_folder = $this->get_entry(Processor::instance()->get_last_folder());

        if (false === $cached_target) {
            Helpers::log_error('Failed to move as target folder is not found.', 'Client', ['target' => $target], __LINE__);

            return $entries_to_move;
        }

        foreach ($entries as $key => $entry_id) {
            $entries_to_move[$entry_id] = false; // Set after Request is finished

            $cached_node = $this->get_entry($entry_id);

            if (false === $cached_node) {
                unset($entries[$key]);

                continue;
            }

            $entry = $cached_node->get_entry();

            if (!$copy && $entry->is_dir() && (false === User::can_move_folders())) {
                Helpers::log_error('Failed to move as user is not allowed to move folders.', 'Client', ['target' => $cached_node->get_id()], __LINE__);
                unset($entries[$key]);

                continue;
            }

            if ($copy && $entry->is_dir() && (false === User::can_copy_folders())) {
                Helpers::log_error('Failed to move as user is not allowed to copy folders.', 'Client', ['target' => $cached_node->get_id()], __LINE__);
                unset($entries[$key]);

                continue;
            }

            if (!$copy && $entry->is_file() && (false === User::can_move_files())) {
                Helpers::log_error('Failed to move as user is not allowed to move files.', 'Client', ['target' => $cached_node->get_id()], __LINE__);
                unset($entries[$key]);

                continue;
            }

            if ($copy && $entry->is_file() && (false === User::can_copy_files())) {
                Helpers::log_error('Failed to move as user is not allowed to copy files.', 'Client', ['target' => $cached_node->get_id()], __LINE__);
                unset($entries[$key]);

                continue;
            }

            if ('1' === Processor::instance()->get_shortcode_option('demo')) {
                unset($entries[$key]);

                continue;
            }

            // Check if user is allowed to delete from this dir
            if (!$cached_node->is_in_folder($cached_current_folder->get_id())) {
                Helpers::log_error('Failed to move as user is not allowed to move items in this directory.', 'Client', ['target' => $cached_target->get_id()], __LINE__);
                unset($entries[$key]);

                continue;
            }

            // Check user permission
            if (!$entry->get_permission('canmove')) {
                Helpers::log_error('Failed to move as the sharing permissions on it prevent this.', 'Client', ['target' => $cached_target->get_id()], __LINE__);
                unset($entries[$key]);
            }
        }

        try {
            $entries_to_move = API::move($entries, $target, $copy);
        } catch (\Exception $ex) {
            Helpers::log_error('Client is receiving API error.', 'API', null, __LINE__, $ex);

            return $entries_to_move;
        }

        // Clear Cached Requests
        CacheRequest::clear_local_cache_for_shortcode(App::get_current_account()->get_id(), Processor::instance()->get_listtoken());

        // Send email if needed
        if ($copy && '1' === Processor::instance()->get_shortcode_option('notificationcopy')) {
            Processor::instance()->send_notification_email('copy_multiple', $entries_to_move);
        } elseif ('1' === Processor::instance()->get_shortcode_option('notificationmove')) {
            Processor::instance()->send_notification_email('move_multiple', $entries_to_move);
        }

        return $entries_to_move;
    }

    // Edit description of entry

    public function update_description($new_description = null)
    {
        if (null === $new_description) {
            return new \WP_Error('broke', esc_html__('No new description set', 'wpcloudplugins'));
        }

        // Get entry meta data
        $cached_node = Cache::instance()->is_cached(Processor::instance()->get_requested_entry(), App::get_current_drive_id());

        if (false === $cached_node) {
            $cached_node = $this->get_entry(Processor::instance()->get_requested_entry());
            if (false === $cached_node) {
                return new \WP_Error('broke', esc_html__('Failed to edit file.', 'wpcloudplugins'));
            }
        }

        // Check if user is allowed to delete from this dir
        if (!$cached_node->is_in_folder(Processor::instance()->get_last_folder())) {
            return new \WP_Error('broke', esc_html__('You are not authorized to edit files in this directory', 'wpcloudplugins'));
        }

        $entry = $cached_node->get_entry();

        // Check if entry is allowed
        if (!Processor::instance()->_is_entry_authorized($cached_node)) {
            return new \WP_Error('broke', esc_html__('You are not authorized to edit this file or folder', 'wpcloudplugins'));
        }

        // Set new description, and update the entry
        $updaterequest = ['description' => $new_description];

        try {
            API::set_drive_by_id($cached_node->get_drive_id());
            $edited_entry = API::patch($entry->get_id(), $updaterequest);

            if (false !== $edited_entry && null !== $edited_entry) {
                do_action('shareonedrive_log_event', 'shareonedrive_updated_description', $edited_entry, ['description' => $new_description]);
                Cache::instance()->update_cache();
            }
        } catch (\Exception $ex) {
            Helpers::log_error('Client is receiving API error.', 'API', null, __LINE__, $ex);

            return new \WP_Error('broke', esc_html__('Failed to edit file.', 'wpcloudplugins'));
        }

        return $edited_entry->get_entry()->get_description();
    }

    // Add directory to OneDrive

    public function add_folder($new_folder_name = null)
    {
        if ('1' === Processor::instance()->get_shortcode_option('demo')) {
            return new \WP_Error('broke', esc_html__('Failed to add folder', 'wpcloudplugins'));
        }

        if (null === $new_folder_name) {
            return new \WP_Error('broke', esc_html__('No new foldername set', 'wpcloudplugins'));
        }

        // Get entry meta data of current folder
        $cached_node = Cache::instance()->is_cached(Processor::instance()->get_last_folder(), App::get_current_drive_id());

        if (false === $cached_node) {
            $cached_node = $this->get_entry(Processor::instance()->get_last_folder());
            if (false === $cached_node) {
                return new \WP_Error('broke', esc_html__('Failed to add file.', 'wpcloudplugins'));
            }
        }

        if (!Processor::instance()->_is_entry_authorized($cached_node)) {
            return new \WP_Error('broke', esc_html__('You are not authorized to add folders in this directory', 'wpcloudplugins'));
        }

        $currentfolder = $cached_node->get_entry();

        // Check user permission
        if (!$currentfolder->get_permission('canadd')) {
            return new \WP_Error('broke', esc_html__('You are not authorized to add a folder', 'wpcloudplugins'));
        }

        try {
            API::set_drive_by_id($cached_node->get_drive_id());
            $cached_node = API::create_folder($new_folder_name, $currentfolder->get_id());
        } catch (\Exception $ex) {
            Helpers::log_error('Client is receiving API error.', 'API', null, __LINE__, $ex);

            return new \WP_Error('broke', esc_html__('Failed to add folder', 'wpcloudplugins'));
        }

        return $cached_node;
    }

    public function get_folder_thumbnails($folders_ids = [], $height = 250, $width = 250, $maximages = 3)
    {
        $thumbnails = [];

        foreach ($folders_ids as $folder_id) {
            $folder_data = $this->get_folder($folder_id);
            $cached_folder_node = $folder_data['folder'];

            if (false === $cached_folder_node->has_children()) {
                continue;
            }

            $children = $folder_data['contents'];
            // Try to load default thumbnail (.thumb.jpg or .thumb.png){
            $default_thumb = Cache::instance()->get_node_by_name('.thumb.jpg', $cached_folder_node->get_drive_id(), $cached_folder_node);
            if (empty($default_thumb)) {
                $default_thumb = Cache::instance()->get_node_by_name('.thumb.png', $cached_folder_node->get_drive_id(), $cached_folder_node);
            }

            if (!empty($default_thumb)) {
                $children = [$default_thumb];
            }

            $thumbnails[$folder_id] = $folder_thumbs = [];

            // Else get $maximages images from the folder
            for ($i = 1; $i <= $maximages; ++$i) {
                $cached_child_node = current($children);
                $child_entry = $cached_child_node->get_entry();

                // Skip Folder children or files without thumbnail
                if ($cached_child_node->is_dir() || !$child_entry->has_own_thumbnail()) {
                    --$i;

                    if (false === next($children)) {
                        break;
                    }

                    continue;
                }

                // Get Thumbnail
                $thumbnail = $child_entry->get_thumbnail_with_size($height, $width, 'center');
                $folder_thumbs[$child_entry->get_id()] = $thumbnails[$folder_id][] = $thumbnail;

                // Stop if there are no further children
                if (false === next($children)) {
                    break;
                }
            }

            $cached_folder_node->get_entry()->set_folder_thumbnails(['expires' => time() + 1800, 'thumbs' => $folder_thumbs]);
            $cached_folder_node->get_entry()->set_has_own_thumbnail(true);
        }

        // Clear Cache
        Cache::instance()->set_updated();

        return $thumbnails;
    }

    public function preview_entry()
    {
        // Get file meta data
        $cached_node = $this->get_entry();

        if (false === $cached_node) {
            exit('-1');
        }

        $entry = $cached_node->get_entry();
        if (false === $entry->get_can_preview_by_cloud()) {
            exit('-1');
        }

        if (false === User::can_preview()) {
            exit('-1');
        }

        do_action('shareonedrive_log_event', 'shareonedrive_previewed_entry', $cached_node);

        // Preview for Image files
        if (in_array($entry->get_extension(), ['jpg', 'jpeg', 'gif', 'png', 'webp'])) {
            if (isset($_REQUEST['inline']) && User::can_download()) {
                $url = $this->get_shared_link($cached_node).'?raw=1';
                header('Location: '.$url);

                exit;
            }

            if (('default' === Processor::instance()->get_shortcode_option('lightbox_imagesource') && 'thumbnail' === Settings::get('loadimages'))
                            || 'thumbnail' === Processor::instance()->get_shortcode_option('lightbox_imagesource')
                            || false === User::can_download()) {
                if (null !== $entry->get_thumbnail_original()) {
                    header('Location: '.$entry->get_thumbnail_original());

                    exit;
                }
                if (null !== $entry->get_thumbnail_large()) {
                    header('Location: '.$entry->get_thumbnail_large());

                    exit;
                }
            }
        }

        // Preview for Media files in HTML5 Player + PDF files
        if (in_array($entry->get_extension(), ['jpg', 'jpeg', 'gif', 'png', 'webp', 'mp4', 'm4v', 'ogg', 'ogv', 'webmv', 'mp3', 'm4a', 'ogg', 'oga', 'flac', 'opus'])) {
            $temporarily_link = API::create_temporarily_download_url($cached_node->get_id());
            header('Location: '.$temporarily_link);

            exit;
        }

        // Preview for PDF files - Use viewer selection with overrides for limited account scopes
        if ('pdf' === $entry->get_extension()) {
            // Start from admin setting and allow filtering
            $viewer = apply_filters('shareonedrive_set_pdfviewer', Settings::get('layout_pdfviewer'), $entry);

            // Forced overrides:
            //  - Personal accounts in read-only mode (Files.Read scope only)
            //  - Business accounts where the Graph preview endpoint is compromised by the
            //    documented vulnerability fix (combined read+write scopes)
            $account = App::get_current_account();
            $force_pdfjs = $account->is_drive_readonly() || ('personal' !== $account->get_type() && App::requires_vulnerability_fix());
            if ($force_pdfjs) {
                $viewer = 'pdfjs';
            }

            // Template for rendering the plugin PDF viewer (uses templates/admin/pdfviewer.php).
            $show_template = function () use ($cached_node) {
                $use_proxy = apply_filters('shareonedrive_pdfviewer_proxy_stream', App::requires_vulnerability_fix(), $cached_node);

                if ($use_proxy) {
                    $pdf_url = SHAREONEDRIVE_ADMIN_URL.'?action=shareonedrive-preview&id='.rawurlencode($cached_node->get_id()).'&account_id='.App::get_current_account()->get_uuid().'&drive_id='.$cached_node->get_drive_id().'&listtoken='.Processor::instance()->get_listtoken();
                } else {
                    $pdf_url = API::create_temporarily_download_url($cached_node->get_id());
                }

                include_once SHAREONEDRIVE_ROOTDIR.'/templates/admin/pdfviewer.php';
            };

            switch ($viewer) {
                case 'browser':
                case 'raw':
                    // Stream the PDF bytes inline through the plugin proxy.
                    // The @microsoft.graph.downloadUrl always carries Content-Disposition: attachment,
                    // so redirecting to it would trigger a download instead of rendering.
                    $download_url = API::create_temporarily_download_url($cached_node->get_id());
                    $this->download_via_proxy($entry, $download_url, 'inline');

                    exit;

                case 'microsoft':
                    // Use Microsoft's online PDF viewer (Office Online / OneDrive embed)
                    if ('personal' !== $account->get_type()) {
                        $preview_link = API::get_preview_link($cached_node->get_id());
                    } else {
                        $preview_link = API::get_embed_url($cached_node->get_id());
                    }
                    header('Location: '.$preview_link);

                    exit;

                case 'pdfjs':
                default:
                    // When called as the viewer page (from Embed module / lightbox iframe / direct
                    // viewer link), render the PDF.js HTML viewer template.
                    if (!empty($_REQUEST['viewer']) || (isset($_REQUEST['inline']) )) {
                        $show_template();

                        exit;
                    }

                    // Otherwise stream the PDF bytes inline so the browser's native PDF viewer
                    // can render the file. The @microsoft.graph.downloadUrl always carries
                    // Content-Disposition: attachment, which would force a download if we just
                    // redirected to it, so we proxy the bytes and replace the disposition.
                    $download_url = API::create_temporarily_download_url($cached_node->get_id());

                    if (App::requires_vulnerability_fix()) {
                        $this->download_via_proxy($entry, $download_url, 'inline');

                        exit;
                    }
                    header('Location: '.$download_url);

                    exit;
            }
        }

        // Business accounts can use the Preview API */
        if ('personal' !== App::get_current_account()->get_type() && !App::requires_vulnerability_fix()) {
            $preview_link = API::get_preview_link($cached_node->get_id());
            header('Location: '.$preview_link);

            exit;
        }

        // Preview for Office files
        if (User::can_edit() && in_array($entry->get_extension(), ['csv', 'doc', 'docx', 'dotx', 'odp', 'ods', 'odt', 'pot', 'potm', 'potx', 'pps', 'ppsx', 'ppsxm', 'ppt', 'pptm', 'pptx', 'rtf', 'xls', 'xlsx'])) {
            // If user has permissions to edit the file, show the preview in Office Online
            $shared_link = API::get_embed_url($cached_node->get_id());

            if (!empty($shared_link)) {
                header('Location: '.$shared_link);

                exit;
            }
        }

        // Preview for all other formats - convert the file to PDF via Microsoft Graph and render
        // it through the PDF viewer.
        $export_url = API::create_temporarily_download_url($cached_node->get_id(), 'pdf');

        $viewer = apply_filters('shareonedrive_set_pdfviewer', Settings::get('layout_pdfviewer'), $entry);
        if ('microsoft' === $viewer) {
            // The 'microsoft'  setting does not apply here.
            $viewer = 'pdfjs';
        }

        switch ($viewer) {
            case 'browser':
            case 'raw':
                // Stream the exported PDF inline so the browser's native PDF viewer can render it.
                $this->download_via_proxy($entry, $export_url, 'inline');

                exit;

            case 'pdfjs':
            default:
                // Always render the PDF.js viewer HTML template for this fallback. Unlike the
                // native .pdf branch, this entry point is also reached from contexts that have
                // not preloaded the PDF viewer scripts (e.g. an iframe lightbox for an office
                // file), so we need the template to load PDF.js and its dependencies itself.
                $pdf_url = $export_url;

                include SHAREONEDRIVE_ROOTDIR.'/templates/admin/pdfviewer.php';

                exit;
        }
    }

    public function edit_entry()
    {
        // Get file meta data
        $cached_node = $this->get_entry();

        if (false === $cached_node) {
            exit('-1');
        }

        $entry = $cached_node->get_entry();
        if (false === $entry->get_can_edit_by_cloud()) {
            exit('-1');
        }

        $edit_link = $this->get_shared_link($cached_node, ['type' => 'edit']);

        if (empty($edit_link)) {
            Helpers::log_error('Cannot create a editable shared link.', 'Client', ['entry_id' => $cached_node->get_id()], __LINE__);

            exit;
        }

        do_action('shareonedrive_edit', $cached_node);
        do_action('shareonedrive_log_event', 'shareonedrive_edited_entry', $cached_node);

        header('Location: '.$edit_link);

        exit;
    }

    // Download file

    public function download_entry()
    {
        // Check if file is cached and still valid
        $cached = Cache::instance()->is_cached(Processor::instance()->get_requested_entry(), App::get_current_drive_id());

        if (false === $cached) {
            $cached_node = $this->get_entry(Processor::instance()->get_requested_entry());
        } else {
            $cached_node = $cached;
        }

        if (false === $cached_node) {
            exit;
        }

        $entry = $cached_node->get_entry();

        // get the last-modified-date of this very file
        $lastModified = $entry->get_last_edited();
        // get a unique hash of this file (etag)
        $etagFile = md5($lastModified);
        // get the HTTP_IF_MODIFIED_SINCE header if set
        $ifModifiedSince = (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : false);
        // get the HTTP_IF_NONE_MATCH header if set (etag: unique file hash)
        $etagHeader = (isset($_SERVER['HTTP_IF_NONE_MATCH']) ? trim($_SERVER['HTTP_IF_NONE_MATCH']) : false);

        if (!empty($entry->get_last_edited())) {
            header('Last-Modified: '.gmdate('D, d M Y H:i:s', $lastModified).' GMT');
            header("Etag: {$etagFile}");
        }

        header('Expires: '.gmdate('D, d M Y H:i:s', time() + 60 * 5).' GMT');
        header('Cache-Control: must-revalidate');

        // check if page has changed. If not, send 304 and exit
        if (false !== $lastModified && !empty($entry->get_last_edited()) && false !== $cached && (@strtotime($ifModifiedSince) == $lastModified || $etagHeader == $etagFile)) {
            // Send email if needed
            if ('1' === Processor::instance()->get_shortcode_option('notificationdownload')) {
                Processor::instance()->send_notification_email('download', [$cached_node]);
            }

            do_action('shareonedrive_download', $cached_node);
            header('HTTP/1.1 304 Not Modified');

            exit;
        }

        // Check if entry is allowed
        if (!Processor::instance()->_is_entry_authorized($cached_node)) {
            exit;
        }

        // Send email if needed
        if ('1' === Processor::instance()->get_shortcode_option('notificationdownload')) {
            Processor::instance()->send_notification_email('download', [$cached_node]);
        }

        // Redirect if needed
        if ('url' === $entry->get_extension()) {
            $download_url = API::create_temporarily_download_url($cached_node->get_id());

            $request = new \SODOneDrive_Http_Request($download_url, 'GET');

            try {
                $httpRequest = App::instance()->get_sdk_client()->getAuth()->authenticatedRequest($request);
            } catch (\Exception $ex) {
                Helpers::log_error('Client is receiving API error.', 'API', null, __LINE__, $ex);

                exit;
            }

            preg_match_all('/URL=(.*)/', $httpRequest->getResponseBody(), $location, PREG_SET_ORDER);

            if (2 === count($location[0])) {
                $temporarily_link = $location[0][1];
                header('Location: '.$temporarily_link);

                exit;
            }
        }

        if (isset($_REQUEST['redirect']) && ('redirect' === Processor::instance()->get_shortcode_option('onclick'))) {
            $shared_link = $this->get_shared_link($cached_node, ['type' => 'view']);
            header('Location: '.$shared_link);

            exit;
        }

        // Get the complete file
        $extension = (isset($_REQUEST['extension'])) ? $_REQUEST['extension'] : 'default';
        $this->download_content($cached_node, $extension);

        exit;
    }

    public function download_content(CacheNode $cached_node, $extension = 'default')
    {
        $stream = (isset($_REQUEST['action']) && 'shareonedrive-stream' === $_REQUEST['action'] && !isset($_REQUEST['caption']));

        // Check if usage limits are hit
        if (!$stream && $download_limit_hit_message = Restrictions::has_reached_download_limit($cached_node->get_id(), false)) {
            header('Content-disposition: attachment; filename=Download limit exceeded - '.$cached_node->get_name().'.empty');

            if ('Firefox' === Helpers::get_browser_name()) {
                header('Content-type: text/plain');
                echo $download_limit_hit_message;

                exit;
            }

            http_response_code(429);

            exit;
        }

        // If there is a temporarily download url present for this file, just redirect the user
        $stored_url = ($stream) ? get_transient('shareonedrive'.$cached_node->get_id().'_'.$cached_node->get_entry()->get_extension()) : get_transient('shareonedrive'.$cached_node->get_id().'_'.$cached_node->get_entry()->get_extension());
        if (false !== $stored_url && filter_var($stored_url, FILTER_VALIDATE_URL)) {
            do_action('shareonedrive_download', $cached_node, $stored_url);
            header('Location: '.$stored_url);

            exit;
        }

        $temporarily_link = API::create_temporarily_download_url($cached_node->get_id(), $extension);

        // Download Hook
        do_action('shareonedrive_download', $cached_node, $temporarily_link);

        $event_type = ($stream) ? 'shareonedrive_streamed_entry' : 'shareonedrive_downloaded_entry';
        do_action('shareonedrive_log_event', $event_type, $cached_node);

        header('Location: '.$temporarily_link);

        set_transient('shareonedrive'.(($stream) ? 'stream' : 'download').'_'.$cached_node->get_id().'_'.$cached_node->get_entry()->get_extension(), $temporarily_link, MINUTE_IN_SECONDS * 10);

        exit;
    }

    /**
     * Proxy a remote file to the browser.
     *
     * @param Entry  $entry       the entry being served (used for filename + size headers)
     * @param string $url         remote URL to fetch
     * @param string $disposition Content-Disposition value: 'attachment' (default, forces download)
     *                            or 'inline' (allows the browser/PDF viewer to render the file)
     */
    public function download_via_proxy(Entry $entry, $url, $disposition = 'attachment')
    {
        // Stop WP from buffering, and discard the current buffer.
        $levels = ob_get_level();
        for ($i = 0; $i < $levels; ++$i) {
            ob_end_clean();
        }

        Helpers::set_time_limit(500);

        $filename = basename($entry->get_name());

        // Use the entry mimetype for inline previews so the browser can render the file;
        // fall back to application/octet-stream for downloads to force the save dialog.
        $content_type = ('inline' === $disposition && !empty($entry->get_mimetype()))
            ? $entry->get_mimetype()
            : 'application/octet-stream';

        header('Content-Type: '.$content_type);
        header('Content-Disposition: '.$disposition.'; '.sprintf('filename="%s"; ', rawurlencode($filename)).sprintf("filename*=utf-8''%s", rawurlencode($filename)));
        header("Content-length: {$entry->get_size()}");

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 500);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
        curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($curl, $data) {
            echo $data;

            return strlen($data);
        });
        curl_exec($ch);
        curl_close($ch);

        exit;
    }

    public function stream_entry()
    {
        // Check if file is cached and still valid
        $cached = Cache::instance()->is_cached(Processor::instance()->get_requested_entry(), App::get_current_drive_id());

        if (false === $cached) {
            $cached_node = $this->get_entry(Processor::instance()->get_requested_entry());
        } else {
            $cached_node = $cached;
        }

        if (false === $cached_node) {
            exit;
        }

        $entry = $cached_node->get_entry();

        $extension = $entry->get_extension();
        $allowedextensions = ['mp4', 'm4v', 'ogg', 'ogv', 'webmv', 'mp3', 'm4a', 'oga', 'wav', 'opus', 'webm', 'flac', 'vtt', 'srt'];

        if (empty($extension) || !in_array($extension, $allowedextensions)) {
            exit;
        }

        // Download Captions directly
        if (in_array($extension, ['vtt', 'srt'])) {
            $temporarily_link = API::create_temporarily_download_url($cached_node->get_id());
            header('Location: '.$temporarily_link);

            exit;
        }

        $this->download_entry();
    }

    public function build_thumbnail()
    {
        $src = $_REQUEST['src'];
        preg_match_all('/(.+)_w(\d+)_h(\d+)_c(\d)/', $src, $attr, PREG_SET_ORDER);

        if (1 !== count($attr) || 5 !== count($attr[0])) {
            exit;
        }

        $entry_id = $attr[0][1];
        $width = $attr[0][2];
        $height = $attr[0][3];
        $crop = $attr[0][4];

        $cached_node = $this->get_entry($entry_id, false);

        if (false === $cached_node) {
            exit(-1);
        }

        $entry = $cached_node->get_entry();

        // get the last-modified-date of this very file
        $lastModified = strtotime($entry->get_last_edited());
        // get a unique hash of this file (etag)
        $etagFile = md5($lastModified);
        // get the HTTP_IF_MODIFIED_SINCE header if set
        $ifModifiedSince = (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : false);
        // get the HTTP_IF_NONE_MATCH header if set (etag: unique file hash)
        $etagHeader = (isset($_SERVER['HTTP_IF_NONE_MATCH']) ? trim($_SERVER['HTTP_IF_NONE_MATCH']) : false);

        if (!empty($entry->get_last_edited())) {
            header('Last-Modified: '.gmdate('D, d M Y H:i:s', $lastModified).' GMT');
            header("Etag: {$etagFile}");
        }

        header('Expires: '.gmdate('D, d M Y H:i:s', time() + 60 * 5).' GMT');
        header('Cache-Control: must-revalidate');

        if (false !== $lastModified && !empty($entry->get_last_edited()) && (@strtotime($ifModifiedSince) == $lastModified || $etagHeader == $etagFile)) {
            header('HTTP/1.1 304 Not Modified');

            exit;
        }

        if (false === $entry->has_own_thumbnail()) {
            header('Location: '.$entry->get_icon_large());

            exit;
        }

        $resizeable_url = $entry->get_thumbnail_small_cropped(true);

        $pattern = '/width=\d*/';
        $thumb_url = preg_replace($pattern, 'width='.$width, $resizeable_url);

        $pattern = '/height=\d*/';
        $thumb_url = preg_replace($pattern, 'height='.$height, $thumb_url);

        if (!empty($crop)) {
            $thumb_url .= (false === strpos($thumb_url, 'cropmode')) ? '&cropmode=none' : '';
            $thumb_url = str_replace('cropmode=none', 'cropmode='.$crop, $thumb_url);
        }

        $thumbnail = new Thumbnail($entry, $thumb_url);

        if (false === $thumbnail->does_thumbnail_exist()) {
            $thumbnail_created = $thumbnail->build_thumbnail();

            if (false === $thumbnail_created) {
                header('Location: '.$entry->get_icon_large());

                exit;
            }
        }

        header('Content-type: image/jpeg');
        header('Content-Length: '.filesize($thumbnail->get_location_thumbnail()));

        // Make sure that the file is displayed rather than downloaded
        header('Content-Disposition: inline; filename="'.$cached_node->get_name().'"');

        readfile($thumbnail->get_location_thumbnail());

        exit;
    }

    public function get_folder_recursive(CacheNode $cached_node, $list_of_cached_entries = [])
    {
        if (Processor::instance()->is_filtering_entries() && false === Processor::instance()->_is_entry_authorized($cached_node)) {
            return $list_of_cached_entries;
        }

        if ($cached_node->get_entry()->is_file()) {
            $list_of_cached_entries[$cached_node->get_id()] = $cached_node;

            return $list_of_cached_entries;
        }

        $result = $this->get_folder($cached_node->get_id());
        if (empty($result)) {
            return $list_of_cached_entries;
        }

        $cached_folder = $result['folder'];

        if (false === $cached_folder->has_children()) {
            return $list_of_cached_entries;
        }

        foreach ($cached_folder->get_children() as $cached_child_entry) {
            $new_of_cached_entries = $this->get_folder_recursive($cached_child_entry, $list_of_cached_entries);
            $list_of_cached_entries = array_merge($list_of_cached_entries, $new_of_cached_entries);
        }

        return $list_of_cached_entries;
    }

    public function has_temporarily_link($cached_node, $extension = 'default')
    {
        if ($cached_node instanceof Entry) {
            $cached_node = $this->get_entry($cached_node->get_id());
        }

        if (false !== $cached_node && $cached_node->get_temporarily_link($extension)) {
            return true;
        }

        return false;
    }

    public function get_shared_link($cached_node, $link_settings = ['type' => 'view'])
    {
        if ($cached_node instanceof Entry) {
            $cached_node = $this->get_entry($cached_node->get_id());
        }

        // Custom link settings for Premium & Business accounts
        if (empty($link_settings)) {
            // Add Password
            $password = Processor::instance()->get_shortcode_option('share_password');
            if (!empty($password)) {
                $link_settings['password'] = $password;
            }

            // Add Expire date
            $expire_after = Processor::instance()->get_shortcode_option('share_expire_after');
            if (!empty($expire_after)) {
                $expire_date = current_datetime()->modify('+'.$expire_after);
                $link_settings['expirationDateTime'] = $expire_date->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d\TH:i:s\Z');
            }
        }

        if (empty($link_settings['type'])) {
            $link_settings['type'] = 'view';
        }

        if (empty($link_settings['scope']) && 'business' === App::get_current_account()->get_type()) {
            $link_settings['scope'] = Settings::get('link_scope');
        }

        if (false !== $cached_node && ($shared_link = $cached_node->get_shared_link($link_settings))) {
            do_action('shareonedrive_log_event', 'shareonedrive_created_link_to_entry', $cached_node, ['url' => $shared_link]);

            return $shared_link;
        }

        $shared_link = API::create_shared_url($cached_node->get_id(), $link_settings);
        do_action('shareonedrive_log_event', 'shareonedrive_created_link_to_entry', $cached_node, ['url' => $shared_link]);

        return $shared_link;
    }

    public function get_shared_link_for_output($entry_id = false, $as_editable = false)
    {
        $cached_node = $this->get_entry($entry_id);

        if (false === $cached_node) {
            exit(-1);
        }

        $entry = $cached_node->get_entry();

        if ($as_editable) {
            $shared_link = $this->get_shared_link($cached_node, ['type' => 'edit']);
        } else {
            $shared_link = $this->get_shared_link($cached_node, []);
        }

        $embed_link = API::get_embed_url($cached_node->get_id());

        return [
            'name' => $entry->get_name(),
            'extension' => $entry->get_extension(),
            'link' => API::shorten_url($shared_link, null, ['name' => $cached_node->get_name()]),
            'embeddedlink' => API::shorten_url($embed_link, null, ['name' => $cached_node->get_name()]),
            'size' => Helpers::bytes_to_size_1024($entry->get_size()),
            'error' => false,
        ];
    }

    // Pull for changes
    public function get_changes($change_token = false, $drive_id = null)
    {
        if (empty($drive_id) || 'null' === $drive_id) {
            return false;
        }

        if (empty($change_token)) {
            try {
                $result = App::instance()->get_drive()->changes->getlatest('root', ['driveId' => $drive_id]);
                preg_match("/delta.token=\\'?([a-zA-Z0-9;%\\-=_]+)\\'?/", $result['@odata.deltaLink'], $matches);
                $new_change_token = $result['@odata.deltaLink'];
            } catch (\Exception $ex) {
                Helpers::log_error('Failed to receive changes', 'Client', null, __LINE__, $ex);

                return false;
            }
        }

        $changes = [];

        while (null != $change_token) {
            try {
                $httpRequest = new \SODOneDrive_Http_Request($change_token, 'GET');
                $httpRequest = App::instance()->get_sdk_client()->getAuth()->sign($httpRequest);
                $httpRequest->setExpectedClass('SODOneDrive_Service_Drive_Changes');
                $result = App::instance()->get_sdk_client()->execute($httpRequest);

                if (isset($result['@odata.nextLink'])) {
                    $change_token = $result['@odata.nextLink'];
                } else {
                    $change_token = null;
                    $new_change_token = $result['@odata.deltaLink'];
                }

                $changes = array_merge($changes, $result->getValue());
            } catch (\Exception $ex) {
                Helpers::log_error('Client is receiving API error.', 'API', null, __LINE__, $ex);

                if (false !== \strpos($ex->getMessage(), 'resyncRequired')) {
                    Processor::reset_complete_cache();
                }

                return false;
            }
        }

        $list_of_update_entries = [];
        foreach ($changes as $change) {
            if ('root' === $change->getName()) {
                continue;
            }

            // File is updated
            $change_entry = new Entry($change);
            $list_of_update_entries[$change->getId()] = $change_entry;
        }

        return [$new_change_token, $list_of_update_entries];
    }
}
