<?php

/**
 * @package     Joomla.Site
 * @subpackage  com_content
 *
 * @copyright   (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\Component\Content\Site\Model;

use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Multilanguage;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\ItemModel;
use Joomla\CMS\Table\Table;
use Joomla\Component\Content\Administrator\Extension\ContentComponent;
use Joomla\Database\ParameterType;
use Joomla\Registry\Registry;
use Joomla\Utilities\IpHelper;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Content Component Article Model
 *
 * @since  1.5
 */
class ArticleModel extends ItemModel
{
    /**
     * Model context string.
     *
     * @var        string
     */
    protected $_context = 'com_content.article';

    /**
     * Method to auto-populate the model state.
     *
     * Note. Calling getState in this method will result in recursion.
     *
     * @since   1.6
     *
     * @return void
     */
    protected function populateState()
    {
        $app = Factory::getApplication();

        // Load state from the request.
        $pk = $app->getInput()->getInt('id');
        $this->setState('article.id', $pk);

        $offset = $app->getInput()->getUint('limitstart');
        $this->setState('list.offset', $offset);

        // Load the parameters.
        $params = $app->getParams();
        $this->setState('params', $params);

        $user = $this->getCurrentUser();

        // If $pk is set then authorise on complete asset, else on component only
        $asset = empty($pk) ? 'com_content' : 'com_content.article.' . $pk;

        if ((!$user->authorise('core.edit.state', $asset)) && (!$user->authorise('core.edit', $asset))) {
            $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED);
            $this->setState('filter.archived', ContentComponent::CONDITION_ARCHIVED);
        }

        $this->setState('filter.language', Multilanguage::isEnabled());
    }

    /**
     * Method to get article data.
     *
     * @param   integer  $pk  The id of the article.
     *
     * @return  object|boolean  Menu item data object on success, boolean false
     */
    public function getItem($pk = null)
    {
        $user = $this->getCurrentUser();

        $pk = (int) ($pk ?: $this->getState('article.id'));

        if ($this->_item === null) {
            $this->_item = [];
        }

        if (!isset($this->_item[$pk])) {
            try {
                $db    = $this->getDatabase();
                $query = $db->getQuery(true);

                $query->select(
                    $this->getState(
                        'item.select',
                        [
                            $db->quoteName('a.id'),
                            $db->quoteName('a.asset_id'),
                            $db->quoteName('a.title'),
                            $db->quoteName('a.alias'),
                            $db->quoteName('a.introtext'),
                            $db->quoteName('a.fulltext'),
                            $db->quoteName('a.state'),
                            $db->quoteName('a.catid'),
                            $db->quoteName('a.created'),
                            $db->quoteName('a.created_by'),
                            $db->quoteName('a.created_by_alias'),
                            $db->quoteName('a.modified'),
                            $db->quoteName('a.modified_by'),
                            $db->quoteName('a.checked_out'),
                            $db->quoteName('a.checked_out_time'),
                            $db->quoteName('a.publish_up'),
                            $db->quoteName('a.publish_down'),
                            $db->quoteName('a.images'),
                            $db->quoteName('a.urls'),
                            $db->quoteName('a.attribs'),
                            $db->quoteName('a.version'),
                            $db->quoteName('a.ordering'),
                            $db->quoteName('a.metakey'),
                            $db->quoteName('a.metadesc'),
                            $db->quoteName('a.access'),
                            $db->quoteName('a.hits'),
                            $db->quoteName('a.metadata'),
                            $db->quoteName('a.featured'),
                            $db->quoteName('a.language'),
                        ]
                    )
                )
                    ->select(
                        [
                            $db->quoteName('fp.featured_up'),
                            $db->quoteName('fp.featured_down'),
                            $db->quoteName('c.title', 'category_title'),
                            $db->quoteName('c.alias', 'category_alias'),
                            $db->quoteName('c.access', 'category_access'),
                            $db->quoteName('c.language', 'category_language'),
                            $db->quoteName('fp.ordering'),
                            $db->quoteName('u.name', 'author'),
                            $db->quoteName('parent.title', 'parent_title'),
                            $db->quoteName('parent.id', 'parent_id'),
                            $db->quoteName('parent.path', 'parent_route'),
                            $db->quoteName('parent.alias', 'parent_alias'),
                            $db->quoteName('parent.language', 'parent_language'),
                            'ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 1) AS '
                                . $db->quoteName('rating'),
                            $db->quoteName('v.rating_count', 'rating_count'),
                        ]
                    )
                    ->from($db->quoteName('#__content', 'a'))
                    ->join(
                        'INNER',
                        $db->quoteName('#__categories', 'c'),
                        $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')
                    )
                    ->join('LEFT', $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id'))
                    ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by'))
                    ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id'))
                    ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id'))
                    ->where(
                        [
                            $db->quoteName('a.id') . ' = :pk',
                            $db->quoteName('c.published') . ' > 0',
                        ]
                    )
                    ->bind(':pk', $pk, ParameterType::INTEGER);

                // Filter by language
                if ($this->getState('filter.language')) {
                    $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING);
                }

                if (
                    !$user->authorise('core.edit.state', 'com_content.article.' . $pk)
                    && !$user->authorise('core.edit', 'com_content.article.' . $pk)
                ) {
                    // Filter by start and end dates.
                    $nowDate = Factory::getDate()->toSql();

                    $query->extendWhere(
                        'AND',
                        [
                            $db->quoteName('a.publish_up') . ' IS NULL',
                            $db->quoteName('a.publish_up') . ' <= :publishUp',
                        ],
                        'OR'
                    )
                        ->extendWhere(
                            'AND',
                            [
                                $db->quoteName('a.publish_down') . ' IS NULL',
                                $db->quoteName('a.publish_down') . ' >= :publishDown',
                            ],
                            'OR'
                        )
                        ->bind([':publishUp', ':publishDown'], $nowDate);
                }

                // Filter by published state.
                $published = $this->getState('filter.published');
                $archived  = $this->getState('filter.archived');

                if (is_numeric($published)) {
                    $query->whereIn($db->quoteName('a.state'), [(int) $published, (int) $archived]);
                }

                $db->setQuery($query);

                $data = $db->loadObject();

                if (empty($data)) {
                    throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404);
                }

                // Check for published state if filter set.
                if ((is_numeric($published) || is_numeric($archived)) && ($data->state != $published && $data->state != $archived)) {
                    throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404);
                }

                // Convert parameter fields to objects.
                $registry = new Registry($data->attribs);

                $data->params = clone $this->getState('params');
                $globalParams = ComponentHelper::getParams('com_content', true);

                /**
                 * For menu item parameters set to use_article, we will take value from article option and fallback
                 * to global value if the article option set to Use Global
                 */
                foreach ($data->params->toArray() as $key => $value) {
                    if ($value === 'use_article') {
                        $data->params->set($key, $registry->get($key, $globalParams->get($key)));
                    }
                }

                $data->params->merge($registry);

                $data->metadata = new Registry($data->metadata);

                // Technically guest could edit an article, but lets not check that to improve performance a little.
                if (!$user->guest) {
                    $userId = $user->id;
                    $asset  = 'com_content.article.' . $data->id;

                    // Check general edit permission first.
                    if ($user->authorise('core.edit', $asset)) {
                        $data->params->set('access-edit', true);
                    } elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) {
                        // Now check if edit.own is available.
                        // Check for a valid user and that they are the owner.
                        if ($userId == $data->created_by) {
                            $data->params->set('access-edit', true);
                        }
                    }
                }

                // Compute view access permissions.
                if ($this->getState('filter.access')) {
                    // If the access filter has been set, we already know this user can view.
                    $data->params->set('access-view', true);
                } else {
                    // If no access filter is set, the layout takes some responsibility for display of limited information.
                    $user   = $this->getCurrentUser();
                    $groups = $user->getAuthorisedViewLevels();

                    if ($data->catid == 0 || $data->category_access === null) {
                        $data->params->set('access-view', \in_array($data->access, $groups));
                    } else {
                        $data->params->set('access-view', \in_array($data->access, $groups) && \in_array($data->category_access, $groups));
                    }
                }

                $this->_item[$pk] = $data;
            } catch (\Exception $e) {
                if ($e->getCode() == 404) {
                    // Need to go through the error handler to allow Redirect to work.
                    throw $e;
                }

                $this->setError($e);
                $this->_item[$pk] = false;
            }
        }

        return $this->_item[$pk];
    }

    /**
     * Increment the hit counter for the article.
     *
     * @param   integer  $pk  Optional primary key of the article to increment.
     *
     * @return  boolean  True if successful; false otherwise and internal error set.
     */
    public function hit($pk = 0)
    {
        $input    = Factory::getApplication()->getInput();
        $hitcount = $input->getInt('hitcount', 1);

        if ($hitcount) {
            $pk = (!empty($pk)) ? $pk : (int) $this->getState('article.id');

            $table = Table::getInstance('Content', '\\Joomla\\CMS\\Table\\');
            $table->hit($pk);
        }

        return true;
    }

    /**
     * Save user vote on article
     *
     * @param   integer  $pk    Joomla Article Id
     * @param   integer  $rate  Voting rate
     *
     * @return  boolean          Return true on success
     */
    public function storeVote($pk = 0, $rate = 0)
    {
        $pk   = (int) $pk;
        $rate = (int) $rate;

        if ($rate >= 1 && $rate <= 5 && $pk > 0) {
            $userIP = IpHelper::getIp();

            // Initialize variables.
            $db    = $this->getDatabase();
            $query = $db->getQuery(true);

            // Create the base select statement.
            $query->select('*')
                ->from($db->quoteName('#__content_rating'))
                ->where($db->quoteName('content_id') . ' = :pk')
                ->bind(':pk', $pk, ParameterType::INTEGER);

            // Set the query and load the result.
            $db->setQuery($query);

            // Check for a database error.
            try {
                $rating = $db->loadObject();
            } catch (\RuntimeException $e) {
                Factory::getApplication()->enqueueMessage($e->getMessage(), 'error');

                return false;
            }

            // There are no ratings yet, so lets insert our rating
            if (!$rating) {
                $query = $db->getQuery(true);

                // Create the base insert statement.
                $query->insert($db->quoteName('#__content_rating'))
                    ->columns(
                        [
                            $db->quoteName('content_id'),
                            $db->quoteName('lastip'),
                            $db->quoteName('rating_sum'),
                            $db->quoteName('rating_count'),
                        ]
                    )
                    ->values(':pk, :ip, :rate, 1')
                    ->bind(':pk', $pk, ParameterType::INTEGER)
                    ->bind(':ip', $userIP)
                    ->bind(':rate', $rate, ParameterType::INTEGER);

                // Set the query and execute the insert.
                $db->setQuery($query);

                try {
                    $db->execute();
                } catch (\RuntimeException $e) {
                    Factory::getApplication()->enqueueMessage($e->getMessage(), 'error');

                    return false;
                }
            } else {
                if ($userIP != $rating->lastip) {
                    $query = $db->getQuery(true);

                    // Create the base update statement.
                    $query->update($db->quoteName('#__content_rating'))
                        ->set(
                            [
                                $db->quoteName('rating_count') . ' = ' . $db->quoteName('rating_count') . ' + 1',
                                $db->quoteName('rating_sum') . ' = ' . $db->quoteName('rating_sum') . ' + :rate',
                                $db->quoteName('lastip') . ' = :ip',
                            ]
                        )
                        ->where($db->quoteName('content_id') . ' = :pk')
                        ->bind(':rate', $rate, ParameterType::INTEGER)
                        ->bind(':ip', $userIP)
                        ->bind(':pk', $pk, ParameterType::INTEGER);

                    // Set the query and execute the update.
                    $db->setQuery($query);

                    try {
                        $db->execute();
                    } catch (\RuntimeException $e) {
                        Factory::getApplication()->enqueueMessage($e->getMessage(), 'error');

                        return false;
                    }
                } else {
                    return false;
                }
            }

            $this->cleanCache();

            return true;
        }

        Factory::getApplication()->enqueueMessage(Text::sprintf('COM_CONTENT_INVALID_RATING', $rate), 'error');

        return false;
    }

    /**
     * Cleans the cache of com_content and content modules
     *
     * @param   string   $group     The cache group
     * @param   integer  $clientId  No longer used, will be removed without replacement
     *                              @deprecated   4.3 will be removed in 6.0
     *
     * @return  void
     *
     * @since   3.9.9
     */
    protected function cleanCache($group = null, $clientId = 0)
    {
        parent::cleanCache('com_content');
        parent::cleanCache('mod_articles');
        parent::cleanCache('mod_articles_archive');
        parent::cleanCache('mod_articles_categories');
        parent::cleanCache('mod_articles_category');
        parent::cleanCache('mod_articles_latest');
        parent::cleanCache('mod_articles_news');
        parent::cleanCache('mod_articles_popular');
    }
}
