<?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) 2016 (original work) Open Assessment Technologies SA ;
 */

/**
 * @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
 */

namespace oat\taoQtiTest\models\runner\map;

use oat\oatbox\service\ConfigurableService;
use oat\taoQtiTest\models\ExtendedStateService;
use oat\taoQtiTest\models\cat\CatService;
use oat\taoQtiTest\models\runner\QtiRunnerServiceContext;
use oat\taoQtiTest\models\runner\RunnerServiceContext;
use oat\taoQtiTest\models\runner\config\RunnerConfig;
use oat\taoQtiTest\models\runner\session\TestSession;
use oat\taoQtiTest\models\runner\time\QtiTimeConstraint;
use qtism\data\NavigationMode;
use qtism\data\QtiComponent;
use qtism\runtime\tests\AssessmentTestSession;
use qtism\runtime\tests\RouteItem;
use taoQtiTest_helpers_TestRunnerUtils as TestRunnerUtils;
use oat\taoQtiTest\models\cat\CatUtils;

/**
 * Class QtiRunnerMap
 * @package oat\taoQtiTest\models\runner\map
 */
class QtiRunnerMap extends ConfigurableService implements RunnerMap
{
    const SERVICE_ID = 'taoQtiTest/QtiRunnerMap';

    /**
     * Fallback index in case of the delivery was compiled without the index of item href
     * @var array
     */
    protected $itemHrefIndex;

    /**
     * Gets the file that contains the href for the AssessmentItemRef Identifier
     * @param QtiRunnerServiceContext $context
     * @param string $itemIdentifier
     * @return \oat\oatbox\filesystem\File
     */
    protected function getItemHrefIndexFile(QtiRunnerServiceContext $context, $itemIdentifier)
    {
        $compilationDirectory = $context->getCompilationDirectory()['private'];
        return $compilationDirectory->getFile(\taoQtiTest_models_classes_QtiTestCompiler::buildHrefIndexPath($itemIdentifier));
    }

    /**
     * Checks if the AssessmentItemRef Identifier is in the index.
     *
     * @param QtiRunnerServiceContext $context
     * @param string $itemIdentifier
     * @return boolean
     */
    protected function hasItemHrefIndexFile(QtiRunnerServiceContext $context, $itemIdentifier)
    {
        // In case the context is adaptive, it means that the delivery was compiled in a version
        // we are 100% sure it produced Item Href Index Files.
        if ($context->isAdaptive()) {
            return true;
        } else {
            $indexFile = $this->getItemHrefIndexFile($context, $itemIdentifier);
            return $indexFile->exists();
        }
    }

    /**
     * Gets AssessmentItemRef's Href by AssessmentItemRef Identifier.
     *
     * Returns the AssessmentItemRef href attribute value from a given $identifier.
     *
     * @param QtiRunnerServiceContext $context
     * @param string $itemIdentifier
     * @return boolean|string The href value corresponding to the given $identifier. If no corresponding href is found, false is returned.
     */
    public function getItemHref(QtiRunnerServiceContext $context, $itemIdentifier)
    {
        $href = false;

        $indexFile = $this->getItemHrefIndexFile($context, $itemIdentifier);

        if ($indexFile->exists()) {
            $href = $indexFile->read();
        } else {
            if (!isset($this->itemHrefIndex)) {
                $storage = $this->getServiceLocator()->get(ExtendedStateService::SERVICE_ID);
                $this->itemHrefIndex = $storage->loadItemHrefIndex($context->getTestExecutionUri());
            }

            if (isset($this->itemHrefIndex[$itemIdentifier])) {
                $href = $this->itemHrefIndex[$itemIdentifier];
            }
        }

        return $href;
    }

    /**
     * Builds the map of an assessment test
     * @param RunnerServiceContext $context The test context
     * @param RunnerConfig $config The runner config
     * @return mixed
     * @throws \common_exception_InvalidArgumentType
     */
    public function getMap(RunnerServiceContext $context, RunnerConfig $config)
    {
        return $this->getScopedMap($context, $config, RunnerMap::SCOPE_TEST);
    }

    /**
     * Get the testMap for the current context but limited to the given scope
     * @param RunnerServiceContext $context The test context
     * @param RunnerConfig $config The runner config
     * @param string $scope the target scope, section by default
     * @return mixed
     * @throws \common_exception_InvalidArgumentType
     */
    public function getScopedMap(RunnerServiceContext $context, RunnerConfig $config, $scope = RunnerMap::SCOPE_SECTION)
    {
        if (!($context instanceof QtiRunnerServiceContext)) {
            throw new \common_exception_InvalidArgumentType(
                'QtiRunnerMap',
                'getMap',
                0,
                'oat\taoQtiTest\models\runner\QtiRunnerServiceContext',
                $context
            );
        }

        $map = [
            'scope' => $scope,
            'parts' => []
        ];

        // get config for the sequence number option
        $reviewConfig = $config->getConfigValue('review');
        $checkInformational = $config->getConfigValue('checkInformational');
        $forceTitles = !empty($reviewConfig['forceTitle']);
        $forceInformationalTitles = !empty($reviewConfig['forceInformationalTitle']);
        $useTitle = !empty($reviewConfig['useTitle']);
        $uniqueTitle = isset($reviewConfig['itemTitle']) ? $reviewConfig['itemTitle'] : '%d';
        $uniqueInformationalTitle = isset($reviewConfig['informationalItemTitle']) ? $reviewConfig['informationalItemTitle'] : 'Instructions';
        $displaySubsectionTitle = isset($reviewConfig['displaySubsectionTitle']) ? (bool) $reviewConfig['displaySubsectionTitle'] : true;

        /* @var AssessmentTestSession $session */
        $session = $context->getTestSession();
        $extendedStorage = $this->getServiceLocator()->get(ExtendedStateService::SERVICE_ID);
        if ($session->isRunning() !== false) {
            $route         = $session->getRoute();
            $store         = $session->getAssessmentItemSessionStore();

            switch($scope){
                case RunnerMap::SCOPE_SECTION :
                    $routeItems = $route->getRouteItemsByAssessmentSection($session->getCurrentAssessmentSection());
                    break;
                case RunnerMap::SCOPE_PART :
                    $routeItems = $route->getRouteItemsByTestPart($session->getCurrentTestPart());
                    break;
                case RunnerMap::SCOPE_TEST:
                default:
                    $routeItems = $route->getAllRouteItems();
                    break;
            }

            $offset = $this->getOffsetPosition($context, $routeItems[0]);
            $offsetSection = 0;
            $lastSection   = null;

            // fallback index in case of the delivery was compiled without the index of item href
            $this->itemHrefIndex = [];
            $shouldBuildItemHrefIndex = !$this->hasItemHrefIndexFile($context, $session->getCurrentAssessmentItemRef()->getIdentifier());
            \common_Logger::t('Store index ' . ($shouldBuildItemHrefIndex ? 'must be built' : 'is part of the package'));

            /** @var \qtism\runtime\tests\RouteItem $routeItem */
            foreach ($routeItems as $routeItem) {

                $catSession = false;
                $itemRefs = $this->getRouteItemAssessmentItemRefs($context, $routeItem, $catSession);
                $previouslySeenItems = ($catSession) ? $context->getPreviouslySeenCatItemIds($routeItem) : [];

                foreach ($itemRefs as $itemRef) {
                    $occurrence = ($catSession !== false) ? 0 : $routeItem->getOccurence();

                    // get the jump definition
                    $itemSession = ($catSession !== false) ? false : $store->getAssessmentItemSession($itemRef, $occurrence);

                    // load item infos
                    $isItemInformational = ($itemSession) ? TestRunnerUtils::isItemInformational($routeItem, $itemSession) : false;
                    $testPart = $routeItem->getTestPart();
                    $partId = $testPart->getIdentifier();
                    $navigationMode = $testPart->getNavigationMode();

                    if ($displaySubsectionTitle) {
                        $section = $routeItem->getAssessmentSection();
                    } else {
                        $sections = $routeItem->getAssessmentSections()->getArrayCopy();
                        $section = $sections[0];
                    }
                    $sectionId = $section->getIdentifier();
                    $itemId = $itemRef->getIdentifier();

                    if ($lastSection != $sectionId) {
                        $offsetSection = 0;
                        $lastSection = $sectionId;
                    }

                    if ($forceInformationalTitles && $isItemInformational) {
                        $itemUri = strstr($itemRef->getHref(), '|', true);
                        $label = $uniqueInformationalTitle === false
                            ? $this->getItemLabel($context, $itemUri, $useTitle)
                            : $uniqueInformationalTitle;

                    } elseif ($forceTitles) {
                        $label = __($uniqueTitle, $offsetSection + 1);

                    } else {
                        $itemUri = strstr($itemRef->getHref(), '|', true);
                        $label = $this->getItemLabel($context, $itemUri, $useTitle);
                    }

                    // fallback in case of the delivery was compiled without the index of item href
                    if ($shouldBuildItemHrefIndex) {
                        $this->itemHrefIndex[$itemId] = $itemRef->getHref();
                    }

                    $itemInfos = [
                        'id' => $itemId,
                        'label' => $label,
                        'position' => $offset,
                        'occurrence' => $occurrence,
                        'remainingAttempts' => ($itemSession) ? $itemSession->getRemainingAttempts() : -1,
                        'answered' => ($itemSession) ? TestRunnerUtils::isItemCompleted($routeItem, $itemSession) : in_array($itemId, $previouslySeenItems),
                        'flagged' => $extendedStorage->getItemFlag($session->getSessionId(), $itemId),
                        'viewed' => ($itemSession) ? $itemSession->isPresented() : in_array($itemId, $previouslySeenItems),
                        'categories' => $itemRef->getCategories()->getArrayCopy()
                    ];

                    if ($checkInformational) {
                        $itemInfos['informational'] = $isItemInformational;
                    }

                    if($itemRef->hasTimeLimits()){
                        $itemInfos['timeConstraint'] = $this->getTimeConstraint($session, $itemRef, $navigationMode);
                    }

                    if (!isset($map['parts'][$partId]) && $scope != RunnerMap::SCOPE_SECTION) {
                        $map['parts'][$partId]['id'] = $partId;
                        $map['parts'][$partId]['label'] = $partId;
                        $map['parts'][$partId]['position'] = $offset;
                        $map['parts'][$partId]['isLinear'] = $navigationMode == NavigationMode::LINEAR;

                        if($testPart->hasTimeLimits()){
                            $map['parts'][$partId]['timeConstraint'] =  $this->getTimeConstraint($session, $testPart, $navigationMode);
                        }
                    }

                    if (!isset($map['parts'][$partId]['sections'][$sectionId])) {
                        $map['parts'][$partId]['sections'][$sectionId]['id'] = $sectionId;
                        $map['parts'][$partId]['sections'][$sectionId]['label'] = $section->getTitle();
                        $map['parts'][$partId]['sections'][$sectionId]['isCatAdaptive'] = CatUtils::isAssessmentSectionAdaptive($section);
                        $map['parts'][$partId]['sections'][$sectionId]['position'] = $offset;

                        if($section->hasTimeLimits()){
                            $map['parts'][$partId]['sections'][$sectionId]['timeConstraint'] = $this->getTimeConstraint($session, $section, $navigationMode);
                        }
                    }

                    $map['parts'][$partId]['sections'][$sectionId]['items'][$itemId] = $itemInfos;

                    // update the stats
                    if($scope == RunnerMap::SCOPE_TEST) {
                        $this->updateStats($map, $itemInfos);
                        $this->updateStats($map['parts'][$partId], $itemInfos);
                        $this->updateStats($map['parts'][$partId]['sections'][$sectionId], $itemInfos);
                    }
                    if($scope == RunnerMap::SCOPE_PART) {
                        $this->updateStats($map['parts'][$partId], $itemInfos);
                        $this->updateStats($map['parts'][$partId]['sections'][$sectionId], $itemInfos);
                    }
                    if($scope == RunnerMap::SCOPE_SECTION) {
                        $this->updateStats($map['parts'][$partId]['sections'][$sectionId], $itemInfos);
                    }

                    $offset ++;
                    if (!$forceInformationalTitles || ($forceInformationalTitles && !$isItemInformational)) {
                        $offsetSection++;
                    };
                }
            }
            // fallback in case of the delivery was compiled without the index of item href
            if ($shouldBuildItemHrefIndex) {
                \common_Logger::t('Store index of item href into the test state storage');
                $storage = $this->getServiceLocator()->get(ExtendedStateService::SERVICE_ID);
                $storage->storeItemHrefIndex($context->getTestExecutionUri(), $this->itemHrefIndex);
            }
        }

        return $map;
    }

    /**
     * Update the stats inside the target
     * @param array $target
     * @param array $itemInfos
     */
    protected function updateStats(&$target, $itemInfos)
    {
        if (!isset($target['stats'])) {
            $target['stats'] = [
                'questions' => 0,
                'answered' => 0,
                'flagged' => 0,
                'viewed' => 0,
                'total' => 0,
                'questionsViewed' => 0,
            ];
        }

        if (empty($itemInfos['informational'])) {
            $target['stats']['questions'] ++;

            if (!empty($itemInfos['answered'])) {
                $target['stats']['answered'] ++;
            }

            if (!empty($itemInfos['viewed'])) {
                $target['stats']['questionsViewed'] ++;
            }
        }

        if (!empty($itemInfos['flagged'])) {
            $target['stats']['flagged'] ++;
        }

        if (!empty($itemInfos['viewed'])) {
            $target['stats']['viewed'] ++;
        }

        $target['stats']['total'] ++;
    }

    /**
     * Get AssessmentItemRef objects.
     *
     * Get the AssessmentItemRef objects bound to a RouteItem object. In most of cases, an array of a single
     * AssessmentItemRef object will be returned. But in case of the given $routeItem is a CAT Adaptive Placeholder,
     * multiple AssessmentItemRef objects might be returned.
     *
     * @param RunnerServiceContext $context
     * @param RouteItem $routeItem
     * @param mixed $catSession A reference to a variable that will be fed with the CatSession object related to the $routeItem. In case the $routeItem is not bound to a CatSession object, $catSession will be set with false.
     * @return array An array of AssessmentItemRef objects.
     */
    protected function getRouteItemAssessmentItemRefs(RunnerServiceContext $context, RouteItem $routeItem, &$catSession)
    {
        /* @var CatService */
        $catService = $this->getServiceManager()->get(CatService::SERVICE_ID);
        $compilationDirectory = $context->getCompilationDirectory()['private'];
        $itemRefs = [];
        $catSession = false;

        if ($context->isAdaptive($routeItem->getAssessmentItemRef())) {
            $catSession = $context->getCatSession($routeItem);

            $itemRefs = $catService->getAssessmentItemRefByIdentifiers(
                $compilationDirectory,
                $context->getShadowTest($routeItem)
            );
        } else {
            $itemRefs[] = $routeItem->getAssessmentItemRef();
        }

        return $itemRefs;
    }

     /**
      * Get the relative position of the given RouteItem within the test.
      * The position takes into account adaptive sections (and count items instead of placeholders).
      *
      * @param RunnerServiceContext $context
      * @param RouteItem $routeItem
      * @return int the offset position
      */
     protected function getOffsetPosition(RunnerServiceContext $context, RouteItem $currentRouteItem)
     {
        $session = $context->getTestSession();
        $route = $session->getRoute();
        $routeCount = $route->count();

        $finalPosition = 0;

        for ($i = 0; $i < $routeCount; $i++) {
            $routeItem = $route->getRouteItemAt($i);

            if ($routeItem !== $currentRouteItem) {
                if (!$context->isAdaptive($routeItem->getAssessmentItemRef())) {
                    $finalPosition++;
                } else {
                    $finalPosition += count($context->getShadowTest($routeItem));
                }
            } else {
                break;
            }
        }

        return $finalPosition;
    }

    /**
     * Get the time constraint for the given component
     * @param TestSession  $session the running test session
     * @param QtiComponent $source the component with the time limits (testPart, section, itemRef)
     * @param int          $navigationMode the testPart navigation mode
     * @return QtiTimeConstraint the constraint
     */
    private function getTimeConstraint(TestSession $session, QtiComponent $source, $navigationMode)
    {
        $constraint = new QtiTimeConstraint(
            $source,
            $session->getTimerDuration($source->getIdentifier()),
            $navigationMode,
            true,
            true,
            $session->getTimerTarget()
        );
        $constraint->setTimer($session->getTimer());
        return $constraint;
    }

     /**
      * Get the label of a Map item
      *
      * @param RunnerServiceContext $context
      * @param string $itemUri
      * @param int $useTitle
      * @return string the title
      */
    private function getItemLabel(RunnerServiceContext $context, $itemUri, $useTitle = false)
    {
        $label = '';

        if ($useTitle) {
            $label = $context->getItemIndexValue($itemUri, 'title');
        }

        if (!$label) {
            $label = $context->getItemIndexValue($itemUri, 'label');
        }

        if (!$label) {
            $item = new \core_kernel_classes_Resource($itemUri);
            $label = $item->getLabel();
        }
        return $label;
    }
}


<?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) 2016 (original work) Open Assessment Technologies SA ;
 */
/**
 * @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
 */

namespace oat\taoQtiTest\models\runner\navigation;

use oat\taoQtiTest\models\event\QtiMoveEvent;
use oat\taoQtiTest\models\runner\RunnerServiceContext;
use oat\taoQtiTest\models\runner\QtiRunnerServiceContext;
use qtism\data\ExtendedAssessmentItemRef;
use qtism\runtime\tests\AssessmentItemSession;
use oat\oatbox\service\ServiceManager;
use oat\oatbox\event\EventManager;
use qtism\runtime\tests\AssessmentTestSession;

/**
 * Class QtiRunnerNavigation
 * @package oat\taoQtiTest\models\runner\navigation
 */
class QtiRunnerNavigation
{
    /**
     * Gets a QTI runner navigator
     * @param string $direction
     * @param string $scope
     * @return RunnerNavigation
     * @throws \common_exception_InconsistentData
     * @throws \common_exception_NotImplemented
     */
    public static function getNavigator($direction, $scope)
    {
        $className = __NAMESPACE__ . '\QtiRunnerNavigation' . ucfirst($direction) . ucfirst($scope);
        if (class_exists($className)) {
            $navigator = new $className();
            if ($navigator instanceof RunnerNavigation) {
                return $navigator;
            } else {
                throw new \common_exception_InconsistentData('Navigator must be an instance of RunnerNavigation');
            }
        } else {
            throw new \common_exception_NotImplemented('The action is invalid!');
        }
    }

    /**
     * @param string $direction
     * @param string $scope
     * @param RunnerServiceContext $context
     * @param integer $ref
     * @throws \common_exception_InvalidArgumentType
     * @throws \common_exception_NotImplemented
     * @return boolean
     */
    public static function move($direction, $scope, RunnerServiceContext $context, $ref)
    {
        $navigator = self::getNavigator($direction, $scope);

        if ($context instanceof QtiRunnerServiceContext) {
            $from = $context->getTestSession()->isRunning() === true ? $context->getTestSession()->getRoute()->current() : null;
            $event = new QtiMoveEvent(QtiMoveEvent::CONTEXT_BEFORE, $context->getTestSession(), $from);
            ServiceManager::getServiceManager()->get(EventManager::SERVICE_ID)->trigger($event);
        }

        $result = $navigator->move($context, $ref);

        if ($context instanceof QtiRunnerServiceContext) {
            $to = $context->getTestSession()->isRunning() === true ? $context->getTestSession()->getRoute()->current() : null;
            $event = new QtiMoveEvent(QtiMoveEvent::CONTEXT_AFTER, $context->getTestSession(), $from, $to);
            ServiceManager::getServiceManager()->get(EventManager::SERVICE_ID)->trigger($event);
        }

        return $result;
    }

    /**
     * Check if a timed section is exited
     * @param RunnerServiceContext $context
     * @param int $nextPosition
     */
    public static function checkTimedSectionExit(RunnerServiceContext $context, $nextPosition)
    {
        $timerConfig = $context->getTestConfig()->getConfigValue('timer');
        if (empty($timerConfig['keepUpToTimeout'])) {
            /* @var AssessmentTestSession $session */
            $session = $context->getTestSession();
            $route = $session->getRoute();
            $section = $session->getCurrentAssessmentSection();
            $limits = $section->getTimeLimits();

            // As we have only one identifier for the whole adaptive section it will consider a jump of section on the first item
            if(!($context instanceof QtiRunnerServiceContext) || !$context->isAdaptive() ){
                $isJumpOutOfSection = false;
                if (($nextPosition >= 0) && ($nextPosition < $route->count())) {
                    $nextSection = $route->getRouteItemAt($nextPosition);

                    $isJumpOutOfSection = ($section->getIdentifier() !== $nextSection->getAssessmentSection()->getIdentifier());
                }

                if ($isJumpOutOfSection && $limits != null && $limits->hasMaxTime()) {
                    $assessmentItemRefs = $section->getComponentsByClassName('assessmentItemRef');
                    foreach ($assessmentItemRefs as $assessmentItemRef) {
                        $itemSessions = $session->getAssessmentItemSessions($assessmentItemRef->getIdentifier());

                        if($itemSessions !== false){
                            foreach ($itemSessions as $itemSession) {
                                $itemSession->endItemSession();
                            }
                        }
                    }
                }

            }
        }
    }
}


<?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\generis\model\OntologyAwareTrait;
use oat\oatbox\service\ConfigurableService;
use oat\tao\model\resources\ResourceService;

/**
 * This service let's you access the test creator's items
 *
 * @author Bertrand Chevrier <bertrand@taotesting.com>
 */
class CreatorItems extends ConfigurableService
{
    use OntologyAwareTrait;

    const SERVICE_ID = 'taoQtiTest/CreatorItems';

    const ITEM_ROOT_CLASS_URI       = 'http://www.tao.lu/Ontologies/TAOItem.rdf#Item';
    const PROPERTY_ITEM_CONTENT_URI = 'http://www.tao.lu/Ontologies/TAOItem.rdf#ItemContent';
    const PROPERTY_ITEM_MODEL_URI   = 'http://www.tao.lu/Ontologies/TAOItem.rdf#ItemModel';
    const ITEM_MODEL_QTI_URI        = 'http://www.tao.lu/Ontologies/TAOItem.rdf#QTI';
    const LABEL_URI                 = 'http://www.w3.org/2000/01/rdf-schema#label';

    const ITEM_MODEL_SEARCH_OPTION  = 'itemModel';
    const ITEM_CONTENT_SEARCH_OPTION= 'itemContent';

    /**
     * The different lookup formats
     */
    private static $formats = ['list', 'tree'];

    /**
     * Get the list of items classes
     * @return array the classes hierarchy
     */
    public function getItemClasses()
    {
        $itemClass = $this->getClass(self::ITEM_ROOT_CLASS_URI);
        return $this->getResourceService()->getAllClasses($itemClass);
    }

    /**
     * Retrieve QTI Items for the given parameters
     * @param \core_kernel_classes_Class $itemClass the item class
     * @param string $format the lookup format
     * @param string|array  $search to filter by label if a string or provides the search filters
     * @param int $offset for paging
     * @param int $limit  for paging
     * @return array the items
     */
    public function getQtiItems(\core_kernel_classes_Class $itemClass, $format = 'list', $search = '', $offset = 0, $limit = 30)
    {
        $propertyFilters = [];

        if($this->hasOption(self::ITEM_MODEL_SEARCH_OPTION) && $this->getOption(self::ITEM_MODEL_SEARCH_OPTION) !== false){
            $propertyFilters[self::PROPERTY_ITEM_MODEL_URI] = $this->getOption(self::ITEM_MODEL_SEARCH_OPTION);
        }

        if($this->hasOption(self::ITEM_CONTENT_SEARCH_OPTION) && $this->getOption(self::ITEM_MODEL_SEARCH_OPTION) !== false){
            $propertyFilters[self::PROPERTY_ITEM_CONTENT_URI] = '*';
        }

        if(is_string($search) && strlen(trim($search)) > 0){
            $propertyFilters[self::LABEL_URI] = $search;
        }
        if(is_array($search)){
            foreach($search as $uri => $value){
                if( is_string($uri) &&
                    (is_string($value) && strlen(trim($value)) > 0) ||
                    (is_array($value) && count($value) > 0) ) {
                    $propertyFilters[$uri] = $value;
                }
            }
        }

        $result = [];

        //whitelisting's mandatory to prevent hijacking the dependency injection
        if(in_array($format, self::$formats)){

            //load the lookup dynamically using the format
            $itemLookup = $this->getServiceLocator()->get(self::SERVICE_ID . '/' . $format);
            if(!is_null($itemLookup) && $itemLookup instanceof ItemLookup){
                $result = $itemLookup->getItems($itemClass, $propertyFilters, $offset, $limit);
            }
        }
        return $result;
    }

    protected function getResourceService()
    {
        return $this->getServiceLocator()->get(ResourceService::SERVICE_ID);
    }
}


<?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) 2016 (original work) Open Assessment Technologies SA ;
 */
/**
 * @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
 */

namespace oat\taoQtiTest\models\runner\navigation;

use oat\taoQtiTest\models\runner\RunnerServiceContext;

/**
 * Class QtiRunnerNextItem
 * @package oat\taoQtiTest\models\runner\navigation
 */
class QtiRunnerNavigationNextItem implements RunnerNavigation
{
    /**
     * Do the move
     * @param RunnerServiceContext $context
     * @param mixed $ref
     * @return boolean
     * @throws \common_Exception
     */
    public function move(RunnerServiceContext $context, $ref)
    {
        /* @var AssessmentTestSession $session */
        $session = $context->getTestSession();
        $nextPosition = $session->getRoute()->getPosition() + 1;

        if ($context->isAdaptive()) {
            $context->selectAdaptiveNextItem();

            $currentCatItemId = $context->getCurrentCatItemId();
            $shadowTest = $context->getShadowTest();

            $search = array_search($currentCatItemId, $shadowTest);

            if ($search === count($shadowTest) - 1) {
                QtiRunnerNavigation::checkTimedSectionExit($context, $nextPosition);
                $session->moveNext();
            } else {
                $nextCatItemId = $shadowTest[$search + 1];
                $context->persistCurrentCatItemId($nextCatItemId);
                $context->persistSeenCatItemIds($nextCatItemId);
            }
        } else {
            QtiRunnerNavigation::checkTimedSectionExit($context, $nextPosition);
            $session->moveNext();

            // In case of we are now in an adaptive context, let's initialize the CAT session
            // and ask for a new item dynamically.
            if ($context->isAdaptive()) {
                $context->selectAdaptiveNextItem();

                $firstItemId = $context->getShadowTest()[0];

                $context->persistCurrentCatItemId($firstItemId);
                $context->persistSeenCatItemIds($firstItemId);
            }
        }

        return true;
    }
}


<?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) 2013-2017 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
 *
 *
 */
namespace oat\taoQtiTest\models\compilation;

use oat\oatbox\service\ConfigurableService;
use oat\taoQtiItem\model\ItemModel;
use oat\taoQtiItem\model\QtiJsonItemCompiler;
/**
 * TestCompiler factory
 *
 * @access public
 * @author Joel Bout, <joel.bout@tudor.lu>
 * @package taoQtiTest
 */
class CompilationService extends ConfigurableService
{
    /**
     * Boolean option to enable/disable css scoping (enabled by default)
     * @var string
     */
    const OPTION_RUBRIC_BLOCK_CSS_SCOPE = 'rubric-block-stylesheet-scoping';

    /**
     * Boolean option to enable/disable client container for testrunner (enabled by default)
     * @var string
     */
    const OPTION_CLIENT_TESTRUNNER = 'client-testrunner';

    /**
     * Whenever or not style sheets in rubric blocks should be scoped
     * @param boolean $boolean
     */
    public function setRubricBlockStyleSheetScoping($boolean)
    {
        $this->setOption(self::OPTION_RUBRIC_BLOCK_CSS_SCOPE, (boolean)$boolean);
    }

    /**
     * @return \taoQtiTest_models_classes_QtiTestCompiler
     */
    public function getCompiler($resource, $storage)
    {
        $compilerClass = $this->getCompilerClass();
        $compiler = new $compilerClass($resource, $storage);
        $this->propagate($compiler);
        $compiler->setCssScoping($this->getOption(self::OPTION_RUBRIC_BLOCK_CSS_SCOPE));
        $compiler->setClientContainer($this->useClientContainer());
        return $compiler;
    }

    /**
     * Whenever or not to use client container for test runner
     * Fallback determined by qtiItem model extension
     * @return boolean
     */
    public function useClientContainer()
    {
        if ($this->hasOption(self::OPTION_CLIENT_TESTRUNNER)) {
            return $this->getOption(self::OPTION_CLIENT_TESTRUNNER);
        } else {
            // fallback to taoQtiItem config
            $itemModel = $this->getServiceLocator()->get(ItemModel::SERVICE_ID);
            return $itemModel->getCompilerClass() == QtiJsonItemCompiler::class;
        }
    }

    /**
     * Class to use for the compiler object, should not need to be exposed anymore
     * @deprecated
     * @return string
     */
    public function getCompilerClass()
    {
        return \taoQtiTest_models_classes_QtiTestCompiler::class;
    }
}


<?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) 2016-2018 (original work) Open Assessment Technologies SA ;
 */

/**
 * @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
 */

namespace oat\taoQtiTest\models\runner\time;

use oat\oatbox\service\ServiceManager;
use oat\taoTests\models\runner\time\TimePoint;
use qtism\common\datatypes\QtiDuration;
use qtism\data\NavigationMode;
use qtism\data\QtiComponent;
use qtism\runtime\tests\TimeConstraint;
use taoQtiTest_helpers_TestRunnerUtils as TestRunnerUtils;

/**
 * Class QtiTimeConstraint
 *
 * Represents a time constraint during an AssessmentTestSession.
 *
 * @package oat\taoQtiTest\models\runner\time
 */
class QtiTimeConstraint extends TimeConstraint implements \JsonSerializable
{
    /**
     * @var QtiTimer
     */
    protected $timer;

    /**
     * Allow to take care of extra time
     * @var boolean
     */
    protected $applyExtraTime;

    /**
     * @var integer
     */
    protected $timerTarget;

    /**
     * @return QtiTimer
     */
    public function getTimer()
    {
        return $this->timer;
    }

    /**
     * @param QtiTimer $timer
     * @return QtiTimeConstraint
     */
    public function setTimer($timer)
    {
        $this->timer = $timer;
        return $this;
    }

    /**
     * @return boolean
     */
    public function getApplyExtraTime()
    {
        return $this->applyExtraTime;
    }

    /**
     * @param boolean $applyExtraTime
     * @return QtiTimeConstraint
     */
    public function setApplyExtraTime($applyExtraTime)
    {
        $this->applyExtraTime = $applyExtraTime;
        return $this;
    }

    /**
     * @param integer $timerTarget
     * @return QtiTimeConstraint
     */
    public function setTimerTarget($timerTarget)
    {
        $this->timerTarget = $timerTarget;
        return $this;
    }

    /**
     * Create a new TimeConstraint object.
     *
     * @param QtiComponent $source The TestPart or SectionPart the constraint applies on.
     * @param QtiDuration $duration The already spent duration by the candidate on $source.
     * @param int|NavigationMode $navigationMode The current navigation mode.
     * @param boolean $considerMinTime Whether or not to consider minimum time limits.
     * @param boolean $applyExtraTime Allow to take care of extra time
     * @param integer $timerTarget client/server
     */
    public function __construct(
        QtiComponent $source,
        QtiDuration $duration,
        $navigationMode = NavigationMode::LINEAR,
        $considerMinTime = true,
        $applyExtraTime = false,
        $timerTarget = TimePoint::TARGET_SERVER
    ) {
        $this->setSource($source);
        $this->setDuration($duration);
        $this->setNavigationMode($navigationMode);
        $this->setApplyExtraTime($applyExtraTime);
        $this->setTimerTarget($timerTarget);
    }

    /**
     * Get the remaining duration from a source (min or max time, usually)
     * @param QtiDuration $duration the source duration
     * @return Duration|false A Duration object (or false of not available) that represents the remaining time
     */
    protected function getRemainingTimeFrom(QtiDuration $duration)
    {
        if(!is_null($duration)){
            $remaining = clone $duration;

            if ($this->getApplyExtraTime() && $this->timer) {
                // take care of the already consumed extra time under the current constraint
                // and append the full remaining extra time
                // the total must correspond to the already elapsed time plus the remaining time
                $remaining->add(new QtiDuration('PT' . $this->timer->getExtraTime($duration->getSeconds(true)) . 'S'));
            }
            $remaining->sub($this->getDuration());
            return ($remaining->isNegative() === true) ? new QtiDuration('PT0S') : $remaining;
        }
        return false;
    }

    /**
     * Get the time remaining to be spent by the candidate on the source of the time
     * constraint. Please note that this method will never return negative durations.
     *
     * @return Duration A Duration object or null if there is no maxTime constraint running for the source of the time constraint.
     */
    public function getMaximumRemainingTime()
    {
        if (($timeLimits = $this->getSource()->getTimeLimits()) !== null && ($maxTime = $timeLimits->getMaxTime()) !== null) {
            return $this->getRemainingTimeFrom($maxTime);
        }
        return false;
    }

    /**
     * Get the time remaining the candidate has to spend by the candidate on the source of the time
     * constraint. Please note that this method will never return negative durations.
     *
     * @return Duration A Duration object or null if there is no minTime constraint running for the source of the time constraint.
     */
    public function getMinimumRemainingTime()
    {
        if (($timeLimits = $this->getSource()->getTimeLimits()) !== null && ($minTime = $timeLimits->getMinTime()) !== null) {
            return $this->getRemainingTimeFrom($minTime);
        }
        return false;
    }

    /**
     * Converts a duration to milliseconds
     * @param QtiDuration|null $duration the duration to convert
     * @return int|false the duration in ms or false if none
     */
    private function durationToMs($duration)
    {
        if(!is_null($duration) && $duration instanceof QtiDuration){
            return TestRunnerUtils::getDurationWithMicroseconds($duration);
        }
        return false;
    }

    /**
     * Serialize the constraint the expected way by the TestContext and the TestMap
     * @return array
     */
    public function jsonSerialize()
    {
        $source = $this->getSource();
        $timeLimits = $source->getTimeLimits();
        if(!is_null($timeLimits)){
            $identifier = $source->getIdentifier();

            $maxTime = $timeLimits->getMaxTime();
            $minTime = $timeLimits->getMinTime();
            $maxTimeRemaining = $this->getMaximumRemainingTime();
            $minTimeRemaining = $this->getMinimumRemainingTime();
            if ($maxTimeRemaining !== false || $minTimeRemaining !== false) {

                $label = method_exists($source, 'getTitle') ? $source->getTitle() : $identifier;

                $extraTime = [];
                if (!is_null($this->getTimer()) && $source->getTimeLimits()->hasMaxTime()) {
                    $timer = $this->getTimer();
                    $maxTimeSeconds = $source->getTimeLimits()->getMaxTime()->getSeconds(true);
                    $extraTime = [
                        'total' => $timer->getExtraTime($maxTimeSeconds),
                        'consumed' => $timer->getConsumedExtraTime($identifier, $maxTimeSeconds, $this->timerTarget),
                        'remaining' => $timer->getRemainingExtraTime($identifier, $maxTimeSeconds, $this->timerTarget),
                    ];
                }

                /** @var TimerLabelFormatterService $labelFormatter */
                $labelFormatter = ServiceManager::getServiceManager()->get(TimerLabelFormatterService::SERVICE_ID);
                return [
                    'label'               => $labelFormatter->format($label),
                    'source'              => $identifier,
                    'qtiClassName'        => $source->getQtiClassName(),
                    'extraTime'           => $extraTime,
                    'allowLateSubmission' => $this->allowLateSubmission(),
                    'minTime'             => $this->durationToMs($minTime),
                    'minTimeRemaining'    => $this->durationToMs($minTimeRemaining),
                    'maxTime'             => $this->durationToMs($maxTime),
                    'maxTimeRemaining'    => $this->durationToMs($maxTimeRemaining),
                ];
            }
        }
        return null;
    }
}


<?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) 2016 (original work) Open Assessment Technologies SA ;
 */
/**
 * @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
 */

namespace oat\taoQtiTest\models\runner\navigation;

use oat\taoQtiTest\models\runner\RunnerServiceContext;

/**
 * Class QtiRunnerPreviousItem
 * @package oat\taoQtiTest\models\runner\navigation
 */
class QtiRunnerNavigationPreviousItem implements RunnerNavigation
{
    /**
     * Do the move
     * @param RunnerServiceContext $context
     * @param mixed $ref
     * @return boolean
     * @throws \common_Exception
     */
    public function move(RunnerServiceContext $context, $ref)
    {
        /* @var AssessmentTestSession $session */
        $session = $context->getTestSession();
        $nextPosition = $session->getRoute()->getPosition() - 1;

        if ($context->isAdaptive()) {
            $shadowTest = $context->getShadowTest();
            $currentCatItemId = $context->getCurrentCatItemId();
            $search = array_search($currentCatItemId, $shadowTest);

            // Consider potential changes in the selected items.
            $context->selectAdaptiveNextItem();

            if ($search === 0) {
                QtiRunnerNavigation::checkTimedSectionExit($context, $nextPosition);
                $session->moveBack();
            } else {
                $context->persistCurrentCatItemId($shadowTest[$search - 1]);
            }
        } else {
            QtiRunnerNavigation::checkTimedSectionExit($context, $nextPosition);
            $session->moveBack();
        }

        return true;
    }
}


<?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\synchronisation\action;

use common_Exception;
use common_exception_Error;
use common_exception_InconsistentData;
use Exception;
use oat\taoQtiTest\models\runner\synchronisation\TestRunnerAction;

/**
 * Skip item into the test context.
 *
 * @package oat\taoQtiTest\models\runner\synchronisation\action
 */
class Skip extends TestRunnerAction
{
    /**
     * Process the skip action.
     *
     * Validate required fields.
     * Stop timer.
     * Wrap the skip to runner service.
     * Start next timer.
     *
     * @return array
     * @throws common_Exception
     * @throws common_exception_Error
     * @throws common_exception_InconsistentData
     */
    public function process()
    {
        $this->validate();

        $ref = $this->getRequestParameter('ref') ?: null;
        $itemDuration = null;

        $scope = $this->getRequestParameter('scope');
        $start = $this->getRequestParameter('start') !== false;

        try {
            $serviceContext = $this->getServiceContext();

            $this->saveToolStates();

            $this->getRunnerService()->endTimer($serviceContext, $itemDuration, $this->getTime());

            if ($this->getRequestParameter('offline') === true) {
                $this->setOffline();
            }

            $result = $this->getRunnerService()->skip($serviceContext, $scope, $ref);

            $response = [
                'success' => $result,
            ];

            if ($result) {
                $response['testContext'] = $this->getRunnerService()->getTestContext($serviceContext);
                if ($serviceContext->containsAdaptive()) {
                    // Force map update.
                    $response['testMap'] = $this->getRunnerService()->getTestMap($serviceContext, true);
                }
            }

            if ($start == true) {
                // start the timer only when move starts the item session
                // and after context build to avoid timing error
                $this->getRunnerService()->startTimer($serviceContext, $this->getTime());
            }
        } catch (Exception $e) {
            $response = $this->getErrorResponse($e);
        }

        return $response;
    }

    /**
     * Scope parameter is required.
     *
     * @return array
     */
    protected function getRequiredFields()
    {
        return array_merge(parent::getRequiredFields(), ['scope']);
    }
}


<?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) 2016 (original work) Open Assessment Technologies SA ;
 */
/**
 * @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
 */

namespace oat\taoQtiTest\models\runner\time;

use oat\taoTests\models\runner\time\ExtraTime;
use oat\taoTests\models\runner\time\InconsistentCriteriaException;
use oat\taoTests\models\runner\time\InconsistentRangeException;
use oat\taoTests\models\runner\time\InvalidDataException;
use oat\taoTests\models\runner\time\InvalidStorageException;
use oat\taoTests\models\runner\time\TimeException;
use oat\taoTests\models\runner\time\TimeLine;
use oat\taoTests\models\runner\time\TimePoint;
use oat\taoTests\models\runner\time\TimeStorage;
use oat\taoTests\models\runner\time\Timer;

/**
 * Class QtiTimer
 * @package oat\taoQtiTest\models\runner\time
 */
class QtiTimer implements Timer, ExtraTime, \JsonSerializable
{
    /**
     * The name of the storage key for the TimeLine
     */
    const STORAGE_KEY_TIME_LINE = 'timeLine';

    /**
     * The name of the storage key for the extra time
     */
    const STORAGE_KEY_EXTRA_TIME = 'extraTime';

    const STORAGE_KEY_EXTENDED_TIME = 'extendedTime';

    /**
     * The name of the storage key for the consumed extra time
     */
    const STORAGE_KEY_CONSUMED_EXTRA_TIME = 'consumedExtraTime';

    /**
     * The TimeLine used to compute the duration
     * @var TimeLine
     */
    protected $timeLine;

    /**
     * The storage used to maintain the data
     * @var TimeStorage
     */
    protected $storage;

    /**
     * The total added extra time
     * @var float
     */
    protected $extraTime = 0.0;

    /**
     * The extended time
     * @var float
     */
    protected $extendedTime = 0.0;

    /**
     * The already consumed extra time
     * @var float
     */
    protected $consumedExtraTime = 0.0;

    /**
     * QtiTimer constructor.
     */
    public function __construct()
    {
        $this->timeLine = new QtiTimeLine();
    }

    /**
     * Adds a "server start" TimePoint at a particular timestamp for the provided ItemRef
     * @param string|array $tags
     * @param float $timestamp
     * @return Timer
     * @throws TimeException
     */
    public function start($tags, $timestamp)
    {
        // check the provided arguments
        if (!is_numeric($timestamp) || $timestamp < 0) {
            throw new InvalidDataException('start() needs a valid timestamp!');
        }

        // extract the TimePoint identification from the provided item, and find existing range
        $range = $this->getRange($tags);

        // validate the data consistence
        if ($this->isRangeOpen($range)) {
            // unclosed range found, auto closing
            // auto generate the timestamp for the missing END point, one microsecond earlier
            \common_Logger::t('Missing END TimePoint in QtiTimer, auto add an arbitrary value');
            $point = new TimePoint($tags, $timestamp - (1 / TimePoint::PRECISION), TimePoint::TYPE_END, TimePoint::TARGET_SERVER);
            $this->timeLine->add($point);
            $range[] = $point;
        }
        $this->checkTimestampCoherence($range, $timestamp);

        // append the new START TimePoint
        $point = new TimePoint($tags, $timestamp, TimePoint::TYPE_START, TimePoint::TARGET_SERVER);
        $this->timeLine->add($point);

        return $this;
    }

    /**
     * Adds a "server end" TimePoint at a particular timestamp for the provided ItemRef
     * @param string|array $tags
     * @param float $timestamp
     * @return Timer
     * @throws TimeException
     */
    public function end($tags, $timestamp)
    {
        // check the provided arguments
        if (!is_numeric($timestamp) || $timestamp < 0) {
            throw new InvalidDataException('end() needs a valid timestamp!');
        }

        // extract the TimePoint identification from the provided item, and find existing range
        $range = $this->getRange($tags);

        // validate the data consistence
        if ($this->isRangeOpen($range)) {
            $this->checkTimestampCoherence($range, $timestamp);

            // append the new END TimePoint
            $point = new TimePoint($tags, $timestamp, TimePoint::TYPE_END, TimePoint::TARGET_SERVER);
            $this->timeLine->add($point);
        } else {
            // already closed range found, just log the info
            \common_Logger::t('Range already closed, or missing START TimePoint in QtiTimer, continue anyway');
        }

        return $this;
    }

    /**
     * Gets the first timestamp of the range for the provided tags
     * @param string|array $tags
     * @return float $timestamp
     */
    public function getFirstTimestamp($tags)
    {
        // extract the TimePoint identification from the provided item, and find existing range
        $range = $this->getRange($tags);
        $last = false;

        if (count($range)) {
            $last = $range[0]->getTimestamp();
        }

        return $last;
    }

    /**
     * Gets the last timestamp of the range for the provided tags
     * @param string|array $tags
     * @return bool|float $timestamp Returns the last timestamp of the range or false if none
     */
    public function getLastTimestamp($tags)
    {
        // extract the TimePoint identification from the provided item, and find existing range
        $range = $this->getRange($tags);
        $length = count($range);
        $last = false;

        if ($length) {
            $last = $range[$length - 1]->getTimestamp();
        }

        return $last;
    }

    /**
     * Gets the last registered timestamp
     * @return bool|float $timestamp Returns the last timestamp or false if none
     */
    public function getLastRegisteredTimestamp()
    {
        $points = $this->timeLine->getPoints();
        $length = count($points);
        $last = false;

        if ($length) {
            $last = end($points)->getTimestamp();
        }

        return $last;
    }

    /**
     * Adds "client start" and "client end" TimePoint based on the provided duration for a particular ItemRef
     * @param string|array $tags
     * @param float $duration
     * @return Timer
     * @throws TimeException
     */
    public function adjust($tags, $duration)
    {
        // check the provided arguments
        if (!is_null($duration) && (!is_numeric($duration) || $duration < 0)) {
            throw new InvalidDataException('adjust() needs a valid duration!');
        }

        // extract the TimePoint identification from the provided item, and find existing range
        $itemTimeLine = $this->timeLine->filter($tags, TimePoint::TARGET_SERVER);
        $range = $itemTimeLine->getPoints();

        // validate the data consistence
        $rangeLength = count($range);
        if (!$rangeLength || ($rangeLength % 2)) {
            throw new InconsistentRangeException('The time range does not seem to be consistent, the range is not complete!');
        }

        $serverDuration = $itemTimeLine->compute();

        // take care of existing client range
        $clientTimeLine = $this->timeLine->filter($tags, TimePoint::TARGET_CLIENT);
        $clientRange = $clientTimeLine->getPoints();
        $clientRangeLength = count($clientRange);
        if ($clientRangeLength) {
            $clientDuration = 0;
            try {
                $clientDuration = $clientTimeLine->compute();
            } catch(TimeException $e) {
                \common_Logger::t('Handled client range error');
            }

            if (is_null($duration)) {
                if ($clientDuration) {
                    $duration = $clientDuration;
                    \common_Logger::t("No client duration provided to adjust the timer, but a range already exist: ${duration}");
                } else {
                    $duration = $serverDuration;
                    \common_Logger::t("No client duration provided to adjust the timer, fallback to server duration: ${duration}");
                }
            }

            $removed = $this->timeLine->remove($tags, TimePoint::TARGET_CLIENT);
            if ($removed == $clientRangeLength) {
                \common_Logger::t("Replace client duration in timer: ${clientDuration} to ${duration}");
            } else {
                \common_Logger::w("Unable to replace client duration in timer: ${clientDuration} to ${duration}");
            }
        }

        // check if the client side duration is bound by the server side duration
        if (is_null($duration)) {
            $duration = $serverDuration;
            \common_Logger::t("No client duration provided to adjust the timer, fallback to server duration: ${duration}");
        } else if ($duration > $serverDuration) {
            \common_Logger::w("A client duration must not be larger than the server time range! (${duration} > ${serverDuration})");
            $duration = $serverDuration;
        }

        // extract range boundaries
        TimePoint::sort($range);
        $serverStart = $range[0];
        $serverEnd = $range[$rangeLength - 1];

        // adjust the range by inserting the client duration between the server overall time range boundaries
        $overallDuration = $serverEnd->getTimestamp() - $serverStart->getTimestamp();
        $delay = ($overallDuration - $duration) / 2;

        $start = new TimePoint($tags, $serverStart->getTimestamp() + $delay, TimePoint::TYPE_START, TimePoint::TARGET_CLIENT);
        $this->timeLine->add($start);

        $end = new TimePoint($tags, $serverEnd->getTimestamp() - $delay, TimePoint::TYPE_END, TimePoint::TARGET_CLIENT);
        $this->timeLine->add($end);

        return $this;
    }

    /**
     * Computes the total duration represented by the filtered TimePoints
     * @param string|array $tags A tag or a list of tags to filter
     * @param int $target The type of target TimePoint to filter
     * @return float Returns the total computed duration
     * @throws TimeException
     */
    public function compute($tags, $target)
    {
        // cannot compute a duration across different targets
        if (!$this->onlyOneFlag($target)) {
            throw new InconsistentCriteriaException('Cannot compute a duration across different targets!');
        }

        return $this->timeLine->compute($tags, $target);
    }

    /**
     * Checks if the duration of a TimeLine subset reached the timeout
     * @param float $timeLimit The time limit against which compare the duration
     * @param string|array $tags A tag or a list of tags to filter
     * @param int $target The type of target TimePoint to filter
     * @return bool Returns true if the timeout is reached
     * @throws TimeException
     */
    public function timeout($timeLimit, $tags, $target)
    {
        $duration = $this->compute($tags, $target);
        return $duration >= $timeLimit;
    }

    /**
     * Sets the storage used to maintain the data
     * @param TimeStorage $storage
     * @return Timer
     */
    public function setStorage(TimeStorage $storage)
    {
        $this->storage = $storage;
        return $this;
    }

    /**
     * Gets the storage used to maintain the data
     * @return TimeStorage
     */
    public function getStorage()
    {
        return $this->storage;
    }

    /**
     * Exports the internal state to an array
     * @return array
     */
    public function toArray()
    {
        return [
            self::STORAGE_KEY_TIME_LINE => $this->timeLine,
            self::STORAGE_KEY_EXTRA_TIME => $this->extraTime,
            self::STORAGE_KEY_EXTENDED_TIME => $this->extendedTime,
            self::STORAGE_KEY_CONSUMED_EXTRA_TIME => $this->consumedExtraTime,
        ];
    }

    /**
     * Specify data which should be serialized to JSON
     * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
     * @return mixed data which can be serialized by <b>json_encode</b>,
     * which is a value of any type other than a resource.
     * @since 5.4.0
     */
    public function jsonSerialize()
    {
        return $this->toArray();
    }

    /**
     * Saves the data to the storage
     * @return Timer
     * @throws InvalidStorageException
     */
    public function save()
    {
        if (!$this->storage) {
            throw new InvalidStorageException('A storage must be defined in order to store the data!');
        }

        $this->storage->store($this->toArray());
        return $this;
    }

    /**
     * @param mixed $data
     * @return QtiTimeLine
     */
    protected function unserializeTimeLine($data)
    {
        if (is_array($data)) {
            $timeLine = new QtiTimeLine();
            $timeLine->fromArray($data);
        } else {
            $timeLine = $data;
        }

        return $timeLine;
    }

    /**
     * Loads the data from the storage
     * @return Timer
     * @throws InvalidStorageException
     * @throws InvalidDataException
     */
    public function load()
    {
        if (!$this->storage) {
            throw new InvalidStorageException('A storage must be defined in order to store the data!');
        }

        $data = $this->storage->load();

        if (isset($data)) {
            if (!is_array($data)) {
                $data = [
                    self::STORAGE_KEY_TIME_LINE => $data,
                ];
            }

            if (isset($data[self::STORAGE_KEY_TIME_LINE])) {
                $this->timeLine = $this->unserializeTimeLine($data[self::STORAGE_KEY_TIME_LINE]);
            } else {
                $this->timeLine = new QtiTimeLine();
            }

            if (isset($data[self::STORAGE_KEY_EXTRA_TIME])) {
                $this->extraTime = $data[self::STORAGE_KEY_EXTRA_TIME];
            } else {
                $this->extraTime = 0;
            }

            if (isset($data[self::STORAGE_KEY_EXTENDED_TIME])) {
                $this->extendedTime = $data[self::STORAGE_KEY_EXTENDED_TIME];
            } else {
                $this->extendedTime = 0;
            }
            if (isset($data[self::STORAGE_KEY_CONSUMED_EXTRA_TIME])) {
                $this->consumedExtraTime = $data[self::STORAGE_KEY_CONSUMED_EXTRA_TIME];
            } else {
                $this->consumedExtraTime = 0;
            }

            if (!$this->timeLine instanceof TimeLine) {
                throw new InvalidDataException('The storage did not provide acceptable data when loading!');
            }
        }

        return $this;
    }

    /**
     * Gets the added extra time
     * @param int $maxTime
     * @return float
     */
    public function getExtraTime($maxTime = 0)
    {
        if ($maxTime && $this->getExtendedTime()) {
            $secondsNew = $maxTime * $this->getExtendedTime();
            $extraTime = $secondsNew - $maxTime;
            $this->setExtraTime($extraTime);
            return $extraTime;
        }
        return $this->extraTime;
    }

    /**
     * @return float
     */
    public function getExtendedTime()
    {
        return $this->extendedTime;
    }

    /**
     * @param $extendedTime
     * @return $this
     */
    public function setExtendedTime($extendedTime)
    {
        $this->extendedTime = $extendedTime;
        return $this;
    }

    /**
     * Sets the added extra time
     * @param float $time
     * @return $this
     */
    public function setExtraTime($time)
    {
        $this->extraTime = max(0, floatval($time));
        return $this;
    }

    /**
     * Sets the added extra time
     * @param float $time
     * @return $this
     */
    public function setConsumedExtraTime($time)
    {
        $this->consumedExtraTime = max($this->consumedExtraTime, floatval($time));
        return $this;
    }

    /**
     * Gets the amount of already consumed extra time. If tags are provided, only take care of the related time.
     * @param string|array $tags A tag or a list of tags to filter
     * @param integer $maxTime initial (total) timer value without extra time
     * @param integer $target (server/client)
     * @return float
     * @throws
     */
    public function getConsumedExtraTime($tags = null, $maxTime = 0, $target = TimePoint::TARGET_SERVER)
    {
        if ($maxTime) {
            $totalConsumed = $this->compute($tags, $target);
            $consumedExtraTime = $totalConsumed - $maxTime < 0 ? 0 : $totalConsumed - $maxTime;
            $this->setConsumedExtraTime($consumedExtraTime)->save();
        }
        return $this->consumedExtraTime;
    }

    /**
     * Gets the amount of remaining extra time
     * @param string|array $tags A tag or a list of tags to filter
     * @param integer $maxTime initial (total) timer value without extra time
     * @param integer $target (server/client)
     * @return float
     */
    public function getRemainingExtraTime($tags = null, $maxTime = 0, $target = TimePoint::TARGET_SERVER)
    {
        return max(0, $this->getExtraTime($maxTime) - $this->getConsumedExtraTime($tags, $maxTime, $target));
    }

    /**
     * @inheritdoc
     */
    public function delete()
    {
       $storage = $this->getStorage();
       return $storage->delete();
    }

    /**
     * Checks if a timestamp is consistent with existing TimePoint within a range
     * @param array $points
     * @param float $timestamp
     * @throws InconsistentRangeException
     */
    protected function checkTimestampCoherence($points, $timestamp)
    {
        foreach($points as $point) {
            if ($point->getTimestamp() > $timestamp) {
                throw new InconsistentRangeException('A new TimePoint cannot be set before an existing one!');
            }
        }
    }

    /**
     * Check if the provided range is open (START TimePoint and no related END)
     * @param array $range
     * @return bool
     */
    protected function isRangeOpen($range)
    {
        $nb = count($range);
        return $nb && ($nb % 2) && ($range[$nb - 1]->getType() == TimePoint::TYPE_START);
    }

    /**
     * Extracts a sorted range of TimePoint
     *
     * @param array $tags
     * @return array
     */
    protected function getRange($tags)
    {
        $range = $this->timeLine->find($tags, TimePoint::TARGET_SERVER);

        TimePoint::sort($range);

        return $range;
    }

    /**
     * Checks if a binary flag contains exactly one flag set
     * @param $value
     * @return bool
     */
    protected function onlyOneFlag($value)
    {
        return $this->binaryPopCount($value) == 1;
    }

    /**
     * Count the number of bits set in a 32bits integer
     * @param int $value
     * @return int
     */
    protected function binaryPopCount($value)
    {
        $value -= (($value >> 1) & 0x55555555);
        $value = ((($value >> 2) & 0x33333333) + ($value & 0x33333333));
        $value = ((($value >> 4) + $value) & 0x0f0f0f0f);
        $value += ($value >> 8);
        $value += ($value >> 16);
        return $value & 0x0000003f;
    }
}


<?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\generis\model\OntologyAwareTrait;
use oat\generis\model\OntologyRdfs;
use oat\oatbox\service\ConfigurableService;
use oat\tao\model\resources\ListResourceLookup;
use oat\taoItems\model\CategoryService;

/**
 * Look up items and format them as a flat list
 *
 * @author Bertrand Chevrier <bertrand@taotesting.com>
 */
class ListItemLookup extends ConfigurableService implements ItemLookup
{
    use OntologyAwareTrait;

    const SERVICE_ID = 'taoQtiTest/CreatorItems/list';

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

    /**
     * Get the ListResourceLookup
     */
    protected function getListResourceLookupService()
    {
        return $this->getServiceLocator()->get(ListResourceLookup::SERVICE_ID);
    }

    /**
     * Retrieve QTI Items for the given parameters.
     * @param \core_kernel_classes_Class $itemClass the item class
     * @param array $propertyFilters the lookup format
     * @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)
    {
        $result = $this->getListResourceLookupService()->getResources($itemClass, [], $propertyFilters, $offset, $limit);

        array_map(function($item){
            return array_merge($item, [
                'categories' => $this->getCategoryService()->getItemCategories($this->getResource($item['uri']))
            ]);
        }, $result['nodes']);

        return $result;
    }
}


<?php
/**
 * WPThemeReview Coding Standard.
 *
 * @package WPTRT\WPThemeReview
 * @link    https://github.com/WPTRT/WPThemeReview
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WPThemeReview\Sniffs\CoreFunctionality;

use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;

/**
 * Forbids deregistering of core scripts (javascript).
 *
 * @link  https://make.wordpress.org/themes/handbook/review/required/#stylesheets-and-scripts
 *
 * @since 0.1.0
 */
class NoDeregisterCoreScriptSniff extends AbstractFunctionParameterSniff {

	/**
	 * The group name for this group of functions.
	 *
	 * @since 0.1.0
	 *
	 * @var string
	 */
	protected $group_name = 'wp_deregister_script';

	/**
	 * Array of function and script handles used in core.
	 *
	 * {@internal List was probably based on the documentation of the wp_enqueue_script function.
	 *            List needs further verification and update.
	 *
	 *            @link https://developer.wordpress.org/reference/functions/wp_enqueue_script/}}
	 *
	 * @since 0.1.0
	 *
	 * @var array
	 */
	protected $target_functions = array(
		'wp_deregister_script' => array(
			'jcrop'                    => true,
			'swfobject'                => true,
			'swfupload'                => true,
			'swfupload-degrade'        => true,
			'swfupload-queue'          => true,
			'swfupload-handlers'       => true,
			'jquery'                   => true,
			'jquery-form'              => true,
			'jquery-color'             => true,
			'jquery-masonry'           => true,
			'masonry'                  => true,
			'jquery-ui-core'           => true,
			'jquery-ui-widget'         => true,
			'jquery-ui-accordion'      => true,
			'jquery-ui-autocomplete'   => true,
			'jquery-ui-button'         => true,
			'jquery-ui-datepicker'     => true,
			'jquery-ui-dialog'         => true,
			'jquery-ui-draggable'      => true,
			'jquery-ui-droppable'      => true,
			'jquery-ui-menu'           => true,
			'jquery-ui-mouse'          => true,
			'jquery-ui-position'       => true,
			'jquery-ui-progressbar'    => true,
			'jquery-ui-selectable'     => true,
			'jquery-ui-resizable'      => true,
			'jquery-ui-selectmenu'     => true,
			'jquery-ui-sortable'       => true,
			'jquery-ui-slider'         => true,
			'jquery-ui-spinner'        => true,
			'jquery-ui-tooltip'        => true,
			'jquery-ui-tabs'           => true,
			'jquery-effects-core'      => true,
			'jquery-effects-blind'     => true,
			'jquery-effects-bounce'    => true,
			'jquery-effects-clip'      => true,
			'jquery-effects-drop'      => true,
			'jquery-effects-explode'   => true,
			'jquery-effects-fade'      => true,
			'jquery-effects-fold'      => true,
			'jquery-effects-highlight' => true,
			'jquery-effects-pulsate'   => true,
			'jquery-effects-scale'     => true,
			'jquery-effects-shake'     => true,
			'jquery-effects-slide'     => true,
			'jquery-effects-transfer'  => true,
			'wp-mediaelement'          => true,
			'schedule'                 => true,
			'suggest'                  => true,
			'thickbox'                 => true,
			'hoverIntent'              => true,
			'jquery-hotkeys'           => true,
			'sack'                     => true,
			'quicktags'                => true,
			'iris'                     => true,
			'farbtastic'               => true,
			'colorpicker'              => true,
			'tiny_mce'                 => true,
			'autosave'                 => true,
			'wp-ajax-response'         => true,
			'wp-lists'                 => true,
			'common'                   => true,
			'editorremov'              => true,
			'editor-functions'         => true,
			'ajaxcat'                  => true,
			'admin-categories'         => true,
			'admin-tags'               => true,
			'admin-custom-fields'      => true,
			'password-strength-meter'  => true,
			'admin-comments'           => true,
			'admin-users'              => true,
			'admin-forms'              => true,
			'xfn'                      => true,
			'upload'                   => true,
			'postbox'                  => true,
			'slug'                     => true,
			'post'                     => true,
			'page'                     => true,
			'link'                     => true,
			'comment'                  => true,
			'comment-reply'            => true,
			'admin-gallery'            => true,
			'media-upload'             => true,
			'admin-widgets'            => true,
			'word-count'               => true,
			'theme-preview'            => true,
			'json2'                    => true,
			'plupload'                 => true,
			'plupload-all'             => true,
			'plupload-html4'           => true,
			'plupload-html5'           => true,
			'plupload-flash'           => true,
			'plupload-silverlight'     => true,
			'underscore'               => true,
			'backbone'                 => true,
			'imagesloaded'             => true,
		),
	);

	/**
	 * Process the parameters of a matched function.
	 *
	 * @since 0.1.0
	 *
	 * @param int    $stackPtr        The position of the current token in the stack.
	 * @param array  $group_name      The name of the group which was matched.
	 * @param string $matched_content The token content (function name) which was matched.
	 * @param array  $parameters      Array with information about the parameters.
	 *
	 * @return void
	 */
	public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {

		if ( ! isset( $parameters[1] ) ) {
			return;
		}

		$matched_parameter = $this->strip_quotes( $parameters[1]['raw'] );
		$first_param_token = $this->phpcsFile->findNext(
			Tokens::$emptyTokens,
			$parameters[1]['start'],
			( $parameters[1]['end'] + 1 ),
			true
		);

		if ( isset( $this->target_functions[ $matched_content ][ $matched_parameter ] ) ) {
			$this->throw_prohibited_error( $first_param_token, array( $matched_parameter ) );
			return;
		}

		// More extensive check to prevent people circumventing the sniff.
		$text                 = '';
		$found_variable_token = false;

		for ( $i = $parameters[1]['start']; $i <= $parameters[1]['end']; $i++ ) {
			if ( isset( Tokens::$stringTokens[ $this->tokens[ $i ]['code'] ] ) === false ) {
				if ( T_VARIABLE === $this->tokens[ $i ]['code']
					|| T_STRING === $this->tokens[ $i ]['code']
				) {
					$found_variable_token = true;
				}

				continue;
			}

			$text .= $this->strip_quotes( $this->tokens[ $i ]['content'] );
		}

		if ( isset( $this->target_functions[ $matched_content ][ $text ] ) ) {
			$this->throw_prohibited_error( $first_param_token, array( $text ) );
			return;
		}

		if ( true === $found_variable_token ) {
			$this->throw_variable_handle_warning( $first_param_token, array( $matched_content, $matched_parameter ) );
		}
	}

	/**
	 * Throw the error for deregistering a core script.
	 *
	 * @param int   $stackPtr The position of the first non-empty parameter token in the stack.
	 * @param array $data     Optional input for the data replacements.
	 */
	public function throw_prohibited_error( $stackPtr, $data = array() ) {
		$this->phpcsFile->addError(
			'Deregistering core script "%s" is prohibited.',
			$stackPtr,
			'Found',
			$data
		);
	}

	/**
	 * Throw a warning when a variable handle for deregistering a script is detected.
	 *
	 * @param int   $stackPtr The position of the first non-empty parameter token in the stack.
	 * @param array $data     Optional input for the data replacements.
	 */
	public function throw_variable_handle_warning( $stackPtr, $data = array() ) {
		$this->phpcsFile->addWarning(
			'Deregistering core scripts is prohibited. A variable script handle was found. Inspection of the %s() call needed. Found: %s',
			$stackPtr,
			'VariableHandleFound',
			$data
		);
	}
}


<?php
/**
 * WPThemeReview Coding Standard.
 *
 * @package WPTRT\WPThemeReview
 * @link    https://github.com/WPTRT/WPThemeReview
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WPThemeReview\Sniffs\ThouShallNotUse;

use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;

/**
 * Check for use of <iframe>. Often used for malicious code.
 *
 * @link  https://make.wordpress.org/themes/handbook/review/required/theme-check-plugin/#info
 *
 * @since 0.1.0
 */
class ForbiddenIframeSniff implements Sniff {

	/**
	 * The regex to catch usage of <iframe ...>.
	 *
	 * This regex will prevent matches being made on `<iframe>` without attributes.
	 *
	 * @var string
	 */
	const IFRAME_REGEX = '`(<iframe\s+[^>]+>)`i';

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {
		return Tokens::$textStringTokens;
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
	 *                                               token was found.
	 * @param int                         $stackPtr  The position of the current token
	 *                                               in the stack.
	 *
	 * @return void
	 */
	public function process( File $phpcsFile, $stackPtr ) {

		$tokens = $phpcsFile->getTokens();

		if ( preg_match( self::IFRAME_REGEX, $tokens[ $stackPtr ]['content'], $matches ) > 0 ) {
			$phpcsFile->addError(
				'Usage of the iframe HTML element is prohibited. Found: %s',
				$stackPtr,
				'Found',
				array( $matches[1] )
			);
		}
	}

}


<?php
/**
 * WPThemeReview Coding Standard.
 *
 * @package WPTRT\WPThemeReview
 * @link    https://github.com/WPTRT/WPThemeReview
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WPThemeReview\Sniffs\ThouShallNotUse;

use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;

/**
 * Check for auto generated themes.
 *
 * @since 0.1.0
 */
class NoAutoGenerateSniff implements Sniff {

	/**
	 * A list of tokenizers this sniff supports.
	 *
	 * @var array
	 */
	public $supportedTokenizers = array(
		'PHP',
		'CSS',
	);

	/**
	 * A list of strings found in the generated themes.
	 *
	 * @var array
	 */
	protected $generated_themes = array(
		'artisteer', // Artisteer.
		'themler', // Themler.
		'lubith', // Lubith.
		'templatetoaster', // TemplateToaster.
		'wpthemegenerator', // WP Theme Generator.
		'pinegrow', // Pinegrow.
	);

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {
		$tokens   = Tokens::$textStringTokens;
		$tokens[] = T_STRING; // Functions named after or prefixed with the generator name.
		$tokens[] = T_COMMENT;
		$tokens[] = T_DOC_COMMENT_STRING;
		return $tokens;
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
	 *                                               token was found.
	 * @param int                         $stackPtr  The position of the current token
	 *                                               in the stack.
	 *
	 * @return void
	 */
	public function process( File $phpcsFile, $stackPtr ) {

		$tokens  = $phpcsFile->getTokens();
		$token   = $tokens[ $stackPtr ];
		$content = trim( strtolower( $token['content'] ) );

		// No need to check an empty string.
		if ( '' === $content ) {
			return;
		}

		foreach ( $this->generated_themes as $generated_theme ) {
			/*
			 * The check can have false positives like artisteers but chances of that happening
			 * in a valid theme is low.
			 */
			if ( false === strpos( $content, $generated_theme ) ) {
				continue;
			}

			$phpcsFile->addError(
				'Auto generated themes are not allowed in the theme directory. Found: %s',
				$stackPtr,
				'AutoGeneratedFound',
				array( $generated_theme )
			);
		}
	}

}


<?php
/**
 * WPThemeReview Coding Standard.
 *
 * @package WPTRT\WPThemeReview
 * @link    https://github.com/WPTRT/WPThemeReview
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WPThemeReview\Sniffs\PluginTerritory;

use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;

/**
 * Discourages removal of the admin bar.
 *
 * @link  https://make.wordpress.org/themes/handbook/review/required/#core-functionality-and-features
 *
 * @since WPCS 0.3.0
 * @since WPCS 0.11.0 - Extends the WPCS native `AbstractFunctionParameterSniff` class.
 *                    - Added the $remove_only property.
 *                    - Now also sniffs for manipulation of the admin bar visibility through CSS.
 * @since WPCS 0.13.0 Class name changed: this class is now namespaced.
 *
 * @since TRTCS 0.1.0 As this sniff will be removed from WPCS in version 2.0, the
 *                    sniff has been cherry-picked into the WPThemeReview standard.
 */
class AdminBarRemovalSniff extends AbstractFunctionParameterSniff {

	/**
	 * A list of tokenizers this sniff supports.
	 *
	 * @since WPCS 0.11.0
	 *
	 * @var array
	 */
	public $supportedTokenizers = array( 'PHP', 'CSS' );

	/**
	 * Whether or not the sniff only checks for removal of the admin bar
	 * or any manipulation to the visibility of the admin bar.
	 *
	 * Defaults to true: only check for removal of the admin bar.
	 * Set to false to check for any form of manipulation of the visibility
	 * of the admin bar.
	 *
	 * @since WPCS 0.11.0
	 *
	 * @var bool
	 */
	public $remove_only = true;

	/**
	 * Functions this sniff is looking for.
	 *
	 * @since WPCS 0.11.0
	 *
	 * @var array
	 */
	protected $target_functions = array(
		'show_admin_bar' => true,
		'add_filter'     => true,
	);

	/**
	 * CSS properties this sniff is looking for.
	 *
	 * @since WPCS 0.11.0
	 *
	 * @var array
	 */
	protected $target_css_properties = array(
		'visibility' => array(
			'type'  => '!=',
			'value' => 'hidden',
		),
		'display' => array(
			'type'  => '!=',
			'value' => 'none',
		),
		'opacity' => array(
			'type'  => '>',
			'value' => 0.3,
		),
	);

	/**
	 * CSS selectors this sniff is looking for.
	 *
	 * @since WPCS 0.11.0
	 *
	 * @var array
	 */
	protected $target_css_selectors = array(
		'.show-admin-bar',
		'#wpadminbar',
	);

	/**
	 * Regex template for use with the CSS selectors in combination with PHP text strings.
	 *
	 * @since WPCS 0.11.0
	 *
	 * @var string
	 */
	private $target_css_selectors_regex = '`(?:%s).*?\{(.*)$`';

	/**
	 * Property to keep track of whether a <style> open tag has been encountered.
	 *
	 * @since WPCS 0.11.0
	 *
	 * @var array
	 */
	private $in_style;

	/**
	 * Property to keep track of whether a one of the target selectors has been encountered.
	 *
	 * @since WPCS 0.11.0
	 *
	 * @var array
	 */
	private $in_target_selector;

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {
		$targets = Tokens::$textStringTokens;

		// Add CSS style target.
		$targets[] = \T_STYLE;

		// Set the target selectors regex only once.
		$selectors = array_map(
			'preg_quote',
			$this->target_css_selectors,
			array_fill( 0, \count( $this->target_css_selectors ), '`' )
		);
		// Parse the selectors array into the regex string.
		$this->target_css_selectors_regex = sprintf( $this->target_css_selectors_regex, implode( '|', $selectors ) );

		// Add function call targets.
		$parent = parent::register();
		if ( ! empty( $parent ) ) {
			$targets[] = \T_STRING;
		}

		return $targets;
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 *
	 * @return int|void Integer stack pointer to skip forward or void to continue
	 *                  normal file processing.
	 */
	public function process_token( $stackPtr ) {

		$file_name      = $this->phpcsFile->getFileName();
		$file_extension = substr( strrchr( $file_name, '.' ), 1 );

		if ( 'css' === $file_extension ) {
			if ( \T_STYLE === $this->tokens[ $stackPtr ]['code'] ) {
				return $this->process_css_style( $stackPtr );
			}
		} elseif ( isset( Tokens::$textStringTokens[ $this->tokens[ $stackPtr ]['code'] ] ) ) {
			/*
			 * Set $in_style && $in_target_selector to false if it is the first time
			 * this sniff is run on a file.
			 */
			if ( ! isset( $this->in_style[ $file_name ] ) ) {
				$this->in_style[ $file_name ] = false;
			}
			if ( ! isset( $this->in_target_selector[ $file_name ] ) ) {
				$this->in_target_selector[ $file_name ] = false;
			}

			return $this->process_text_for_style( $stackPtr, $file_name );

		} else {
			return parent::process_token( $stackPtr );
		}
	}

	/**
	 * Process the parameters of a matched function.
	 *
	 * @since WPCS 0.11.0
	 *
	 * @param int    $stackPtr        The position of the current token in the stack.
	 * @param array  $group_name      The name of the group which was matched.
	 * @param string $matched_content The token content (function name) which was matched.
	 * @param array  $parameters      Array with information about the parameters.
	 *
	 * @return void
	 */
	public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
		$error = false;
		switch ( $matched_content ) {
			case 'show_admin_bar':
				$error = true;
				if ( true === $this->remove_only ) {
					if ( 'true' === $parameters[1]['raw'] ) {
						$error = false;
					}
				}
				break;

			case 'add_filter':
				$filter_name = $this->strip_quotes( $parameters[1]['raw'] );
				if ( 'show_admin_bar' !== $filter_name ) {
					break;
				}

				$error = true;
				if ( true === $this->remove_only && isset( $parameters[2]['raw'] ) ) {
					if ( '__return_true' === $this->strip_quotes( $parameters[2]['raw'] ) ) {
						$error = false;
					}
				}
				break;

			default:
				// Left empty on purpose.
				break;
		}

		if ( true === $error ) {
			$this->phpcsFile->addError( 'Removal of admin bar is prohibited.', $stackPtr, 'RemovalDetected' );
		}
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @since WPCS 0.11.0
	 *
	 * @param int    $stackPtr  The position of the current token in the stack.
	 * @param string $file_name The file name of the current file being processed.
	 *
	 * @return void
	 */
	public function process_text_for_style( $stackPtr, $file_name ) {
		$content = trim( $this->tokens[ $stackPtr ]['content'] );

		// No need to check an empty string.
		if ( '' === $content ) {
			return;
		}

		// Are we in a <style> tag ?
		if ( true === $this->in_style[ $file_name ] ) {
			if ( false !== strpos( $content, '</style>' ) ) {
				// Make sure we check any content on this line before the closing style tag.
				$this->in_style[ $file_name ] = false;
				$content                      = trim( substr( $content, 0, strpos( $content, '</style>' ) ) );
			}
		} elseif ( false !== strpos( $content, '<style' ) ) {
			// Ok, found a <style> open tag.
			if ( false === strpos( $content, '</style>' ) ) {
				// Make sure we check any content on this line after the opening style tag.
				$this->in_style[ $file_name ] = true;
				$content                      = trim( substr( $content, ( strpos( $content, '<style' ) + 6 ) ) );
			} else {
				// Ok, we have open and close style tag on the same line with possibly content within.
				$start   = ( strpos( $content, '<style' ) + 6 );
				$end     = strpos( $content, '</style>' );
				$content = trim( substr( $content, $start, ( $end - $start ) ) );
				unset( $start, $end );
			}
		} else {
			return;
		}

		// Are we in one of the target selectors ?
		if ( true === $this->in_target_selector[ $file_name ] ) {
			if ( false !== strpos( $content, '}' ) ) {
				// Make sure we check any content on this line before the selector closing brace.
				$this->in_target_selector[ $file_name ] = false;
				$content                                = trim( substr( $content, 0, strpos( $content, '}' ) ) );
			}
		} elseif ( preg_match( $this->target_css_selectors_regex, $content, $matches ) > 0 ) {
			// Ok, found a new target selector.
			$content = '';

			if ( isset( $matches[1] ) && '' !== $matches[1] ) {
				if ( false === strpos( $matches[1], '}' ) ) {
					// Make sure we check any content on this line before the closing brace.
					$this->in_target_selector[ $file_name ] = true;
					$content                                = trim( $matches[1] );
				} else {
					// Ok, we have the selector open and close brace on the same line.
					$content = trim( substr( $matches[1], 0, strpos( $matches[1], '}' ) ) );
				}
			} else {
				$this->in_target_selector[ $file_name ] = true;
			}
		} else {
			return;
		}
		unset( $matches );

		// Now let's do the check for the CSS properties.
		if ( ! empty( $content ) ) {
			foreach ( $this->target_css_properties as $property => $requirements ) {
				if ( false !== strpos( $content, $property ) ) {
					$error = true;

					if ( true === $this->remove_only ) {
						// Check the value of the CSS property.
						if ( preg_match( '`' . preg_quote( $property, '`' ) . '\s*:\s*(.+?)\s*(?:!important)?;`', $content, $matches ) > 0 ) {
							$value = trim( $matches[1] );
							$valid = $this->validate_css_property_value( $value, $requirements['type'], $requirements['value'] );
							if ( true === $valid ) {
								$error = false;
							}
						}
					}

					if ( true === $error ) {
						$this->phpcsFile->addError( 'Hiding of the admin bar is not allowed.', $stackPtr, 'HidingDetected' );
					}
				}
			}
		}
	}

	/**
	 * Processes this test for T_STYLE tokens in CSS files.
	 *
	 * @since WPCS 0.11.0
	 *
	 * @param int $stackPtr  The position of the current token in the stack passed in $tokens.
	 *
	 * @return void
	 */
	protected function process_css_style( $stackPtr ) {
		if ( ! isset( $this->target_css_properties[ $this->tokens[ $stackPtr ]['content'] ] ) ) {
			// Not one of the CSS properties we're interested in.
			return;
		}

		$css_property = $this->target_css_properties[ $this->tokens[ $stackPtr ]['content'] ];

		// Check if the CSS selector matches.
		$opener = $this->phpcsFile->findPrevious( \T_OPEN_CURLY_BRACKET, $stackPtr );
		if ( false !== $opener ) {
			for ( $i = ( $opener - 1 ); $i >= 0; $i-- ) {
				if ( isset( Tokens::$commentTokens[ $this->tokens[ $i ]['code'] ] )
					|| \T_CLOSE_CURLY_BRACKET === $this->tokens[ $i ]['code']
				) {
					break;
				}
			}
			$start    = ( $i + 1 );
			$selector = trim( $this->phpcsFile->getTokensAsString( $start, ( $opener - $start ) ) );
			unset( $i );

			foreach ( $this->target_css_selectors as $target_selector ) {
				if ( false !== strpos( $selector, $target_selector ) ) {
					$error = true;

					if ( true === $this->remove_only ) {
						// Check the value of the CSS property.
						$valuePtr = $this->phpcsFile->findNext( array( \T_COLON, \T_WHITESPACE ), ( $stackPtr + 1 ), null, true );
						$value    = $this->tokens[ $valuePtr ]['content'];
						$valid    = $this->validate_css_property_value( $value, $css_property['type'], $css_property['value'] );
						if ( true === $valid ) {
							$error = false;
						}
					}

					if ( true === $error ) {
						$this->phpcsFile->addError( 'Hiding of the admin bar is not allowed.', $stackPtr, 'HidingDetected' );
					}
				}
			}
		}
	}

	/**
	 * Verify if a CSS property value complies with an expected value.
	 *
	 * {@internal This is a method stub, doing only what is needed for this sniff.
	 * If at some point in the future other sniff would need similar functionality,
	 * this method should be moved to the WordPress_Sniff class and expanded to cover
	 * all types of comparisons.}}
	 *
	 * @since WPCS 0.11.0
	 *
	 * @param mixed  $value         The value of CSS property.
	 * @param string $compare_type  The type of comparison to use for the validation.
	 * @param string $compare_value The value to compare against.
	 *
	 * @return bool True if the property value complies, false otherwise.
	 */
	protected function validate_css_property_value( $value, $compare_type, $compare_value ) {
		switch ( $compare_type ) {
			case '!=':
				return $value !== $compare_value;

			case '>':
				return $value > $compare_value;

			default:
				return false;
		}
	}

}