Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @since 15 Apr 2013 */ namespace IPS\core\modules\admin\customization; /* To prevent PHP errors (extending class does not exist) revealing path */ use ErrorException; use Exception; use IPS\Application; use IPS\Data\Store; use IPS\Db; use IPS\Dispatcher; use IPS\File; use IPS\Helpers\Form; use IPS\Helpers\Form\CheckboxSet; use IPS\Helpers\Form\Codemirror; use IPS\Helpers\Form\Number; use IPS\Helpers\Form\Text; use IPS\Helpers\Form\Upload; use IPS\Helpers\MultipleRedirect; use IPS\Http\Url; use IPS\IPS; use IPS\Lang; use IPS\Member; use IPS\Member\Group; use IPS\Node\Controller; use IPS\Node\Model; use IPS\Output; use IPS\Request; use IPS\Session; use IPS\Settings; use IPS\Theme; use IPS\Theme\Editor\Category; use IPS\Theme\Editor\Setting; use IPS\Xml\XMLReader; use OutOfRangeException; use UnderflowException; use UnexpectedValueException; use XMLWriter; use function array_keys; use function array_merge; use function base64_decode; use function count; use function defined; use function implode; use function in_array; use function intval; use function is_array; use function is_int; use function preg_match; use function str_replace; use function substr; use function time; use function uniqid; use const IPS\TEMP_DIRECTORY; if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) ) { header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' ); exit; } /** * themes */ class themes extends Controller { /** * @brief Has been CSRF-protected */ public static bool $csrfProtected = TRUE; /** * Node Class * * @var Theme|string */ protected string $nodeClass = 'IPS\Theme'; /** * @brief If true, will prevent any item from being moved out of its current parent, only allowing them to be reordered within their current parent */ protected bool $lockParents = TRUE; /** * Title can contain HTML? */ public bool $_titleHtml = TRUE; /** * Description can contain HTML? */ public bool $_descriptionHtml = TRUE; /** * Execute * * @return void */ public function execute() : void { Dispatcher::i()->checkAcpPermission( 'theme_sets_manage' ); parent::execute(); } /** * Manage * * @return void */ protected function manage() : void { Output::i()->sidebar['actions']['add'] = array( 'primary' => true, 'icon' => 'plus', 'title' => 'add', 'link' => Url::internal( 'app=core&module=customization&controller=themes&do=form' ) ); Output::i()->sidebar['actions']['import'] = array( 'primary' => false, 'icon' => 'upload', 'title' => 'upload', 'link' => Url::internal( 'app=core&module=customization&controller=themes&do=upload' ), 'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('upload') ) ); if ( \IPS\IN_DEV ) { Output::i()->sidebar['actions']['rebuildstatic'] = array( 'primary' => false, 'icon' => 'cogs', 'title' => 'theme_set_import_to_static', 'link' => Url::internal( "app=core&module=customization&controller=themes&do=rebuildStatic" )->csrf() ); } parent::manage(); Output::i()->output .= Theme::i()->getTemplate('customization')->designerModeToggle(); } /** * Allow overloading to change how the title is displayed in the tree * * @param $node Model Node * @return string * @throws ErrorException */ protected static function nodeTitle( Model $node ): string { return Theme::i()->getTemplate('customization')->themeRowTitle( $node ); } /** * Get Root Buttons * * @return array */ public function _getRootButtons(): array { return array(); } /** * Add/Edit Form * * @return void * @throws ErrorException */ protected function form() : void { if ( Request::i()->id ) { /* Is this theme currenly being edited? */ try { $theme = Theme::load( Request::i()->id ); if ( $theme->edit_in_progress ) { $memberEditing = Member::load( $theme->edit_in_progress ); if ( $memberEditing->member_id == Member::loggedIn()->member_id ) { $message = Member::loggedIn()->language()->addToStack( 'theme_editing_you_are_editing' ); } else { $message = Member::loggedIn()->language()->addToStack( 'theme_editing_they_are_editing', null, [ 'sprintf' => [ $memberEditing->name ] ] ); } Output::i()->output .= Theme::i()->getTemplate('global', 'core', 'admin')->message( Theme::i()->getTemplate('customization')->themeEditingMessage( $theme, $message ), 'info i-margin-bottom_2', null, null ); } else { Output::i()->sidebar['actions']['editor'] = array( 'primary' => true, 'icon' => 'brush', 'title' => 'theme_editor_open', 'link' => Url::internal( 'app=core&module=customization&controller=themes&do=startEditing&id=' . $theme->id ), 'target' => "blank", 'tooltip' => Member::loggedIn()->language()->addToStack( 'theme_editor_open_tooltip' ) ); } } catch ( OutOfRangeException $e ) { Output::i()->error( 'node_error', '2S101/Z', 404, '' ); } } parent::form(); } /** * End any theme editing sessions for this theme * * @return void * @throws Exception */ public function startEditing() : void { $theme = Theme::load( Request::i()->id ); $theme->editingStart(); Output::i()->redirect( Url::internal( 'app=core&module=system&controller=themeeditor', 'front', 'theme_editor' ) ); } /** * End any theme editing sessions for this theme * * @return void */ public function endEditing() : void { $theme = Theme::load( Request::i()->id ); if ( $theme->edit_in_progress ) { try { $member = Member::load( $theme->edit_in_progress ); $theme->editingFinish( $member ); Output::i()->redirect( Url::internal( 'app=core&module=customization&controller=themes&do=form&id=' . $theme->id ), 'theme_editing_ended' ); } catch( Exception ) { Output::i()->error( 'no_permission', '2S101/Y', 404, '' ); } } } /** * Revert theme setting * * @return void * @throws Exception */ public function revertThemeSetting(): void { Session::i()->csrfCheck(); $theme = Theme::load( Request::i()->id ); $value = NULL; try { $themeSetting = Db::i()->select( 'sc.*, sv.sv_value', array('core_theme_settings_fields', 'sc'), array( "sc_set_id=? AND sc_key=?", $theme->id, Request::i()->key ) ) ->join( array('core_theme_settings_values', 'sv'), 'sv.sv_id=sc.sc_id' ) ->first(); } catch( UnderflowException $e ) { return; } foreach( $theme->parents() as $parent ) { try { $setting = Db::i()->select( 'sc.*, sv.sv_value', array('core_theme_settings_fields', 'sc'), array( "sc_set_id=? AND sc_key=?", $parent->id, Request::i()->key ) ) ->join( array('core_theme_settings_values', 'sv'), 'sv.sv_id=sc.sc_id' ) ->first(); if ( $setting['sv_value'] !== $themeSetting['sv_value'] ) { /* Value different from theme set we're reverting from? use this, then */ $value = $setting['sv_value']; break; } } catch( UnderflowException $e ) { } } if ( $value === NULL ) { /* Just use the default */ $value = $themeSetting['sc_default']; } Session::i()->log( 'acplogs__theme_setting_deleted', array( $themeSetting['sc_key'] => FALSE ) ); if ( Request::i()->isAjax() ) { /* Just return the value */ Output::i()->json( array( 'value' => $value ) ); } else { /* Update */ Db::i()->update( 'core_theme_settings_values', array( 'sv_value' => $value ), array( 'sv_id=?', $themeSetting['sc_id'] ) ); Output::i()->redirect( Url::internal( 'app=core&module=customization&controller=themes&do=form&id=' . Request::i()->id ), 'completed' ); } } /** * Manual Theme Upload * * @return void * @throws ErrorException */ public function upload(): void { Dispatcher::i()->checkAcpPermission( 'theme_download_upload' ); $form = new Form( 'form', 'next' ); $form->add( new Upload( 'core_theme_set_new_import', NULL, FALSE, array( 'allowedFileTypes' => array( 'xml' ), 'temporary' => TRUE ), NULL, NULL, NULL, 'core_theme_set_new_import' ) ); if ( $values = $form->values() ) { /* Move it to a temporary location */ $tempFile = tempnam( TEMP_DIRECTORY, 'IPS' ); move_uploaded_file( $values['core_theme_set_new_import'], $tempFile ); $max = Db::i()->select( 'MAX(set_order)', 'core_themes' )->first(); /* Create a default theme */ $theme = new Theme; $theme->editor_skin = 'ips'; $theme->order = $max + 1; $theme->save(); /* Initate a redirector */ Output::i()->redirect( Url::internal( 'app=core&module=customization&controller=themes&do=import' )->setQueryString( array( 'file' => $tempFile, 'key' => md5_file( $tempFile ), 'id' => $theme->id ) )->csrf() ); } Output::i()->output = Theme::i()->getTemplate( 'global' )->block( 'theme_set_add_button', $form, FALSE ); } /** * Import from upload * * @return void */ public function import(): void { Session::i()->csrfCheck(); if ( !file_exists( Request::i()->file ) or md5_file( Request::i()->file ) !== Request::i()->key ) { Output::i()->error( 'generic_error', '3C130/1', 500, '' ); } Output::i()->output = new MultipleRedirect( Url::internal( 'app=core&module=customization&controller=themes&do=import' )->setQueryString( array( 'file' => Request::i()->file, 'key' => Request::i()->key, 'id' => Request::i()->id ) )->csrf(), function( $data ) { $set = Theme::load( Request::i()->id ); $iMap = $set->resource_map; $css = Theme::load( Request::i()->id )->getAllCss( null, null, null, Theme::RETURN_ALL_NO_CONTENT, true ); $masterCss = Theme::load( Request::i()->id )->getAllCss( null, null, null, Theme::RETURN_BIT_NAMES, false ); /* Open XML file */ $xml = XMLReader::safeOpen( Request::i()->file ); if ( ! @$xml->read() ) { @unlink( Request::i()->file ); Output::i()->error( 'xml_upload_invalid', '2C163/1', 403, '' ); } /* Is this the first batch? */ if ( !is_array( $data ) ) { $_SESSION['theme_import'] = array( 'css' => array(), 'isNewSet' => false ); /* Save snapshot */ Theme::load( Request::i()->id )->saveHistorySnapshot(); /* No Name? Then this is a brand-new theme */ if ( empty( $set->name ) ) { $_SESSION['theme_import']['isNewSet'] = TRUE; while ( $xml->read() ) { if ( $xml->name == 'theme' ) { $groups = array_keys( Member::administrators()['g'] ); $set->saveSet( array( 'set_name' => $xml->getAttribute('name'), 'set_author_name' => $xml->getAttribute('author_name'), 'set_author_url' => $xml->getAttribute('author_url'), 'set_version' => $xml->getAttribute('version'), 'set_update_check' => $xml->getAttribute('update_check'), 'set_long_version' => ( $xml->getAttribute('long_version') ) ? $xml->getAttribute('long_version') : Application::load('core')->long_version, 'set_is_default' => $set->is_default, 'set_permissions' => implode( ',', $groups ) ) ); if ( $xml->getAttribute('easy_mode') ) { $set->save(); } break; } } } else { /* We are importing an update to a theme */ while ( $xml->read() ) { if ( $xml->name == 'theme' ) { $set->saveSet( array( 'set_author_name' => $xml->getAttribute('author_name'), 'set_author_url' => $xml->getAttribute('author_url'), 'set_version' => $xml->getAttribute('version'), 'set_update_check' => $xml->getAttribute('update_check'), 'set_long_version' => ( $xml->getAttribute('long_version') ) ? $xml->getAttribute('long_version') : Application::load('core')->long_version ) ); break; } } } /* Start importing */ $data = array( 'apps' => array() ); return array( $data, Member::loggedIn()->language()->addToStack('processing') ); } /* Move to correct app */ $appKey = NULL; $version = Theme::load( Request::i()->id )->long_version; $xml->read(); while ( $xml->read() ) { /* If this is a closing tag, OR if the tag is empty, move on */ if( $xml->nodeType == $xml::END_ELEMENT or empty( $xml->readInnerXml() ) ) { $xml->next(); continue; } $name = $xml->name; switch( $name ) { case 'header': $set->custom_header = $xml->readString(); break; case 'footer': $set->custom_footer = $xml->readString(); break; case 'css': $set->custom_css = $xml->readString(); break; case 'layout': $set->view_options = $xml->readString(); break; case 'core_js': $set->core_js = $xml->readString(); break; case 'core_css': $set->core_css = $xml->readString(); /* Rebuild any designer core data */ $content = Theme::minifyCss( $set->core_css ); /* Replace any tags in the CSS */ Output::i()->parseFileObjectUrls( $content ); $set->core_css_filename = File::create( 'core_Theme', 'theme.css', $content ); break; case 'editor_data': $editorData = $xml->readString(); $set->theme_editor_data = !empty( $editorData ) ? json_decode( $editorData, true ) : null; break; case 'css_variables': $set->css_variables = $xml->readString(); break; case 'editor_category': $key = $xml->getAttribute( 'key' ); try { $row = Db::i()->select( '*', 'core_theme_editor_categories', [ 'cat_key=? and cat_set_id=?', $key, $set->id ] )->first(); $category = Category::constructFromData( $row ); } catch( UnderflowException ) { $category = new Category; $category->key = $key; $category->set_id = $set->id; /* Set the position first, in case there is no parent specified */ $position = (int) Db::i()->select( 'max(cat_position)', 'core_theme_editor_categories', array( 'cat_parent=?', 0 ) )->first(); $category->position = $position + 1; } $category->app = $xml->getAttribute( 'app' ); $xml->read(); while( $xml->read() ) { if( $xml->nodeType == $xml::END_ELEMENT ) { break; } switch( $xml->name ) { case 'name': $category->name = $xml->readString(); break; case 'icon': $category->icon = json_encode( Category::buildIconData( $xml->readString() ) ); break; case 'parent': try { $parent = Db::i()->select( '*', 'core_theme_editor_categories', [ 'cat_key=? and (cat_set_id=? or cat_set_id=?)', $xml->readString(), $set->id, 0 ], 'cat_set_id desc' )->first(); $parentId = $parent['cat_id']; /* Reset the position if we changed the category */ if( $parentId != $category->parent ) { $position = (int) Db::i()->select( 'max(cat_position)', 'core_theme_editor_categories', array( 'cat_parent=?', $category->parent ) )->first(); $category->position = $position + 1; } $category->parent = $parentId; } catch( UnderflowException ){} break; } $category->save(); $xml->next(); } break; case 'editor_setting': $key = $xml->getAttribute( 'key' ); try { $row = Db::i()->select( '*', 'core_theme_editor_settings', [ 'setting_key=? and setting_set_id=?', $key, $set->id ] )->first(); $setting = Setting::constructFromData( $row ); } catch( UnderflowException ) { $setting = new Setting; $setting->key = $key; $setting->set_id = $set->id; } $setting->app = $xml->getAttribute( 'app' ); $setting->type = $xml->getAttribute( 'type' ); /* Figure out the category - it will either be custom to this theme, or it will be in the master */ try { $category = Db::i()->select( '*', 'core_theme_editor_categories', [ 'cat_key=? and (cat_set_id=? or cat_set_id=?)', $xml->getAttribute( 'category' ), $set->id, 0 ], 'cat_set_id desc' )->first(); $setting->category_id = $category['cat_id']; } catch( UnderflowException ) { /* This should never happen, but we'll do a fail-safe here so we don't break everything */ $setting->category_id = 0; } if( !$setting->position ) { $position = (int) Db::i()->select( 'max(setting_position)', 'core_theme_editor_settings', array( 'setting_category_id=?', $setting->category_id ) )->first(); $setting->position = $position + 1; } $setting->refresh = (bool) $xml->getAttribute( 'refresh' ); $xml->read(); while( $xml->read() ) { if( $xml->nodeType == $xml::END_ELEMENT ) { break; } switch( $xml->name ) { case 'name': $setting->name = $xml->readString(); break; case 'desc': $setting->desc = $xml->readString(); break; case 'default': case 'data': $field = $xml->name; $value = trim( $xml->readString() ); $setting->$field = $value ?: null; break; } $setting->save(); $xml->next(); } break; case 'app': $appKey = $xml->getAttribute('key'); if( Application::appIsEnabled( $appKey ) ) { /* Import */ $xml->read(); while ( $xml->read() ) { if( $xml->nodeType == $xml::END_ELEMENT ) { break; } switch ( $xml->name ) { case 'css': $location = $xml->getAttribute('css_location'); $path = $xml->getAttribute('css_path'); $name = $xml->getAttribute('css_name'); /* Keep this */ $_SESSION['theme_import']['css'][] = Db::i()->replace( 'core_theme_css', array( 'css_set_id' => Request::i()->id, 'css_app' => $appKey, 'css_location' => $xml->getAttribute('css_location'), 'css_path' => $xml->getAttribute('css_path'), 'css_name' => $xml->getAttribute('css_name'), 'css_attributes' => $xml->getAttribute('css_attributes'), 'css_content' => $xml->readString(), 'css_version' => $version, 'css_user_edited'=> 0 ), true ); break; case 'resource': /* Theme resources should be raw binary data everywhere (filesystem and DB) except in the theme XML download where they are base64 encoded. */ $content = base64_decode( $xml->readString() ); $name = Theme::makeBuiltTemplateLookupHash( $appKey, $xml->getAttribute('location'), $xml->getAttribute('path') ) . '_' . $xml->getAttribute('name'); $fileName = (string) File::create( 'core_Theme', $name, $content, 'set_resources_' . Request::i()->id, TRUE, NULL, FALSE ); try { $existingImage = Db::i()->select( '*', 'core_theme_resources', array( 'resource_set_id=? AND resource_app=? AND resource_location=? AND resource_path=? AND resource_name=?', Request::i()->id, $appKey, $xml->getAttribute('location'), $xml->getAttribute('path'), $xml->getAttribute('name') ) )->first(); if ( $existingImage['resource_filename'] ) { try { File::get( 'core_Theme', $existingImage['resource_filename'] )->delete(); } catch( Exception $e ) { } } Db::i()->delete( 'core_theme_resources', array( 'resource_id=?', $existingImage['resource_id'] ) ); } catch( UnderflowException $e ) { } Db::i()->replace( 'core_theme_resources', array( 'resource_set_id' => Request::i()->id, 'resource_app' => $appKey, 'resource_location' => $xml->getAttribute('location'), 'resource_path' => $xml->getAttribute('path'), 'resource_name' => $xml->getAttribute('name'), 'resource_data' => $content, 'resource_added' => time(), 'resource_filename' => $fileName, 'resource_user_edited' => intval( $xml->getAttribute('user_edited') ) ) ); break; } $xml->next(); } /* Update set so far so that mappings are saved */ $set->saveSet(); } break; case 'language': $xml->read(); while ( $xml->read() ) { if ( $xml->name == 'word' ) { $languageIds = $languageIds ?? array_keys( Lang::languages() ); foreach ( $languageIds as $langId ) { $default = $xml->readString(); $exists = Db::i()->select( 'COUNT(*)', 'core_sys_lang_words', array( array( 'lang_id=? and word_key=? and word_theme=?', $langId, $xml->getAttribute('key'), Request::i()->id ) ) )->first(); if ( $exists ) { Db::i()->update( 'core_sys_lang_words', array( 'word_default' => $default, 'word_default_version' => $version, ), array( array( 'lang_id=? and word_key=? and word_theme=?', $langId, $xml->getAttribute('key'), Request::i()->id ) ) ); } else { Db::i()->insert( 'core_sys_lang_words', array( 'lang_id' => $langId, 'word_app' => NULL, 'word_theme' => Request::i()->id, 'word_key' => $xml->getAttribute('key'), 'word_default' => $default, 'word_custom' => NULL, 'word_default_version' => $version, 'word_custom_version' => NULL, 'word_js' => FALSE, 'word_export' => TRUE ) ); } } } if( $xml->nodeType == $xml::END_ELEMENT ) { break; } $xml->next(); } break; case 'templates': $xml->read(); while ( $xml->read() ) { if ( $xml->name == 'template' ) { $hookPoint = $xml->getAttribute( 'hookpoint' ); $key = $xml->getAttribute( 'key' ); $where = [ [ 'template_set_id=?', Request::i()->id ], [ 'template_name=?', $xml->getAttribute('name') ], [ 'template_hookpoint_type=?', $xml->getAttribute( 'hookpoint_type' ) ] ]; if( empty( $hookPoint ) ) { $where[] = [ 'template_hookpoint is null']; } else { $where[] = [ 'template_hookpoint=?', $hookPoint ]; } Db::i()->delete( 'core_theme_templates_custom', $where ); Db::i()->insert( 'core_theme_templates_custom', [ 'template_set_id' => Request::i()->id, 'template_name' => $xml->getAttribute('name'), 'template_hookpoint' => ( $hookPoint ?: null ), 'template_hookpoint_type' => $xml->getAttribute('hookpoint_type'), 'template_key' => ( $key ?: null ), 'template_version' => $xml->getAttribute('version'), 'template_content' => $xml->readString(), 'template_updated' => time(), ] ); if( $xml->nodeType == $xml::END_ELEMENT ) { break; } $xml->next(); } } break; case 'logo': $type = $xml->getAttribute('type'); $name = $xml->getAttribute('name'); /* Sharer logo and favicons may be present from themes generated in 4.3 and earlier, but are no longer used */ if( $type != 'sharer' AND $type != 'favicon' ) { if( isset( $set->logo[ $type ]['url'] ) ) { File::get( 'core_Theme', $set->logo[ $type ]['url'] )->delete(); } File::$safeFileExtensions[] = 'svg'; $url = (string) File::create( 'core_Theme', $name, base64_decode( $xml->readString() ) ); $set->saveSet( array( 'logo' => array( $type => array( 'filename' => $name, 'setting_height' => $xml->getAttribute('height') ?? 100 ) ) ) ); } break; } $set->save(); $xml->next(); } return null; }, function() { $set = Theme::load( Request::i()->id ); /* Do we need to clean up orphaned CSS files? */ if ( $_SESSION['theme_import']['isNewSet'] === false and isset( $_SESSION['theme_import']['css'] ) ) { Db::i()->delete( 'core_theme_css', array( array( Db::i()->in( 'css_id', $_SESSION['theme_import']['css'], true ) ), array( 'css_set_id=?', $set->id ) ) ); } Theme::deleteCompiledResources( null, null, null, null, $set->id ); Theme::deleteCompiledTemplate( null, null, null, $set->id ); Theme::deleteCompiledCss( null, null, null, null, $set->id ); Store::i()->delete( 'core_theme_import_' . md5_file( Request::i()->file ) ); @unlink( Request::i()->file ); /* Update theme settings */ foreach( Application::applications() as $app ) { $app->installThemeEditorSettings(); } /* Clear the entire data store, there are too many to list here */ Store::i()->clearAll(); Output::i()->redirect( Url::internal( 'app=core&module=customization&controller=themes' ) ); } ); } /** * Upload a new version * * @return void * @throws ErrorException */ public function importForm(): void { $id = intval( Request::i()->id ); /* Check permission */ Dispatcher::i()->checkAcpPermission( 'theme_download_upload' ); $themeSet = Theme::load( $id ); $form = new Form( 'form', 'theme_set_import_button' ); $form->add( new Upload( 'core_theme_set_new_import', NULL, FALSE, array( 'allowedFileTypes' => array( 'xml' ), 'temporary' => TRUE ), NULL, NULL, NULL, 'core_theme_set_new_import' ) ); if ( $values = $form->values() ) { /* Move it to a temporary location */ $tempFile = tempnam( TEMP_DIRECTORY, 'IPS' ); move_uploaded_file( $values['core_theme_set_new_import'], $tempFile ); /* Initate a redirector */ Output::i()->redirect( Url::internal( 'app=core&module=customization&controller=themes&do=import' )->setQueryString( array( 'file' => $tempFile, 'key' => md5_file( $tempFile ), 'id' => Request::i()->id) )->csrf() ); } /* Display */ Output::i()->output = Theme::i()->getTemplate( 'global' )->block( Member::loggedIn()->language()->addToStack('theme_set_import_title', FALSE, array( 'sprintf' => array( $themeSet->name ) ) ), $form, FALSE ); } /** * Export a theme set form * * @return void */ public function exportForm(): void { $id = intval( Request::i()->id ); /* Check permission */ Dispatcher::i()->checkAcpPermission( 'theme_download_upload' ); $themeSet = Theme::load( $id ); $form = new Form( 'form', 'theme_set_export_button' ); $storedAuthor = ( isset( Store::i()->theme_stored_author ) AND is_array( Store::i()->theme_stored_author ) ) ? Store::i()->theme_stored_author : null; $form->addHeader( Member::loggedIn()->language()->addToStack('theme_set_export_title', FALSE, array( 'sprintf' => array( $themeSet->_title ) ) ) ); if ( \IPS\IN_DEV ) { $form->add( new Text( 'theme_template_export_author_name', ( $storedAuthor !== null ) ? $storedAuthor['name'] : false, false ) ); $form->add( new Text( 'theme_template_export_author_url' , ( $storedAuthor !== null ) ? $storedAuthor['url'] : false, false ) ); $form->add( new Text( 'theme_update_check' , $themeSet->update_check, false ) ); $form->add( new Text( 'theme_template_export_version' , $themeSet->version , false, array( 'placeholder' => '1.0.0' ) ) ); $form->add( new Number( 'theme_template_export_long_version' , $themeSet->long_version, false ) ); } if ( $values = $form->values() or Request::i()->form_submitted ) { $authorName = $values['theme_template_export_author_name'] ?: $themeSet->author_name; $authorUrl = $values['theme_template_export_author_url'] ?: $themeSet->author_url; $version = $values['theme_template_export_version'] ?: $themeSet->version; $longVersion = is_int( $values['theme_template_export_long_version'] ) ? $values['theme_template_export_long_version'] : $themeSet->long_version; $updateCheck = $values['theme_update_check'] ?: $themeSet->update_check; Store::i()->theme_stored_author = array( 'name' => $authorName, 'url' => $authorUrl, ); /* Init */ $xml = new XMLWriter; $xml->openMemory(); $xml->setIndent( TRUE ); $xml->startDocument( '1.0', 'UTF-8' ); /* Root tag */ $xml->startElement('theme'); $xml->startAttribute('name'); $xml->text( Member::loggedIn()->language()->get('core_theme_set_title_' . $themeSet->_id ) ); $xml->endAttribute(); $xml->startAttribute('author_name'); $xml->text( $authorName ); $xml->endAttribute(); $xml->startAttribute('author_url'); $xml->text( $authorUrl ); $xml->endAttribute(); $xml->startAttribute('version'); $xml->text( $version ); $xml->endAttribute(); $xml->startAttribute('long_version'); $xml->text( $longVersion ); $xml->endAttribute(); $xml->startAttribute('update_check'); $xml->text( $updateCheck ); $xml->endAttribute(); $xml->startElement('header'); $xml->writeCData( str_replace( ']]>', ']]]]>', (string) $themeSet->custom_header ) ); $xml->endElement(); $xml->startElement('footer'); $xml->writeCData( str_replace( ']]>', ']]]]>', (string) $themeSet->custom_footer ) ); $xml->endElement(); $xml->startElement('css'); $xml->writeCData( str_replace( ']]>', ']]]]>', (string) $themeSet->custom_css ) ); $xml->endElement(); $xml->startElement('layout'); $xml->writeCData( str_replace( ']]>', ']]]]>', (string) $themeSet->view_options ) ); $xml->endElement(); $xml->startElement('core_js'); $xml->writeCData( str_replace( ']]>', ']]]]>', (string) $themeSet->core_js ) ); $xml->endElement(); $xml->startElement('core_css'); $xml->writeCData( str_replace( ']]>', ']]]]>', (string) $themeSet->core_css ) ); $xml->endElement(); $xml->startElement('css_variables'); $xml->writeCData( str_replace( ']]>', ']]]]>', (string) $themeSet->css_variables ) ); $xml->endElement(); $xml->startElement('editor_data'); $editorData = $themeSet->theme_editor_data; $xml->writeCData( str_replace( ']]>', ']]]]>', ( $editorData === Theme::$defaultThemeEditorData ? '' : json_encode( $editorData ) ) ) ); $xml->endElement(); /* Copy logos */ if( $logos = $themeSet->logo ) { if( is_array( $logos ) ) { foreach ( $logos as $file => $data ) { if( isset( $data['filename'] ) ) { /* Start the XML */ $xml->startElement('logo'); $xml->startAttribute('type'); $xml->text( $file ); $xml->endAttribute(); if( isset( $data['setting_height'] ) ) { $xml->startAttribute('height'); $xml->text( $data['setting_height'] ); $xml->endAttribute(); } /* Get image data */ $original = File::get( 'core_Theme', $data['url'] ); $xml->startAttribute('name'); $xml->text( $data['filename'] ); $xml->endAttribute(); $xml->text( base64_encode( $original->contents() ) ); /* Close the