<?php
/**
 * @file classes/services/PKPSiteService.php
 *
 * Copyright (c) 2014-2021 Simon Fraser University
 * Copyright (c) 2000-2021 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class PKPSiteService
 *
 * @ingroup services
 *
 * @brief Helper class that encapsulates business logic for the overall site
 */

namespace PKP\services;

use APP\core\Application;
use APP\core\Request;
use APP\core\Services;
use APP\file\PublicFileManager;
use PKP\core\Core;
use PKP\db\DAORegistry;
use PKP\file\TemporaryFile;
use PKP\file\TemporaryFileManager;
use PKP\plugins\Hook;
use PKP\plugins\PluginRegistry;
use PKP\services\interfaces\EntityPropertyInterface;
use PKP\services\interfaces\EntityWriteInterface;
use PKP\site\Site;
use PKP\site\SiteDAO;
use PKP\validation\ValidatorFactory;

class PKPSiteService implements EntityPropertyInterface
{
    /**
     * @copydoc \PKP\services\interfaces\EntityPropertyInterface::getProperties()
     *
     * @param null|mixed $args
     */
    public function getProperties($site, $props, $args = null)
    {
        $request = $args['request'];
        $router = $request->getRouter();
        $dispatcher = $request->getDispatcher();

        $values = [];
        foreach ($props as $prop) {
            $values[$prop] = $site->getData($prop);
        }

        $values = Services::get('schema')->addMissingMultilingualValues(PKPSchemaService::SCHEMA_SITE, $values, $site->getSupportedLocales());

        Hook::call('Site::getProperties', [&$values, $site, $props, $args]);

        ksort($values);

        return $values;
    }

    /**
     * @copydoc \PKP\services\interfaces\EntityPropertyInterface::getSummaryProperties()
     *
     * @param null|mixed $args
     */
    public function getSummaryProperties($site, $args = null)
    {
        return $this->getFullProperties($site, $args);
    }

    /**
     * @copydoc \PKP\services\interfaces\EntityPropertyInterface::getFullProperties()
     *
     * @param null|mixed $args
     */
    public function getFullProperties($site, $args = null)
    {
        $props = Services::get('schema')->getFullProps(PKPSchemaService::SCHEMA_SITE);

        return $this->getProperties($site, $props, $args);
    }

    /**
     * Validate the properties of a site
     *
     * Passes the properties through the SchemaService to validate them, and
     * performs any additional checks needed to validate a site.
     *
     * This does NOT authenticate the current user to perform the action.
     *
     * @param array $props The data to validate
     * @param array $allowedLocales Which locales are allowed for this context
     * @param string $primaryLocale
     *
     * @return array List of error messages. The array keys are property names
     */
    public function validate($props, $allowedLocales, $primaryLocale)
    {
        $schemaService = Services::get('schema');

        $validator = ValidatorFactory::make(
            $props,
            $schemaService->getValidationRules(PKPSchemaService::SCHEMA_SITE, $allowedLocales),
            [
                'primaryLocale.regex' => __('validator.localeKey'),
                'supportedLocales.regex' => __('validator.localeKey'),
            ]
        );

        // Check required fields
        ValidatorFactory::required(
            $validator,
            EntityWriteInterface::VALIDATE_ACTION_EDIT,
            $schemaService->getRequiredProps(PKPSchemaService::SCHEMA_PUBLICATION),
            $schemaService->getMultilingualProps(PKPSchemaService::SCHEMA_PUBLICATION),
            $allowedLocales,
            $primaryLocale
        );

        // Check for input from disallowed locales
        ValidatorFactory::allowedLocales(
            $validator,
            $schemaService->getMultilingualProps(PKPSchemaService::SCHEMA_SITE),
            $allowedLocales
        );

        // If a new file has been uploaded, check that the temporary file exists and
        // the current user owns it
        $user = Application::get()->getRequest()->getUser();
        ValidatorFactory::temporaryFilesExist(
            $validator,
            ['pageHeaderTitleImage', 'styleSheet'],
            ['pageHeaderTitleImage'],
            $props,
            $allowedLocales,
            $user ? $user->getId() : null
        );

        // If sidebar blocks are passed, ensure the block plugin exists and is
        // enabled
        $validator->after(function ($validator) use ($props) {
            if (!empty($props['sidebar']) && !$validator->errors()->get('sidebar')) {
                $plugins = PluginRegistry::loadCategory('blocks', true);
                foreach ($props['sidebar'] as $pluginName) {
                    if (empty($plugins[$pluginName])) {
                        $validator->errors()->add('sidebar', __('manager.setup.layout.sidebar.invalidBlock', ['name' => $pluginName]));
                    }
                }
            }
        });

        // Ensure the theme plugin is installed and enabled
        $validator->after(function ($validator) use ($props) {
            if (!empty($props['themePluginPath']) && !$validator->errors()->get('themePluginPath')) {
                $plugins = PluginRegistry::loadCategory('themes', true);
                $found = false;
                foreach ($plugins as $plugin) {
                    if ($props['themePluginPath'] === $plugin->getDirName()) {
                        $found = true;
                        break;
                    }
                }
                if (!$found) {
                    $validator->errors()->add('themePluginPath', __('manager.setup.theme.notFound'));
                }
            }
        });

        $validator->after(function ($validator) use ($props) {
            if (!empty($props['isSiteSushiPlatform']) && $props['isSiteSushiPlatform']) {
                if (empty($props['sushiPlatformID'])) {
                    $validator->errors()->add('sushiPlatformID', __('admin.settings.statistics.sushiPlatform.sushiPlatformID.required'));
                }
            }
        });

        if ($validator->fails()) {
            $errors = $schemaService->formatValidationErrors($validator->errors());
        }

        Hook::call('Site::validate', [&$errors, $props, $allowedLocales, $primaryLocale]);

        return $errors;
    }

    /**
     * Edit the site
     *
     * This does not check if the user is authorized to edit the site, or validate or sanitize
     * the new content.
     *
     * @param Site $site The context to edit
     * @param array $params Key/value array of new data
     * @param Request $request
     *
     * @return Site
     */
    public function edit($site, $params, $request)
    {
        $siteDao = DAORegistry::getDAO('SiteDAO'); /** @var SiteDAO $siteDao */

        // Move uploaded files into place and update the params
        $userId = $request->getUser() ? $request->getUser()->getId() : null;
        $supportedLocales = $site->getSupportedLocales();
        if (array_key_exists('pageHeaderTitleImage', $params)) {
            foreach ($supportedLocales as $localeKey) {
                if (!array_key_exists($localeKey, $params['pageHeaderTitleImage'])) {
                    continue;
                }
                $params['pageHeaderTitleImage'][$localeKey] = $this->_saveFileParam($site, $params['pageHeaderTitleImage'][$localeKey], 'pageHeaderTitleImage', $userId, $localeKey, true);
            }
        }
        if (array_key_exists('styleSheet', $params)) {
            $params['styleSheet'] = $this->_saveFileParam($site, $params['styleSheet'], 'styleSheet', $userId);
        }

        $newSite = $siteDao->newDataObject();
        $newSite->_data = array_merge($site->_data, $params);

        Hook::call('Site::edit', [&$newSite, $site, $params, $request]);

        $siteDao->updateObject($newSite);
        $newSite = $siteDao->getSite();

        return $newSite;
    }

    /**
     * Move a temporary file to the site's public directory
     *
     * @param Site $context
     * @param TemporaryFile $temporaryFile
     * @param string $fileNameBase Unique identifier to use for the filename. The
     *  Extension and locale will be appended.
     * @param int $userId ID of the user who uploaded the temporary file
     * @param string $localeKey Example: en. Leave empty for a file that is
     *  not localized.
     *
     * @return string|boolean The new filename or false on failure
     */
    public function moveTemporaryFile($site, $temporaryFile, $fileNameBase, $userId, $localeKey = '')
    {
        $publicFileManager = new PublicFileManager();
        $temporaryFileManager = new TemporaryFileManager();

        $fileName = $fileNameBase;
        if ($localeKey) {
            $fileName .= '_' . $localeKey;
        }

        $extension = $publicFileManager->getDocumentExtension($temporaryFile->getFileType());
        if (!$extension) {
            $extension = $publicFileManager->getImageExtension($temporaryFile->getFileType());
        }
        $fileName .= $extension;

        if (!$publicFileManager->copyFile($temporaryFile->getFilePath(), $publicFileManager->getSiteFilesPath() . '/' . $fileName)) {
            return false;
        }

        $temporaryFileManager->deleteById($temporaryFile->getId(), $userId);

        return $fileName;
    }

    /**
     * Handle a site setting for an uploaded file
     *
     * - Moves the temporary file to the public directory
     * - Resets the param value to what is expected to be stored in the db
     *
     * This method is protected because all operations which edit the site should
     * go through the editSite method in order to ensure that the appropriate hooks are fired.
     *
     * @param Site $site The site being edited
     * @param mixed $value The param value to be saved. Contains the temporary
     *  file ID if a new file has been uploaded.
     * @param string $settingName The name of the setting to save, typically used
     *  in the filename.
     * @param int $userId ID of the user who owns the temporary file
     * @param string $localeKey Optional. Used in the filename for multilingual
     *  properties.
     * @param bool $isImage Optional. For image files which return alt text,
     *  width, height, etc in the param value.
     *
     * @return string|array|null New param value or null on failure
     */
    protected function _saveFileParam($site, $value, $settingName, $userId, $localeKey = '', $isImage = false)
    {
        $temporaryFileManager = new TemporaryFileManager();

        // If the value is null, clean up any existing file in the system
        if (is_null($value)) {
            $setting = $site->getData($settingName, $localeKey);
            if ($setting) {
                $fileName = $isImage ? $setting['uploadName'] : $setting;
                $publicFileManager = new PublicFileManager();
                $publicFileManager->removeSiteFile($fileName);
            }
            return null;
        }

        // Check if there is something to upload
        if (empty($value['temporaryFileId'])) {
            return $value;
        }

        $temporaryFile = $temporaryFileManager->getFile((int) $value['temporaryFileId'], $userId);
        $fileName = $this->moveTemporaryFile($site, $temporaryFile, $settingName, $userId, $localeKey);

        if ($fileName) {
            // Get the details for image uploads
            if ($isImage) {
                $publicFileManager = new PublicFileManager();

                [$width, $height] = getimagesize($publicFileManager->getSiteFilesPath() . '/' . $fileName);
                $altText = !empty($value['altText']) ? $value['altText'] : '';

                return [
                    'originalFilename' => $temporaryFile->getOriginalFileName(),
                    'uploadName' => $fileName,
                    'width' => $width,
                    'height' => $height,
                    'dateUploaded' => Core::getCurrentDate(),
                    'altText' => $altText,
                ];
            } else {
                return [
                    'originalFilename' => $temporaryFile->getOriginalFileName(),
                    'uploadName' => $fileName,
                    'dateUploaded' => Core::getCurrentDate(),
                ];
            }
        }

        return false;
    }
}
