HEX
Server: Apache
System: Linux server2.voipitup.com.au 4.18.0-553.109.1.lve.el8.x86_64 #1 SMP Thu Mar 5 20:23:46 UTC 2026 x86_64
User: posscale (1027)
PHP: 8.2.30
Disabled: exec,passthru,shell_exec,system
Upload Files
File: /home/posscale/subdomains/xibo/lib/Widget/Currencies.php
<?php
/*
 * Xibo - Digital Signage - http://www.xibo.org.uk
 * Copyright (C) 2014-2015 Daniel Garner
 *
 * This file is part of Xibo.
 *
 * Xibo is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version. 
 *
 * Xibo 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Xibo.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
namespace Xibo\Widget;

use GuzzleHttp\Exception\RequestException;
use Stash\Invalidation;
use Xibo\Exception\NotFoundException;
use Xibo\Factory\ModuleFactory;

/**
 * Class Currencies
 * @package Xibo\Widget
 */
class Currencies extends AlphaVantageBase
{
    public $codeSchemaVersion = 1;

    /**
     * Install or Update this module
     * @param ModuleFactory $moduleFactory
     */
    public function installOrUpdate($moduleFactory)
    {
        if ($this->module == null) {
            // Install
            $module = $moduleFactory->createEmpty();
            $module->name = 'Currencies';
            $module->type = 'currencies';
            $module->class = 'Xibo\Widget\Currencies';
            $module->description = 'A module for showing Currency pairs and exchange rates';
            $module->imageUri = 'forms/library.gif';
            $module->enabled = 1;
            $module->previewEnabled = 1;
            $module->assignable = 1;
            $module->regionSpecific = 1;
            $module->renderAs = 'html';
            $module->schemaVersion = $this->codeSchemaVersion;
            $module->defaultDuration = 30;
            $module->settings = [];

            $this->setModule($module);
            $this->installModule();
        }

        // Check we are all installed
        $this->installFiles();
    }

    /**
     * Install Files
     */
    public function installFiles()
    {
        $this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/web/modules/vendor/jquery-1.11.1.min.js')->save();
        $this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/web/modules/xibo-finance-render.js')->save();
        $this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/web/modules/xibo-layout-scaler.js')->save();
        $this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/web/modules/xibo-image-render.js')->save();
        $this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/web/modules/vendor/bootstrap.min.css')->save();
    }

    /**
     * Form for updating the module settings
     */
    public function settingsForm()
    {
        return 'currencies-form-settings';
    }

    /**
     * Process any module settings
     */
    public function settings()
    {
        $this->module->settings['apiKey'] = $this->getSanitizer()->getString('apiKey');
        $this->module->settings['cachePeriod'] = $this->getSanitizer()->getInt('cachePeriod', 300);

        // Return an array of the processed settings.
        return $this->module->settings;
    }

    public function validate()
    {
        if($this->getOption('overrideTemplate') == 0 && ( $this->getOption('templateId') == '' || $this->getOption('templateId') == null) )
            throw new \InvalidArgumentException(__('Please choose a template'));
            
        if ($this->getUseDuration() == 1 && $this->getDuration() == 0)
            throw new \InvalidArgumentException(__('Please enter a duration'));
    }

    /**
     * Adds a Currencies Widget
     * @SWG\Post(
     *  path="/playlist/widget/currencies/{playlistId}",
     *  operationId="WidgetCurrenciesAdd",
     *  tags={"widget"},
     *  summary="Add a Currencies Widget",
     *  description="Add a new Currencies Widget to the specified playlist",
     *  @SWG\Parameter(
     *      name="playlistId",
     *      in="path",
     *      description="The playlist ID to add a Currencies widget",
     *      type="integer",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="name",
     *      in="formData",
     *      description="Optional Widget Name",
     *      type="string",
     *      required=false
     *  ),
     *  @SWG\Parameter(
     *      name="duration",
     *      in="formData",
     *      description="Widget Duration",
     *      type="integer",
     *      required=false
     *  ),
     *  @SWG\Parameter(
     *      name="useDuration",
     *      in="formData",
     *      description="(0, 1) Select 1 only if you will provide duration parameter as well",
     *      type="integer",
     *      required=false
     *  ),
     *  @SWG\Parameter(
     *      name="base",
     *      in="formData",
     *      description="The base currency",
     *      type="string",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="items",
     *      in="formData",
     *      description="Items wanted",
     *      type="string",
     *      required=true
     *   ),
     *  @SWG\Parameter(
     *      name="reverseConversion",
     *      in="formData",
     *      description="(0, 1) Select 1 if you'd like your base currency to be used as the comparison currency you've entered",
     *      type="integer",
     *      required=false
     *  ),
     *  @SWG\Parameter(
     *      name="effect",
     *      in="formData",
     *      description="Effect that will be used to transitions between items, available options: fade, fadeout, scrollVert, scollHorz, flipVert, flipHorz, shuffle, tileSlide, tileBlind ",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="speed",
     *      in="formData",
     *      description="The transition speed of the selected effect in milliseconds (1000 = normal)",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="backgroundColor",
     *      in="formData",
     *      description="A HEX color to use as the background color of this widget",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="noRecordsMessage",
     *      in="formData",
     *      description="A message to display when there are no records returned by the search query",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="dateFormat",
     *      in="formData",
     *      description="The format to apply to all dates returned by he widget",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="updateInterval",
     *      in="formData",
     *      description="Update interval in minutes, should be kept as high as possible, if data change once per hour, this should be set to 60",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="durationIsPerPage",
     *      in="formData",
     *      description="A flag (0, 1), The duration specified is per page/item, otherwise the widget duration is divided between the number of pages/items",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="templateId",
     *      in="formData",
     *      description="Use pre-configured templates, available options: currencies1, currencies2",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="overrideTemplate",
     *      in="formData",
     *      description="flag (0, 1) set to 0 and use templateId or set to 1 and provide whole template in the next parameters",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="widgetOriginalWidth",
     *      in="formData",
     *      description="This is the intended Width of the template and is used to scale the Widget within it's region when the template is applied, Pass only with overrideTemplate set to 1",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="widgetOriginalHeight",
     *      in="formData",
     *      description="This is the intended Height of the template and is used to scale the Widget within it's region when the template is applied, Pass only with overrideTemplate set to 1",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="maxItemsPerPage",
     *      in="formData",
     *      description="This is the intended number of items on each page",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="mainTemplate",
     *      in="formData",
     *      description="Main template, Pass only with overrideTemplate set to 1 ",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="itemtemplate",
     *      in="formData",
     *      description="Template for each item, replaces [itemsTemplate] in main template, Pass only with overrideTemplate set to 1 ",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="styleSheet",
     *      in="formData",
     *      description="Optional StyleSheet Pass only with overrideTemplate set to 1 ",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="javaScript",
     *      in="formData",
     *      description="Optional JavaScript, Pass only with overrideTemplate set to 1 ",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Response(
     *      response=201,
     *      description="successful operation",
     *      @SWG\Schema(ref="#/definitions/Widget"),
     *      @SWG\Header(
     *          header="Location",
     *          description="Location of the new widget",
     *          type="string"
     *      )
     *  )
     * )
     */
    public function add()
    {
        $this->setCommonOptions();

        // Save the widget
        $this->validate();
        $this->saveWidget();
    }

    /**
     * Edit Media
     */
    public function edit()
    {
        $this->setCommonOptions();

        // Save the widget
        $this->validate();
        $this->saveWidget();
    }

    public function setCommonOptions()
    {
        $this->setDuration($this->getSanitizer()->getInt('duration', $this->getDuration()));
        $this->setUseDuration($this->getSanitizer()->getCheckbox('useDuration'));
        $this->setOption('name', $this->getSanitizer()->getString('name'));
        $this->setOption('base', $this->getSanitizer()->getString('base', ''));
        $this->setOption('items', $this->getSanitizer()->getString('items'));
        $this->setOption('effect', $this->getSanitizer()->getString('effect'));
        $this->setOption('speed', $this->getSanitizer()->getInt('speed'));
        $this->setOption('backgroundColor', $this->getSanitizer()->getString('backgroundColor'));
        $this->setOption('noRecordsMessage', $this->getSanitizer()->getString('noRecordsMessage'));
        $this->setOption('dateFormat', $this->getSanitizer()->getString('dateFormat'));
        $this->setOption('reverseConversion', $this->getSanitizer()->getCheckbox('reverseConversion'));
        $this->setOption('updateInterval', $this->getSanitizer()->getInt('updateInterval', 60));
        $this->setOption('templateId', $this->getSanitizer()->getString('templateId'));
        $this->setOption('durationIsPerPage', $this->getSanitizer()->getCheckbox('durationIsPerPage'));
        $this->setRawNode('javaScript', $this->getSanitizer()->getParam('javaScript', ''));
        $this->setOption('overrideTemplate', $this->getSanitizer()->getCheckbox('overrideTemplate'));
        
        if( $this->getOption('overrideTemplate') == 1 ){
            $this->setRawNode('mainTemplate', $this->getSanitizer()->getParam('mainTemplate', $this->getSanitizer()->getParam('mainTemplate', null)));
            $this->setRawNode('itemTemplate', $this->getSanitizer()->getParam('itemTemplate', $this->getSanitizer()->getParam('itemTemplate', null)));
            $this->setRawNode('styleSheet', $this->getSanitizer()->getParam('styleSheet', $this->getSanitizer()->getParam('styleSheet', null)));
            $this->setOption('widgetOriginalWidth', $this->getSanitizer()->getInt('widgetOriginalWidth'));
            $this->setOption('widgetOriginalHeight', $this->getSanitizer()->getInt('widgetOriginalHeight'));
            $this->setOption('maxItemsPerPage', $this->getSanitizer()->getInt('maxItemsPerPage', 4));
        }
    }

    /**
     * Get Results
     * @return array|bool an array of results. false if an invalid value is returned.
     */
    protected function getResults()
    {
        // Does this require a reversed conversion?
        $reverseConversion = ($this->getOption('reverseConversion', 0) == 1);

        // What items/base currencies are we interested in?
        $items = $this->getOption('items');
        $base = $this->getOption('base');

        if ($items == '' || $base == '' ) {
            $this->getLog()->error('Missing Items for Currencies Module with WidgetId ' . $this->getWidgetId());
            return false;
        }

        // Parse items out into an array
        $items = explode(',', $items);
        
        // Get current item template
        $itemTemplate = null;

        if ($this->getOption('overrideTemplate') == 0) {
            $template = $this->getTemplateById($this->getOption('templateId'));
            
            if (isset($template)) {
                $itemTemplate = $template['item'];
            }    
        } else {
            $itemTemplate = $this->getRawNode('itemTemplate');
        }
        
        // Does the template require a percentage change calculation.
        $percentageChangeRequested = stripos($itemTemplate, '[ChangePercentage]') > -1;

        // Our cache key is based on the base/items/changepercentage
        /** @var \Stash\Item $cache */
        $cache = $this->getPool()->getItem($this->makeCacheKey(md5($base . implode(',', $items) . $percentageChangeRequested . $reverseConversion)));
        $cache->setInvalidationMethod(Invalidation::SLEEP, 5000, 15);

        $data = $cache->get();

        if ($cache->isMiss()) {
            // Lock this cache record
            $cache->lock();

            // Start fresh
            $data = [];
            $priorDay = [];

            // Do we need to get the data for percentage change?
            if ($percentageChangeRequested && !$reverseConversion) {
                try {
                    // Get the prior day
                    $priorDay = $this->getPriorDay($base, $items);

                    $this->getLog()->debug('Percentage change requested, prior day is ' . var_export($priorDay, true));

                } catch (RequestException $requestException) {
                    $this->getLog()->error('Problem getting percentage change currency information. E = ' . $requestException->getMessage());
                    $this->getLog()->debug($requestException->getTraceAsString());
                }
            }

            // Each item we want is a call to the results API
            try {
                foreach ($items as $currency) {
                    // Remove the multiplier if there's one (this is handled when we substitute the results into the template)
                    $currency = explode('|', $currency)[0];

                    // Do we need to reverse the from/to currency for this comparison?
                    if ($reverseConversion) {
                        $result = $this->getCurrencyExchangeRate($currency, $base);

                        // We need to get the proir day for this pair only (reversed)
                        $priorDay = $this->getPriorDay($currency, $base);

                        $this->getLog()->debug('Percentage change requested, prior day is ' . var_export($priorDay, true));

                    } else {
                        $result = $this->getCurrencyExchangeRate($base, $currency);
                    }

                    $this->getLog()->debug('Results are: ' . var_export($result, true));

                    $parsedResult = [
                        'time' => $result['Realtime Currency Exchange Rate']['6. Last Refreshed'],
                        'ToName' => $result['Realtime Currency Exchange Rate']['3. To_Currency Code'],
                        'ToCurrency' => $result['Realtime Currency Exchange Rate']['4. To_Currency Name'],
                        'FromName' => $result['Realtime Currency Exchange Rate']['1. From_Currency Code'],
                        'FromCurrency' => $result['Realtime Currency Exchange Rate']['2. From_Currency Name'],
                        'Bid' => round($result['Realtime Currency Exchange Rate']['5. Exchange Rate'], 4),
                        'Ask' => round($result['Realtime Currency Exchange Rate']['5. Exchange Rate'], 4),
                        'LastTradePriceOnly' => round($result['Realtime Currency Exchange Rate']['5. Exchange Rate'], 4),
                        'RawLastTradePriceOnly' => $result['Realtime Currency Exchange Rate']['5. Exchange Rate'],
                        'TimeZone' => $result['Realtime Currency Exchange Rate']['7. Time Zone'],
                    ];

                    // Set the name/currency to be the full name including the base currency
                    $parsedResult['Name'] = $parsedResult['FromName'] . '/' . $parsedResult['ToName'];
                    $parsedResult['Currency'] = $parsedResult['FromCurrency'] . '/' . $parsedResult['ToCurrency'];

                    // work out the change when compared to the previous day
                    if ($percentageChangeRequested && isset($priorDay[$parsedResult['ToName']]) && is_numeric($priorDay[$parsedResult['ToName']])) {
                        $parsedResult['YesterdayTradePriceOnly'] = $priorDay[$parsedResult['ToName']];
                        $parsedResult['Change'] = $parsedResult['RawLastTradePriceOnly'] - $parsedResult['YesterdayTradePriceOnly'];
                    } else {
                        $parsedResult['YesterdayTradePriceOnly'] = 0;
                        $parsedResult['Change'] = 0;
                    }

                    // Parse the result and add it to our data array
                    $data[] = $parsedResult;
                }
            } catch (RequestException $requestException) {
                $this->getLog()->error('Problem getting currency information. E = ' . $requestException->getMessage());
                $this->getLog()->debug($requestException->getTraceAsString());

                return false;
            }

            $this->getLog()->debug('Parsed Results are: ' . var_export($data, true));

            // Cache it
            $cache->set($data);
            $cache->expiresAfter($this->getSetting('cachePeriod', 3600));
            $this->getPool()->saveDeferred($cache);
        }

        return $data;
    }

    /**
     * Run through the data and substitute into the template
     * @param $data
     * @param $source
     * @param $baseCurrency
     * @return mixed
     */
    private function makeSubstitutions($data, $source, $baseCurrency)
    {
        // Replace all matches.
        $matches = '';
        preg_match_all('/\[.*?\]/', $source, $matches);
        
        // Get the currencies' items
        $items = $this->getOption('items');
        
        if (strstr($items, ','))
            $items = explode(',', $items);
        else
            $items = [$items];
        
        $reverseConversion = ($this->getOption('reverseConversion', 0) == 1);

        // Substitute
        foreach ($matches[0] as $sub) {
            $replace = str_replace('[', '', str_replace(']', '', $sub));
            $replacement = 'NULL';
            
            $isPreview = ($this->getSanitizer()->getCheckbox('preview') == 1);
            
            // Match that in the array
            if (isset($data[$replace])) {
                // If the tag exists on the data variables use that var
                $replacement = $data[$replace];
            } else {
                // Custom tags
                
                // Replace the time tag
                if (stripos($replace, 'time|') > -1) {
                    $timeSplit = explode('|', $replace);

                    $time = $this->getDate()->parse($data['time']. 'Y-m-d H:i:s')->format($timeSplit[1]);

                    $replacement = $time;

                } else if (stripos($replace, 'NameTrimmed|') > -1) {
                    $nameSplit = explode('|', $replace);
                    $name = $data['Name'];
                    
                    // Remove the last word until the string is inside the pretended Serializable
                    while (strlen($name) > $nameSplit[1]) {
                        $name = substr($name, 0, strrpos($name, " "));
                    }

                    $replacement = strtoupper($name);

                } else {
                    
                    // Replace the other tags
                    switch ($replace) {
                        case 'NameShort':

                            $replacement = ($reverseConversion) ? $data['FromName'] : $data['ToName'];

                            break;
                            
                        case 'Multiplier':
                            
                            // Initialize replacement with empty string 
                            $replacement = '';
                            
                            // Get the current currency name/code
                            $currencyName = ($reverseConversion) ? $data['FromName'] : $data['ToName'];
                            
                            // Search for the item that relates to the actual currency
                            foreach ($items as $item) {
                                
                                // Get the item name
                                $itemName = trim(explode('|', $item)[0]);
                                
                                // Compare the item name with the actual currency and test if the inputed value has a multiplier flag
                                if( sizeof(explode('|', $item)) > 1 && strcmp($itemName, $currencyName) == 0 ){
                                    
                                    // Get the multiplier
                                    $replacement = explode('|', $item)[1];
                                }
                            }
                            
                            break;
                            
                        case 'CurrencyFlag':

                            $currencyCode = ($reverseConversion) ? $data['FromName'] : $data['ToName'];
                            
                            if (!file_exists(PROJECT_ROOT . '/web/modules/currencies/currency-flags/' . $currencyCode . '.svg')) 
                                $currencyCode = 'default';
                            
                            $file = $this->mediaFactory->createModuleFile('currency_' . $currencyCode, PROJECT_ROOT . '/web/modules/currencies/currency-flags/' . $currencyCode . '.svg');
                            $file->alwaysCopy = true;
                            $file->storedAs = 'currency_' . $currencyCode . '.svg';
                            $file->save();
                            
                            // Tag this layout with this file
                            $this->assignMedia($file->mediaId);
                            
                            $replacement = ($isPreview) ? $this->getResourceUrl('currencies/currency-flags/' . $currencyCode . '.svg') : $file->storedAs;
                            
                            break;
                            
                        case 'LastTradePriceOnlyValue':
                        case 'BidValue':
                        case 'AskValue':
                            
                            // Get the converted currency name
                            $currencyName = ($reverseConversion) ? $data['FromName'] : $data['ToName'];
                            
                            // Get the field's name and set the replacement as the default value from the API
                            $fieldName = str_replace('Value', '', $replace);
                            $replacement = $data[$fieldName];
                                
                            // Search for the item that relates to the actual currency
                            foreach ($items as $item) {
                                
                                // Get the item name
                                $itemName = trim(explode('|', $item)[0]);
                                
                                // Compare the item name with the actual currency and test if the inputed value has a multiplier flag
                                if( sizeof(explode('|', $item)) > 1 && strcmp($itemName, $currencyName) == 0 ){
                                    // Get the multiplier
                                    $multiplier = explode('|', $item)[1];
                                    
                                    // Set the replacement to be the API value times the multiplier
                                    $replacement = $data[$fieldName] * (float)$multiplier;
                                }
                            }        
                            
                            break;

                        case 'ChangePercentage':
                            // Protect against null values
                            if(($data['Change'] == null || $data['LastTradePriceOnly'] == null)){
                                $replacement = "NULL";
                            } else {
                                // Calculate the percentage dividing the change by the ( previous value minus the change )
                                $percentage = $data['Change'] / ( $data['LastTradePriceOnly'] - $data['Change'] );

                                // Convert the value to percentage and round it
                                $replacement = round($percentage*100, 2);
                            }

                            break;
                            
                        case 'ChangeStyle':
                            // Default value as no change
                            $replacement = 'value-equal';
                            
                            // Protect against null values
                            if (($data['Change'] != null && $data['LastTradePriceOnly'] != null)) {
                    
                                if ($data['Change'] > 0) {
                                    $replacement = 'value-up';
                                } else if ( $data['Change'] < 0 ){
                                    $replacement = 'value-down';
                                }
                            }
                            
                            break;
                            
                        case 'ChangeIcon':
                        
                            // Default value as no change
                            $replacement = 'right-arrow';
                            
                            // Protect against null values
                            if (($data['Change'] != null && $data['LastTradePriceOnly'] != null)) {
                    
                                if ( $data['Change'] > 0 ) {
                                    $replacement = 'up-arrow';
                                } else if ( $data['Change'] < 0 ){
                                    $replacement = 'down-arrow';
                                }
                            }
                            
                            break;
                            
                        case 'CurrencyUpper':
                            // Currency in uppercase
                            $replacement = strtoupper($data['Currency']);
                            
                            break;
                                
                        default:
                            $replacement = 'NULL';
                            
                            break;
                    }    
                }
            }
            
            // Replace the variable on the source string
            $source = str_replace($sub, $replacement, $source);
        }

        return $source;
    }

    /**
     * Get Tab
     */
    public function getTab($tab)
    {
        if (!$data = $this->getResults())
            throw new NotFoundException(__('No data returned, please check error log.'));

        return ['results' => $data[0]];
    }

    /**
     * Get Resource
     * @param int $displayId
     * @return mixed
     */
    public function getResource($displayId = 0)
    {        
        $data = [];
        $isPreview = ($this->getSanitizer()->getCheckbox('preview') == 1);

        // Replace the View Port Width?
        $data['viewPortWidth'] = ($isPreview) ? $this->region->width : '[[ViewPortWidth]]';

        // Information from the Module        
        $duration = $this->getCalculatedDurationForGetResource();
        $durationIsPerItem = $this->getOption('durationIsPerItem', 1);

        // Generate a JSON string of items.
        if (!$items = $this->getResults()) {
            return '';
        }

        if( $this->getOption('overrideTemplate') == 0 ) {
            
            $template = $this->getTemplateById($this->getOption('templateId'));
            
            if (isset($template)) {
                $mainTemplate = $template['main'];
                $itemTemplate = $template['item'];
                $styleSheet = $template['css'];
                $widgetOriginalWidth = $template['widgetOriginalWidth'];
                $widgetOriginalHeight = $template['widgetOriginalHeight'];
                $maxItemsPerPage = $template['maxItemsPerPage'];
            }
            
        } else {
            
            $mainTemplate = $this->getRawNode('mainTemplate');
            $itemTemplate = $this->getRawNode('itemTemplate');
            $styleSheet = $this->getRawNode('styleSheet', '');
            $widgetOriginalWidth = $this->getSanitizer()->int($this->getOption('widgetOriginalWidth'));
            $widgetOriginalHeight = $this->getSanitizer()->int($this->getOption('widgetOriginalHeight'));
            $maxItemsPerPage = $this->getSanitizer()->int($this->getOption('maxItemsPerPage'));
        }
        
        // Run through each item and substitute with the template
        $mainTemplate = $this->parseLibraryReferences($isPreview, $mainTemplate);
        $itemTemplate = $this->parseLibraryReferences($isPreview, $itemTemplate);
        
        $renderedItems = [];
        
        $base = $this->getOption('base');

        foreach ($items as $item) {
            $renderedItems[] = $this->makeSubstitutions($item, $itemTemplate, $base);
        }        

        $options = array(
            'type' => $this->getModuleType(),
            'fx' => $this->getOption('effect', 'none'),
            'speed' => $this->getOption('speed', 500),
            'duration' => $duration,
            'durationIsPerPage' => ($this->getOption('durationIsPerPage', 0) == 1),
            'numItems' => count($renderedItems),
            'originalWidth' => $this->region->width,
            'originalHeight' => $this->region->height,
            'previewWidth' => $this->getSanitizer()->getDouble('width', 0),
            'previewHeight' => $this->getSanitizer()->getDouble('height', 0),
            'widgetDesignWidth' => $widgetOriginalWidth,
            'widgetDesignHeight'=> $widgetOriginalHeight,
            'scaleOverride' => $this->getSanitizer()->getDouble('scale_override', 0), 
            'maxItemsPerPage' => $maxItemsPerPage
        );

        $itemsPerPage = $options['maxItemsPerPage'];
        $pages = count($renderedItems);
        $pages = ($itemsPerPage > 0) ? ceil($pages / $itemsPerPage) : $pages;
        $totalDuration = ($durationIsPerItem == 0) ? $duration : ($duration * $pages);
        
        // Replace and Control Meta options
        $data['controlMeta'] = '<!-- NUMITEMS=' . $pages . ' -->' . PHP_EOL . '<!-- DURATION=' . $totalDuration . ' -->';

        // Get the JavaScript node
        $javaScript = $this->parseLibraryReferences($isPreview, $this->getRawNode('javaScript', ''));

        // Replace the head content
        $headContent = '';

        // Add our fonts.css file
        $headContent .= '<link href="' . (($isPreview) ? $this->getApp()->urlFor('library.font.css') : 'fonts.css') . '" rel="stylesheet" media="screen">
        <link href="' . $this->getResourceUrl('vendor/bootstrap.min.css')  . '" rel="stylesheet" media="screen">';
        
        $backgroundColor = $this->getOption('backgroundColor');
        if ($backgroundColor != '') {
            $headContent .= '<style type="text/css"> body { background-color: ' . $backgroundColor . ' }</style>';
        } else {
          $headContent .= '<style type="text/css"> body { background-color: transparent }</style>';
        }
        
        // Add the CSS if it isn't empty, and replace the wallpaper
        $css = $this->makeSubstitutions([], $styleSheet, '');

        if ($css != '') {
            $headContent .= '<style type="text/css">' . $this->parseLibraryReferences($isPreview, $css) . '</style>';
        }
        $headContent .= '<style type="text/css">' . file_get_contents($this->getConfig()->uri('css/client.css', true)) . '</style>';

        // Replace the Head Content with our generated javascript
        $data['head'] = $headContent;

        // Add some scripts to the JavaScript Content
        $javaScriptContent = '<script type="text/javascript" src="' . $this->getResourceUrl('vendor/jquery-1.11.1.min.js') . '"></script>';

        $javaScriptContent .= '<script type="text/javascript" src="' . $this->getResourceUrl('vendor/jquery-cycle-2.1.6.min.js') . '"></script>';

        $javaScriptContent .= '<script type="text/javascript" src="' . $this->getResourceUrl('xibo-layout-scaler.js') . '"></script>';
        $javaScriptContent .= '<script type="text/javascript" src="' . $this->getResourceUrl('xibo-finance-render.js') . '"></script>';
        $javaScriptContent .= '<script type="text/javascript" src="' . $this->getResourceUrl('xibo-image-render.js') . '"></script>';

        $javaScriptContent .= '<script type="text/javascript">';
        $javaScriptContent .= '   var options = ' . json_encode($options) . ';';
        $javaScriptContent .= '   var items = ' . json_encode($renderedItems) . ';';
        $javaScriptContent .= '   var body = ' . json_encode($mainTemplate) . ';';
        $javaScriptContent .= '   $(document).ready(function() { ';
        $javaScriptContent .= '       $("body").xiboLayoutScaler(options); $("#content").xiboFinanceRender(options, items, body); $("#content").find("img").xiboImageRender(options); ';
        $javaScriptContent .= '   }); ';
        $javaScriptContent .= $javaScript;
        $javaScriptContent .= '</script>';

        // Replace the Head Content with our generated javascript
        $data['javaScript'] = $javaScriptContent;

        // Update and save widget if we've changed our assignments.
        if ($this->hasMediaChanged())
            $this->widget->save(['saveWidgetOptions' => false, 'notifyDisplays' => true, 'audit' => false]);

        return $this->renderTemplate($data);
    }

    public function isValid()
    {
        // Using the information you have in your module calculate whether it is valid or not.
        // 0 = Invalid
        // 1 = Valid
        // 2 = Unknown
        return 1;
    }
    
}