<?php
/**
 * Slim Framework (http://slimframework.com)
 *
 * @link      https://github.com/slimphp/PHP-View
 * @copyright Copyright (c) 2011-2015 Josh Lockhart
 * @license   https://github.com/slimphp/PHP-View/blob/master/LICENSE.md (MIT License)
 */
namespace Slim\Views;

use \InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;

/**
 * Class PhpRenderer
 * @package Slim\Views
 *
 * Render PHP view scripts into a PSR-7 Response object
 */
class PhpRenderer
{
    /**
     * @var string
     */
    protected $templatePath;

    /**
     * @var array
     */
    protected $attributes;

    /**
     * @var string
     */
    protected $layout;

    /**
     * SlimRenderer constructor.
     *
     * @param string $templatePath
     * @param array $attributes
     * @param string $layout
     */
    public function __construct($templatePath = "", $attributes = [], $layout = "")
    {
        $this->templatePath = rtrim($templatePath, '/\\') . '/';
        $this->attributes = $attributes;
        $this->setLayout($layout);
    }

    /**
     * Render a template
     *
     * $data cannot contain template as a key
     *
     * throws RuntimeException if $templatePath . $template does not exist
     *
     * @param ResponseInterface $response
     * @param string             $template
     * @param array              $data
     *
     * @return ResponseInterface
     *
     * @throws \InvalidArgumentException
     * @throws \RuntimeException
     */
    public function render(ResponseInterface $response, $template, array $data = [])
    {
        $output = $this->fetch($template, $data);

        $response->getBody()->write($output);

        return $response;
    }

    /**
     * Get layout template
     *
     * @return string
     */
    public function getLayout()
    {
        return $this->layout;
    }

    /**
     * Set layout template
     *
     * @param string $layout
     */
    public function setLayout($layout)
    {
        if ($layout === "" || $layout === null) {
            $this->layout = null;
        } else {
            $layoutPath = $this->templatePath . $layout;
            if (!is_file($layoutPath)) {
                throw new \RuntimeException("Layout template `$layout` does not exist");
            }
            $this->layout = $layoutPath;
        }
    }

    /**
     * Get the attributes for the renderer
     *
     * @return array
     */
    public function getAttributes()
    {
        return $this->attributes;
    }

    /**
     * Set the attributes for the renderer
     *
     * @param array $attributes
     */
    public function setAttributes(array $attributes)
    {
        $this->attributes = $attributes;
    }

    /**
     * Add an attribute
     *
     * @param $key
     * @param $value
     */
    public function addAttribute($key, $value) {
        $this->attributes[$key] = $value;
    }

    /**
     * Retrieve an attribute
     *
     * @param $key
     * @return mixed
     */
    public function getAttribute($key) {
        if (!isset($this->attributes[$key])) {
            return false;
        }

        return $this->attributes[$key];
    }

    /**
     * Get the template path
     *
     * @return string
     */
    public function getTemplatePath()
    {
        return $this->templatePath;
    }

    /**
     * Set the template path
     *
     * @param string $templatePath
     */
    public function setTemplatePath($templatePath)
    {
        $this->templatePath = rtrim($templatePath, '/\\') . '/';
    }

    /**
     * Renders a template and returns the result as a string
     *
     * cannot contain template as a key
     *
     * throws RuntimeException if $templatePath . $template does not exist
     *
     * @param $template
     * @param array $data
     *
     * @return mixed
     *
     * @throws \InvalidArgumentException
     * @throws \RuntimeException
     */
    public function fetch($template, array $data = []) {
        if (isset($data['template'])) {
            throw new \InvalidArgumentException("Duplicate template key found");
        }

        if (!is_file($this->templatePath . $template)) {
            throw new \RuntimeException("View cannot render `$template` because the template does not exist");
        }

        /*
        foreach ($data as $k=>$val) {
            if (in_array($k, array_keys($this->attributes))) {
                throw new \InvalidArgumentException("Duplicate key found in data and renderer attributes. " . $k);
            }
        }
        */
        $data = array_merge($this->attributes, $data);

        try {
            ob_start();
            $this->protectedIncludeScope($this->templatePath . $template, $data);
            $output = ob_get_clean();

            if ($this->layout !== null) {
                ob_start();
                $data['content'] = $output;
                $this->protectedIncludeScope($this->layout, $data);
                $output = ob_get_clean();
            }
        } catch(\Throwable $e) { // PHP 7+
            ob_end_clean();
            throw $e;
        } catch(\Exception $e) { // PHP < 7
            ob_end_clean();
            throw $e;
        }

        return $output;
    }

    /**
     * @param string $template
     * @param array $data
     */
    protected function protectedIncludeScope ($template, array $data) {
        extract($data);
        include func_get_arg(0);
    }
}


<?php

namespace Tumblr\API;

/**
 * A client to access the Tumblr API
 */
class Client
{

    private $apiKey;

    /**
     * Create a new Client
     *
     * @param string $consumerKey    the consumer key
     * @param string $consumerSecret the consumer secret
     * @param string $token          oauth token
     * @param string $secret         oauth token secret
     */
    public function __construct($consumerKey, $consumerSecret = null, $token = null, $secret = null)
    {
        $this->requestHandler = new RequestHandler();
        $this->setConsumer($consumerKey, $consumerSecret);

        if ($token && $secret) {
             $this->setToken($token, $secret);
        }
    }

    /**
     * Set the consumer for this client
     *
     * @param string $consumerKey    the consumer key
     * @param string $consumerSecret the consumer secret
     */
    public function setConsumer($consumerKey, $consumerSecret)
    {
        $this->apiKey = $consumerKey;
        $this->requestHandler->setConsumer($consumerKey, $consumerSecret);
    }

    /**
     * Set the token for this client
     *
     * @param string $token  the oauth token
     * @param string $secret the oauth secret
     */
    public function setToken($token, $secret)
    {
        $this->requestHandler->setToken($token, $secret);
    }

    /**
     * Retrieve RequestHandler instance
     *
     * @return RequestHandler
     */
    public function getRequestHandler()
    {
        return $this->requestHandler;
    }

    /**
     * Get info on the authenticating user
     *
     * @return array the response array
     */
    public function getUserInfo()
    {
        return $this->getRequest('v2/user/info', null, false);
    }

    /**
     * Get user dashboard for the authenticating user
     *
     * @param  array $options the options for the call
     * @return array the response array
     */
    public function getDashboardPosts($options = null)
    {
        return $this->getRequest('v2/user/dashboard', $options, false);
    }

    /**
     * Get followings for the authenticating user
     *
     * @param  array $options the options for the call
     * @return array the response array
     */
    public function getFollowedBlogs($options = null)
    {
        return $this->getRequest('v2/user/following', $options, false);
    }

    /**
     * Get likes for the authenticating user
     *
     * @param  array $options the options for the call
     * @return array the response array
     */
    public function getLikedPosts($options = null)
    {
        return $this->getRequest('v2/user/likes', $options, false);
    }

    /**
     * Follow a blog
     *
     * @param  string $blogName the name of the blog to follow
     * @return array  the response array
     */
    public function follow($blogName)
    {
        $options = array('url' => $this->blogUrl($blogName));

        return $this->postRequest('v2/user/follow', $options, false);
    }

    /**
     * Unfollow a blog
     *
     * @param  string $blogName the name of the blog to follow
     * @return array  the response array
     */
    public function unfollow($blogName)
    {
        $options = array('url' => $this->blogUrl($blogName));

        return $this->postRequest('v2/user/unfollow', $options, false);
    }

    /**
     * Like a post
     *
     * @param int    $postId    the id of the post
     * @param string $reblogKey the reblog_key of the post
     *
     * @return array the response array
     */
    public function like($postId, $reblogKey)
    {
        $options = array('id' => $postId, 'reblog_key' => $reblogKey);

        return $this->postRequest('v2/user/like', $options, false);
    }

    /**
     * Unlike a post
     *
     * @param int    $postId    the id of the post
     * @param string $reblogKey the reblog_key of the post
     *
     * @return array the response array
     */
    public function unlike($postId, $reblogKey)
    {
        $options = array('id' => $postId, 'reblog_key' => $reblogKey);

        return $this->postRequest('v2/user/unlike', $options, false);
    }

    /**
     * Delete a post
     *
     * @param string $blogName  the name of the blog the post is on
     * @param int    $postId    the id of the post
     * @param string $reblogKey the reblog_key of the post
     *
     * @return array the response array
     */
    public function deletePost($blogName, $postId, $reblogKey)
    {
        $options = array('id' => $postId, 'reblog_key' => $reblogKey);
        $path = $this->blogPath($blogName, '/post/delete');

        return $this->postRequest($path, $options, false);
    }

    /**
     * Reblog a post
     *
     * @param string $blogName  the name of the blog
     * @param int    $postId    the id of the post
     * @param string $reblogKey the reblog key of the post
     * @param array  $options   the options for the call
     *
     * @return array the response array
     */
    public function reblogPost($blogName, $postId, $reblogKey, $options = null)
    {
        $params = array('id' => $postId, 'reblog_key' => $reblogKey);
        $params = array_merge($options ?: array(), $params);
        $path = $this->blogPath($blogName, '/post/reblog');

        return $this->postRequest($path, $params, false);
    }

    /**
     * Edit a post
     *
     * @param string $blogName the name of the blog
     * @param int    $postId   the id of the post to edit
     * @param array  $data     the data to save
     *
     * @return array the response array
     */
    public function editPost($blogName, $postId, $data)
    {
        $data['id'] = $postId;
        $path = $this->blogPath($blogName, '/post/edit');

        return $this->postRequest($path, $data, false);
    }

    /**
     * Create a post
     *
     * @param string $blogName the name of the blog
     * @param array  $data     the data to save
     *
     * @return array the response array
     */
    public function createPost($blogName, $data)
    {
        $path = $this->blogPath($blogName, '/post');

        return $this->postRequest($path, $data, false);
    }

    /**
     * Get tagged posts
     *
     * @param string $tag     the tag to look up
     * @param array  $options the options for the call
     *
     * @return array the response array
     */
    public function getTaggedPosts($tag, $options = null)
    {
        if (!$options) {
            $options = array();
        }
        $options['tag'] = $tag;

        return $this->getRequest('v2/tagged', $options, true);
    }

    /**
     * Get information about a given blog
     *
     * @param  string $blogName the name of the blog to look up
     * @return array  the response array
     */
    public function getBlogInfo($blogName)
    {
        $path = $this->blogPath($blogName, '/info');

        return $this->getRequest($path, null, true);
    }

    /**
     * Get blog avatar URL
     *
     * @param string $blogName the nae of the blog to look up
     * @param int    $size     the size to retrieve
     *
     * @return string the avatar url
     */
    public function getBlogAvatar($blogName, $size = null)
    {
        $path = $this->blogPath($blogName, '/avatar');
        if ($size) {
            $path .= "/$size";
        }

        return $this->getRedirect($path, null, true);
    }

    /**
     * Get blog likes for a given blog
     *
     * @param string $blogName the name of the blog to look up
     * @param array  $options  the options for the call
     *
     * @return array the response array
     */
    public function getBlogLikes($blogName, $options = null)
    {
        $path = $this->blogPath($blogName, '/likes');

        return $this->getRequest($path, $options, true);
    }

    /**
     * Get blog followers for a given blog
     *
     * @param string $blogName the name of the blog to look up
     * @param array  $options  the options for the call
     *
     * @return array the response array
     */
    public function getBlogFollowers($blogName, $options = null)
    {
        $path = $this->blogPath($blogName, '/followers');

        return $this->getRequest($path, $options, false);
    }

    /**
     * Get posts for a given blog
     *
     * @param string $blogName the name of the blog
     * @param array  $options  the options for the call
     *
     * @return array the response array
     */
    public function getBlogPosts($blogName, $options = null)
    {
        $path = $this->blogPath($blogName, '/posts');
        if ($options && isset($options['type'])) {
            $path .= '/' . $options['type'];
            unset($options['type']);
        }

        return $this->getRequest($path, $options, true);
    }

    /**
     * Get queue posts for a given blog
     *
     * @param string $blogName the name of the blog
     * @param array  $options  the options for the call
     *
     * @return array the response array
     */
    public function getQueuedPosts($blogName, $options = null)
    {
        $path = $this->blogPath($blogName, '/posts/queue');

        return $this->getRequest($path, $options, false);
    }

    /**
     * Get draft posts for a given blog
     *
     * @param string $blogName the name of the blog
     * @param array  $options  the options for the call
     *
     * @return array the response array
     */
    public function getDraftPosts($blogName, $options = null)
    {
        $path = $this->blogPath($blogName, '/posts/draft');

        return $this->getRequest($path, $options, false);
    }

    /**
     * Get submission posts for a given blog
     *
     * @param string $blogName the name of the blog
     * @param array  $options  the options for the call
     *
     * @return array the response array
     */
    public function getSubmissionPosts($blogName, $options = null)
    {
        $path = $this->blogPath($blogName, '/posts/submission');

        return $this->getRequest($path, $options, false);
    }

    /**
     * Make a GET request to the given endpoint and return the response
     *
     * @param string $path      the path to call on
     * @param array  $options   the options to call with
     * @param bool   $addApiKey whether or not to add the api key
     *
     * @return array the response object (parsed)
     */
    public function getRequest($path, $options, $addApiKey)
    {
        $response = $this->makeRequest('GET', $path, $options, $addApiKey);

        return $this->parseResponse($response);
    }

    /**
     * Make a POST request to the given endpoint and return the response
     *
     * @param string $path      the path to call on
     * @param array  $options   the options to call with
     * @param bool   $addApiKey whether or not to add the api key
     *
     * @return array the response object (parsed)
     */
    public function postRequest($path, $options, $addApiKey)
    {
        if (isset($options['source']) && is_array($options['source'])) {
            $sources = $options['source'];
            unset($options['source']);
            foreach ($sources as $i => $source) {
                $options["source[$i]"] = $source;
            }
        }

        $response = $this->makeRequest('POST', $path, $options, $addApiKey);
        return $this->parseResponse($response);
    }

    /**
     * Parse a response and return an appropriate result
     *
     * @param  \stdClass $response the response from the server
     *
     * @throws RequestException
     * @return array  the response data
     */
    private function parseResponse($response)
    {
        $response->json = json_decode($response->body);
        if ($response->status < 400) {
            return $response->json->response;
        } else {
            throw new RequestException($response);
        }
    }

    /**
     * Make a GET request to the given endpoint and return the response
     *
     * @param string $path      the path to call on
     * @param array  $options   the options to call with
     * @param bool   $addApiKey whether or not to add the api key
     *
     * @return string url redirected to (or null)
     */
    private function getRedirect($path, $options, $addApiKey)
    {
        $response = $this->makeRequest('GET', $path, $options, $addApiKey);
        if ($response->status === 301 || $response->status === 302) {
            return $response->headers['Location'][0];
        }

        return null;
    }

    /**
     * Make a request to the given endpoint and return the response
     *
     * @param string $method    the method to call: GET, POST
     * @param string $path      the path to call on
     * @param array  $options   the options to call with
     * @param bool   $addApiKey whether or not to add the api key
     *
     * @return \stdClass the response object (not parsed)
     */
    private function makeRequest($method, $path, $options, $addApiKey)
    {
        if ($addApiKey) {
            $options = array_merge(
                array('api_key' => $this->apiKey),
                $options ?: array()
            );
        }

        return $this->requestHandler->request($method, $path, $options);
    }

    /**
     * Expand the given blogName into a base path for the blog
     *
     * @param string $blogName the name of the blog
     * @param string $ext      the url extension
     *
     * @return string the blog base path
     */
    private function blogPath($blogName, $ext)
    {
        $blogUrl = $this->blogUrl($blogName);

        return "v2/blog/$blogUrl$ext";
    }

    /**
     * Get the URL of a blog by name or URL
     *
     * @param  string $blogName the name of the blog
     * @return string the blog URL
     */
    private function blogUrl($blogName)
    {
        if (strpos($blogName, '.') === false) {
            return "$blogName.tumblr.com";
        }

        return $blogName;
    }

}


<?php

namespace Tumblr\API;

/**
 * A request handler for Tumblr authentication
 * and requests
 */
class RequestHandler
{

    private $consumer;
    private $token;
    private $signatureMethod;

    private $baseUrl;
    private $version;

    /**
     * Instantiate a new RequestHandler
     */
    public function __construct()
    {
        $this->baseUrl = 'https://api.tumblr.com/';
        $this->version = '0.1.2';

        $this->signatureMethod = new \Eher\OAuth\HmacSha1();
        $this->client = new \GuzzleHttp\Client(array(
            'allow_redirects' => false,
        ));
    }

    /**
     * Set the consumer for this request handler
     *
     * @param string $key    the consumer key
     * @param string $secret the consumer secret
     */
    public function setConsumer($key, $secret)
    {
        $this->consumer = new \Eher\OAuth\Consumer($key, $secret);
    }

    /**
     * Set the token for this request handler
     *
     * @param string $token  the oauth token
     * @param string $secret the oauth secret
     */
    public function setToken($token, $secret)
    {
        $this->token = new \Eher\OAuth\Token($token, $secret);
    }

    /**
     * Set the base url for this request handler.
     *
     * @param string $url The base url (e.g. https://api.tumblr.com)
     */
    public function setBaseUrl($url)
    {
        // Ensure we have a trailing slash since it is expected in {@link request}.
        if (substr($url, -1) !== '/') {
            $url .= '/';
        }

        $this->baseUrl = $url;
    }

    /**
     * Make a request with this request handler
     *
     * @param string $method  one of GET, POST
     * @param string $path    the path to hit
     * @param array  $options the array of params
     *
     * @return \stdClass response object
     */
    public function request($method, $path, $options)
    {
        // Ensure we have options
        $options = $options ?: array();

        // Take off the data param, we'll add it back after signing
        $file = isset($options['data']) ? $options['data'] : false;
        unset($options['data']);

        // Get the oauth signature to put in the request header
        $url = $this->baseUrl . $path;
        $oauth = \Eher\OAuth\Request::from_consumer_and_token(
            $this->consumer,
            $this->token,
            $method,
            $url,
            $options
        );
        $oauth->sign_request($this->signatureMethod, $this->consumer, $this->token);
        $authHeader = $oauth->to_header();
        $pieces = explode(' ', $authHeader, 2);
        $authString = $pieces[1];

        // Set up the request and get the response
        $uri = new \GuzzleHttp\Psr7\Uri($url);
        $guzzleOptions = [
            'headers' => [
                'Authorization' => $authString,
                'User-Agent' => 'tumblr.php/' . $this->version,
            ],
            // Swallow exceptions since \Tumblr\API\Client will handle them
            'http_errors' => false,
        ];
        if ($method === 'GET') {
            $uri = $uri->withQuery(http_build_query($options));
        } elseif ($method === 'POST') {
            if (!$file) {
                $guzzleOptions['form_params'] = $options;
            } else {
                // Add the files back now that we have the signature without them
                $content_type = 'multipart';
                $form = [];
                foreach ($options as $name => $contents) {
                    $form[] = [
                        'name'      => $name,
                        'contents'  => $contents,
                    ];
                }
                foreach ((array) $file as $idx => $path) {
                    $form[] = [
                        'name'      => "data[$idx]",
                        'contents'  => file_get_contents($path),
                        'filename'  => pathinfo($path, PATHINFO_FILENAME),
                    ];
                }
                $guzzleOptions['multipart'] = $form;
            }
        }

        $response = $this->client->request($method, $uri, $guzzleOptions);

        // Construct the object that the Client expects to see, and return it
        $obj = new \stdClass;
        $obj->status = $response->getStatusCode();
        // Turn the stream into a string
        $obj->body = $response->getBody()->__toString();
        $obj->headers = $response->getHeaders();

        return $obj;
    }

}


<?php
/**
 * 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; under version 2
 * of the License (non-upgradable).
 *
 * 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 Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;
 *
 */

namespace oat\taoQtiTest\models\runner\time\storageFormat;

use oat\taoQtiTest\models\runner\time\QtiTimeLine;
use oat\taoQtiTest\models\runner\time\QtiTimeStorageFormat;

/**
 * Class QtiTimeStorageJsonFormat.
 *
 * Encode/decode QtiTimer using JSON format.
 *
 * @package oat\taoQtiTest\models\runner\time\storageFormat
 * @author Jean-Sébastien Conan <jean-sebastien@taotesting.com>
 */
class QtiTimeStorageJsonFormat implements QtiTimeStorageFormat
{
    /**
     * Encode a dataset with the managed format.
     * @param mixed $data
     * @return string
     */
    public function encode($data)
    {
        return json_encode($data);
    }

    /**
     * Decode a string encoded with the managed format.
     * @param string $data
     * @return mixed
     */
    public function decode($data)
    {
        $decodedData = json_decode($data, true);

        // fallback for old storage that uses PHP serialize format
        if (is_null($decodedData) && $data) {
            $decodedData = unserialize($data);
        }

        if (is_array($decodedData)) {
            foreach ($decodedData as $key => &$value) {
                if (is_array($value)) {
                    $timeLine = new QtiTimeLine();
                    $timeLine->fromArray($value);
                    $decodedData[$key] = $timeLine;
                }
            }
        }

        return $decodedData;
    }
}


<?php
/**
 * 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; under version 2
 * of the License (non-upgradable).
 *
 * 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 Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2015 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
 *
 */
namespace oat\taoQtiTest\models;

use oat\oatbox\service\ConfigurableService;
use oat\taoDelivery\model\execution\ServiceProxy;
use oat\taoQtiTest\models\cat\CatService;
use qtism\data\AssessmentItemRef;
use qtism\runtime\tests\AssessmentTestSession;
use oat\taoDelivery\model\execution\DeliveryExecution;
use qtism\runtime\tests\RouteItem;

/**
 * The SessionStateService
 *
 * Service used for pausing and resuming the delivery execution.
 * All timers in paused session will be paused.
 *
 * Usage example:
 *

 * //Pause session:
 * $sessionStateService = ServiceManager::getServiceManager()->get('taoQtiTest/SessionStateService');
 * $sessionStateService->pauseSession($session);
 *
 * //resume session:
 * $sessionStateService = ServiceManager::getServiceManager()->get('taoQtiTest/SessionStateService');
 * $sessionStateService->resumeSession($session);
 *

 * @author Aleh Hutnikau <hutnikau@1pt.com>
 */
class SessionStateService extends ConfigurableService
{
    const SERVICE_ID = 'taoQtiTest/SessionStateService';

    const OPTION_STATE_FORMAT = 'stateFormat';

    /**
     * @var ServiceProxy
     */
    private $deliveryExecutionService;

    public function __construct(array $options = array())
    {
        $this->deliveryExecutionService = ServiceProxy::singleton();
        parent::__construct($options);
    }

    /**
     * Pause delivery execution.
     * @param AssessmentTestSession $session
     * @return boolean success
     */
    public function pauseSession(AssessmentTestSession $session) {
        $session->updateDuration();
        return $this->getDeliveryExecution($session)->setState(DeliveryExecution::STATE_PAUSED);
    }

    /**
     * Resume delivery execution
     * @param AssessmentTestSession $session
     */
    public function resumeSession(AssessmentTestSession $session) {
        $deliveryExecutionState = $this->getSessionState($session);
        if ($deliveryExecutionState === DeliveryExecution::STATE_PAUSED) {
            $this->updateTimeReference($session);
            $this->getDeliveryExecution($session)->setState(DeliveryExecution::STATE_ACTIVE);
        }
    }

    /**
     * Get delivery execution state
     * @param AssessmentTestSession $session
     * @return string
     */
    public function getSessionState(AssessmentTestSession $session) {
        $deliveryExecution = $this->getDeliveryExecution($session);
        return $deliveryExecution->getState()->getUri();
    }

    /**
     * Set time reference of current assessment item session to now instead of time of last update.
     * This ensures that time when delivery execution was paused will not be taken in account.
     * Make sure that method invoked right after retrieving assessment test session
     * and before the first AssessmentTestSession::updateDuration method call
     * @param AssessmentTestSession $session
     * @param \DateTime|null $time Time to be specified. Current time by default. Make sure that $time has UTC timezone.
     */
    public function updateTimeReference(AssessmentTestSession $session, \DateTime $time = null) {
        if ($time === null) {
            $time = new \DateTime('now', new \DateTimeZone('UTC'));
        }

        $itemSession = $session->getCurrentAssessmentItemSession();

        if ($itemSession) {
            $itemSession->setTimeReference($time);
            $session->updateDuration();
        }
    }

    /**
     * @param AssessmentTestSession $session
     * @return DeliveryExecution
     */
    private function getDeliveryExecution(AssessmentTestSession $session) {
        return $this->deliveryExecutionService->getDeliveryExecution($session->getSessionId());
    }

    /**
     * Returns appropriate JS service implementation for testRunner
     *
     * @param boolean $resetTimerAfterResume
     *
     * @return string
     */
    public function getClientImplementation($resetTimerAfterResume = false){
        if ($resetTimerAfterResume) {
            return 'taoQtiTest/testRunner/resumingStrategy/keepAfterResume';
        }
        return 'taoQtiTest/testRunner/resumingStrategy/resetAfterResume';
    }

    /**
     * Return a human readable description of the test session
     *
     * @return string
     */
    public function getSessionDescription(\taoQtiTest_helpers_TestSession $session)
    {
        if ($session->isRunning()) {
            $config = \common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiTest')->getConfig('testRunner');
            $progressScope = isset($config['progress-indicator-scope']) ? $config['progress-indicator-scope'] : 'test';
            $progress = $this->getSessionProgress($session);
            $itemPosition = $progress[$progressScope];
            $itemCount = $progress[$progressScope . 'Length'];

            $format = $this->hasOption(self::OPTION_STATE_FORMAT)
                ? $this->getOption(self::OPTION_STATE_FORMAT)
                : __('%s - item %p/%c');
            $map = array(
                '%s' => $session->getCurrentAssessmentSection()->getTitle(),
                '%p' => $itemPosition,
                '%c' => $itemCount
            );
            return strtr($format, $map);
        } else {
            return __('finished');
        }
    }

    /**
     * Gets the current progress inside a delivery execution
     * @param \taoQtiTest_helpers_TestSession $session
     * @return array|bool
     */
    protected function getSessionProgress(\taoQtiTest_helpers_TestSession $session)
    {
        if ($session->isRunning() !== false) {
            $config = \common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiTest')->getConfig('testRunner');
            $categories = [];
            if (isset($config['progress-indicator']) && $config['progress-indicator'] == 'categories') {
                $categories = $config['progress-categories'];
            }
            $route = $session->getRoute();
            $routeItems = $route->getAllRouteItems();
            $offset = $route->getRouteItemPosition($routeItems[0]);
            $offsetPart = 0;
            $offsetSection = 0;
            $lastPart = null;
            $lastSection = null;

            $positions = [];
            $lengthParts = [];
            $lengthSections = [];
            $sectionIndex = 0;
            $partIndex = 0;

            // compute positions from the test map
            /** @var RouteItem $routeItem */
            foreach ($routeItems as $routeItem) {
                $testPart = $routeItem->getTestPart();
                $partId = $testPart->getIdentifier();
                if ($lastPart != $partId) {
                    $offsetPart = 0;
                    $lastPart = $partId;
                    $partIndex ++;
                }

                $section = $routeItem->getAssessmentSection();
                $sectionId = $section->getIdentifier();
                if ($lastSection != $sectionId) {
                    $offsetSection = 0;
                    $lastSection = $sectionId;
                    $sectionIndex ++;
                }

                $offset ++;
                $offsetSection ++;

                if ($categories && $config['progress-indicator-scope'] == 'testPart') {
                    /** @var AssessmentItemRef $assessmentItemRef */
                    $assessmentItemRef = $routeItem->getAssessmentItemRef();
                    $assessmenCategories = $assessmentItemRef->getCategories()->getArrayCopy();
                    if (array_intersect($categories, $assessmenCategories)) {
                        $offsetPart ++;
                    }
                }
                $lengthParts[$partIndex] = $offsetPart;
                $lengthSections[$sectionIndex] = $offsetSection;

                $positions[] = [
                    'test' => $offset,
                    'part' => $offsetPart,
                    'partId' => $partIndex,
                    'section' => $offsetSection,
                    'sectionId' => $sectionIndex,
                ];
            }

            $progress = $positions[$route->getPosition()];

            $catService = $this->getServiceManager()->get(CatService::SERVICE_ID);
            $currentItem = $route->current();
            if ($catService->isAdaptive($session, $currentItem->getAssessmentItemRef())) {
                $testSessionService = $this->getServiceManager()->get(TestSessionService::SERVICE_ID);
                $testSessionData = $testSessionService->getTestSessionDataById($session->getSessionId());
                $sectionItems = $catService->getShadowTest($session, $testSessionData['compilation']['private'], $currentItem);
                $currentItem = $catService->getCurrentCatItemId($session, $testSessionData['compilation']['private'], $currentItem);
                $positionInSection = array_search($currentItem, $sectionItems);

                // When in an adaptive section, the actual section is just a placeholder that is dynamically
                // replaced by the adaptive content. To set the right position, just grab the offset within
                // this dynamic content and add it to the placeholder position.
                $progress['test'] += $positionInSection;
                $progress['part'] += $positionInSection;
                $progress['section'] += $positionInSection;
                $lengthSections[$progress['sectionId']] = count($sectionItems);
            }

            return [
                'test' => $progress['test'],
                'testPart' => $progress['part'],
                'testSection' => $progress['section'],
                'testLength' => $session->getRouteCount(),
                'testPartLength' => $lengthParts[$progress['partId']],
                'testSectionLength' => $lengthSections[$progress['sectionId']],
            ];
        }
        return false;
    }
}


<?php
/**
 * 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; under version 2
 * of the License (non-upgradable).
 *
 * 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 Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2017 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
 *
 */
namespace oat\taoQtiTest\models\creator;

use oat\oatbox\service\ConfigurableService;
use oat\tao\model\GenerisTreeFactory;
use oat\tao\model\resources\TreeResourceLookup;
use oat\taoItems\model\CategoryService;

/**
 * Look up items and format them as a tree hierarchy
 *
 * @author Bertrand Chevrier <bertrand@taotesting.com>
 */
class TreeItemLookup extends ConfigurableService implements ItemLookup
{
    const SERVICE_ID = 'taoQtiTest/CreatorItems/tree';

    /**
     * Get the CategoryService
     * @return CategoryService the service
     */
    public function getCategoryService()
    {
        return $this->getServiceLocator()->get(CategoryService::SERVICE_ID);
    }

    /**
     * @return TreeResourceLookup
     */
    public function getTreeResourceLookupService()
    {
        return $this->getServiceLocator()->get(TreeResourceLookup::SERVICE_ID);
    }

    /**
     * Retrieve QTI Items in their hierarchy, for the given parameters as format them as tree.
     * @param \core_kernel_classes_Class $itemClass the item class
     * @param array $propertyFilters propUri/propValue to search items
     * @param int    $offset for paging
     * @param int    $limit  for paging
     * @return array the items
     */
    public function getItems(\core_kernel_classes_Class $itemClass, array $propertyFilters = [], $offset = 0, $limit = 30)
    {
        $data =  $this->getTreeResourceLookupService()->getResources($itemClass, [], $propertyFilters, $offset, $limit);
        return $this->formatTreeData($data);
    }

    /**
     * Reformat the the tree : state and count
     * Add the item's categories
     * @param array $treeData
     * @return array the formated data
     */
    private function formatTreeData(array $treeData)
    {
        return array_map(function($data){
            $formatted = [
                'categories' => $this->getCategoryService()->getItemCategories(new \core_kernel_classes_Resource($data['uri']))
            ];
            if(isset($data['children'])){
                $formatted['children'] = $this->formatTreeData($data['children']);
            }
            return array_merge($data, $formatted);
        }, $treeData);
    }
}


<?php
/**
 * 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; under version 2
 * of the License (non-upgradable).
 *
 * 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 Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;
 *
 */

namespace oat\taoQtiTest\models\runner;

use oat\oatbox\service\ConfigurableService;
use oat\tao\model\state\StateStorage;

/**
 * Class StorageManager
 *
 * Manage the storage in order to centralize its access.
 * The reading of data can be done at any time, the first call will put the data in memory cache.
 * Each change will only update the cache, and mark it to be processed upon persisting.
 * The actual writing should be done once, at the end of the request, by invoking the `persist()` method.
 *
 * @package oat\taoQtiTest\models\classes\runner
 * @author Jean-Sébastien Conan <jean-sebastien@taotesting.com>
 */
class StorageManager extends ConfigurableService
{
    const SERVICE_ID = 'taoQtiTest/StorageManager';

    /**
     * The data does not exist in the storage
     */
    const STATE_NOT_FOUND = -1;

    /**
     * The data is aligned with the storage
     */
    const STATE_ALIGNED = 0;

    /**
     * The data is pending write to the storage
     */
    const STATE_PENDING_WRITE = 1;

    /**
     * The data is pending delete from the storage
     */
    const STATE_PENDING_DELETE = 2;

    /**
     * Link to the actual storage adapter
     * @var StateStorage
     */
    protected $storage;

    /**
     * In memory cache for read/pending data
     * @var array
     */
    protected $cache = [];

    /**
     * Gets a key that will be used to cache data.
     *
     * @param string $userId
     * @param string $callId
     * @return string
     */
    protected function getCacheKey($userId, $callId)
    {
        return $userId . '/' . $callId;
    }

    /**
     * Puts data in the cache. Maintain the link to the userId/callId pair.
     * Also keep the dirty state that will be used when persisting the data to the actual storage.
     *
     * @param string $key
     * @param string $userId
     * @param string $callId
     * @param string $data
     * @param int $state
     */
    protected function putInCache($key, $userId, $callId, $data, $state = self::STATE_ALIGNED)
    {
        $this->cache[$key] = [
            'userId' => $userId,
            'callId' => $callId,
            'state' => $state,
            'data' => $data
        ];
    }

    /**
     * Checks if a dataset exists for the provided key.
     *
     * @param string $key
     * @return bool
     */
    protected function exists($key)
    {
        return isset($this->cache[$key]) && in_array($this->cache[$key]['state'], [self::STATE_ALIGNED, self::STATE_PENDING_WRITE]);
    }

    /**
     * Gets a dataset from the cache.
     *
     * @param string $key
     * @return mixed
     */
    protected function getFromCache($key)
    {
        if ($this->exists($key)) {
            return $this->cache[$key]['data'];
        }
        return null;
    }

    /**
     * Persists a cache entry and update its status.
     *
     * @param string $key
     * @return bool
     */
    protected function persistCacheEntry($key)
    {
        $success = true;
        if (isset($this->cache[$key])) {
            $cache = $this->cache[$key];

            switch ($cache['state']) {
                case self::STATE_PENDING_WRITE:
                    $success = $this->getStorage()->set($cache['userId'], $cache['callId'], $cache['data']);
                    if (!$success) {
                        throw new \common_exception_Error('Can\'t write into test runner state storage at '.static::class);
                    }
                    $this->cache[$key]['state'] = self::STATE_ALIGNED;
                    break;

                case self::STATE_PENDING_DELETE:
                    $success = $this->getStorage()->del($cache['userId'], $cache['callId']);
                    if ($success) {
                        unset($this->cache[$key]);
                    }
                    break;
            }
        }
        return $success;
    }

    /**
     * @return StateStorage
     */
    public function getStorage()
    {
        if (!$this->storage) {
            $this->storage = $this->getServiceLocator()->get(StateStorage::SERVICE_ID);
        }
        return $this->storage;
    }

    /**
     * @param StateStorage $storage
     * @return StorageManager
     */
    public function setStorage(StateStorage $storage)
    {
        $this->storage = $storage;
        return $this;
    }

    /**
     * Applies a dataset to be stored.
     *
     * @param string $userId
     * @param string $callId
     * @param string $data
     * @return boolean
     */
    public function set($userId, $callId, $data)
    {
        $key = $this->getCacheKey($userId, $callId);
        $cache = $this->getFromCache($key);
        if (is_null($cache) || $cache != $data) {
            $this->putInCache($key, $userId, $callId, $data, self::STATE_PENDING_WRITE);
        }
        return true;
    }

    /**
     * Gets a dataset from the store using the provided keys.
     * Will return null if the dataset doesn't exist.
     *
     * @param string $userId
     * @param string $callId
     * @return string
     */
    public function get($userId, $callId)
    {
        $key = $this->getCacheKey($userId, $callId);
        if (!isset($this->cache[$key])) {
            $data = $this->getStorage()->get($userId, $callId);
            $state = is_null($data) ? self::STATE_NOT_FOUND : self::STATE_ALIGNED;
            $this->putInCache($key, $userId, $callId, $data, $state);
        }

        return $this->getFromCache($key);
    }

    /**
     * Whenever or not a dataset exists.
     *
     * @param string $userId
     * @param string $callId
     * @return boolean
     */
    public function has($userId, $callId)
    {
        $key = $this->getCacheKey($userId, $callId);
        if (!isset($this->cache[$key])) {
            return $this->getStorage()->has($userId, $callId);
        }
        return $this->exists($key);
    }

    /**
     * Marks the the dataset to be removed from the storage.
     *
     * @param string $userId
     * @param string $callId
     * @return boolean
     */
    public function del($userId, $callId)
    {
        $key = $this->getCacheKey($userId, $callId);
        $this->putInCache($key, $userId, $callId, null, self::STATE_PENDING_DELETE);
        return true;
    }

    /**
     * Sends the changes to the storage.
     *
     * @param string $userId
     * @param string $callId
     * @return bool
     */
    public function persist($userId = null, $callId = null)
    {
        if ($userId && $callId) {
            $keys = [$this->getCacheKey($userId, $callId)];
        } else {
            $keys = array_keys($this->cache);
        }

        $success = true;
        foreach ($keys as $key) {
            if (!$this->persistCacheEntry($key)) {
                $success = false;
            }
        }
        return $success;
    }
}


<?php
/**
 * 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; under version 2
 * of the License (non-upgradable).
 *
 * 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 Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;
 *
 */

namespace oat\taoQtiTest\models\runner\time\storageFormat;

use oat\taoQtiTest\models\runner\time\QtiTimeLine;
use oat\taoTests\models\runner\time\TimeLine;
use oat\taoTests\models\runner\time\TimePoint;

/**
 * Class QtiTimeStoragePackedFormat
 *
 * Encode/decode QtiTimer using JSON format.
 * Apply a compression on the QtiTimeLine entries to reduce the weight of the encoded data.
 *
 * Applied format:
 *
 * ```
 * {
 *      // For each tags set an index that points to each linked timestamps.
 *      // Each index value is the offset of the timestamp in the "points" array.
 *      "index": {
 *          "tag1": [0, 1, 2, 6, ...],
 *          "tag2": [0, 2, 4, ...],
 *          ...
 *          "tagN": [100, 101, 110, 111, ...],
 *      },
 *
 *      // A list of tags that applies for all timestamps.
 *      "tags": [
 *          "global-tag-1",
 *          "global-tag-2",
 *          ...
 *          "global-tag-N",
 *      ],
 *
 *      // A flat list of timestamps, their tags are moved into the index map.
 *      // Each timestamp is reduced by a reference value to minimize the final size.
 *      "points": [
 *          [target, type, timestamp - todayTimestamp],
 *          [target, type, timestamp - todayTimestamp],
 *          ...
 *          [target, type, timestamp - todayTimestamp]
 *      ],
 *
 *      // The arbitrary reference value that has been used to reduce the timestamps.
 *      // Usually it is the timestamp of today morning at 00:00.
 *      // (could be improved to be based on the assessment start time)
 *      "epoch": todayTimestamp
 * }
 * ```
 *
 * @package oat\taoQtiTest\models\runner\time\storageFormat
 * @author Jean-Sébastien Conan <jean-sebastien@taotesting.com>
 */
class QtiTimeStoragePackedFormat extends QtiTimeStorageJsonFormat
{
    /**
     * A storage key added to the sored data set to keep format type
     */
    const STORAGE_KEY_FORMAT = 'format';

    /**
     * A storage key added to the sored data set to keep version info
     */
    const STORAGE_KEY_VERSION = 'version';

    /**
     * The storage key for the TimeLine index in the packed format
     */
    const STORAGE_KEY_TIMELINE_INDEX = 'index';

    /**
     * The storage key for the TimeLine tags in the packed format
     */
    const STORAGE_KEY_TIMELINE_TAGS = 'tags';

    /**
     * The storage key for the TimeLine points in the packed format
     */
    const STORAGE_KEY_TIMELINE_POINTS = 'points';

    /**
     * The storage key for the timestamp reference in the packed format
     */
    const STORAGE_KEY_TIMELINE_EPOCH = 'epoch';

    /**
     * The type of format managed by this class
     */
    const STORAGE_FORMAT = 'pack';

    /**
     * The version of the format. Could be useful in case of changes to take care of legacy.
     */
    const STORAGE_VERSION = 1;

    /**
     * The type of format applied by this class
     * @return string
     */
    public function getFormat()
    {
        return static::STORAGE_FORMAT;
    }

    /**
     * The version of the packing format applied
     * @return int
     */
    public function getVersion()
    {
        return static::STORAGE_VERSION;
    }

    /**
     * The reference value used to compress the timestamps
     * @return int
     */
    public function getEpoch()
    {
        // align the reference value with the timestamp of today morning.
        $today = time();
        return $today - $today % 86400;
    }

    /**
     * Packs a TimeLine in order to reduce the storage footprint
     * @param TimeLine $timeLine
     * @return array
     */
    protected function packTimeLine(&$timeLine)
    {
        $epoch = $this->getEpoch();
        $data = [
            self::STORAGE_KEY_TIMELINE_INDEX => [],
            self::STORAGE_KEY_TIMELINE_TAGS => [],
            self::STORAGE_KEY_TIMELINE_POINTS => [],
            self::STORAGE_KEY_TIMELINE_EPOCH => $epoch,
        ];

        // Will split tags from the list of TimePoint, and put them into a dedicated index.
        // The other TimePoint info are put in a simple array with predictable order, this way: [target, type, timestamp].
        // To save more space a reference value is removed from each timestamp.
        $index = 0;
        foreach ($timeLine->getPoints() as &$point) {
            /** @var TimePoint $point */
            $data[self::STORAGE_KEY_TIMELINE_POINTS][$index] = [$point->getTarget(), $point->getType(), round($point->getTimestamp() - $epoch, 6)];

            foreach ($point->getTags() as &$tag) {
                $data[self::STORAGE_KEY_TIMELINE_INDEX][$tag][] = $index;
            }

            $index++;
        }

        // try to reduce the size of the index by simplifying those that target all TimePoint
        if ($index) {
            foreach ($data[self::STORAGE_KEY_TIMELINE_INDEX] as $tag => &$list) {
                if (count($list) == $index) {
                    unset($data[self::STORAGE_KEY_TIMELINE_INDEX][$tag]);
                    $data[self::STORAGE_KEY_TIMELINE_TAGS][] = $tag;
                }
            }
        } else {
            $data = [];
        }

        return $data;
    }

    /**
     * Unpack a dataset to a workable TimeLine
     * @param array $data
     * @return TimeLine
     */
    protected function unpackTimeLine(&$data)
    {
        $timeLine = new QtiTimeLine();

        // the stored data can be packed or not
        if (isset($data[self::STORAGE_KEY_TIMELINE_POINTS])) {
            // get the reference value used to compress the timestamps
            $epoch = 0;
            if (isset($data[self::STORAGE_KEY_TIMELINE_EPOCH])) {
                $epoch = $data[self::STORAGE_KEY_TIMELINE_EPOCH];
            }

            // rebuild the TimeLine from the list of stored TimePoint
            $tags = $data[self::STORAGE_KEY_TIMELINE_TAGS];
            foreach ($data[self::STORAGE_KEY_TIMELINE_POINTS] as &$dataPoint) {
                $point = new TimePoint($tags, $dataPoint[2] + $epoch, $dataPoint[1], $dataPoint[0]);
                $timeLine->add($point);
            }

            // reassign the tags from the stored index
            $points = $timeLine->getPoints();
            foreach ($data[self::STORAGE_KEY_TIMELINE_INDEX] as $tag => &$list) {
                foreach ($list as $index) {
                    $points[$index]->addTag($tag);
                }
            }
        } else {
            $timeLine->fromArray($data);
        }

        return $timeLine;
    }

    /**
     * Encode a dataset with the managed format.
     * @param mixed $data
     * @return string
     */
    public function encode($data)
    {
        if (is_array($data)) {
            $encodedData = [
                self::STORAGE_KEY_FORMAT => $this->getFormat(),
                self::STORAGE_KEY_VERSION => $this->getVersion(),
            ];

            foreach ($data as $key => &$value) {
                if ($value instanceof TimeLine) {
                    $encodedData[$key] = $this->packTimeLine($value);
                } else {
                    $encodedData[$key] = &$value;
                }
            }

            return json_encode($encodedData);
        }

        return json_encode($data);
    }

    /**
     * Decode a string encoded with the managed format.
     * @param string $data
     * @return mixed
     */
    public function decode($data)
    {
        $decodedData = json_decode($data, true);

        // fallback for old storage that uses PHP serialize format
        if (is_null($decodedData) && $data) {
            $decodedData = unserialize($data);
        }

        if (is_array($decodedData)) {
            if (isset($decodedData[self::STORAGE_KEY_FORMAT])) {
                if ($decodedData[self::STORAGE_KEY_FORMAT] != $this->getFormat()) {
                    \common_Logger::w(sprintf('QtiTimeStorage: wrong decoder applied! (Expected: %s, Applied: %s)', $decodedData[self::STORAGE_KEY_FORMAT], $this->getFormat()));
                }

                foreach ($decodedData as $key => &$value) {
                    if (is_array($value)) {
                        $decodedData[$key] = $this->unpackTimeLine($value);
                    }
                }

                unset($decodedData[self::STORAGE_KEY_FORMAT]);
                unset($decodedData[self::STORAGE_KEY_VERSION]);
            } else {
                foreach ($decodedData as $key => &$value) {
                    if (is_array($value)) {
                        $timeLine = new QtiTimeLine();
                        $timeLine->fromArray($value);
                        $decodedData[$key] = $timeLine;
                    }
                }
            }
        }

        return $decodedData;
    }
}


<?php
/**
 * 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; under version 2
 * of the License (non-upgradable).
 *
 * 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 Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2018 (original work) Open Assessment Technologies SA;
 */

namespace oat\taoQtiTest\scripts\tools;

use oat\generis\model\OntologyAwareTrait;
use \common_report_Report as Report;
use oat\taoDelivery\model\AssignmentAggregator\UnionAssignmentService;
use oat\taoDeliveryRdf\model\DeliveryAssemblyService;
use oat\taoQtiItem\model\QtiJsonItemCompiler;
use qtism\data\AssessmentItemRef;
use oat\oatbox\extension\script\ScriptAction;

/**
 * Class RecompileItemsElements
 *
 * Update every deliveries in order to add the metadata of AssessmentItemRef's by AssessmentItemRef Identifier.
 *
 * php index.php 'oat\taoQtiTest\scripts\tools\RecompileItemsElements'
 *
 * Usage example:
 * ```
 * sudo -u www-data php index.php '\oat\taoQtiTest\scripts\tools\RecompileItemsElements'
 * ```
 *
 * @package oat\taoQtiTest\scripts\tools
 */
class RecompileItemsElements extends ScriptAction
{
    use OntologyAwareTrait;

    /** @var Report */
    private $report;

    private $wetRun;

    /**
     * @return string
     */
    protected function provideDescription()
    {
        return 'Update every deliveries in order to add the metadata of AssessmentItemRef\'s by AssessmentItemRef Identifier.';
    }

    /**
     * @return array
     */
    protected function provideOptions()
    {
        return [
            'wetRun' => [
                'longPrefix' => 'wetRun',
                'required' => false,
                'description' => 'Wet run',
                'defaultValue' => 0
            ]
        ];
    }

    /**
     * @return Report
     * @throws \common_Exception
     * @throws \common_exception_Error
     */
    protected function run()
    {
        try {
            $this->init();
        } catch (\Exception $e) {
            return new Report(Report::TYPE_ERROR, $e->getMessage());
        }
        $this->recompileItems();
        return $this->report;
    }

    /**
     * Initialize parameters
     */
    protected function init()
    {
        $this->wetRun = (boolean) $this->getOption('wetRun');
        $this->report = new Report(Report::TYPE_INFO, 'Starting recompile deliveries');
    }

    /**
     * Recompile items
     *
     * @throws \common_Exception
     * @throws \common_exception_Error
     */
    protected function recompileItems()
    {
        $extManager = $this->getServiceLocator()->get(\common_ext_ExtensionsManager::SERVICE_ID);

        if ($extManager->isInstalled('taoDeliveryRdf') === true && $extManager->isEnabled('taoDeliveryRdf') === true) {
            $compiledDeliveryClass = $this->getClass(DeliveryAssemblyService::CLASS_URI);
            if ($compiledDeliveryClass->exists() === true) {
                foreach ($compiledDeliveryClass->getInstances(true) as $compiledDelivery) {
                    try {
                        $this->getAssessmentsFromDelivery($compiledDelivery);
                    } catch (\Exception $e) {
                        $this->report->add(
                            new Report(
                                Report::TYPE_WARNING,
                                "Delivery {$compiledDelivery->getUri()} was skipped with message: '{$e->getMessage()}'"
                            )
                        );
                    }
                }
            }
        } else {
            $this->report->add(
                new Report(
                    Report::TYPE_WARNING,
                    "Extension taoDeliveryRdf is not installed. No compilation environment is available."
                )
            );
        }
    }

    /**
     * Get assessmentItemRef from delivery
     *
     * @param \core_kernel_classes_Resource $compiledDelivery
     *
     * @throws \common_Exception
     * @throws \common_exception_Error
     */
    protected function getAssessmentsFromDelivery(\core_kernel_classes_Resource $compiledDelivery)
    {
        /** @var UnionAssignmentService $unionAssignmentService */
        $unionAssignmentService = $this->getServiceLocator()->get(UnionAssignmentService::SERVICE_ID);
        $runtime = $unionAssignmentService->getRuntime($compiledDelivery);

        $inputParameters = \tao_models_classes_service_ServiceCallHelper::getInputValues($runtime, []);
        $testDefinition = \taoQtiTest_helpers_Utils::getTestDefinition($inputParameters['QtiTestCompilation']);

        $assessmentItemRefs = $testDefinition->getComponentsByClassName('assessmentItemRef');

        $this->report->add(
            new Report(
                Report::TYPE_INFO,
                "Starting to recompile items for delivery {$compiledDelivery->getLabel()} with identifier {$compiledDelivery->getUri()}:"
            )
        );

        $count = 0;

        /** @var AssessmentItemRef $assessmentItemRef */
        foreach ($assessmentItemRefs as $assessmentItemRef) {
            $directoryIds = explode('|', $assessmentItemRef->getHref());
            $item = $this->getResource($directoryIds[0]);
            $properties = [];
            foreach ($item->getRdfTriples() as $triple) {
                $properties[$triple->predicate] = $triple->object;
            }

            if ($properties) {
                $directory = \tao_models_classes_service_FileStorage::singleton()->getDirectoryById($directoryIds[2]);
                $languages = $item->getUsedLanguages(
                    $this->getProperty(\taoItems_models_classes_ItemsService::PROPERTY_ITEM_CONTENT)
                );
                foreach ($languages as $lang) {
                    $path = $lang.DIRECTORY_SEPARATOR.QtiJsonItemCompiler::METADATA_FILE_NAME;
                    if (!$directory->has($path)) {
                        $this->writeMetadata($item, $directory, $path, $properties);
                        $count++;
                    }
                }
            }
        }
        $this->report->add(new Report(Report::TYPE_INFO, "Was updated {$count} items."));
    }

    /**
     * @param \core_kernel_classes_Resource $item
     * @param \tao_models_classes_service_StorageDirectory $directory
     * @param $path
     * @param $properties
     * @throws \common_Exception
     * @throws \common_exception_Error
     */
    protected function writeMetadata(
        \core_kernel_classes_Resource $item,
        \tao_models_classes_service_StorageDirectory $directory,
        $path,
        $properties
    ) {

        if ($this->wetRun === true) {
            $directory->write(
                $path,
                json_encode($properties)
            );
            $this->report->add(
                new Report(
                    Report::TYPE_SUCCESS,
                    "Metadata for assessmentItemRef '{$item->getLabel()}' with identifier '{$item->getUri()}' was successfully compiled."
                )
            );
        } else {
            $this->report->add(
                new Report(
                    Report::TYPE_SUCCESS,
                    "Metadata for assessmentItemRef '{$item->getLabel()}' with identifier '{$item->getUri()}' will successfully compiled."
                )
            );
        }
    }
}


<?php
/**
 * 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; under version 2
 * of the License (non-upgradable).
 *
 * 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 Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;
 */

namespace oat\taoQtiTest\models\runner;

use oat\oatbox\service\ServiceManager;

trait RunnerToolStates
{
    /**
     * @param string $name
     * @return string
     */
    abstract public function hasRequestParameter($name);

    /**
     * @param string $name
     * @return string mixed
     */
    abstract public function getRequestParameter($name);

    /**
     * @param string $name
     * @return string
     */
    abstract public function getRawRequestParameter($name);

    /**
     * @return ServiceManager
     */
    abstract public function getServiceLocator();

    /**
     * @return RunnerService
     */
    abstract public function getRunnerService();

    /**
     * @return RunnerServiceContext
     */
    abstract public function getServiceContext();

    /**
     * Save the tool state if some are found in the current request
     *
     * @throws \oat\oatbox\service\exception\InvalidServiceManagerException
     * @throws \common_Exception
     *
     * @return boolean true if some state was found and saved
     */
    protected function saveToolStates()
    {
        $toolStateParameter = 'toolStates';
        if ($this->hasRequestParameter($toolStateParameter)) {
            // since the parameter content is a JSON string
            // we need to load it using the raw mode
            $param = $this->getRawRequestParameter($toolStateParameter);
            if ($param) {
                $toolStates = json_decode($param, true);

                if (count($toolStates) > 0) {
                    array_walk($toolStates, function (&$toolState) {
                        $toolState = json_encode($toolState);
                    });
                    $this->getRunnerService()->setToolsStates(
                        $this->getServiceContext(),
                        $toolStates
                    );
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Get the current tools states
     *
     * @throws \oat\oatbox\service\exception\InvalidServiceManagerException
     * @throws \common_Exception
     * @throws \common_ext_ExtensionException
     *
     * @return array the tools states
     */
    protected function getToolStates()
    {
        $toolStates = $this->getRunnerService()->getToolsStates($this->getServiceContext());
        array_walk($toolStates, function (&$toolState) {
            $toolState = json_decode($toolState);
        });
        return $toolStates;
    }
}