<?php
/**
* @brief Javascript Handler
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 12 Aug 2013
*/
namespace IPS\Output;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Javascript: Javascript handler
*/
class _Javascript extends \IPS\Patterns\ActiveRecord
{
/**
* @brief [Javascript] Array of found javascript keys and objects
*/
protected static $foundJsObjects = array();
/**
* @brief [Javascript] Position index for writing javascript to core_javascript
*/
protected static $positions = array();
/**
* @brief [ActiveRecord] Multiton Store
*/
protected static $multitons;
/**
* @brief [ActiveRecord] Database Table
*/
public static $databaseTable = 'core_javascript';
/**
* @brief [ActiveRecord] Database Prefix
*/
public static $databasePrefix = 'javascript_';
/**
* @brief Javascript map of file object URLs
*/
protected static $javascriptObjects = null;
/**
* Find JavaScript file
*
* @param string $app Application key or Plugin key
* @param string $location Location (front, admin, etc)
* @param string $path Path
* @param string $name Filename
* @return \IPS\Output\Javascript
* @throws \OutOfRangeException
*/
public static function find( $app, $location, $path, $name )
{
$key = md5( $app . '-' . $location . '-' . $path . '-' . $name );
if ( !in_array( $key, static::$foundJsObjects ) )
{
$where = array( 'javascript_app=?', 'javascript_location=?', 'javascript_path=?', 'javascript_name=?' );
if ( ! is_string( $app ) )
{
$where[] = 'javascript_plugin=?';
$bindings = array( 'core', 'plugins', '/', $name, $app );
}
else
{
$bindings = array( $app, $location, $path, $name );
}
try
{
$js = \IPS\Db::i()->select( '*', 'core_javascript', array_merge( array( implode( ' AND ', $where ) ), $bindings ) )->first();
static::$foundJsObjects[ $key ] = parent::constructFromData( $js );
}
catch ( \UnderflowException $e )
{
throw new \OutOfRangeException;
}
}
return static::$foundJsObjects[ $key ];
}
/**
* Set class properties if this object belongs to an application or a plugin
*
* @return void
*/
protected function setAppOrPluginProperties()
{
if ( ( $this->app AND ! is_string( $this->app ) ) OR ( $this->plugin ) )
{
$this->app = 'core';
$this->location = 'plugins';
$this->path = '/';
$this->type = 'plugin';
$this->plugin = ( ! is_string( $this->app ) ) ? $this->app : $this->plugin;
}
}
/**
* Create a javascript file. This overwrites any existing JS that matches the same parameters.
* If a $this->app is not a string, then it will assume plugin and automatically determine the correct 'app', 'location' and 'path' so these do not need to
* be defined.
*
* @throws \InvalidArgumentException
* @throws \RuntimeException
* @return void
*/
public function save()
{
$this->setAppOrPluginProperties();
if ( ! isset( $this->path ) OR empty( $this->path ) )
{
$this->path = '/';
}
if ( ! $this->app OR ! $this->location OR ! $this->name )
{
throw new \InvalidArgumentException;
}
if ( ! $this->type )
{
$this->type = static::_getType( $this->path, $this->name );
}
$key = '';
if ( \IPS\IN_DEV AND $this->type == 'plugin' )
{
$key = md5( $this->app . ';' . $this->location . ';' . $this->path . ';' . $this->name );
}
\IPS\Db::i()->insert( 'core_javascript', array(
'javascript_app' => $this->app,
'javascript_location' => $this->location,
'javascript_plugin' => $this->plugin,
'javascript_path' => $this->path,
'javascript_name' => $this->name,
'javascript_type' => $this->type,
'javascript_content' => $this->content,
'javascript_version' => $this->version,
'javascript_position' => ( $this->position ) ? $this->position : 2000000,
'javascript_key' => $key
) );
}
/**
* Delete a javascript file
* be defined.
*
* @throws \InvalidArgumentException
* @return void
*/
public function delete()
{
$this->setAppOrPluginProperties();
if ( ! isset( $this->path ) OR empty( $this->path ) )
{
$this->path = '/';
}
if ( ! $this->app OR ! $this->location OR ! $this->name )
{
throw new \InvalidArgumentException;
}
if ( ! $this->type )
{
$this->type = static::_getType( $this->path, $this->name );
}
if ( \IPS\IN_DEV AND $this->location == 'plugins' )
{
/* Write the file to disk in the correct location */
$file = \IPS\ROOT_PATH . '/plugins/' . $this->plugin . '/dev/js/' . $this->name;
if ( \is_file( $file ) )
{
\unlink( $file );
}
}
$_where = "javascript_app=? AND javascript_location=? AND javascript_path=? AND javascript_name=?";
$where = array( $this->app, $this->location, $this->path, $this->name );
if ( $this->location == 'plugins' )
{
$_where .= " AND javascript_plugin=?";
$where = array_merge( $where, array( $this->plugin ) );
}
array_unshift( $where, $_where );
\IPS\Db::i()->delete( 'core_javascript', $where );
}
/**
* Create an XML document
*
* @param string $app Application
* @return XML Object
*/
public static function createXml( $app )
{
static::importDev($app);
if ( $app === 'core' )
{
static::importDev('global');
}
/* Build XML and write to app directory */
$xml = new \XMLWriter;
$xml->openMemory();
$xml->setIndent( TRUE );
$xml->startDocument( '1.0', 'UTF-8' );
/* Root tag */
$xml->startElement('javascript');
$xml->startAttribute('app');
$xml->text( $app );
$xml->endAttribute();
/* Loop */
foreach ( \IPS\Db::i()->select( '*', 'core_javascript', ( $app === 'core' ) ? \IPS\Db::i()->in( 'javascript_app', array('core', 'global') ) : array( 'javascript_app=?', $app ), 'javascript_path, javascript_location, javascript_name' ) as $js )
{
/* Initiate the <template> tag */
$xml->startElement('file');
foreach( $js as $k => $v )
{
if ( in_array( \substr( $k, 11 ), array('app', 'location', 'path', 'name', 'type', 'version', 'position' ) ) )
{
$xml->startAttribute( $k );
$xml->text( $v );
$xml->endAttribute();
}
}
/* Write value */
if ( preg_match( '/<|>|&/', $js['javascript_content'] ) )
{
$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $js['javascript_content'] ) );
}
else
{
$xml->text( $js['javascript_content'] );
}
/* Close the <template> tag */
$xml->endElement();
}
if( count( static::$_orders ) )
{
foreach( static::$_orders as $_app => $orderArray )
{
foreach( $orderArray as $order )
{
$xml->startElement('order');
$xml->startAttribute( 'app' );
$xml->text( $_app );
$xml->endAttribute();
$xml->startAttribute( 'path' );
$xml->text( $order['path'] );
$xml->endAttribute();
$xml->text( $order['contents'] );
$xml->endElement();
}
}
}
/* Finish */
$xml->endDocument();
return $xml;
}
/**
* Import from an XML file on disk
*
* @param string $file File to import from (can be from applications dir, or tmp uploaded file)
* @param int|null $offset Offset to begin import from
* @param int|null $limit Number of rows to import
* @return bool|int False if the file is invalid, otherwise the number of rows inserted
*/
public static function importXml( $file, $offset=null, $limit=null )
{
if ( ! \is_file( $file ) )
{
return false;
}
$i = 0;
$inserted = 0;
/* Try to prevent timeouts to the extent possible */
$cutOff = null;
if( $maxExecution = @ini_get( 'max_execution_time' ) )
{
/* If max_execution_time is set to "no limit" we should add a hard limit to prevent browser timeouts */
if ( $maxExecution == -1 )
{
$maxExecution = 30;
}
$cutOff = time() + ( $maxExecution * .5 );
}
/* Open XML file */
$xml = new \IPS\Xml\XMLReader;
$xml->open( $file );
$xml->read();
$app = $xml->getAttribute('app');
/* Remove existing elements */
if( $offset === null or $offset === 0 )
{
\IPS\Db::i()->delete( 'core_javascript', array( 'javascript_app=? and (javascript_plugin is null or javascript_plugin=?)', $app, '' ) );
if ( $app === 'core' )
{
\IPS\Db::i()->delete( 'core_javascript', array( 'javascript_app=?', 'global' ) );
}
}
while( $xml->read() )
{
if( $xml->nodeType != \XMLReader::ELEMENT )
{
continue;
}
if( $cutOff !== null AND time() >= $cutOff )
{
break;
}
$i++;
if ( $offset !== null )
{
if ( $i - 1 < $offset )
{
$xml->next();
continue;
}
}
$inserted++;
if( $xml->name == 'file' )
{
/* We have a unique key on app, location, path, name so we use replace into to prevent duplicates */
\IPS\Db::i()->replace( 'core_javascript', array(
'javascript_app' => $xml->getAttribute('javascript_app'),
'javascript_key' => '',
'javascript_plugin' => '',
'javascript_location' => $xml->getAttribute('javascript_location'),
'javascript_path' => $xml->getAttribute('javascript_path'),
'javascript_name' => $xml->getAttribute('javascript_name'),
'javascript_type' => $xml->getAttribute('javascript_type'),
'javascript_content' => $xml->readString(),
'javascript_version' => $xml->getAttribute('javascript_version'),
'javascript_position' => $xml->getAttribute('javascript_position')
) );
}
if( $limit !== null AND $i === ( $limit + $offset ) )
{
break;
}
}
return $inserted;
}
/**
* Export Javascript to /dev/js
*
* @param string $app Application Directory
* @return void
* @throws \RuntimeException
*/
public static function exportDev( $app )
{
try
{
\IPS\Developer::writeDirectory( $app, 'js' );
}
catch( \RuntimeException $e )
{
throw new \RuntimeException( $e->getMessage() );
}
foreach( \IPS\Db::i()->select( '*', 'core_javascript', array( 'javascript_app=?', $app ) )->setKeyField('javascript_id') as $jsId => $js )
{
try
{
$pathToWrite = \IPS\Developer::writeDirectory( $app, 'js/', $js['javascript_location'] );
}
catch( \RuntimeException $e )
{
throw new \RuntimeException( $e->getMessage() );
}
if ( $js['javascript_path'] != '/' )
{
$_path = '';
foreach( explode( '/', trim( $js['javascript_path'], '/' ) ) as $dir )
{
$_path .= '/' . trim( $dir, '/' );
try
{
$pathToWrite = \IPS\Developer::writeDirectory( $app, 'js/' . $js['javascript_location'] . $_path );
}
catch( \RuntimeException $e )
{
throw new \RuntimeException( $e->getMessage() );
}
}
}
if ( ! @\file_put_contents( $pathToWrite . '/' . $js['javascript_name'], $js['javascript_content'] ) )
{
throw new \RuntimeException('core_theme_dev_cannot_write_js,' . $pathToWrite . '/' . $js['javascript_name']);
}
else
{
@chmod( $pathToWrite . '/' . $js['javascript_name'], 0777 );
}
}
/* Open XML file */
if( file_exists( \IPS\ROOT_PATH . '/applications/' . $app . '/data/javascript.xml' ) )
{
$xml = new \IPS\Xml\XMLReader;
$xml->open( \IPS\ROOT_PATH . '/applications/' . $app . '/data/javascript.xml' );
$xml->read();
while( $xml->read() )
{
if( $xml->nodeType != \XMLReader::ELEMENT )
{
continue;
}
if( $xml->name == 'order' )
{
$path = rtrim( $xml->getAttribute('path'), '/' ) . '/';
$app = $xml->getAttribute('app');
$content = $xml->readString();
file_put_contents( \IPS\ROOT_PATH . $path . 'order.txt', $content );
}
}
}
}
/**
* @brief Track order.txt files for writing
*/
protected static $_orders = array();
/**
* Import JS from dev folders and store into core_javascript
*
* @param string $app Application
* @return void
*/
public static function importDev( $app )
{
$root = \IPS\ROOT_PATH . '/applications/' . $app . '/dev/js';
if ( $app == 'global' )
{
$root = \IPS\ROOT_PATH . '/dev/js/';
}
static::$_orders[ $app ] = array();
if ( is_dir( $root ) )
{
\IPS\Db::i()->delete( 'core_javascript', array( 'javascript_app=? and javascript_plugin = \'\'', $app ) );
static::$positions = array();
foreach( new \DirectoryIterator( $root ) as $location )
{
if ( $location->isDot() OR mb_substr( $location->getFilename(), 0, 1 ) === '.' )
{
continue;
}
if ( $location->isDir() )
{
static::_importDevDirectory( $root, $app, $location->getFilename() );
}
}
}
}
/**
* Import a /dev directory recursively.
*
* @param string $root Root directory to recurse
* @param string $app Application key
* @param string $location Location (front, global, etc)
* @param string $path Additional path information
* @return void
*/
protected static function _importDevDirectory( $root, $app, $location, $path='' )
{
$dir = $root . '/' . $location . '/' . $path;
$parentDir = preg_replace( '#^(.*)/([^\/]+?)$#', '\2', $dir );
if ( \file_exists( $dir . '/order.txt' ) )
{
static::$_orders[ $app ][] = array( 'path' => str_replace( \IPS\ROOT_PATH, '', $dir ), 'contents' => file_get_contents( $dir . '/order.txt' ) );
$order = \file( $dir . '/order.txt' );
foreach( $order as $item )
{
$item = trim( $item );
if ( isset( static::$positions[ $app . '-' . $location . '-' . $parentDir ] ) AND static::$positions[ $app . '-' . $location . '-' . $parentDir ] < 1000000 )
{
static::$positions[ $app . '-' . $location . '-' . $item ] = ++static::$positions[ $app . '-' . $location . '-' . $parentDir ];
}
else
{
static::$positions[ $app . '-' . $location . '-' . $item ] = static::_getNextPosition( $app, $location );
}
}
}
foreach ( new \DirectoryIterator( $dir ) as $file )
{
if ( $file->isDot() || mb_substr( $file->getFilename(), 0, 1 ) === '.' || $file == 'index.html' )
{
continue;
}
if ( $file->isDir() )
{
static::_importDevDirectory( $root, $app, $location, $path . '/' . $file->getFileName() );
}
else if ( mb_substr( $file->getFileName(), -3 ) === '.js' )
{
$js = \file_get_contents( $dir . '/' . $file->getFilename() );
if ( isset( static::$positions[ $app . '-' . $location . '-' . $file->getFilename() ] ) )
{
$position = static::$positions[ $app . '-' . $location . '-' . $file->getFilename() ];
}
else
{
/* Attempt to order by files */
if ( ! isset( static::$positions[ $app . '-' . $location . '-' . $parentDir ] ) )
{
static::$positions[ $app . '-' . $location . '-' . $parentDir ] = static::_getNextPosition( $app, $location ) + 1000000;
}
$position = static::$positions[ $app . '-' . $location . '-' . $parentDir ];
}
/* Check to see if 'ips.{dir}.js' exists and if so, put that first */
if ( $file->getFilename() == 'ips.' . $parentDir . '.js' )
{
$position = $position - 1;
}
$path = trim( $path, '/' );
\IPS\Db::i()->delete( 'core_javascript', array( 'javascript_app=? AND javascript_location=? AND javascript_path=? AND javascript_name=?', $app, $location, $path, $file->getFileName() ) );
\IPS\Db::i()->insert( 'core_javascript', array(
'javascript_app' => $app,
'javascript_location' => $location,
'javascript_plugin' => '',
'javascript_path' => $path,
'javascript_name' => $file->getFileName(),
'javascript_type' => static::_getType( $dir . '/', $file->getFileName() ),
'javascript_content' => $js,
'javascript_position' => $position,
'javascript_version' => \IPS\Application::load( ( $app == 'global' ? 'core' : $app ) )->long_version,
'javascript_key' => md5( $app . ';' . $location . ';' . $path . ';' . $file->getFileName() )
) );
}
}
}
/**
* Delete a compiled JS file
*
* @param string $app Application
* @param string|null $location Location (front, global, etc)
* @param string|null $file File to remove
* @return boolean|null
*/
public static function deleteCompiled( $app, $location=null, $file=null )
{
$map = ( isset( \IPS\Data\Store::i()->javascript_map ) ) ? \IPS\Data\Store::i()->javascript_map : array();
$files = ( isset( \IPS\Data\Store::i()->javascript_file_map ) ) ? \IPS\Data\Store::i()->javascript_file_map : array();
if ( $location === NULL and $file === NULL )
{
if ( isset( $map[ $app ] ) )
{
foreach( $map[ $app ] as $hash => $path )
{
try
{
\IPS\File::get( 'core_Theme', $path )->delete();
}
catch( \Exception $e ) { }
}
$map[ $app ] = array();
}
}
else
{
if ( isset( $map[ $app ] ) )
{
foreach( $map[ $app ] as $hash => $path )
{
if ( $file === NULL )
{
$lookFor = 'javascript_' . $app . '/' . $location . '_';
}
else
{
$lookFor = 'javascript_' . $app . '/' . $location . '_' . $file;
}
if ( mb_substr( $path, 0, mb_strlen( $lookFor ) ) === $lookFor )
{
try
{
\IPS\File::get( 'core_Theme', $path )->delete();
}
catch( \Exception $e ) { }
unset( $map[ $app ][ $hash ] );
}
}
}
}
\IPS\Data\Store::i()->javascript_map = $map;
}
/**
* Compiles JS into fewer minified files suitable for non IN_DEV use.
* Imports the fewer files into a database for writing out.
*
* @param string $app Application
* @param string|null $location Location (front, global, etc)
* @param string|null $file File to build
* @return boolean|null
*/
public static function compile( $app, $location=null, $file=null )
{
$flagKey = 'js_compiling_' . md5( $app . ',' . $location . ',' . $file );
if ( \IPS\Theme::checkLock( $flagKey ) )
{
return NULL;
}
\IPS\Theme::lock( $flagKey );
$map = ( isset( \IPS\Data\Store::i()->javascript_map ) and is_array( \IPS\Data\Store::i()->javascript_map ) ) ? \IPS\Data\Store::i()->javascript_map : array();
$files = ( isset( \IPS\Data\Store::i()->javascript_file_map ) and is_array( \IPS\Data\Store::i()->javascript_file_map ) ) ? \IPS\Data\Store::i()->javascript_file_map : array();
if ( $location === null and $file === null )
{
$map[ $app ] = array();
$files[ $app ] = array();
\IPS\Data\Store::i()->javascript_file_map = $files;
}
if ( $app == 'global' )
{
if ( $location === null and $file === null )
{
\IPS\File::getClass('core_Theme')->deleteContainer( 'javascript_' . $app );
}
if ( mb_substr( $file, 0, 8 ) === 'js_lang_' )
{
$langId = intval( mb_substr( $file, 8, -3 ) );
if ( $langId > 0 )
{
/* Write it */
$obj = static::_writeJavascriptFromResultset( array( array(
'javascript_app' => 'global',
'javascript_location' => 'root',
'javascript_path' => '',
'javascript_name' => $file,
'javascript_type' => 'lang',
'javascript_content' => static::getJavascriptLanguage( $langId ),
'javascript_position' => 0
) ), $file, 'global', 'root' );
$map[ $app ][ md5( 'global-root-' . $file ) ] = (string) $obj;
}
}
foreach( \IPS\Output::$globalJavascript as $fileName )
{
if ( $file === null OR $file == $fileName )
{
if ( $fileName === 'map.js' )
{
/* Write it */
$obj = static::_writeJavascriptFromResultset( array( array(
'javascript_app' => 'global',
'javascript_location' => 'root',
'javascript_path' => '',
'javascript_name' => 'map.js',
'javascript_type' => 'framework',
'javascript_content' => static::_getMapAsScript(),
'javascript_position' => 0
) ), 'map.js', 'global', 'root' );
$map[ $app ][ md5( 'global-root-map.js' ) ] = (string) $obj;
}
else
{
$rows = iterator_to_array( \IPS\Db::i()->select(
'*',
'core_javascript', array( 'javascript_app=? AND javascript_location=?', $app, mb_substr( $fileName, 0, -3 ) ),
'javascript_app, javascript_location, javascript_position'
)->setKeyField('javascript_id') );
/* Write it */
$obj = static::_writeJavascriptFromResultset( $rows, $fileName, $app, 'root' );
$map[ $app ][ md5( $app .'-' . 'root' . '-' . $fileName ) ] = $obj ? (string) $obj : null;
}
}
}
}
else
{
if ( $location === null and $file === null )
{
\IPS\File::getClass('core_Theme')->deleteContainer( 'javascript_' . $app );
}
/* Plugins */
if ( $app === 'core' )
{
if ( $file === null OR $file === 'plugins.js' )
{
$ids = array();
foreach ( \IPS\Db::i()->select( '*', 'core_plugins', 'plugin_enabled=1' ) as $row )
{
$ids[] = intval( $row['plugin_id'] );
}
if ( count( $ids ) )
{
$obj = static::_writeJavascriptFromResultset( \IPS\Db::i()->select( '*', 'core_javascript', array( array( 'javascript_app=? AND javascript_location=? AND javascript_type=? AND javascript_plugin IN(' . implode( ',', array_values( $ids ) ) . ')', 'core', 'plugins', 'plugin' ) ), 'javascript_app, javascript_location, javascript_position' )->setKeyField('javascript_id'), 'plugins.js', 'core', 'plugins' );
$map[ $app ][ md5( 'core-plugins-plugins.js' ) ] = $obj ? (string) $obj : null;
}
}
}
foreach( array( 'front', 'admin', 'global' ) as $loc )
{
if ( ( $file === null OR $file === 'app.js' ) AND ( $location === null OR $location === $loc ) )
{
/* app.js: All models and ui for the app */
$obj = static::_writeJavascriptFromResultset( \IPS\Db::i()->select( '*', 'core_javascript', array( 'javascript_app=? AND javascript_location=? AND javascript_type IN (\'mixins\', \'model\',\'ui\')', $app, $loc ), 'javascript_app, javascript_location, javascript_position' )->setKeyField('javascript_id'), 'app.js', $app, $loc );
$map[ $app ][ md5( $app .'-' . $loc . '-' . 'app.js' ) ] = $obj ? (string) $obj : null;
}
/* {location}_{controller}.js: Controllers and templates bundles */
$controllers = array();
$templates = array();
foreach( \IPS\Db::i()->select( '*', 'core_javascript', array( 'javascript_app=? AND javascript_location=? AND javascript_type IN (\'controller\', \'template\')', $app, $loc ), 'javascript_app, javascript_location, javascript_position' )->setKeyField('javascript_id') as $id => $row )
{
if ( $row['javascript_type'] == 'controller' )
{
list( $dir, $controller ) = explode( '/', $row['javascript_path'] );
$controllers[ $controller ][] = $row;
}
else
{
/* ips . templates . {controller} . js */
$bits = explode( '.', $row['javascript_name'] );
$templates[ $bits[2] ][] = $row;
}
}
/* Check to see if we have a template that does not have a controller */
$templateOnlyKeys = array_diff( array_keys( $templates ), array_keys( $controllers ) );
foreach( array_merge( array_keys( $controllers ), array_keys( $templates ) ) as $key )
{
if ( ( $file === null ) OR ( $file === $loc . '_' . $key . '.js' ) )
{
$files = array();
if ( isset( $templates[ $key ] ) )
{
foreach( $templates[ $key ] as $id => $row )
{
$files[ $row['javascript_id'] ] = $row;
}
}
if ( isset( $controllers[ $key ] ) )
{
foreach( $controllers[ $key ] as $controller )
{
$files[ $controller['javascript_id'] ] = $controller;
}
}
/* Template only? */
if ( count( $templateOnlyKeys ) AND in_array( $key, $templateOnlyKeys ) )
{
foreach( $templates[ $key ] as $tmpl )
{
$files[ $tmpl['javascript_id'] ] = $tmpl;
}
}
$obj = static::_writeJavascriptFromResultset( $files, $loc . '_' . $key . '.js', $app, $loc );
$map[ $app ][ md5( $app .'-' . $loc . '-' . $loc . '_' . $key . '.js' ) ] = $obj ? (string) $obj : null;
}
}
}
}
\IPS\Data\Store::i()->javascript_map = $map;
/* As the filename has changed, rebuild the JS map */
if ( ( $app !== 'global' and ( $location === NULL or in_array( $location, array( 'admin', 'front', 'global' ) ) ) ) )
{
\IPS\Output\Javascript::compile( 'global', 'root', 'map.js' );
}
\IPS\Theme::unlock( $flagKey );
return TRUE;
}
/**
* Combines the DB rows into a single string for writing.
*
* @param \IPS\Db\Select $files Result set
* @param string $fileName Filename to use
* @param string $app Application
* @param string $location Location (front, global, etc)
* @return object|null \IPS\File object
*/
protected static function _writeJavascriptFromResultset( $files, $fileName, $app, $location )
{
$content = array();
$jsMap = ( isset( \IPS\Data\Store::i()->javascript_map ) and is_array( \IPS\Data\Store::i()->javascript_map ) ) ? \IPS\Data\Store::i()->javascript_map : array();
/* Try and remove any existing files */
try
{
$md5 = md5( $app . '-' . $location . '-' . $fileName );
if ( isset( $jsMap[ $app ] ) and in_array( $md5, array_keys( $jsMap[ $app ] ) ) )
{
\IPS\File::get( 'core_Theme', $jsMap[ $app ][ $md5 ] )->delete();
}
}
catch ( \InvalidArgumentException $e ) { }
if ( ! count( $files ) )
{
return null;
}
foreach( $files as $row )
{
$content[] = static::_minify( $row['javascript_content'] ) . ";";
}
$fileObject = static::_writeJavascript( implode( "\n", $content ), $fileName, $app, $location );
try
{
$map = \IPS\Data\Store::i()->javascript_file_map;
}
catch ( \OutOfRangeException $e )
{
$map = array();
}
foreach( $files as $row )
{
$path = ( ( ! empty( $row['javascript_path'] ) AND $row['javascript_path'] !== '/' ) ? '/' . $row['javascript_path'] . '/' : '/' );
$map[ $row['javascript_app'] ][ $row['javascript_location'] ][ $path ][ $row['javascript_name'] ] = (string) $fileObject;
}
\IPS\Data\Store::i()->javascript_file_map = $map;
return $fileObject;
}
/**
* Combines the DB rows into a single string for writing.
*
* @param string $content Javascript string to write
* @param string $fileName Filename to use
* @param string $app Application
* @param string $location Location (front, global, etc)
* @return object \IPS\File object
*/
protected static function _writeJavascript( $content, $fileName, $app, $location )
{
return \IPS\File::create( 'core_Theme', $location . '_' . $fileName, $content, 'javascript_' . $app, TRUE );
}
/**
* Get javascript language as a script
*
* @param int $langId ID of language to fetch
* @return string
*/
public static function getJavascriptLanguage( $langId )
{
$_lang = array();
foreach ( \IPS\Db::i()->select( '*', 'core_sys_lang_words', array( 'lang_id=? AND word_js=?', $langId, TRUE ) ) as $row )
{
$_lang[ $row['word_key'] ] = $row['word_custom'] ?: $row['word_default'];
}
if ( \IPS\IN_DEV )
{
foreach ( \IPS\Application::enabledApplications() as $app )
{
if ( file_exists( \IPS\ROOT_PATH . "/applications/{$app->directory}/dev/jslang.php" ) )
{
require \IPS\ROOT_PATH . "/applications/{$app->directory}/dev/jslang.php";
$_lang = array_merge( $_lang, $lang );
}
}
foreach ( \IPS\Plugin::enabledPlugins() as $plugin )
{
if ( file_exists( \IPS\ROOT_PATH . "/plugins/{$plugin->location}/dev/jslang.php" ) )
{
require \IPS\ROOT_PATH . "/plugins/{$plugin->location}/dev/jslang.php";
$_lang = array_merge( $_lang, $lang );
}
}
}
return 'ips.setString( ' . json_encode( $_lang ) . ')';
}
/**
* Get javascript map as a script
*
* @return string
*/
protected static function _getMapAsScript()
{
$fileMap = isset(\IPS\Data\Store::i()->javascript_file_map) ? \IPS\Data\Store::i()->javascript_file_map : array();
$map = array();
/* Fix up the map a little */
foreach( $fileMap as $app => $location )
{
if ( $app === 'global' )
{
continue;
}
foreach( $location as $locName => $locData )
{
foreach( $locData as $name => $items )
{
if ( mb_stristr( $name, '/controllers/' ) )
{
$url = array_pop( $items );
$map[ $app ][ $locName . '_' . trim( str_replace( '/controllers/', '', $name ) , '/' ) ] = (string) \IPS\File::get( 'core_Theme', $url )->url;
}
}
}
}
$json = version_compare( PHP_VERSION, '5.4.0' ) >= 0 ? json_encode( $map, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) : str_replace( '\\/', '/', json_encode( $map ) );
return 'var ipsJavascriptMap = ' . $json . ';';
}
/**
* Minifies javascript
*
* @param string $js Javascript code
* @return string
*/
protected static function _minify( $js )
{
require_once( \IPS\ROOT_PATH . '/system/3rd_party/JShrink/Minifier.php' );
$js = \JShrink\Minifier::minify( $js, array( 'flaggedComments' => false ) );
return $js;
}
/**
* Get JS
*
* @param string $file Filename
* @param string|null $app Application
* @param string|null $location Location (e.g. 'admin', 'front')
* @return array URL to JS files
*/
public static function inDevJs( $file, $app=NULL, $location=NULL )
{
/* 1: Is this the magic plugin JS */
if ( $app === 'core' and $location === 'plugins' and $file === 'plugins.js' )
{
$return = array();
foreach ( new \GlobIterator( \IPS\ROOT_PATH . '/plugins/*/dev/js/*' ) as $file )
{
try
{
$plugin = \IPS\Plugin::getPluginFromPath( $file );
if( $plugin->enabled )
{
$url = str_replace( \IPS\ROOT_PATH, rtrim( \IPS\Settings::i()->base_url, '/' ), $file );
$return[] = str_replace( '\\', '/', $url );
}
}
catch( \OutOfRangeException $e ){}
}
return $return;
}
/* 2: Is it a named grouped collection? */
if ( $app === NULL AND $location === NULL )
{
if ( $file === 'map.js' )
{
return array();
}
if ( in_array( $file, \IPS\Output::$globalJavascript ) )
{
$app = 'global';
$location = '/';
}
if ( mb_substr( $file, 0, 8 ) === 'js_lang_' )
{
return array( \IPS\Http\Url::baseUrl() . "/applications/core/interface/js/jslang.php?langId=" . intval( mb_substr( $file, 8, -3 ) ) );
}
}
$app = $app ?: ( \IPS\Dispatcher::i()->application ? \IPS\Dispatcher::i()->application->directory : NULL );
$location = $location ?: \IPS\Dispatcher::i()->controllerLocation;
/* 3: App JS? */
if ( $file == 'app.js' )
{
return static::_appJs( $app, $location );
}
/* 3: Is this a controller/template combo? */
if ( mb_strstr( $file, '_') AND mb_substr( $file, -3 ) === '.js' )
{
list( $location, $key ) = explode( '_', mb_substr( $file, 0, -3 ) );
if ( ( $location == 'front' OR $location == 'admin' OR $location == 'global' ) AND ! empty( $key ) )
{
return static::_sectionJs( $key, $location, $app );
}
}
/* 4: Is it in the interface directory? */
if ( $location === 'interface' )
{
$path = \IPS\ROOT_PATH . "/applications/{$app}/interface/{$file}";
}
else if ( $app === 'global' )
{
$return = array();
if ( in_array( $file, \IPS\Output::$globalJavascript ) )
{
return static::_directoryJs( \IPS\ROOT_PATH . "/dev/js/" . mb_substr( $file, 0, -3 ) );
}
$path = \IPS\ROOT_PATH . "/dev/js";
}
else
{
$path = \IPS\ROOT_PATH . "/applications/{$app}/dev/js/{$location}/{$file}";
}
if ( is_dir( $path ) )
{
return static::_directoryJs( $path );
}
else
{
return array( str_replace( \IPS\ROOT_PATH, \IPS\Http\Url::baseUrl(), $path ) );
}
}
/**
* Get the map for IN_DEV use
*
* @return array
*/
public static function inDevMapJs()
{
$files = array();
foreach( \IPS\Application::enabledApplications() as $app => $data )
{
$root = \IPS\ROOT_PATH . "/applications/{$app}/dev/js/";
foreach( array( 'front', 'admin', 'global' ) as $location )
{
if ( is_dir( $root . "{$location}/controllers" ) )
{
foreach ( new \DirectoryIterator( $root . "{$location}/controllers" ) as $controllerDir )
{
if ( $controllerDir->isDot() || mb_substr( $controllerDir->getFilename(), 0, 1 ) === '.' )
{
continue;
}
if ( $controllerDir->isDir() )
{
$controllerPath = \IPS\ROOT_PATH . "/applications/{$app}/dev/js/{$location}/controllers/{$controllerDir}";
foreach ( new \DirectoryIterator( $root . "{$location}/controllers/{$controllerDir}" ) as $file )
{
if ( $file->isDot() || mb_substr( $file->getFilename(), 0, 1 ) === '.' || $file == 'index.html' )
{
continue;
}
$files[ $app ][ $location . '_' . $controllerDir ] = str_replace( \IPS\ROOT_PATH, rtrim( \IPS\Http\Url::baseUrl( \IPS\Http\Url::PROTOCOL_RELATIVE ), '/' ), $controllerPath ) . '/' . $file->getFileName();
}
}
}
}
}
}
$json = version_compare( PHP_VERSION, '5.4.0' ) >= 0 ? json_encode( $files, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) : str_replace( '\\/', '/', json_encode( $files ) );
return 'var ipsJavascriptMap = ' . $json . ';';
}
/**
* Returns the component parts from a URL
*
* @param string $url Full URL of javascript file
* @return array Array( 'app' => .., 'location' => .., 'path' => .., 'name' => .. );
*/
protected static function _urlToComponents( $url )
{
$url = ltrim( str_replace( \IPS\Http\Url::baseUrl( \IPS\Http\Url::PROTOCOL_RELATIVE ) . 'applications', '', $url ), '/' );
$bits = explode( "/", $url );
$app = array_shift( $bits );
/* Remove dev/js */
array_shift( $bits );
array_shift( $bits );
$location = array_shift( $bits );
$name = array_pop( $bits );
$path = preg_replace( '#/{2,}#', '/', '/' . trim( implode( '/', $bits ), '/' ) . '/' );
return array( 'app' => $app, 'location' => $location, 'name' => $name, 'path' => $path );
}
/**
* Gets app specific Javascript
*
* @param string $app Application
* @param string $location Location (front, global, etc)
* @return array
*/
protected static function _appJs( $app, $location )
{
$models = array();
/* Only include if the app is enabled */
if( !in_array( $app, array_keys( \IPS\Application::enabledApplications() ) ) )
{
return $models;
}
if ( \is_dir( \IPS\ROOT_PATH . "/applications/" . $app ) )
{
foreach( array( \IPS\ROOT_PATH . "/applications/{$app}/dev/js/{$location}/mixins", \IPS\ROOT_PATH . "/applications/{$app}/dev/js/{$location}/models", \IPS\ROOT_PATH . "/applications/{$app}/dev/js/{$location}/ui" ) as $durr )
{
/* Models */
if ( \is_dir( $durr ) )
{
$models = static::_directoryJs( $durr );
}
}
}
return $models;
}
/**
* Returns section specific JS (controller, models and any template files required)
*
* @param string $key Controller Key (messages, reports, etc)
* @param string $location Location (front, admin)
* @param string $app Application
*/
protected static function _sectionJs( $key, $location, $app )
{
$return = array();
$controllerDir = \IPS\ROOT_PATH . "/applications/{$app}/dev/js/{$location}/controllers/{$key}";
$templatesDir = \IPS\ROOT_PATH . "/applications/{$app}/dev/js/{$location}/templates";
$controllers = array();
$templates = array();
/* Get controllers */
if ( \is_dir( $controllerDir ) )
{
$controllers = static::_directoryJs( $controllerDir );
}
/* Templates */
if ( \is_dir( $templatesDir . '/' . $key ) )
{
$templates = static::_directoryJs( $templatesDir . '/' . $key );
}
else if ( \file_exists( $templatesDir . '/ips.templates.' . $key . '.js' ) )
{
$templates = array( str_replace( \IPS\ROOT_PATH, rtrim( \IPS\Http\Url::baseUrl( \IPS\Http\Url::PROTOCOL_RELATIVE ), '/' ), $templatesDir . '/ips.templates.' . $key . '.js' ) );
}
return array_merge( $templates, $controllers );
}
/**
* Get Javascript files recursively.
*
* @param string $path Path to open
* @param array $return Items retreived so far
* @return array
*/
protected static function _directoryJs( $path, $return=array() )
{
$path = rtrim( $path, '/' );
$contents = array();
foreach ( new \DirectoryIterator( $path ) as $file )
{
if ( $file->isDot() || mb_substr( $file->getFilename(), 0, 1 ) === '.' )
{
continue;
}
if ( $file->isDir() )
{
$return = static::_directoryJs( $path . '/' . $file->getFileName(), $return );
}
else if ( mb_substr( $file->getFileName(), -3 ) === '.js' )
{
$contents[] = str_replace( \IPS\ROOT_PATH, rtrim( \IPS\Http\Url::baseUrl( \IPS\Http\Url::PROTOCOL_RELATIVE ), '/' ), $path ) . '/' . $file->getFileName();
}
}
if ( count( $contents ) )
{
/* Check to see if 'ips.{dir}.js' exists and if so, put that first */
$parentDir = preg_replace( '#^(.*)/([^\/]+?)$#', '\2', $path );
$reordered = array();
foreach( $contents as $url )
{
if ( mb_strstr( $url, '/' . $parentDir . '/' ) AND mb_strstr( $url, '/' . $parentDir . '/ips.' . $parentDir . '.js' ) )
{
$reordered[] = $url;
break;
}
}
$return = array_merge( $reordered, array_diff( $contents, $reordered ), $return );
}
$reordered = array();
if ( is_dir( $path ) AND \file_exists( $path . '/order.txt' ) )
{
$order = \file( $path . '/order.txt' );
foreach( $order as $item )
{
foreach( $return as $url )
{
$item = trim( $item );
if ( mb_substr( $item, -3 ) === '.js' )
{
if ( mb_substr( $url, -(mb_strlen( $item ) ) ) == $item )
{
$reordered[] = $url;
}
}
else
{
if ( mb_substr( str_replace( \IPS\Http\Url::baseUrl( \IPS\Http\Url::PROTOCOL_RELATIVE ) . ltrim( str_replace( \IPS\ROOT_PATH, '', $path ), '/' ), '', $url ), 1, (mb_strlen( $item ) ) ) == $item )
{
$reordered[] = $url;
}
}
}
}
}
if ( count( $reordered ) )
{
/* Add in items not specified in the order */
$diff = array_diff( $return, $reordered );
if ( count( $diff ) )
{
foreach( $diff as $url )
{
$reordered[] = $url;
}
}
return $reordered;
}
return $return;
}
/**
* Returns the type of javascript file
* @param string $path Path
* @param string $name File Name
* @return string
*/
protected static function _getType( $path, $name )
{
$type = 'framework';
if ( mb_strstr( $path, '/controllers/' ) )
{
$type = 'controller';
}
else if ( mb_strstr( $path, '/models/' ) )
{
$type = 'model';
}
else if ( mb_strstr( $path, '/mixins/' ) )
{
$type = 'mixins';
}
else if ( mb_strstr( $path, '/ui/' ) )
{
$type = 'ui';
}
else if ( mb_strstr( $name, 'ips.templates.' ) )
{
$type = 'template';
}
return $type;
}
/**
* Returns an incremented position integer for this app and location
*
* @param string $app Application key
* @param string $location Location (front, global, etc)
* @return int
*/
protected static function _getNextPosition( $app, $location )
{
if ( ! isset( static::$positions[ $app . '-' . $location ] ) )
{
static::$positions[ $app . '-' . $location ] = 0;
}
static::$positions[ $app . '-' . $location ] += 50;
return static::$positions[ $app . '-' . $location ];
}
}