<?php
/*  Copyright 2013 Foobar Studio (contact@foobar.studio)

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 2 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, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/

require_once(CUAR_INCLUDES_DIR . '/core-classes/addon.class.php');
require_once(CUAR_INCLUDES_DIR . '/core-addons/post-owner/helpers/post-owner-admin-interface.class.php');
require_once(CUAR_INCLUDES_DIR . '/core-addons/post-owner/helpers/post-owner-ajax-helper.class.php');

if (!class_exists('CUAR_PostOwnerAddOn')) :

    /**
     * Add-on to provide all the stuff required to set an owner on a post type and include that post type in the
     * customer area.
     *
     * @author Vincent Prat @ Foobar Studio
     */
    class CUAR_PostOwnerAddOn extends CUAR_AddOn
    {
        public static $META_OWNER_QUERYABLE = 'cuar_owner_queryable';
        public static $META_OWNER_DISPLAYNAME = 'cuar_owner_displayname';
        public static $META_OWNER_SORTABLE_DISPLAYNAME = 'cuar_owner_sortable_displayname';
        public static $META_OWNER_QUERY_VAR = 'cuar_owners';

        /** @var array */
        private $owner_types = null;

        /** @var CUAR_PostOwnerAdminInterface */
        private $admin_interface;

        /** @var  CUAR_PostOwnerUserOwnerType */
        private $usr_owner_type;

        /** @var  CUAR_PostOwnerAjaxHelper */
        private $ajax_helper;

        /**
         * CUAR_PostOwnerAddOn constructor.
         */
        public function __construct()
        {
            parent::__construct('post-owner');
        }

        public function get_addon_name()
        {
            return 'Post Owner';
        }

        public function run_addon($plugin)
        {
            $this->usr_owner_type = new CUAR_PostOwnerUserOwnerType($this);
            $this->ajax_helper = new CUAR_PostOwnerAjaxHelper($plugin, $this);

            if (is_admin())
            {
                $this->admin_interface = new CUAR_PostOwnerAdminInterface($plugin, $this);
            }
            else
            {
                add_action('template_redirect', [&$this, 'protect_single_post_access']);
                add_action('init', [&$this, 'register_query_var']);
            }

            add_action('cuar/core/on-plugin-update', [&$this, 'plugin_version_upgrade'], 10, 2);
        }

        /**
         * @return CUAR_PostOwnerAjaxHelper
         */
        public function ajax()
        {
            return $this->ajax_helper;
        }

        /*------- QUERY FUNCTIONS ---------------------------------------------------------------------------------------*/

	    /**
	     * Builds the meta query to check if a user owns a post
	     *
	     * @param int $user_id The user ID of the owner
	     * @param string|array $post_type The queried post type
	     *
	     * @return array See the meta query documentation on WP codex
	     */
        public function get_meta_query_post_owned_by($user_id, $post_type)
        {
            $user_id = apply_filters('cuar/core/ownership/content/meta-query/override-owner-id', $user_id);
            $base_meta_query = [
                'relation' => 'OR',
            ];

            return apply_filters('cuar/core/ownership/content/meta-query', $base_meta_query, $user_id, $this,
	            $post_type);
        }

        /**
         * Given an owner type and ID, get the meta query component (sub-array)
         *
         * @param string     $owner_type
         * @param string|int $owner_id
         *
         * @return array
         */
        public function get_owner_meta_query_component($owner_type, $owner_id)
        {
            return [
                'key'     => self::$META_OWNER_QUERYABLE,
                'value'   => '|' . $owner_type . '_' . $owner_id . '|',
                'compare' => 'LIKE',
            ];
        }

        /**
         * Given an owners array, get the meta query components (array of arrays)
         *
         * @param array $owners
         *
         * @return array
         *
         */
        public function get_owners_meta_query_component($owners)
        {
            $mq = [];
            foreach ($owners as $type => $ids)
            {
                foreach ($ids as $id)
                {
                    $mq[] = $this->get_owner_meta_query_component($type, $id);
                }
            }

            return $mq;
        }

        /**
         * Register query vars
         */
        public function register_query_var()
        {
            global $wp;
            $wp->add_query_var(self::$META_OWNER_QUERY_VAR);
        }

        /*------- PRIVATE FILE STORAGE DIRECTORIES ----------------------------------------------------------------------*/

        /**
         * This is the base directory where we will store the user files
         *
         * @param bool $create_dirs Shall we create the directory if missing
         *
         * @return string
         */
        public function get_base_private_storage_directory($create_dirs = false)
        {
            $dir = WP_CONTENT_DIR . '/customer-area/storage';
            $dir = apply_filters('cuar/core/ownership/base-private-storage-directory', $dir);
            if ($create_dirs
                && !file_exists($dir)
                && !mkdir($dir, 0775, true)
                && !is_dir($dir))
            {
                throw new RuntimeException(sprintf('Directory "%s" was not created', $dir));
            }

            return $dir;
        }

        /**
         * This is the base URL where we can access the user files directly (should be protected to forbid direct
         * downloads)
         *
         * @return string
         */
        public function get_base_private_storage_url()
        {
            return content_url() . '/customer-area/storage';
        }

        /**
         * Get the absolute path to a private file.
         *
         * @param string $filename    The name of the file
         * @param int    $post_id     The ID of the post which is assigned to an owner
         * @param bool   $create_dirs Shall we create the directory if missing
         *
         * @return boolean|string The validated absolute path, or false if invalid
         */
        public function get_private_file_path($filename, $post_id, $create_dirs = false)
        {
            if (empty($post_id) || empty($filename) || !is_string($filename)) {
                return false;
            }

            if (
                strpos($filename, '..') !== false
                || strpos($filename, '/') !== false
                || strpos($filename, '\\') !== false
            ) {
                error_log('WPCA Security: Path traversal attempt detected in filename: ' . $filename);
                return false;
            }

            $base_dir = $this->get_base_private_storage_directory($create_dirs);
            $storage_subdir = $this->get_private_storage_directory($post_id, false, $create_dirs);

            if ($base_dir === false || $storage_subdir === false) {
                return false;
            }

            $dir = $base_dir . '/' . $storage_subdir;
            $full_path = $dir . '/' . $filename;

            // CAS 1 : Le fichier existe déjà
            if (file_exists($full_path)) {
                $real_path = realpath($full_path);

                if ($real_path === false) {
                    error_log('WPCA Security: realpath() failed for existing file: ' . $full_path);
                    return false;
                }
            }
            // CAS 2 : Nouveau fichier
            else {
                // Si le dossier parent existe, valider normalement
                if (is_dir($dir)) {
                    $real_dir = realpath($dir);

                    if ($real_dir === false) {
                        error_log('WPCA Security: realpath() failed for directory: ' . $dir);
                        return false;
                    }

                    $clean_filename = basename($filename);
                    $real_path = $real_dir . '/' . $clean_filename;
                }
                // Si le dossier n'existe pas : validation théorique
                else {
                    // Vérifier que le base_dir existe au moins
                    $real_base = realpath($base_dir);

                    if ($real_base === false) {
                        error_log('WPCA Security: Base directory does not exist: ' . $base_dir);
                        return false;
                    }

                    // Construire le chemin théorique en nettoyant les composants
                    $clean_subdir = basename($storage_subdir);
                    $clean_filename = basename($filename);
                    $theoretical_path = $real_base . '/' . $clean_subdir . '/' . $clean_filename;

                    // Vérifier qu'il commence bien par le base_dir
                    if (strpos($theoretical_path, $real_base) !== 0) {
                        error_log('WPCA Security: Theoretical path outside base directory: ' . $theoretical_path);
                        return false;
                    }

                    $real_path = $theoretical_path;
                }
            }

            $allowed_base = realpath($base_dir);

            if ($allowed_base === false) {
                error_log('WPCA Security: Base directory does not exist: ' . $base_dir);
                return false;
            }

            if (strpos($real_path, $allowed_base) !== 0) {
                error_log('WPCA Security: Path outside allowed directory detected: ' . $real_path);
                return false;
            }

            return $real_path;
        }

        /**
         * Get a user's private storage directory. This directory is relative to the main upload directory
         *
         * @param int  $post_id     The ID of the post which is assigned to an owner
         * @param bool $absolute    Do we need the absolute path?
         * @param bool $create_dirs Shall we create the directory if missing
         *
         * @return bool|string The path
         */
        public function get_private_storage_directory($post_id, $absolute = false, $create_dirs = false)
        {
            if (empty($post_id))
            {
                return false;
            }

            // Do something to make a directory out of the post_id
            $dir = md5('wpca-' . $post_id . md5(-$post_id * $post_id * $post_id));

            if ($absolute)
            {
                $dir = $this->get_base_private_storage_directory() . "/" . $dir;
            }

            if ($create_dirs)
            {
                $tries = 0;
                while (!is_dir($dir) && $tries < 5)
                {
                    if (mkdir($dir, 0775, true) || is_dir($dir))
                    {
                        break;
                    }

                    ++$tries;
                    sleep(1);
                }

                if (!is_dir($dir))
                {
                    throw new RuntimeException(sprintf('Directory "%s" was not created', $dir));
                }
            }

            return $dir;
        }

        // region Deprecated support functions (change of storage directory structure in 6.2)

        /**
         * Support function for versions of WP Customer Area older than 6.2
         *
         * @param bool $create_dirs
         *
         * @return string
         * @deprecated
         */
        public function get_legacy_base_private_storage_directory($create_dirs = false)
        {
            $dir = WP_CONTENT_DIR . '/customer-area';
            $dir = apply_filters('cuar/core/ownership/legacy-base-private-storage-directory', $dir);
            if ($create_dirs && !file_exists($dir))
            {
                if (!mkdir($dir, 0775, true) && !is_dir($dir))
                {
                    throw new RuntimeException(sprintf('Directory "%s" was not created', $dir));
                }
            }

            return $dir;
        }

        /**
         * Get a user's private storage directory. This directory is relative to the main upload directory
         *
         * @param int  $post_id     The ID of the post which is assigned to an owner
         * @param bool $absolute    Do we need the absolute path?
         * @param bool $create_dirs Shall we create the directory if missing
         *
         * @return bool|string The path
         * @deprecated
         */
        public function get_legacy_private_storage_directory($post_id, $absolute = false, $create_dirs = false)
        {
            if (empty($post_id))
            {
                return false;
            }

            $owners = $this->get_post_owners($post_id);

            if (empty($owners))
            {
                return false;
            }

            foreach ($owners as $type => $ids)
            {
                if (!empty($ids))
                {
                    return $this->get_legacy_owner_storage_directory($ids,
                        $type,
                        $absolute,
                        $create_dirs);
                }
            }

            return false;
        }

        /**
         * Get the absolute path to a private file.
         *
         * @param int    $post_id     The ID of the post which is assigned to an owner
         * @param string $filename    The name of the file
         * @param array  $owner_ids   The IDs of the owners
         * @param string $owner_type  The type of owner
         * @param bool   $create_dirs Shall we create the directory if missing
         *
         * @return bool|string
         *
         * @deprecated
         */
        public function get_legacy_owner_file_path($post_id, $filename, $owner_ids, $owner_type, $create_dirs = false)
        {
            if (empty($owner_ids) || empty($owner_type) || empty($filename) || !is_string($filename)) {
                return false;
            }

            if (
                strpos($filename, '..') !== false
                || strpos($filename, '/') !== false
                || strpos($filename, '\\') !== false
            ) {
                error_log('WPCA Security: Path traversal in legacy owner path: ' . $filename);
                return false;
            }

            $base_dir = $this->get_legacy_base_private_storage_directory($create_dirs);
            $storage_subdir = $this->get_legacy_owner_storage_directory($owner_ids, $owner_type, false, $create_dirs);

            if ($base_dir === false || $storage_subdir === false) {
                return false;
            }

            $dir = $base_dir . '/' . $storage_subdir;
            $full_path = $dir . '/' . $filename;

            if (file_exists($full_path)) {
                $real_path = realpath($full_path);

                if ($real_path === false) {
                    return false;
                }
            } else {
                if (is_dir($dir)) {
                    $real_dir = realpath($dir);

                    if ($real_dir === false) {
                        return false;
                    }

                    $real_path = $real_dir . '/' . basename($filename);
                } else {
                    $real_base = realpath($base_dir);

                    if ($real_base === false) {
                        return false;
                    }

                    $theoretical_path = $real_base . '/' . basename($storage_subdir) . '/' . basename($filename);

                    if (strpos($theoretical_path, $real_base) !== 0) {
                        error_log('WPCA Security: Owner path outside base directory: ' . $theoretical_path);
                        return false;
                    }

                    $real_path = $theoretical_path;
                }
            }

            $allowed_base = realpath($base_dir);

            if ($allowed_base === false || strpos($real_path, $allowed_base) !== 0) {
                error_log('WPCA Security: Owner file path outside allowed directory: ' . $real_path);
                return false;
            }

            return $real_path;
        }

        /**
         * Get a user's private storage directory. This directory is relative to the main upload directory
         *
         * @param array  $owner_ids   The IDs of the owners
         * @param string $owner_type  The type of owner
         * @param bool   $absolute    Do we need the absolute path?
         * @param bool   $create_dirs Shall we create the directory if missing
         *
         * @return string The storage directory for an owner
         *
         * @deprecated
         */
        public function get_legacy_owner_storage_directory($owner_ids, $owner_type, $absolute = false,
                                                           $create_dirs = false)
        {
            if (empty($owner_ids) || empty($owner_type))
            {
                return false;
            }

            if (is_array($owner_ids))
            {
                $dir = md5($owner_type . '_' . implode(',', $owner_ids));
            }
            else
            {
                $dir = md5($owner_type . '_' . $owner_ids);
            }

            if ($absolute)
            {
                $dir = $this->get_legacy_base_private_storage_directory() . "/" . $dir;
            }

            if ($create_dirs
                && !file_exists($dir)
                && !mkdir($dir, 0775, true)
                && !is_dir($dir))
            {
                throw new RuntimeException(sprintf('Directory "%s" was not created', $dir));
            }

            return $dir;
        }

        /**
         * Get the absolute path to a private file.
         *
         * @param int    $post_id     The ID of the post which is assigned to an owner
         * @param string $filename    The name of the file
         * @param bool   $create_dirs Shall we create the directory if missing
         *
         * @return boolean|string
         * @deprecated
         */
        public function get_legacy_private_file_path($filename, $post_id, $create_dirs = false)
        {
            if (empty($post_id) || empty($filename) || !is_string($filename)) {
                return false;
            }

            // Interdire path traversal
            if (
                strpos($filename, '..') !== false
                || strpos($filename, '/') !== false
                || strpos($filename, '\\') !== false
            ) {
                error_log('WPCA Security: Path traversal attempt in legacy path: ' . $filename);
                return false;
            }

            // Construire le chemin de base
            $base_dir = $this->get_legacy_base_private_storage_directory($create_dirs);
            $storage_subdir = $this->get_legacy_private_storage_directory($post_id, false, $create_dirs);

            if ($base_dir === false || $storage_subdir === false) {
                return false;
            }

            $dir = $base_dir . '/' . $storage_subdir;
            $full_path = $dir . '/' . $filename;

            // Valider le chemin
            if (file_exists($full_path)) {
                $real_path = realpath($full_path);

                if ($real_path === false) {
                    return false;
                }
            } else {
                if (is_dir($dir)) {
                    $real_dir = realpath($dir);

                    if ($real_dir === false) {
                        return false;
                    }

                    $real_path = $real_dir . '/' . basename($filename);
                } else {
                    $real_base = realpath($base_dir);

                    if ($real_base === false) {
                        return false;
                    }

                    $theoretical_path = $real_base . '/' . basename($storage_subdir) . '/' . basename($filename);

                    if (strpos($theoretical_path, $real_base) !== 0) {
                        error_log('WPCA Security: Legacy path outside base directory: ' . $theoretical_path);
                        return false;
                    }

                    $real_path = $theoretical_path;
                }
            }

            // Vérifier le chemin
            $allowed_base = realpath($base_dir);

            if ($allowed_base === false || strpos($real_path, $allowed_base) !== 0) {
                error_log('WPCA Security: Legacy path outside allowed directory: ' . $real_path);
                return false;
            }

            return $real_path;
        }

        // endregion

        /*------- ACCESS TO OWNER INFO ----------------------------------------------------------------------------------*/

        /**
         * @param $owner_type
         * @param $owner_id
         * @return string
         */
        public function get_owner_display_name($owner_type, $owner_id)
        {
            return apply_filters('cuar/core/ownership/owner-display-name?owner-type=' . $owner_type,
                "$owner_type : $owner_id", $owner_id);
        }

        /** Check if the given value is a valid owner type
         *
         * @param string $type The owner type to test
         *
         * @return bool true if the owner type seems correct
         */
        public function is_valid_owner_type($type)
        {
            $types = $this->get_owner_types();

            return array_key_exists($type, $types);
        }

        /**
         * Returns all the possible owner types in the form of an associative array. The key is the owner type (should
         * remain constant) and the value is a string to be displayed in various places (should be internationalised).
         *
         * @return array
         */
        public function get_owner_types()
        {
            if ($this->owner_types == null)
            {
                $this->owner_types = apply_filters('cuar/core/ownership/owner-types', []);
            }

            return $this->owner_types;
        }

        /**
         * Do we allow more than a single owner for the given type
         *
         * @param $type_id
         *
         * @return bool
         */
        public function is_multiple_selection_enabled($type_id)
        {
            return apply_filters('cuar/core/ownership/enable-multiple-select?owner-type=' . $type_id, false);
        }

        /**
         * Tell if this post type should be protected or not
         *
         * @param string $post_type
         * @param array  $private_types
         *
         * @return bool
         */
        public function is_post_type_protected($post_type, $private_types = null)
        {
            if ($private_types == null)
            {
                $private_types = $this->plugin->get_private_types();
            }

            $is_protected = isset($private_types[$post_type]) ? true : false;

            return apply_filters('cuar/core/ownership/is-post-type-protected', $is_protected, $post_type,
                $private_types);
        }

        /**
         * Tell if this post should be protected or not
         *
         * @param int    $post_id
         * @param string $post_type
         * @param array  $private_types
         *
         * @return bool
         */
        public function is_post_protected($post_id, $post_type = null, $private_types = null)
        {
            if ($post_type == null)
            {
                $post_type = get_post_type($post_id);
            }

            if ($private_types == null)
            {
                $private_types = $this->plugin->get_private_types();
            }

            $is_protected = isset($private_types[$post_type]) ? true : false;

            return apply_filters('cuar/core/ownership/is-post-protected', $is_protected, $post_id, $post_type,
                $private_types);
        }

        /**
         * Check if a user is an owner of the given post.
         *
         * @param int $post_id
         * @param int $user_id
         *
         * @return bool
         */
        public function is_user_owner_of_post($post_id, $user_id)
        {
            $owners = $this->get_post_owners($post_id);
            foreach ($owners as $owner_type => $owner_ids)
            {
                $is_owner = $this->validate_ownership($user_id, $owner_type, $owner_ids, $post_id);
                if ($is_owner)
                {
                    return true;
                }
            }

            return false;
        }

        /**
         * Check if a user is an owner of the given post.
         */
        public function validate_ownership($user_id, $owner_type, $owner_ids, $post_id = 0)
        {
			return apply_filters(
				'cuar/core/ownership/validate-post-ownership', false, $post_id, $user_id, $owner_type, $owner_ids
			);
        }

        /**
         * Get the name to be displayed
         *
         * @param int  $post_id The post ID
         * @param bool $prefix_with_type
         *
         * @return string the type of ownership (defaults to 'usr')
         */
        public function get_post_displayable_owners($post_id, $prefix_with_type = false)
        {
            if ($prefix_with_type)
            {
                $name = get_post_meta($post_id, self::$META_OWNER_SORTABLE_DISPLAYNAME, true);
                if (!$name || empty($name))
                {
                    $name = __('Unknown', 'cuar');
                }

                return apply_filters('cuar/core/ownership/sortable-displayname', $name, $post_id);
            }
            else
            {
                $name = get_post_meta($post_id, self::$META_OWNER_DISPLAYNAME, true);
                if (!$name || empty($name))
                {
                    $name = __('Unknown', 'cuar');
                }
                if (!is_array($name))
                {
                    $name = [$name];
                }

                return apply_filters('cuar/core/ownership/displayname', $name, $post_id);
            }
        }

        /**
         * @param $post_id
         *
         * @return array
         */
        public function get_post_owners($post_id)
        {
            $queryable_owners = get_post_meta($post_id, self::$META_OWNER_QUERYABLE, true);
            $owners = $this->decode_owners($queryable_owners);

            return apply_filters('cuar/core/ownership/post-owners', $owners, $post_id);
        }

        /**
         * Get the real user ids behind the logical owner of the post
         *
         * @param int $post_id The post ID
         *
         * @return array User ids
         */
        public function get_post_owner_user_ids($post_id)
        {
            $user_ids = [];

            $owners = $this->get_post_owners($post_id);
            foreach ($owners as $type => $ids)
            {
                $tmp = apply_filters('cuar/core/ownership/real-user-ids?owner-type=' . $type, [], $ids);
                $user_ids = array_merge($user_ids, $tmp);
            }
            $user_ids = array_unique($user_ids);

            // Let other add-ons return what they want
            return $user_ids;
        }

        /**
         * Save the owner details for the given post
         *
         * @param int   $post_id The post ID
         * @param array $owners
         * @param bool  $ensure_types_exist
         * @param bool  $skip_validation
         */
        public function save_post_owners($post_id, $owners, $ensure_types_exist = true, $skip_validation = false)
        {
            $owner_types = $this->get_owner_types();

            // Check owner type exists
            if ($ensure_types_exist && !empty($owners))
            {
                foreach ($owners as $type => $ids)
                {
                    if (!array_key_exists($type, $owner_types))
                    {
                        $this->plugin->add_admin_notice('Invalid owner type ' . $type . ', some add-on must be doing something wrong');

                        return;
                    }
                }
            }

            // Cleanup owners and leave only the ones we are allowed to assign to
            if (!$skip_validation)
            {
                $submitted_owners = $owners;
                $owners = [];
                $allowed_owner_types = $this->get_selectable_owner_types();
                foreach ($allowed_owner_types as $owner_type => $owner_type_label)
                {
                    $all_owners = isset($submitted_owners[$owner_type]) ? $submitted_owners[$owner_type] : [];
                    $filtered_owners = apply_filters('cuar/core/ownership/validate-owners', $all_owners, $owner_type);

                    if (!empty($filtered_owners))
                    {
                        $owners[$owner_type] = $filtered_owners;
                    }
                }
            }

            // Other addons can do something before we save
            $previous_owners = $this->get_post_owners($post_id);
            do_action('cuar/core/ownership/before-save-owner', $post_id, $previous_owners, $owners);

            // Serialize the owner ids for queries
            $queryable_ids = $this->encode_owners($owners);

            $displayname = [];
            $sortable_displayname = [];
            foreach ($owner_types as $type_id => $type_label)
            {
                if (empty($owners[$type_id]))
                {
                    continue;
                }

                $displayname[$type_id] = apply_filters('cuar/core/ownership/saved-displayname', '', $post_id, $type_id,
                    $owners[$type_id]);
                $sortable_displayname[$type_id] = apply_filters('cuar/core/ownership/saved-sortable-displayname',
                    $type_label . ' - ' . $displayname[$type_id],
                    $post_id, $type_id, $owners[$type_id]);
            }
            $sortable_displayname = implode(' + ', $sortable_displayname);

            // Persist data
            update_post_meta($post_id, self::$META_OWNER_QUERYABLE, $queryable_ids);
            update_post_meta($post_id, self::$META_OWNER_DISPLAYNAME, $displayname);
            update_post_meta($post_id, self::$META_OWNER_SORTABLE_DISPLAYNAME, $sortable_displayname);

            // Other addons can do something after we save
            $new_owners = $this->get_post_owners($post_id);
            $post = get_post($post_id);
            do_action("cuar/core/ownership/after-save-owner", $post_id, $post, $previous_owners, $new_owners);
        }

        /**
         * @param int   $post_id
         * @param array $owners
         *
         * @return string
         */
        public function get_displayable_owners_for_log($post_id, $owners)
        {
            $owner_types = $this->get_owner_types();
            $sortable_display_names = [];

            foreach ($owner_types as $type_id => $type_label)
            {
                if (empty($owners[$type_id]))
                {
                    continue;
                }

                $displayname = apply_filters('cuar/core/ownership/saved-displayname', '', $post_id, $type_id,
                    $owners[$type_id]);
                $sortable_display_names[] = apply_filters('cuar/core/ownership/saved-sortable-displayname',
                    $type_label . ' - ' . $displayname, $post_id,
                    $type_id, $owners[$type_id]);
            }

            return implode(' + ', $sortable_display_names);
        }

        /**
         * Encode an array of users/user groups for storage in the meta table. We expect a dictionnary where the keys are
         * user groups and values are arrays of user IDs.
         *
         * @param $owners
         * @return string
         */
        private function encode_owners($owners)
        {
            $out = '';
            foreach ($owners as $type => $ids)
            {
                $ids = array_filter($ids);
                if (empty($ids))
                {
                    continue;
                }

                $sep = '|' . $type . '_';
                $out .= $sep . implode($sep, $ids);
            }
            $out .= '|';

            return $out;
        }

        /**
         * Decode an array of users/user groups from storage in the meta table.
         */
        private function decode_owners($raw)
        {
            $owners = [];
            $tokens = explode('|', $raw);
            foreach ($tokens as $t)
            {
                if (empty($t))
                {
                    continue;
                }

                $owner = explode('_', $t, 2);
                $type = $owner[0];
                $id = $owner[1];
                if (empty($id) || empty($type) || count($owner) !== 2)
                {
                    continue;
                }

                if (!isset($owners[$type]))
                {
                    $owners[$type] = [];
                }

                $owners[$type][] = $id;
            }

            return $owners;
        }

        /*------- PRINT SELECTION FIELDS ---------------------------------------------------------------------------------------------------------------------*/

        private function get_current_content_type()
        {
            // Find the current content type
            if (isset($_POST['cuar_post_type']))
            {
                return $_POST['cuar_post_type'];
            }

            if (isset($_GET['post_type']))
            {
                return $_GET['post_type'];
            }

            if (isset($_GET['post_type']))
            {
                return $_GET['post_type'];
            }

            global $post;
            if (!empty($post))
            {
                return get_post_type($post);
            }

            return null;
        }

        public function get_selectable_owner_types()
        {
            $owner_types = apply_filters('cuar/core/ownership/selectable-owner-types', null);
            if (null === $owner_types)
            {
                $owner_types = $this->get_owner_types();
            }
            return $owner_types;
        }

        public function print_owner_fields($owners, $field_prefix = 'cuar_owners_', $field_group = null)
        {
            $po_addon = $this;
            $owner_types = $this->get_selectable_owner_types();
            $content_type = $this->get_current_content_type();

            $template_suffix = is_admin() ? '-admin' : '-frontend';

            wp_enqueue_script(is_admin() ? 'cuar.admin' : 'cuar.frontend');
            $print_javascript = false;
            $theme_support = get_theme_support('customer-area.library.jquery.select2');
            if (is_admin()
                || $theme_support === false
                || (is_array($theme_support) && !in_array('markup', $theme_support[0], true))
            )
            {
                $this->plugin->enable_library('jquery.select2');
                $print_javascript = true;
            }

            wp_nonce_field('cuar_save_owners', 'wp_cuar_nonce_owner');

            include($this->plugin->get_template_file_path(
                CUAR_INCLUDES_DIR . '/core-addons/post-owner',
                [
                    'post-owner-fields' . $template_suffix . '.template.php',
                    'post-owner-fields.template.php',
                ]
            ));
        }

        public function print_owner_fields_readonly($owners, $field_prefix = 'cuar_owners_')
        {
            wp_nonce_field('cuar_save_owners', 'wp_cuar_nonce_owner');

            include($this->plugin->get_template_file_path(
                CUAR_INCLUDES_DIR . '/core-addons/post-owner',
                'post-owner-fields-readonly.template.php'
            ));
        }

        /**
         * Get the owner details (id and type) from HTTP POST data
         *
         * @param string $ids_field_prefix
         *
         * @return array
         */
        public function get_owners_from_post_data($ids_field_prefix = 'cuar_owners_')
        {
            $owners = [];

            $owner_types = $this->get_owner_types();
            foreach ($owner_types as $owner_type => $type_label)
            {
                $ids_field_name = $ids_field_prefix . $owner_type;

                // If no owner selected, just skip this type
                if (!isset($_POST[$ids_field_name]) || empty($_POST[$ids_field_name]))
                {
                    continue;
                }

                $owners[$owner_type] = is_array($_POST[$ids_field_name])
                    ? $_POST[$ids_field_name]
                    : [$_POST[$ids_field_name]];
            }

            return $owners;
        }

        /**
         * Get the owner details (id and type) from HTTP GET data
         *
         * @return array
         */
        public function get_owners_from_get_data()
        {
            $owners = [];
            $current_type = '';
            $counter = 1;

            $given_owners = get_query_var(self::$META_OWNER_QUERY_VAR) ?: '';
            $given_owners = sanitize_text_field(wp_strip_all_tags($given_owners));
            $given_owners = explode(',', $given_owners);

            foreach ($given_owners as $given_owner)
            {
                if ($counter & 1)
                {
                    $current_type = is_string($given_owner) && $this->is_valid_owner_type($given_owner)
                        ? $given_owner : '';
                }
                else
                {
                    if ($current_type !== '')
                    {
                        $owners[$current_type][] = $given_owner;
                    }
                }
                $counter++;
            }

            return $owners;
        }

        /*------- FRONTEND ----------------------------------------------------------------------------------------------*/

        /**
         * Get owner sub-menu links
         *
         * @param $owner_id
         * @param $owner_type
         *
         * @return mixed
         */
        public function get_owner_submenu_links($owner_id, $owner_type)
        {
            return apply_filters('cuar/core/ownership/owner-submenu-items', [], $owner_id, $owner_type);
        }

        /**
         * Print owner button with its submenu
         *
         * @param $owner_id
         * @param $owner_type
         *
         * @return void
         */
        public function print_owner_link($owner_id, $owner_type)
        {
            if (!$this->is_valid_owner_type($owner_type))
            {
                return;
            }

            $owner_tpl = '';
            if ($owner_type === 'usr')
            {
                $owner_tpl = 'usr';
            }
            else if ($owner_type === 'grp' || $owner_type === 'sgrp' || $owner_type === 'mgrp')
            {
                $owner_tpl = 'grp';
            }
            else if ($owner_type === 'prj')
            {
                $owner_tpl = 'prj';
            }
            else if ($owner_type === 'rol' || $owner_type === 'glo')
            {
                $owner_tpl = 'rol';
            }
            else
            {
                return;
            }

            /** @noinspection PhpIncludeInspection */
            include($this->plugin->get_template_file_path(
                CUAR_INCLUDES_DIR . '/core-addons/post-owner',
                'post-owner-link-' . $owner_tpl . '.template.php',
                'templates'));
        }

        /**
         * Protect access to single posts: only for author and owner.
         */
        public function protect_single_post_access()
        {
            $private_post_types = $this->plugin->get_content_post_types();

            // If not on a matching post type, we do nothing
            if (empty($private_post_types) || !is_singular($private_post_types))
            {
                return;
            }

            // If post is not protected, we do nothing
            $post = get_queried_object();
            if (!$this->is_post_protected($post->ID))
            {
                return;
            }

            // If not logged-in, we ask for details
            if (!is_user_logged_in())
            {
                $this->plugin->login_then_redirect_to_url(get_permalink());
            }

            // If not authorized to view the page, we bail
            $author_id = $post->post_author;
            $current_user_id = apply_filters('cuar/core/ownership/protect-single-post/override-user-id',
                get_current_user_id());

            $is_current_user_owner = $this->is_user_owner_of_post($post->ID, $current_user_id);
            if (!($is_current_user_owner || $author_id == $current_user_id
                  || current_user_can('cuar_view_any_' . get_post_type()))
            )
            {
                wp_die(__("You are not authorized to view this page", "cuar"));
                exit();
            }

            do_action('cuar/core/ownership/protect-single-post/on-access-granted', $post);
        }

        /*------- OTHER FUNCTIONS ---------------------------------------------------------------------------------------*/

        /**
         * Helper function to to group owners for tiles
         *
         * @param $owners
         * @return array
         */
        public function group_tile_owners($owners)
        {
            if (!isset($owners['usr']))
            {
                $owners['usr'] = [];
            }
            if (!isset($owners['glo']))
            {
                $owners['glo'] = [];
            }
            if (!isset($owners['rol']))
            {
                $owners['rol'] = [];
            }
            if (!isset($owners['grp']))
            {
                $owners['grp'] = [];
            }
            if (!isset($owners['sgrp']))
            {
                $owners['sgrp'] = [];
            }
            if (!isset($owners['mgrp']))
            {
                $owners['mgrp'] = [];
            }
            if (!isset($owners['prj']))
            {
                $owners['prj'] = [];
            }

            $grouped_tiles = [
                'usr' => [
                    'icon'   => 'user',
                    'owners' => ['usr' => $owners['usr']],
                ],
                'rol' => [
                    'icon'   => 'institution',
                    'owners' => ['glo' => $owners['glo'], 'rol' => $owners['rol']],
                ],
                'grp' => [
                    'icon'   => 'users',
                    'owners' => ['grp'  => $owners['grp'],
                                 'mgrp' => $owners['mgrp'],
                                 'sgrp' => $owners['sgrp']],
                ],
                'prj' => [
                    'icon'   => 'suitcase',
                    'owners' => ['prj' => $owners['prj']],
                ]];

            $grouped_tiles['usr']['head'] = sprintf(_n('%s user', '%s users', $this->count_grouped_tile_owners
            ($grouped_tiles['usr']['owners']), 'cuar'), $this->count_grouped_tile_owners
            ($grouped_tiles['usr']['owners']));

            $grouped_tiles['rol']['head'] = sprintf(_n('%s role', '%s roles', $this->count_grouped_tile_owners
            ($grouped_tiles['rol']['owners']), 'cuar'), $this->count_grouped_tile_owners
            ($grouped_tiles['rol']['owners']));

            $grouped_tiles['grp']['head'] = sprintf(_n('%s group', '%s groups', $this->count_grouped_tile_owners
            ($grouped_tiles['grp']['owners']), 'cuar'), $this->count_grouped_tile_owners
            ($grouped_tiles['grp']['owners']));

            $grouped_tiles['prj']['head'] = sprintf(_n('%s project', '%s projects', $this->count_grouped_tile_owners
            ($grouped_tiles['prj']['owners']), 'cuar'), $this->count_grouped_tile_owners
            ($grouped_tiles['prj']['owners']));

            return $grouped_tiles;
        }

        /**
         * Helper function to count grouped owners for tiles
         *
         * @param $owners
         * @return int
         */
        public function count_grouped_tile_owners($owners)
        {
            if (!is_array($owners))
            {
                return 1;
            }
            return count($owners, COUNT_RECURSIVE) - count($owners);
        }

        /**
         * When the plugin is upgraded
         *
         * @param string $from_version
         * @param string $to_version
         */
        public function plugin_version_upgrade($from_version, $to_version)
        {
        }
    }

// Make sure the addon is loaded
    new CUAR_PostOwnerAddOn();

endif; // if (!class_exists('CUAR_PrivateFileAddOn')) 
