EscolaLMS/H5P

View on GitHub
src/Repositories/H5PRepository.php

Summary

Maintainability
F
4 days
Test Coverage
B
89%
<?php

namespace EscolaLms\HeadlessH5P\Repositories;

use EscolaLms\HeadlessH5P\Exceptions\H5PException;
use EscolaLms\HeadlessH5P\Helpers\Helpers;
use EscolaLms\HeadlessH5P\Helpers\JSONHelper;
use EscolaLms\HeadlessH5P\Models\H5PContent;
use EscolaLms\HeadlessH5P\Models\H5PContentLibrary;
use EscolaLms\HeadlessH5P\Models\H5PLibrary;
use EscolaLms\HeadlessH5P\Models\H5PLibraryDependency;
use EscolaLms\HeadlessH5P\Models\H5pLibrariesHubCache;
use EscolaLms\HeadlessH5P\Repositories\Contracts\H5PFrameworkInterface;
use EscolaLms\HeadlessH5P\Repositories\Contracts\H5PLibraryLanguageRepositoryContract;
use H5PPermission;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\DB;
use DateTime;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use JsonSerializable;


class H5PRepository implements H5PFrameworkInterface
{

    private H5PLibraryLanguageRepositoryContract $h5PLibraryLanguageRepository;

    private array $downloadFiles;

    private array $mainData;

    protected $messages = ['error' => [], 'updated' => []];

    public function __construct(H5PLibraryLanguageRepositoryContract $h5PLibraryLanguageRepository)
    {
        $this->h5PLibraryLanguageRepository = $h5PLibraryLanguageRepository;
    }

    public function setMainData(array $mainData): void
    {
        $this->mainData = $mainData;
    }

    /**
     * Returns info for the current platform.
     *
     * @return array
     *               An associative array containing:
     *               - name: The name of the platform, for instance "Wordpress"
     *               - version: The version of the platform, for instance "4.0"
     *               - h5pVersion: The version of the H5P plugin/module
     */
    public function getPlatformInfo()
    {
        return array(
            'name' => 'Wellms.io',
            'version' => '0.1.0',
            'h5pVersion' => '0.1.0',
        );
    }

    /**
     * Fetches a file from a remote server using HTTP GET.
     *
     * @param string $url      where you want to get or send data
     * @param array  $data     data to post to the URL
     * @param bool   $blocking set to 'FALSE' to instantly time out (fire and forget)
     * @param string $stream   path to where the file should be saved
     * @param bool   $fullData Return additional response data such as headers and potentially other data
     * @param array  $headers  Headers to send
     * @param array  $files    Files to send
     * @param string $method
     *
     * @return string|array|null The content (response body), or an array with data. NULL if something went wrong
     */
    public function fetchExternalData($url, $data = null, $blocking = true, $stream = null, $fullData = false, $headers = [], $files = [], $method = 'POST')
    {
        @set_time_limit(0);
        $options = [
            'timeout'  => !empty($blocking) ? 30 : 0.01,
        ];

        if (!empty($stream)) {
            $options['sink'] = $stream;
        }

        $client = new Client(config('hh5p.guzzle'));
        try {
            if ($data !== null) {
                // Post
                $options['form_params'] = $data;
                $response = $client->request('POST', $url, $options);
            } else {
                $response = $client->request('GET', $url, $options);
            }

            if ($response->getStatusCode() === 200) {
                $body = empty($response->getBody()) ? null : $response->getBody()->getContents();

                if ($body) {
                    return $fullData ? ['status' => $response->getStatusCode(), 'data' => json_decode($body)] : $body;
                }

                return ['status' => $response->getStatusCode()];
            } else {
                return null;
            }
        } catch (RequestException $e) {
            return null;
        }
    }

    /**
     * Set the tutorial URL for a library. All versions of the library is set.
     *
     * @param string $machineName
     * @param string $tutorialUrl
     */
    public function setLibraryTutorialUrl($machineName, $tutorialUrl)
    {
    }

    /**
     * Show the user an error message.
     *
     * @param string $message The error message
     * @param string $code    An optional code
     */
    public function setErrorMessage($message, $code = null)
    {
        $this->messages['error'][] = $message;
    }

    /**
     * Show the user an information message.
     *
     * @param string $message
     *                        The error message
     */
    public function setInfoMessage($message)
    {
        $this->messages['updated'][] = $message;
    }

    /**
     * Return messages.
     *
     * @param string $type 'info' or 'error'
     *
     * @return string[]
     */
    public function getMessages($type = 'error')
    {
        return !empty($this->messages[$type]) ? $this->messages[$type] : null;
    }

    /**
     * Translation function.
     *
     * @param string $message
     *                             The english string to be translated
     * @param array  $replacements
     *                             An associative array of replacements to make after translation. Incidences
     *                             of any key in this array are replaced with the corresponding value. Based
     *                             on the first character of the key, the value is escaped and/or themed:
     *                             - !variable: inserted as is
     *                             - @variable: escape plain text to HTML
     *                             - %variable: escape text and theme as a placeholder for user-submitted
     *                             content
     *
     * @return string Translated string
     *                Translated string
     */
    public function t($message, $replacements = []): string
    {
        // Insert !var as is, escape @var and emphasis %var.
        foreach ($replacements as $key => $replacement) {
            if ($key[0] === '@') {
                // $replacements[$key] = esc_html($replacement);
                $replacements[$key] = $replacement;
            } elseif ($key[0] === '%') {
                // $replacements[$key] = '<em>' . esc_html($replacement) . '</em>';
                $replacements[$key] = '<em>' . $replacement . '</em>';
            }
        }
        $message = preg_replace('/(!|@|%)[a-z0-9]+/i', '%s', $message);

        return vsprintf(__($message), $replacements);
    }

    /**
     * Get URL to file in the specific library.
     *
     * @param string $libraryFolderName
     * @param string $fileName
     *
     * @return string URL to file
     */
    public function getLibraryFileUrl($libraryFolderName, $fileName)
    {
        $path = '/h5p/libraries/' . $libraryFolderName . '/' . $fileName;
        return file_exists(storage_path($path)) ? env('APP_URL') . $path : null;
    }

    /**
     * Get the Path to the last uploaded h5p.
     *
     * @return string
     *                Path to the folder where the last uploaded h5p for this session is located
     */
    public function getUploadedH5pFolderPath()
    {
        static $dir; // such a stupid way to have singleton ....
        if (is_null($dir)) {
            $dir = storage_path('app/h5p/temp/') . uniqid('h5p-');
            @mkdir(dirname($dir), 0777, true);
        }

        return $dir;
    }

    /**
     * Get the path to the last uploaded h5p file.
     *
     * @return string
     *                Path to the last uploaded h5p
     */
    public function getUploadedH5pPath()
    {
        static $path; // such a stupid way to have singleton ....
        if (is_null($path)) {
            $path = storage_path('app/h5p/temp/') . uniqid('h5p-') . '.h5p';
            @mkdir(dirname($path), 0777, true);
        }

        return $path;
    }

    /**
     * Load addon libraries.
     *
     * @return array
     */
    public function loadAddons(): array
    {
        return H5PLibrary::query()
            ->select(['l1.id', 'l1.name', 'l1.major_version', 'l1.minor_version', 'l1.patch_version', 'l1.preloaded_js', 'l1.preloaded_css', 'l1.add_to'])
            ->from('hh5p_libraries as l1')
            ->leftJoin('hh5p_libraries as l2', fn($join) => $join
                ->on('l1.name', '=', 'l2.name')
                ->on(fn($query) => $query
                    ->on('l1.major_version', '<', 'l2.major_version')
                    ->orOn(fn ($query) => $query
                        ->orOn('l1.major_version', '=', 'l2.major_version')
                        ->on('l1.minor_version', '<', 'l2.minor_version')
                    )
                )
            )
            ->whereNotNull('l1.add_to')
            ->whereNull('l2.name')
            ->get()
            ->toArray();
    }

    /**
     * Load config for libraries.
     *
     * @param array $libraries
     *
     * @return array
     */
    public function getLibraryConfig($libraries = null)
    {
        return [];
    }

    /**
     * Get a list of the current installed libraries.
     *
     * @return array
     *               Associative array containing one entry per machine name.
     *               For each machineName there is a list of libraries(with different versions)
     */
    public function loadLibraries()
    {
        $results = H5pLibrary::select([
            'id',
            'name',
            'title',
            'major_version',
            'minor_version',
            'patch_version',
            'runnable',
            'restricted',
        ])
            ->orderBy('title', 'ASC')
            ->orderBy('major_version', 'ASC')
            ->orderBy('minor_version', 'ASC')
            ->get();

        $libraries = [];
        foreach ($results as $library) {
            $libraries[$library->name][] = $library;
        }

        return $libraries;
    }

    /**
     * Returns the URL to the library admin page.
     *
     * @return string
     *                URL to admin page
     */
    public function getAdminUrl()
    {
        return '';
    }

    /**
     * Get id to an existing library.
     * If version number is not specified, the newest version will be returned.
     *
     * @param string $machineName
     *                             The librarys machine name
     * @param int    $majorVersion
     *                             Optional major version number for library
     * @param int    $minorVersion
     *                             Optional minor version number for library
     *
     * @return int
     *             The id of the specified library or FALSE
     */
    public function getLibraryId($machineName, $majorVersion = null, $minorVersion = null)
    {
        $where = H5PLibrary::where('name', $machineName);

        if ($majorVersion !== null) {
            $where->where('major_version', $majorVersion);
            if ($minorVersion !== null) {
                $where->where('minor_version', $minorVersion);
            }
        }

        $return = $where->orderBy('major_version', 'DESC')
            ->orderBy('minor_version', 'DESC')
            ->orderBy('patch_version', 'DESC')
            ->first();

        return $return === null ? false : $return->id;
    }

    /**
     * Get file extension whitelist.
     *
     * The default extension list is part of h5p, but admins should be allowed to modify it
     *
     * @param bool   $isLibrary
     *                                        TRUE if this is the whitelist for a library. FALSE if it is the whitelist
     *                                        for the content folder we are getting
     * @param string $defaultContentWhitelist
     *                                        A string of file extensions separated by whitespace
     * @param string $defaultLibraryWhitelist
     *                                        A string of file extensions separated by whitespace
     */
    public function getWhitelist($isLibrary, $defaultContentWhitelist, $defaultLibraryWhitelist)
    {
        $whitelist = $defaultContentWhitelist;
        if ($isLibrary) {
            $whitelist .= ' ' . $defaultLibraryWhitelist;
        }

        return $whitelist;
    }

    /**
     * Is the library a patched version of an existing library?
     *
     * @param object $library
     *                        An associative array containing:
     *                        - machineName: The library machineName
     *                        - majorVersion: The librarys majorVersion
     *                        - minorVersion: The librarys minorVersion
     *                        - patchVersion: The librarys patchVersion
     *
     * @return bool
     *              TRUE if the library is a patched version of an existing library
     *              FALSE otherwise
     */
    public function isPatchedLibrary($library)
    {
        return true;
    }

    /**
     * Is H5P in development mode?
     *
     * @return bool
     *              TRUE if H5P development mode is active
     *              FALSE otherwise
     */
    public function isInDevMode()
    {
        return true;
    }

    /**
     * Is the current user allowed to update libraries?
     *
     * @return bool
     *              TRUE if the user is allowed to update libraries
     *              FALSE if the user is not allowed to update libraries
     */
    public function mayUpdateLibraries()
    {
        return true;
    }

    /**
     * Convert list of file paths to csv.
     *
     * @param array  $library
     *                        Library data as found in library.json files
     * @param string $key
     *                        Key that should be found in $libraryData
     *
     * @return string
     *                file paths separated by ', '
     */
    private function pathsToCsv($library, $key)
    {
        if (isset($library[$key])) {
            $paths = [];
            foreach ($library[$key] as $file) {
                $paths[] = $file['path'];
            }

            return implode(', ', $paths);
        }

        return '';
    }

    /**
     * Store data about a library.
     *
     * Also fills in the libraryId in the libraryData object if the object is new
     *
     * @param object $libraryData
     *                            Associative array containing:
     *                            - libraryId: The id of the library if it is an existing library.
     *                            - title: The library's name
     *                            - machineName: The library machineName
     *                            - majorVersion: The library's majorVersion
     *                            - minorVersion: The library's minorVersion
     *                            - patchVersion: The library's patchVersion
     *                            - runnable: 1 if the library is a content type, 0 otherwise
     *                            - metadataSettings: Associative array containing:
     *                            - disable: 1 if the library should not support setting metadata (copyright etc)
     *                            - disableExtraTitleField: 1 if the library don't need the extra title field
     *                            - fullscreen(optional): 1 if the library supports fullscreen, 0 otherwise
     *                            - embedTypes(optional): list of supported embed types
     *                            - preloadedJs(optional): list of associative arrays containing:
     *                            - path: path to a js file relative to the library root folder
     *                            - preloadedCss(optional): list of associative arrays containing:
     *                            - path: path to css file relative to the library root folder
     *                            - dropLibraryCss(optional): list of associative arrays containing:
     *                            - machineName: machine name for the librarys that are to drop their css
     *                            - semantics(optional): Json describing the content structure for the library
     *                            - language(optional): associative array containing:
     *                            - languageCode: Translation in json format
     * @param bool   $new
     *
     */
    public function saveLibraryData(&$libraryData, $new = true)
    {
        $library = [
            'name' => $libraryData['machineName'],
            'title' => $libraryData['title'],
            'major_version' => $libraryData['majorVersion'],
            'minor_version' => $libraryData['minorVersion'],
            'patch_version' => $libraryData['patchVersion'],
            'runnable' => $libraryData['runnable'],
            'fullscreen' => isset($libraryData['fullscreen']) ? $libraryData['fullscreen'] : 0,
            'embed_types' => isset($libraryData['embedTypes']) ? implode(', ', $libraryData['embedTypes']) : '',
            'preloaded_js' => $this->pathsToCsv($libraryData, 'preloadedJs'),
            'preloaded_css' => $this->pathsToCsv($libraryData, 'preloadedCss'),
            'drop_library_css' => '',
            'semantics' => isset($libraryData['semantics']) ? $libraryData['semantics'] : '',
            'tutorial_url' => isset($libraryData['tutorial_url']) ?: '',
            'has_icon' => isset($libraryData['hasIcon']) ? 1 : 0,
            'add_to' => isset($libraryData['addTo']) ? json_encode($libraryData['addTo']) : null
        ];

        $libObj = H5PLibrary::firstOrCreate($library);
        $library['libraryId'] = $libObj->id;
        $this->deleteLibraryDependencies($library['libraryId']);

        if (isset($libraryData['language'])) {
            foreach ($libraryData['language'] as $languageCode => $translation) {
                $this->h5PLibraryLanguageRepository->createDefaults($libObj, $languageCode, $translation);
            }
            $this->additionalTranslations($libraryData['language'], $libObj);
        }

        // This is essential, as this method should mutate the `libraryData`
        // I hate mutations

        $libraryData = array_merge($libraryData, $library);
    }

    private function additionalTranslations(array $libraryLanguages, H5PLibrary $library): array
    {
        $additionalLanguageCodes = ['pl'];
        foreach ($additionalLanguageCodes as $languageCode) {
            if (!isset($libraryLanguages[$languageCode])) {
                $libraryLanguage = $this->h5PLibraryLanguageRepository->create($library, $languageCode);

                if (isset($libraryLanguage)) {
                    $libraryLanguages[$languageCode] = $libraryLanguage;
                }
            }
        }

        return $libraryLanguages;
    }

    /**
     * Insert new content.
     *
     * @param array $content
     *                             An associative array containing:
     *                             - id: The content id
     *                             - params: The content in json format
     *                             - library: An associative array containing:
     *                             - libraryId: The id of the main library for this content
     * @param int   $contentMainId
     *                             Main id for the content if this is a system that supports versions
     */
    public function insertContent($content, $contentMainId = null)
    {
        return $this->updateContent($content, $contentMainId);
    }

    private function fixContentParamsMetadataLibraryTitle($content)
    {
        $defaultTitle = isset($this->mainData['title']) ? $this->mainData['title'] : 'New Content (from file)';

        if (is_array($content['library'])) {
            $content['library_id'] = isset($content['library']['libraryId']) ? $content['library']['libraryId'] : $content['library']['id'];
            $lib = $this->loadLibrary($content['library']['machineName'], $content['library']['majorVersion'], $content['library']['minorVersion']);
            $content['embed_type'] = $lib['embed_types'];
        }

        // `parameters` is string, encode
        if (isset($content['parameters']) && is_string($content['parameters'])) {
            $parameters = json_decode($content['parameters']);

            if (is_array($parameters) && isset($parameters['metadata'])) {
                $metadata = $parameters['metadata'];
            }
            if (is_object($parameters) && isset($parameters->metadata)) {
                $metadata = $parameters->metadata;
            }
            if (is_object($parameters) && isset($parameters->params)) {
                $parameters = $parameters->params;
            }
            if (is_array($parameters) && isset($parameters['params'])) {
                $parameters = $parameters['params'];
            }
        }

        // `params` is string, encode
        if (!isset($parameters) && is_string($content['params'])) {
            $parameters = json_decode($content['params']);

            if (is_object($parameters) && isset($parameters->metadata)) {
                $metadata = $parameters->metadata;
            }
            if (is_array($parameters) && isset($parameters['metadata'])) {
                $metadata = $parameters['metadata'];
            }
            if (is_object($parameters) && isset($parameters->params)) {
                $parameters = $parameters->params;
            }
            if (is_array($parameters) && isset($parameters['params'])) {
                $parameters = $parameters['params'];
            }

        }
        // `params is array
        if (!isset($parameters) && is_array($content['params'])) {
            if (is_array($content['params']['params'])) {
                $parameters = $content['params']['params'];
            } else {
                $parameters = $content['params'];
            }
        }

        if (!isset($content['nonce'])) {
            $content['nonce'] = bin2hex(random_bytes(4));
        }

        if (!isset($metadata)) {
            $metadata = ['license' => 'U', 'authors' => [], 'changes' => [], 'extraTitle' => $defaultTitle, 'title' => $defaultTitle];
        }

        $parameters = [
            'params' => $parameters ?? [],
            'metadata' => $metadata,
        ];

        $content['parameters'] = json_encode($parameters);

        return $content;
    }

    /**
     * Update old content.
     *
     * @param array $content
     *                             An associative array containing:
     *                             - id: The content id
     *                             - params: The content in json format
     *                             - library: An associative array containing:
     *                             - libraryId: The id of the main library for this content
     * @param int   $contentMainId
     *                             Main id for the content if this is a system that supports versions
     */
    public function updateContent($content, $contentMainId = null)
    {
        $content = $this->fixContentParamsMetadataLibraryTitle($content);
        if (isset($content['id'])) {
            H5PContent::findOrFail($content['id'])->update($content);

            return $content['id'];
        } else {
            unset($content['params']);
            unset($content['library']);

            $newContent = H5PContent::create($content);
            return $newContent->id;
        }
    }

    /**
     * Resets marked user data for the given content.
     *
     * @param int $contentId
     */
    public function resetContentUserData($contentId)
    {
    }

    /**
     * Save what libraries a library is depending on.
     *
     * @param int    $libraryId
     *                                Library Id for the library we're saving dependencies for
     * @param array  $dependencies
     *                                List of dependencies as associative arrays containing:
     *                                - machineName: The library machineName
     *                                - majorVersion: The library's majorVersion
     *                                - minorVersion: The library's minorVersion
     * @param string $dependency_type
     *                                What type of dependency this is, the following values are allowed:
     *                                - editor
     *                                - preloaded
     *                                - dynamic
     */
    public function saveLibraryDependencies($libraryId, $dependencies, $dependency_type)
    {
        foreach ($dependencies as $dependency) {
            $dbLib = H5PLibrary::where([
                'name' => $dependency['machineName'],
                'major_version' => $dependency['majorVersion'],
                'minor_version' => $dependency['minorVersion'],
            ])->latest()->firstOrFail();

            H5PLibraryDependency::firstOrCreate([
                'library_id' => $libraryId,
                'required_library_id' => $dbLib->id,
                'dependency_type' => $dependency_type,
            ]);
        }

        return true;
    }

    /**
     * Give an H5P the same library dependencies as a given H5P.
     *
     * @param int $contentId
     *                           Id identifying the content
     * @param int $copyFromId
     *                           Id identifying the content to be copied
     * @param int $contentMainId
     *                           Main id for the content, typically used in frameworks
     *                           That supports versions. (In this case the content id will typically be
     *                           the version id, and the contentMainId will be the frameworks content id
     */
    public function copyLibraryUsage($contentId, $copyFromId, $contentMainId = null)
    {

    }

    /**
     * Deletes content data.
     *
     * @param int $contentId
     *                       Id identifying the content
     */
    public function deleteContentData($contentId)
    {
    }

    /**
     * Delete what libraries a content item is using.
     *
     * @param int $contentId
     *                       Content Id of the content we'll be deleting library usage for
     */
    public function deleteLibraryUsage($contentId)
    {
        H5PContent::findOrFail($contentId)->libraries()->delete();
    }

    /**
     * Saves what libraries the content uses.
     *
     * @param int   $contentId
     *                              Id identifying the content
     * @param array $librariesInUse
     *                              List of libraries the content uses. Libraries consist of associative arrays with:
     *                              - library: Associative array containing:
     *                              - dropLibraryCss(optional): comma separated list of machineNames
     *                              - machineName: Machine name for the library
     *                              - libraryId: Id of the library
     *                              - type: The dependency type. Allowed values:
     *                              - editor
     *                              - dynamic
     *                              - preloaded
     */
    public function saveLibraryUsage($contentId, $librariesInUse)
    {
        $contentLibraries = array_map(function ($value) use ($contentId) {
            $contentLibrary = H5PContentLibrary::query()
                ->where([
                    'content_id' => $contentId,
                    'library_id' => $value['library']['id'],
                    'dependency_type' => $value['type'],
                ])
                ->first();
            if (!$contentLibrary) {
                $contentLibrary = new H5PContentLibrary([
                    'content_id' => $contentId,
                    'library_id' => $value['library']['id'],
                    'dependency_type' => $value['type'],
                    'drop_css' => boolval($value['library']['dropLibraryCss']),
                    'weight' => $value['weight'],
                ]);
                $contentLibrary->save();
            }
            return $contentLibrary->toArray();
        }, $librariesInUse);

        $content = H5PContent::with('library')->findOrFail($contentId);

        $libraryLibraries = array_map(function ($value) use ($contentId) {
            return H5PContentLibrary::firstOrCreate([
                'content_id' => $contentId,
                'library_id' => $value['required_library_id'],
                'dependency_type' => $value['dependencyType'],
            ], [
                'drop_css' => false,
                'weight' => 0,
            ])->toArray();
        }, $content->library->dependencies->toArray());
    }

    /**
     * Get number of content/nodes using a library, and the number of
     * dependencies to other libraries.
     *
     * @param int  $libraryId
     *                          Library identifier
     * @param bool $skipContent
     *                          Flag to indicate if content usage should be skipped
     *
     * @return array
     *               Associative array containing:
     *               - content: Number of content using the library
     *               - libraries: Number of libraries depending on the library
     */
    public function getLibraryUsage($libraryId, $skipContent = false)
    {
        $contentsCount = $skipContent ? -1 : H5PLibrary::query()
            ->join('hh5p_contents_libraries', 'hh5p_libraries.id', '=', 'hh5p_contents_libraries.library_id')
            ->join('hh5p_contents', 'hh5p_contents_libraries.content_id', '=', 'hh5p_contents.id')
            ->where('hh5p_libraries.id', '=', $libraryId)
            ->distinct()
            ->count('hh5p_contents.id');

        $librariesCount = H5PLibrary::query()
            ->with('dependencies')
            ->whereHas('dependencies', fn($query) => $query->whereRequiredLibraryId($libraryId))
            ->count();

        return [
            'content' => $contentsCount,
            'libraries' => $librariesCount,
        ];
    }

    /**
     * Loads a library.
     *
     * @param string $machineName
     *                             The library's machine name
     * @param int    $majorVersion
     *                             The library's major version
     * @param int    $minorVersion
     *                             The library's minor version
     *
     * @return array|false
     *                     FALSE if the library does not exist.
     *                     Otherwise an associative array containing:
     *                     - libraryId: The id of the library if it is an existing library.
     *                     - title: The library's name
     *                     - machineName: The library machineName
     *                     - majorVersion: The library's majorVersion
     *                     - minorVersion: The library's minorVersion
     *                     - patchVersion: The library's patchVersion
     *                     - runnable: 1 if the library is a content type, 0 otherwise
     *                     - fullscreen(optional): 1 if the library supports fullscreen, 0 otherwise
     *                     - embedTypes(optional): list of supported embed types
     *                     - preloadedJs(optional): comma separated string with js file paths
     *                     - preloadedCss(optional): comma separated sting with css file paths
     *                     - dropLibraryCss(optional): list of associative arrays containing:
     *                     - machineName: machine name for the librarys that are to drop their css
     *                     - semantics(optional): Json describing the content structure for the library
     *                     - preloadedDependencies(optional): list of associative arrays containing:
     *                     - machineName: Machine name for a library this library is depending on
     *                     - majorVersion: Major version for a library this library is depending on
     *                     - minorVersion: Minor for a library this library is depending on
     *                     - dynamicDependencies(optional): list of associative arrays containing:
     *                     - machineName: Machine name for a library this library is depending on
     *                     - majorVersion: Major version for a library this library is depending on
     *                     - minorVersion: Minor for a library this library is depending on
     *                     - editorDependencies(optional): list of associative arrays containing:
     *                     - machineName: Machine name for a library this library is depending on
     *                     - majorVersion: Major version for a library this library is depending on
     *                     - minorVersion: Minor for a library this library is depending on
     */
    public function loadLibrary($machineName, $majorVersion, $minorVersion)
    {
        $library = H5PLibrary::where([
            'name' => $machineName,
            'major_version' => $majorVersion,
            'minor_version' => $minorVersion,
        ])
            ->with('dependencies.requiredLibrary')
            ->latest()->first();

        if (is_null($library)) {
            return false;
        }

        $result = $library->toArray();

        foreach ($library->dependencies as $dependency) {
            $result[$dependency->dependencyType . 'Dependencies'][] = [
                'id' => $dependency->requiredLibrary->id,
                'libraryId' => $dependency->requiredLibrary->id,
                'machineName' => $dependency->requiredLibrary->machineName,
                'majorVersion' => $dependency->requiredLibrary->majorVersion,
                'minorVersion' => $dependency->requiredLibrary->minorVersion,
            ];
        }

        return $result;
    }

    /**
     * Loads library semantics.
     *
     * @param string $machineName
     *                             Machine name for the library
     * @param int    $majorVersion
     *                             The library's major version
     * @param int    $minorVersion
     *                             The library's minor version
     *
     * @return string
     *                The library's semantics as json
     */
    public function loadLibrarySemantics($machineName, $majorVersion, $minorVersion)
    {
        $library = H5PLibrary::where('name', $machineName)
            ->where('major_version', $majorVersion)
            ->where('minor_version', $minorVersion)
            ->latest()
            ->first();

        $semanticsFile = $this->getSemanticsFromFile($machineName, $majorVersion, $minorVersion);

        if ($semanticsFile && !JSONHelper::compareArr(json_decode($semanticsFile), $library->semantics)) {
            $library->semantics = $semanticsFile;
            $library->save();
        }

        return $library === false ? null : json_encode($library->semantics);
    }

    /**
     * Makes it possible to alter the semantics, adding custom fields, etc.
     *
     * @param array  $semantics
     *                             Associative array representing the semantics
     * @param string $machineName
     *                             The library's machine name
     * @param int    $majorVersion
     *                             The library's major version
     * @param int    $minorVersion
     *                             The library's minor version
     */
    public function alterLibrarySemantics(&$semantics, $machineName, $majorVersion, $minorVersion)
    {
    }

    /**
     * Delete all dependencies belonging to given library.
     *
     * @param int $libraryId
     *                       Library identifier
     */
    public function deleteLibraryDependencies($libraryId)
    {
        H5PLibrary::findOrFail($libraryId)->dependencies()->delete();
    }

    /**
     * Start an atomic operation against the dependency storage.
     */
    public function lockDependencyStorage()
    {
    }

    /**
     * Stops an atomic operation against the dependency storage.
     */
    public function unlockDependencyStorage()
    {
    }

    /**
     * Delete a library from database and file system.
     *
     * @param $library
     *                          Library object with id, name, major version and minor version
     */
    public function deleteLibrary($library)
    {
        $libraryObj = H5pLibrary::with(['dependencies', 'languages'])->findOrFail($library->id);

        // Remove main library from files
        $libraryPath = storage_path('app/h5p/libraries/' . $library->name . '-' . $library->major_version . '.' . $library->minor_version);

        $libraryObj->dependencies()->delete();
        $libraryObj->languages()->delete();
        $libraryObj->delete();

        Helpers::deleteFileTree($libraryPath);

        return true;
    }

    /**
     * Load content.
     *
     * @param int $id
     *                Content identifier
     *
     * @return array
     *               Associative array containing:
     *               - contentId: Identifier for the content
     *               - params: json content as string
     *               - embedType: csv of embed types
     *               - title: The contents title
     *               - language: Language code for the content
     *               - libraryId: Id for the main library
     *               - libraryName: The library machine name
     *               - libraryMajorVersion: The library's majorVersion
     *               - libraryMinorVersion: The library's minorVersion
     *               - libraryEmbedTypes: CSV of the main library's embed types
     *               - libraryFullscreen: 1 if fullscreen is supported. 0 otherwise.
     */
    public function loadContent($id)
    {
        $content = H5PContent::with('library')->where(['id' => $id])->firstOrFail();

        if (is_null($content->library)) {
            throw new H5PException(H5PException::LIBRARY_NOT_FOUND);
        }
        $content = $content->toArray();
        $content['contentId'] = $content['id']; // : Identifier for the content
        $content['parameters'] = JSONHelper::clearJson($content['parameters']); // : json content as string
        $content['params'] = JSONHelper::clearJson($content['params']); // : json content as string
        $content['filtered'] = JSONHelper::clearJson($content['filtered']); // : json content as string
        $content['embedType'] = \H5PCore::determineEmbedType($content['embed_type'] ?? 'div', $content['library']['embed_types']); // : csv of embed types
        //$content ['language'] // : Language code for the content
        $content['libraryId'] = $content['library_id']; // : Id for the main library
        $content['libraryName'] = $content['library']['machineName']; // The library machine name
        $content['libraryMajorVersion'] = $content['library']['majorVersion']; // : The library's majorVersion
        $content['libraryMinorVersion'] = $content['library']['minorVersion']; // : The library's minorVersion
        $content['libraryEmbedTypes'] = $content['library']['embed_types']; // : CSV of the main library's embed types
        $content['libraryFullscreen'] = 0; // : 1 if fullscreen is supported. 0 otherwise.
        //$content ['metadata'] = $content ['metadata'] ?? "";
        $content['metadata'] = json_encode($content['metadata']); // : json content as string
        $content['slug'] = $content['slug'] ?? 'slug';

        return $content;
    }

    /**
     * Load dependencies for the given content of the given type.
     *
     * @param int $id
     *                  Content identifier
     * @param int $type
     *                  Dependency types. Allowed values:
     *                  - editor
     *                  - preloaded
     *                  - dynamic
     *
     * @return array
     *               List of associative arrays containing:
     *               - libraryId: The id of the library if it is an existing library.
     *               - machineName: The library machineName
     *               - majorVersion: The library's majorVersion
     *               - minorVersion: The library's minorVersion
     *               - patchVersion: The library's patchVersion
     *               - preloadedJs(optional): comma separated string with js file paths
     *               - preloadedCss(optional): comma separated sting with css file paths
     *               - dropCss(optional): csv of machine names
     */
    public function loadContentDependencies($id, $type = null)
    {
        $where = $type ? [['content_id', $id], ['dependency_type', $type]] : [['content_id', $id]];

        $libs = H5PContentLibrary::with('library')->where($where)->orderBy('weight')->get();

        return array_map(function ($value) {
            return [
                'libraryId' => $value['library_id'], //: The id of the library if it is an existing library.
                'machineName' => $value['library']['machineName'], //: The library machineName
                'majorVersion' => $value['library']['majorVersion'], //: The library's majorVersion
                'minorVersion' => $value['library']['minorVersion'], //: The library's minorVersion
                'patchVersion' => $value['library']['patchVersion'], //: The library's patchVersion
                'preloadedJs' => $value['library']['preloadedJs'], //(optional): comma separated string with js file paths
                'preloadedCss' => $value['library']['preloadedCss'], //(optional): comma separated sting with css file paths
                'dropCss' => $value['dropCss'], //(optional): csv of machine names
            ];
        }, $libs->toArray());
    }

    /**
     * Get stored setting.
     *
     * @param string $name
     *                        Identifier for the setting
     * @param string|bool $default
     *                        Optional default value if settings is not set
     *
     * @return mixed
     *               Whatever has been stored as the setting
     */
    public function getOption($name, $default = false)
    {
        if ($name === 'site_uuid') {
            $name = 'h5p_site_uuid'; // Make up for old core bug
        }

        switch ($name) {
            case "content_type_cache_updated_at":
                return Cache::get("content_type_cache_updated_at", $default);
                break;
            default:
                return config('hh5p.h5p_' . $name, $default);
        }
    }



    /**
     * Stores the given setting.
     * For example when did we last check h5p.org for updates to our libraries.
     *
     * @param string $name
     *                      Identifier for the setting
     * @param mixed  $value Data
     *                      Whatever we want to store as the setting
     */
    public function setOption($name, $value)
    {
        if ($name === 'site_uuid') {
            $name = 'h5p_site_uuid'; // Make up for old core bug
        }
        switch ($name) {
            case "content_type_cache_updated_at":
                Cache::set("content_type_cache_updated_at", $value);
                break;
            default:
                config(['hh5p.h5p_' . $name => $value]);
        }
    }

    /**
     * This will update selected fields on the given content.
     *
     * @param int   $id     Content identifier
     * @param array $fields Content fields, e.g. filtered or slug.
     */
    public function updateContentFields($id, $fields)
    {
        H5PContent::findOrFail($id)->update($fields);
    }

    /**
     * Will clear filtered params for all the content that uses the specified
     * libraries. This means that the content dependencies will have to be rebuilt,
     * and the parameters re-filtered.
     *
     * @param array $library_ids
     */
    public function clearFilteredParameters($library_ids)
    {
         H5PContent::query()->whereIn('library_id', $library_ids)->update(['filtered' => null]);
    }

    /**
     * Get number of contents that has to get their content dependencies rebuilt
     * and parameters re-filtered.
     *
     * @return int
     */
    public function getNumNotFiltered()
    {
        return 0;
    }

    /**
     * Get number of contents using library as main library.
     *
     * @param int   $libraryId
     * @param array $skip
     *
     * @return int
     */
    public function getNumContent($libraryId, $skip = null)
    {
        return 0;
    }

    /**
     * Determines if content slug is used.
     *
     * @param string $slug
     *
     * @return bool
     */
    public function isContentSlugAvailable($slug)
    {
        return (bool) H5PContent::where(['slug' => $slug])->first();
    }

    /**
     * Generates statistics from the event log per library.
     *
     * @param string $type Type of event to generate stats for
     *
     * @return array Number values indexed by library name and version
     */
    public function getLibraryStats($type)
    {
        return []; // TODO is used in h5p.classes.php => public function fetchLibrariesMetadata($fetchingDisabled = FALSE)
    }

    /**
     * Aggregate the current number of H5P authors.
     *
     * @return int
     */
    public function getNumAuthors()
    {
        return H5PContent::query()->select(['user_id'])->distinct()->count('user_id');
    }

    /**
     * Stores hash keys for cached assets, aggregated JavaScripts and
     * stylesheets, and connects it to libraries so that we know which cache file
     * to delete when a library is updated.
     *
     * @param string $key
     *                          Hash key for the given libraries
     * @param array  $libraries
     *                          List of dependencies(libraries) used to create the key
     */
    public function saveCachedAssets($key, $libraries)
    {
    }

    /**
     * Locate hash keys for given library and delete them.
     * Used when cache file are deleted.
     *
     * @param int $library_id
     *                        Library identifier
     *
     * @return array
     *               List of hash keys removed
     */
    public function deleteCachedAssets($library_id): array
    {
        return [];
    }

    /**
     * Get the amount of content items associated to a library
     * return int.
     */
    public function getLibraryContentCount()
    {
        return H5PLibrary::query()
            ->select('name', 'major_version', 'minor_version')
            ->with('contents')
            ->has('contents')
            ->withCount('contents as count')
            ->get()
            ->mapWithKeys(fn($item, $key) => [$item->uberName => $item->count])
            ->toArray();
    }

    /**
     * Will trigger after the export file is created.
     */
    public function afterExportCreated($content, $filename)
    {
        $this->downloadFiles[intval($content['id'])] = $filename;
    }

    public function getDownloadFile($id)
    {
        return $this->downloadFiles[intval($id)];
    }

    /**
     * Check if user has permissions to an action.
     *
     * @param H5PPermission $permission Permission type, ref H5PPermission
     * @param int           $id         Id need by platform to determine permission
     *
     * @return bool
     */
    public function hasPermission($permission, $id = null)
    {
        switch ($permission) {
            case H5PPermission::DOWNLOAD_H5P:
                // var_dump('DOWNLOAD_H5P');
                return true;
            case H5PPermission::EMBED_H5P:
                // var_dump('EMBED_H5P');
                return true;
            case H5PPermission::CREATE_RESTRICTED:
                // var_dump('CREATE_RESTRICTED');
                return true;
            case H5PPermission::UPDATE_LIBRARIES:
                // var_dump('UPDATE_LIBRARIES');
                return true;
            case H5PPermission::INSTALL_RECOMMENDED:
                // var_dump('INSTALL_RECOMMENDED');
                return true;
            case H5PPermission::COPY_H5P:
                // var_dump('COPY_H5P');
                return false;
        }

        // TODO some permissions must be checked here
        // NOTE, in this implementation, we assume that the user has permission to do everything
        // because permissions are set on the request level.

        return true;
    }

    /**
     * Replaces existing content type cache with the one passed in.
     *
     * @param object $contentTypeCache json with an array called 'libraries'
     *                                 containing the new content type cache that should replace the old one
     */
    public function replaceContentTypeCache($contentTypeCache)
    {
        $data = [];
        foreach ($contentTypeCache->contentTypes as $ct) {
           $data[] = [
                'machine_name' => $ct->id,
                'major_version' => $ct->version->major,
                'minor_version' => $ct->version->minor,
                'patch_version' => $ct->version->patch,
                'h5p_major_version' => $ct->coreApiVersionNeeded->major,
                'h5p_minor_version' => $ct->coreApiVersionNeeded->minor,
                'title' => $ct->title,
                'summary' => $ct->summary,
                'description' => $ct->description,
                'icon' => $ct->icon,
                'created_at' => (new DateTime($ct->createdAt))->getTimestamp(),
                'updated_at' => (new DateTime($ct->updatedAt))->getTimestamp(),
                'is_recommended' => $ct->isRecommended === true ? 1 : 0,
                'popularity' => $ct->popularity,
                'screenshots' => json_encode($ct->screenshots),
                'license' => json_encode($ct->license ?? []),
                'example' => $ct->example,
                'tutorial' => $ct->tutorial ?? '',
                'keywords' => json_encode($ct->keywords ?? []),
                'categories' => json_encode($ct->categories ?? []),
                'owner' => $ct->owner,
            ];
        }

        DB::transaction(function () use($data) {
            H5pLibrariesHubCache::query()->delete();
            H5pLibrariesHubCache::insert($data);
        });
    }

    /**
     * Checks if the given library has a higher version.
     *
     * @param array $library
     *
     * @return bool
     */
    public function libraryHasUpgrade($library)
    {
        return false;
    }

    /**
     * Replace content hub metadata cache.
     *
     * @param JsonSerializable $metadata Metadata as received from content hub
     * @param string           $lang     Language in ISO 639-1
     *
     * @return mixed
     */
    public function replaceContentHubMetadataCache($metadata, $lang)
    {
    }

    /**
     * Get content hub metadata cache from db.
     *
     * @param string $lang Language code in ISO 639-1
     *
     */
    public function getContentHubMetadataCache($lang = 'en')
    {
    }

    /**
     * Get time of last content hub metadata check.
     *
     * @param string $lang Language code iin ISO 639-1 format
     *
     * @return string|null Time in RFC7231 format
     */
    public function getContentHubMetadataChecked($lang = 'en')
    {
        return null;
    }

    /**
     * Set time of last content hub metadata check.
     *
     * @param int|null $time Time in RFC7231 format
     * @param string   $lang Language code iin ISO 639-1 format
     *
     * @return bool True if successful
     */
    public function setContentHubMetadataChecked($time, $lang = 'en')
    {
        return false;
    }

    private function getSemanticsFromFile($machineName, $majorVersion, $minorVersion): ?string
    {
        $semanticsFile = null;
        $storagePath = config('hh5p.h5p_storage_path');
        $libraryDirectory = $machineName . '-' . $majorVersion . '.' . $minorVersion;
        $semanticsPath = storage_path($storagePath . '/libraries/' . $libraryDirectory . '/semantics.json');

        if (File::exists($semanticsPath)) {
            $semanticsFile = File::get($semanticsPath);
        }

        return $semanticsFile;
    }
}